"""Golden bit-identical equivalence: IncrementalRiskOverlay == per-date paper_risk_overlay.

Approach B makes the backfill maintain the un-throttled book-return series + DrawdownKillSwitch
INCREMENTALLY across its date loop, instead of re-deriving the whole series via paper_risk_overlay each
date (O(N²) → O(N)). The snapshot path (paper_snapshot.py) keeps using the stateless paper_risk_overlay
(live==replay), so this is only acceptable if the incremental driver is bit-identical to the per-date
stateless call. This test PROVES that with exact `==` on weights / leverage / killed for every date over
an append-only multi-sleeve fixture.
"""
import random

import pytest

from fxhnt.application.paper_book import IncrementalRiskOverlay, paper_risk_overlay


def _fixture():
    """Append-only acc grown day-by-day, 3 sleeves with different vols + staggered starts + a negative-
    drift sleeve so the killswitch actually fires and re-enters (exercises the persistent ks state)."""
    rng = random.Random(20260622)
    # (sleeve, start_day, n_obs, mu, sd)
    spec = [
        ("a", 1, 200, 0.0008, 0.004),
        ("b", 11, 190, 0.0, 0.020),
        ("c", 6, 180, -0.010, 0.020),   # strong negative drift → deep drawdown → kill, then re-enter
    ]
    full: dict[str, dict[int, float]] = {}
    for name, start, n, mu, sd in spec:
        full[name] = {start + i: rng.gauss(mu, sd) for i in range(n)}
    # The "advance" axis = each distinct day on which we'd run an overlay (the union date axis + 1,
    # mimicking the backfill processing dates as the acc grows by one booked return per day).
    all_days = sorted({d for r in full.values() for d in r})
    return full, all_days


def _acc_at(full, day_cutoff):
    """acc holding only returns strictly before `day_cutoff` (the strictly-prior window for that date)."""
    return {s: {d: v for d, v in r.items() if d < day_cutoff} for s, r in full.items()}


def test_incremental_overlay_matches_per_date_exact():
    full, all_days = _fixture()
    inc = IncrementalRiskOverlay()
    # Process each date ascending; acc grows append-only. The backfill builds active from that date's
    # sleeves-with-weights; here we use all sleeves with any prior history (faithful proxy).
    acc: dict[str, dict[int, float]] = {}
    fired_kill = False
    for cutoff in [d + 1 for d in all_days]:    # date whose strictly-prior window is keys < cutoff
        # Grow acc append-only: add the single newest day (cutoff-1) if present in full.
        newest = cutoff - 1
        for s, r in full.items():
            if newest in r:
                acc.setdefault(s, {})[newest] = r[newest]
        active = [s for s, r in acc.items() if r]
        # Reference: stateless per-date overlay over the full current acc.
        w_ref, lev_ref, killed_ref = paper_risk_overlay(acc, active)
        # Incremental driver over the same append-only acc.
        w_inc, lev_inc, killed_inc = inc.overlay(acc, active)
        assert w_inc == w_ref, f"day {cutoff}: weights {w_inc} != {w_ref}"
        assert lev_inc == lev_ref, f"day {cutoff}: leverage {lev_inc!r} != {lev_ref!r}"
        assert killed_inc == killed_ref, f"day {cutoff}: killed {killed_inc} != {killed_ref}"
        fired_kill = fired_kill or killed_ref
    # Sanity: the fixture's negative-drift sleeve must have triggered a kill at some point, so the test
    # actually exercises the persistent killswitch path (not just the calm no-kill branch).
    assert fired_kill, "fixture never triggered the killswitch — not exercising the kill path"


def _combined_fixture():
    """Append-only acc with a DYING sleeve so the combined controller's retirement branch is exercised:
    A healthy throughout, B healthy then a sustained structural decay then a recovery tail (so both the
    retire and re-admit transitions fire over the replay)."""
    import math
    A = {1 + i: 0.0018 + 0.0015 * math.sin(0.37 * i) for i in range(360)}
    B: dict[int, float] = {}
    for i in range(120):
        B[1 + i] = 0.0018 + 0.0015 * math.sin(0.51 * i)
    for i in range(120):
        B[121 + i] = -0.012 + 0.0015 * math.sin(0.51 * (120 + i))
    for i in range(120):
        B[241 + i] = 0.0025 + 0.0015 * math.sin(0.51 * (240 + i))
    full = {"a": A, "b": B}
    all_days = sorted({d for r in full.values() for d in r})
    return full, all_days


@pytest.mark.slow
def test_incremental_overlay_matches_per_date_exact_combined():
    full, all_days = _combined_fixture()
    inc = IncrementalRiskOverlay(controller="combined")
    acc: dict[str, dict[int, float]] = {}
    saw_retired = False
    saw_readmit = False
    prev_b_retired = False
    for cutoff in [d + 1 for d in all_days]:
        newest = cutoff - 1
        for s, r in full.items():
            if newest in r:
                acc.setdefault(s, {})[newest] = r[newest]
        active = [s for s, r in acc.items() if r]
        w_ref, lev_ref, killed_ref = paper_risk_overlay(acc, active, controller="combined")
        w_inc, lev_inc, killed_inc = inc.overlay(acc, active)
        assert w_inc == w_ref, f"day {cutoff}: weights {w_inc} != {w_ref}"
        assert lev_inc == lev_ref, f"day {cutoff}: leverage {lev_inc!r} != {lev_ref!r}"
        assert killed_inc == killed_ref, f"day {cutoff}: killed {killed_inc} != {killed_ref}"
        b_retired = ("b" in active) and (w_ref.get("b", 0.0) == 0.0)
        if b_retired:
            saw_retired = True
        if prev_b_retired and not b_retired:
            saw_readmit = True
        prev_b_retired = b_retired
    # The fixture must exercise BOTH the retire and the re-admit branches of the gate.
    assert saw_retired, "combined fixture never retired the dying sleeve — retirement branch not exercised"
    assert saw_readmit, "combined fixture never re-admitted the recovered sleeve — re-admit not exercised"
