from fxhnt.application.forward_models import ForwardNavRow
from fxhnt.application.funding_status import compute_funding
from fxhnt.application.gate_policy_resolve import ResolvedPolicy
from fxhnt.application.reconciliation_gate import BacktestReference

# min_forward_days=1 so every day yields a non-building verdict; tiny windows so sequences stay short.
_RP = ResolvedPolicy(min_forward_days=1, recon_tolerance_frac=0.5, recon_min_band=0.05,
                     recon_min_corr=0.0, max_gap_days=1, min_days=1, min_total_return=0.0, min_sharpe=0.0)
def _REF(_days):  # fixed expectation, independent of window length
    return BacktestReference(expected_window_return=0.10, daily_returns=None)

_DEFUND, _REFUND = 2, 3


def _rows(navs):
    # consecutive dates so _has_gaps stays False (no EXECUTION_GAP unless we deliberately gap)
    return [ForwardNavRow("s", f"2026-07-{1+i:02d}", 0.0, v) for i, v in enumerate(navs)]


def _fund(navs):
    return compute_funding("s", _rows(navs), _REF, _RP, _DEFUND, _REFUND)


# nav 1.10 -> total_return 0.10 == expected -> PASS ; nav 0.95 -> -0.05, shortfall 0.15 > band 0.05 -> DIVERGENCE

def test_sustained_divergence_defunds_after_window():
    s = _fund([1.10, 0.95, 0.95])          # PASS, DIVERGENCE x2 (== defund_window)
    assert s.state == "de-funded"
    assert s.first_pass_date == "2026-07-01"


def test_transient_dip_does_not_flicker():
    s = _fund([1.10, 0.95, 1.10])          # PASS, DIVERGENCE(1), PASS -> decay run reset
    assert s.state == "funded"
    assert s.decay_run == 0


def test_recovery_refunds_after_window():
    s = _fund([1.10, 0.95, 0.95, 1.10, 1.10, 1.10])   # de-fund at idx2, then 3 PASS == refund_window
    assert s.state == "funded"


def test_never_passed_is_never_defunded():
    s = _fund([0.95, 0.95, 0.95, 0.95])    # all DIVERGENCE, no PASS -> decay run never advances
    assert s.state == "funded"
    assert s.first_pass_date is None


def test_execution_gap_resets_decay_run():
    rows = [ForwardNavRow("s", "2026-07-01", 0.0, 1.10),   # PASS
            ForwardNavRow("s", "2026-07-02", 0.0, 0.95),   # DIVERGENCE (decay_run 1)
            ForwardNavRow("s", "2026-07-04", 0.0, 0.95)]   # gap>1 -> whole record EXECUTION_GAP -> neutral
    s = compute_funding("s", rows, _REF, _RP, _DEFUND, _REFUND)
    assert s.state == "funded"


def test_deterministic():
    assert _fund([1.10, 0.95, 0.95]) == _fund([1.10, 0.95, 0.95])


def test_old_gap_ages_out_then_divergence_defunds():
    # CRITICAL regression: PASS, DIV, GAP(neutral), then CONTIGUOUS DIV x2. The aged-out gap must no longer
    # mask divergence -> the edge de-funds. (Before the fix this stayed "funded" forever.)
    rows = [ForwardNavRow("s", "2026-07-01", 0.0, 1.10),   # PASS -> has_passed
            ForwardNavRow("s", "2026-07-02", 0.0, 0.95),   # DIVERGENCE (decay 1)
            ForwardNavRow("s", "2026-07-04", 0.0, 0.95),   # date gap>1 -> EXECUTION_GAP (decay reset 0)
            ForwardNavRow("s", "2026-07-05", 0.0, 0.95),   # contiguous again -> DIVERGENCE (decay 1)
            ForwardNavRow("s", "2026-07-06", 0.0, 0.95)]   # DIVERGENCE (decay 2 == defund_window)
    assert compute_funding("s", rows, _REF, _RP, _DEFUND, _REFUND).state == "de-funded"


def test_ref_is_prorated_to_min_forward_days():
    # proves the gate ref is pro-rated to max(days, min_forward_days), not raw days.
    calls = []
    def ref(days):
        calls.append(days)
        return BacktestReference(expected_window_return=0.10, daily_returns=None)
    rp = ResolvedPolicy(min_forward_days=5, recon_tolerance_frac=0.5, recon_min_band=0.05,
                        recon_min_corr=0.0, max_gap_days=1, min_days=5, min_total_return=0.0, min_sharpe=0.0)
    compute_funding("s", _rows([1.0, 1.0, 1.0]), ref, rp, _DEFUND, _REFUND)   # days 1,2,3
    assert calls == [5, 5, 5]                                                 # each clamped up to 5


def test_immature_no_ref_is_neutral_after_pass():
    # IMMATURE (no backtest ref) is neutral even AFTER a PASS: decay never advances -> stays funded.
    def ref(days):
        return BacktestReference(0.10, None) if days <= 1 else None           # ref vanishes after day 1
    s = compute_funding("s", _rows([1.10, 0.95, 0.95]), ref, _RP, _DEFUND, _REFUND)
    assert s.state == "funded"


def test_label_is_decaying_mid_decay():
    # a decay run building but below defund_window -> state still funded, label "decaying".
    s = _fund([1.10, 0.95])                                                   # PASS, 1 DIVERGENCE
    assert s.state == "funded"
    assert s.label == "decaying"
    assert s.decay_run == 1


def test_gap_window_floored_detects_current_gap_when_min_forward_days_zero():
    # M2 regression: even with min_forward_days=0 (window would be 1 without the floor), the floor keeps
    # a CURRENT gap detectable -> the gap day is neutral (funded), not silently ignored.
    rp = ResolvedPolicy(min_forward_days=0, recon_tolerance_frac=0.5, recon_min_band=0.05,
                        recon_min_corr=0.0, max_gap_days=1, min_days=0, min_total_return=0.0, min_sharpe=0.0)
    rows = [ForwardNavRow("s", "2026-07-01", 0.0, 1.10),   # PASS
            ForwardNavRow("s", "2026-07-02", 0.0, 0.95),   # DIVERGENCE (decay 1)
            ForwardNavRow("s", "2026-07-04", 0.0, 0.95)]   # current gap>1 -> EXECUTION_GAP -> decay reset 0
    assert compute_funding("s", rows, _REF, rp, _DEFUND, _REFUND).state == "funded"
