"""Unit tests for the soft-day primary filter ported from orb_simulator.py.

Verifies parity with libs/intraday/orb_simulator.py:9932-9983 so live ORB
sessions no longer trade primary candidates that the backtest skips by design.
"""
from __future__ import annotations

from types import SimpleNamespace
from unittest.mock import MagicMock

import pytest

from apps.orb_trader.engine import ORBTradingEngine


def _v49_127_params() -> SimpleNamespace:
    """Soft-day params matching configs/.../orb_gainers_v49_127_*.yaml."""
    return SimpleNamespace(
        settlement_days=0,
        slippage_bps=0.0,
        soft_day_min_score_pct=0.8,
        soft_day_min_candidate_score=None,
        soft_day_min_ret_5d=-0.05,
        soft_day_max_rvol=60.0,
        soft_day_max_trades=2,
        soft_day_setup_profile="skip_day_reclaim_v1",
        soft_day_sector_confirmation_override_enabled=False,
    )


def _make_engine(params: SimpleNamespace) -> ORBTradingEngine:
    return ORBTradingEngine(
        session=SimpleNamespace(
            session_id="s1", session_name="t", initial_equity=10_000.0
        ),
        broker=MagicMock(),
        state=MagicMock(),
        params=params,
    )


def test_no_soft_day_means_no_filter():
    eng = _make_engine(_v49_127_params())
    eng._soft_day_reason = None
    result = eng._evaluate_soft_day_primary_filter(
        cand={"score": 0.1, "ret_5d": -1.0, "rvol": 999.0,
              "gap_pct": 0.0, "premarket_dollar_vol": 0.0,
              "body_ratio": 0.0, "close_location": 0.0},
        trigger_type="orb",
        score_rank_pct=0.0,
        ticker_enrich={},
        total_trades_today=10,
    )
    assert result is None


def test_market_regime_only_rejects_all_primary_orb():
    """skip_day_reclaim_v1 profile rejects all candidates on market_regime-only soft days."""
    eng = _make_engine(_v49_127_params())
    eng._soft_day_reason = "market_regime"
    result = eng._evaluate_soft_day_primary_filter(
        cand={"score": 1.5, "ret_5d": 0.10, "rvol": 5.0,
              "gap_pct": 0.05, "premarket_dollar_vol": 100_000_000,
              "body_ratio": 0.9, "close_location": 0.95},
        trigger_type="orb",
        score_rank_pct=1.0,
        ticker_enrich={},
        total_trades_today=0,
    )
    assert result == "soft_day_profile"


def test_mu_5_14_regression_rejected_by_score():
    """MU 5/14: live filled this; backtest skipped. Live must now reject."""
    eng = _make_engine(_v49_127_params())
    eng._soft_day_reason = "market_regime+breadth"
    result = eng._evaluate_soft_day_primary_filter(
        cand={
            "score": 0.7193, "ret_5d": 0.05, "rvol": 3.25,
            "gap_pct": -0.0200092, "premarket_dollar_vol": 49_905_618.9,
            "body_ratio": 0.292, "close_location": 0.938,
        },
        trigger_type="orb",
        # Live recorded rvol_rank=0.7534; this is < soft_day_min_score_pct=0.8
        score_rank_pct=0.7534,
        ticker_enrich={},
        total_trades_today=0,
    )
    assert result == "soft_day_score"


def test_sndk_5_15_regression_rejected_by_score():
    """SNDK 5/15: live filled (+$107); backtest skipped. Live must now reject."""
    eng = _make_engine(_v49_127_params())
    eng._soft_day_reason = "market_regime+breadth"
    result = eng._evaluate_soft_day_primary_filter(
        cand={
            "score": 0.5284, "ret_5d": 0.05, "rvol": 1.86,
            "gap_pct": -0.0443148, "premarket_dollar_vol": 1_589_816.1,
            "body_ratio": 0.897, "close_location": 0.977,
        },
        trigger_type="orb",
        score_rank_pct=0.4795,
        ticker_enrich={},
        total_trades_today=0,
    )
    assert result == "soft_day_score"


def test_mu_5_14_score_ok_still_rejected_by_profile():
    """If MU had passed score_rank, profile (skip_day_reclaim_v1) still rejects:
    close_location 0.94 > 0.10 fails gap-down branch; gap_pct < 0 fails gap-up branch.
    """
    eng = _make_engine(_v49_127_params())
    eng._soft_day_reason = "market_regime+breadth"
    result = eng._evaluate_soft_day_primary_filter(
        cand={
            "score": 0.7193, "ret_5d": 0.05, "rvol": 3.25,
            "gap_pct": -0.0200092, "premarket_dollar_vol": 49_905_618.9,
            "body_ratio": 0.292, "close_location": 0.938,
        },
        trigger_type="orb",
        score_rank_pct=0.95,  # well above 0.8
        ticker_enrich={},
        total_trades_today=0,
    )
    assert result == "soft_day_profile"


def test_max_trades_caps_primary_count():
    """soft_day_max_trades=2 → 3rd primary orb candidate rejected."""
    eng = _make_engine(_v49_127_params())
    eng._soft_day_reason = "breadth"
    result = eng._evaluate_soft_day_primary_filter(
        cand={"score": 1.5, "ret_5d": 0.05, "rvol": 5.0,
              "gap_pct": 0.05, "premarket_dollar_vol": 100_000_000,
              "body_ratio": 0.9, "close_location": 0.95},
        trigger_type="orb",
        score_rank_pct=1.0,
        ticker_enrich={},
        total_trades_today=2,
    )
    assert result == "soft_day_max_trades"


def test_max_trades_exempt_for_market_thrust_burst():
    """market_thrust_opening_burst is exempt from soft_day_max_trades cap."""
    eng = _make_engine(_v49_127_params())
    eng._soft_day_reason = "breadth"
    result = eng._evaluate_soft_day_primary_filter(
        cand={"score": 1.0, "ret_5d": 0.05, "rvol": 5.0,
              "gap_pct": 0.05, "premarket_dollar_vol": 100_000_000,
              "body_ratio": 0.9, "close_location": 0.95},
        trigger_type="market_thrust_opening_burst",
        score_rank_pct=0.5,
        ticker_enrich={},
        total_trades_today=10,
    )
    assert result is None


def test_score_and_profile_skipped_for_aux_trigger():
    """Aux triggers (e.g. broad_gapup_continuation) bypass score/profile gates."""
    eng = _make_engine(_v49_127_params())
    eng._soft_day_reason = "market_regime+breadth"
    result = eng._evaluate_soft_day_primary_filter(
        cand={"score": 0.1, "ret_5d": -1.0, "rvol": 999.0,
              "gap_pct": 0.0, "premarket_dollar_vol": 0.0,
              "body_ratio": 0.0, "close_location": 0.0},
        trigger_type="broad_gapup_continuation",
        score_rank_pct=0.0,
        ticker_enrich={},
        total_trades_today=0,
    )
    assert result is None


def test_ret_5d_falls_back_to_ticker_enrich():
    """When cand lacks ret_5d, fall back to ticker_enrich."""
    params = _v49_127_params()
    params.soft_day_min_score_pct = None  # skip score gate to reach ret5
    params.soft_day_setup_profile = "none"  # skip profile gate
    eng = _make_engine(params)
    eng._soft_day_reason = "breadth"
    result = eng._evaluate_soft_day_primary_filter(
        cand={"rvol": 5.0, "gap_pct": 0.05, "premarket_dollar_vol": 1e8,
              "body_ratio": 0.9, "close_location": 0.9},
        trigger_type="orb",
        score_rank_pct=1.0,
        ticker_enrich={"ret_5d": -0.5},  # below soft_day_min_ret_5d=-0.05
        total_trades_today=0,
    )
    assert result == "soft_day_ret5"


def test_rvol_max_filter():
    params = _v49_127_params()
    params.soft_day_min_score_pct = None
    params.soft_day_setup_profile = "none"
    eng = _make_engine(params)
    eng._soft_day_reason = "breadth"
    result = eng._evaluate_soft_day_primary_filter(
        cand={"score": 1.0, "ret_5d": 0.10, "rvol": 100.0,
              "gap_pct": 0.05, "premarket_dollar_vol": 1e8,
              "body_ratio": 0.9, "close_location": 0.9},
        trigger_type="orb",
        score_rank_pct=1.0,
        ticker_enrich={},
        total_trades_today=0,
    )
    assert result == "soft_day_rvol"


def test_high_conviction_passes_setup_profile():
    """Profile preserves high-conviction fallback: score>=1.0015, ret_5d>=0.03, rvol<=30."""
    eng = _make_engine(_v49_127_params())
    eng._soft_day_reason = "breadth"
    result = eng._evaluate_soft_day_primary_filter(
        cand={"score": 1.05, "ret_5d": 0.05, "rvol": 5.0,
              "gap_pct": 0.03, "premarket_dollar_vol": 50_000_000,
              "body_ratio": 0.7, "close_location": 0.9},
        trigger_type="orb",
        score_rank_pct=1.0,  # above 0.8
        ticker_enrich={},
        total_trades_today=0,
    )
    assert result is None
