"""Reconciliation-based forward gate: PASS only when the forward track has run ~2 weeks AND reconciles with
the backtest (does not diverge/contradict it) AND executed cleanly (no gaps). Otherwise WAIT with the SPECIFIC
failing reason. Replaces the blind 60-day calendar wait — a 5y backfill is in-sample, so the forward track is
the only true OOS check, and the real signal (does forward track the backtest? did the pipeline run clean?)
arrives in ~2 weeks; more calendar days add ~no statistical power."""
from __future__ import annotations

import datetime as _dt

from fxhnt.application.forward_models import ForwardNavRow, ForwardSummary
from fxhnt.application.gate_policy import POLICY
from fxhnt.application.gate_policy_resolve import resolve_policy
from fxhnt.application.reconciliation_gate import BacktestReference, evaluate_reconciliation_gate

_EPOCH = _dt.date(2026, 6, 1)


def _pol(**overrides):
    return resolve_policy(POLICY, overrides)


def _rows(rets: list[float], *, start: _dt.date = _EPOCH, gap_after: int | None = None) -> list[ForwardNavRow]:
    """Build a contiguous daily ForwardNavRow series from per-day returns. `gap_after` drops one calendar
    day after that index (to simulate a missing trading day → execution gap)."""
    out: list[ForwardNavRow] = []
    nav = 1.0
    day = start
    for i, r in enumerate(rets):
        nav *= 1.0 + r
        out.append(ForwardNavRow("bybit_4edge", day.isoformat(), float(r), nav))
        day = day + _dt.timedelta(days=1)
        if gap_after is not None and i == gap_after:
            day = day + _dt.timedelta(days=1)   # skip a calendar day → a hole in the sequence
    return out


def _summary(rows: list[ForwardNavRow]) -> ForwardSummary:
    nav = rows[-1].nav if rows else 1.0
    return ForwardSummary("bybit_4edge", rows[-1].date if rows else "", len(rows), nav, nav - 1.0, 0.0, 0.0)


def _bt(expected_window_return: float, daily: list[float] | None = None) -> BacktestReference:
    return BacktestReference(expected_window_return=expected_window_return, daily_returns=daily)


def test_defaults_are_named_constants() -> None:
    # ~2 weeks, not a magic 60; tolerance + correlation floor are named + documented (now on POLICY, the
    # single versioned source, rather than scattered module constants).
    assert POLICY.min_forward_days == 14
    assert POLICY.recon_tolerance_frac == 0.5
    assert POLICY.recon_min_corr == 0.0


def test_tracks_backtest_clean_and_enough_days_passes() -> None:
    # Forward earns ~+0.2%/day over 14 contiguous days; backtest expected ~+2.8% over the window; corr +1.
    rets = [0.002] * 14
    rows = _rows(rets)
    bt = _bt(expected_window_return=0.028, daily=rets[:])
    v = evaluate_reconciliation_gate(_summary(rows), rows, bt, _pol())
    assert v.status == "PASS", v.reason


def test_diverging_from_backtest_waits_even_past_min_days() -> None:
    # THE MIRAGE CATCHER: backtest predicts strongly positive over the window, but the forward track is
    # strongly negative (Binance's +7097% backtest is -36% live). Past MIN days, still WAIT — not PASS.
    rets = [-0.03] * 20                       # forward collapses: ~-45% cumulative
    rows = _rows(rets)
    bt = _bt(expected_window_return=2.0, daily=[0.03] * 20)   # backtest "should" be +200%, perfectly opposite
    v = evaluate_reconciliation_gate(_summary(rows), rows, bt, _pol())
    assert v.status == "WAIT"
    assert "diverg" in v.reason.lower()


def test_building_below_min_days_waits() -> None:
    rets = [0.002] * 9                        # only 9 < 14 forward days
    rows = _rows(rets)
    bt = _bt(expected_window_return=0.02, daily=rets[:])
    v = evaluate_reconciliation_gate(_summary(rows), rows, bt, _pol())
    assert v.status == "WAIT"
    assert "building" in v.reason.lower()
    assert "9" in v.reason and "14" in v.reason


def test_execution_gap_waits() -> None:
    # A missing calendar day mid-window = a hole in the forward record → WAIT (execution gap), even though
    # the returns themselves track the backtest and there are >= MIN days present.
    rets = [0.002] * 16
    rows = _rows(rets, gap_after=7)           # drop one day after index 7
    bt = _bt(expected_window_return=0.032, daily=rets[:])
    v = evaluate_reconciliation_gate(_summary(rows), rows, bt, _pol())
    assert v.status == "WAIT"
    assert "gap" in v.reason.lower() or "execution" in v.reason.lower()


def test_negative_daily_correlation_waits() -> None:
    # Cumulative return is in-band, but day-to-day the forward ANTI-correlates with the backtest (it is
    # winning on the days the backtest loses) — a contradiction of the edge's shape → WAIT.
    fwd = [0.01, -0.01, 0.01, -0.01, 0.01, -0.01, 0.01, -0.01, 0.01, -0.01, 0.01, -0.01, 0.01, -0.01]
    bt_daily = [-r for r in fwd]             # perfectly anti-correlated
    rows = _rows(fwd)
    bt = _bt(expected_window_return=0.0, daily=bt_daily)
    v = evaluate_reconciliation_gate(_summary(rows), rows, bt, _pol())
    assert v.status == "WAIT"
    assert "diverg" in v.reason.lower() or "corr" in v.reason.lower()


def test_no_backtest_reference_withholds_pass() -> None:
    # REAL-MONEY SAFETY: with no backtest to reconcile against, the gate must NOT PASS — a bare min-window +
    # clean-exec timer is not a validated reconciliation, and a "PASS" there is a false-PASS (the dangerous
    # error). PASS is WITHHELD (WAIT) until a backtest_summary row exists. (Was previously a hollow PASS
    # mislabelled "reconciles with backtest".)
    rets = [0.001] * 14
    rows = _rows(rets)
    v = evaluate_reconciliation_gate(_summary(rows), rows, None, _pol())
    assert v.status == "WAIT", v.reason
    assert "no backtest reference" in v.reason.lower()
    assert "reconciles with backtest" not in v.reason.lower()


def test_weekday_only_gaps_tolerated_via_max_gap_days() -> None:
    # A weekday-only equity NAV (multistrat) has Fri→Mon 3-day steps — NORMAL, not a pipeline gap. The
    # crypto-centric default (max_gap_days=1, calendar-continuous) would falsely flag them; max_gap_days=4
    # tolerates weekend + holiday steps so the equity track can actually reconcile.
    start = _dt.date(2026, 6, 1)   # a Monday
    dates = [start + _dt.timedelta(days=d) for d in range(21)
             if (start + _dt.timedelta(days=d)).weekday() < 5]   # 15 weekdays, Fri→Mon 3-day steps
    nav = 1.0
    rows = []
    for d in dates:
        nav *= 1.0 + 0.001
        rows.append(ForwardNavRow("multistrat", d.isoformat(), 0.001, nav))
    bt = _bt(expected_window_return=len(dates) * 0.001)
    assert evaluate_reconciliation_gate(
        _summary(rows), rows, bt, _pol(min_forward_days=10, max_gap_days=4)).status == "PASS"
    strict = evaluate_reconciliation_gate(
        _summary(rows), rows, bt, _pol(min_forward_days=10, max_gap_days=1))
    assert strict.status == "WAIT" and "gap" in strict.reason.lower()   # the crypto default flags the weekend


def test_min_forward_days_overridable_via_spec() -> None:
    rets = [0.001] * 14
    rows = _rows(rets)
    bt = _bt(expected_window_return=0.014, daily=rets[:])
    v = evaluate_reconciliation_gate(_summary(rows), rows, bt, _pol(min_forward_days=30))
    assert v.status == "WAIT" and "building" in v.reason.lower()


# ---- band stays TIGHT (real-money gate must err toward WAIT, not PASS) --------------------------------

def test_band_is_the_fixed_tight_formula() -> None:
    # The band must be the FIXED tight formula `tol_frac * max(|expected|, min_band)` — NOT widened by the
    # forward's realized dispersion. In prod the correlation sub-check does not run (daily_returns=None), so
    # this band is the only always-on divergence check; loosening it would risk a false-PASS on a dead edge.
    # Construct expected + a shortfall exactly straddling the fixed band and confirm the tight threshold holds.
    expected = 0.10
    fixed_band = POLICY.recon_tolerance_frac * max(abs(expected), POLICY.recon_min_band)   # 0.05
    # a forward exactly at (expected - fixed_band) - epsilon is in-band → PASS; below it → WAIT.
    in_band = [((1.0 + (expected - fixed_band + 0.005)) ** (1 / 21) - 1.0)] * 21   # ends just above the floor
    rows = _rows(in_band)
    bt = _bt(expected_window_return=expected)                                # no daily series (prod shape)
    v = evaluate_reconciliation_gate(_summary(rows), rows, bt, _pol(min_forward_days=21))
    assert v.status == "PASS", v.reason


def test_lumpy_negative_window_below_fixed_band_waits_no_dispersion_widening() -> None:
    # SAFETY: a lumpy forward with big daily swings whose cumulative falls BELOW the fixed shortfall band must
    # WAIT — the band is NOT widened by the swing dispersion. For a real-capital gate the safe error is a
    # false-WAIT (costs time), never a false-PASS (deploys on a dead edge). Even with NO backtest daily series
    # (the prod shape — no correlation backstop), the tight cumulative band still catches it.
    fwd = ([0.07, -0.075] * 10) + [-0.02]           # 21 days, big swings, net slightly negative
    rows = _rows(fwd)
    total = rows[-1].nav - 1.0
    expected = 0.10
    fixed_band = POLICY.recon_tolerance_frac * max(abs(expected), POLICY.recon_min_band)
    assert (expected - total) > fixed_band, "the window must fall below the fixed band"
    bt = _bt(expected_window_return=expected)        # daily_returns=None → prod: no correlation backstop
    v = evaluate_reconciliation_gate(_summary(rows), rows, bt, _pol(min_forward_days=21))
    assert v.status == "WAIT" and "diverg" in v.reason.lower()


def test_steady_mirage_below_fixed_band_waits() -> None:
    # THE MIRAGE: backtest strongly positive, forward a steady collapse well below the fixed band → WAIT (never
    # PASS), on the cumulative band alone (prod: daily_returns=None). The tight band is the always-on catcher.
    fwd = [-0.02] * 21                               # ~-35% cumulative
    rows = _rows(fwd)
    bt = _bt(expected_window_return=1.0)             # backtest "should" be +100%
    v = evaluate_reconciliation_gate(_summary(rows), rows, bt, _pol(min_forward_days=21))
    assert v.status == "WAIT" and "diverg" in v.reason.lower()
