"""RECONCILIATION HARNESS — the live Bybit paper book ≡ the reconstruction, on ONE shared fixture.

Two implementations of "the naive equal-weight 4-edge Bybit book" disagreed ~2.6x on real data
(reconstruction +761%/Sharpe 1.77 vs live paper book +294%/Sharpe 1.45 on the SAME 59 coins). This
harness root-caused the gap and now GUARDS the fix (they must reconcile):

  * LIVE paper book  — `bybit_paper_backfill.backfill_bybit_paper_book` → `paper_book.persist_paper_book`
    → `paper_nav`. Sizes per-symbol positions for the cockpit view; books the per-day book return.
  * RECONSTRUCTION   — `bybit_liquidity_sweep.per_coin_book_returns` (the cockpit Backtest /
    `compare_measured_cost._bybit_measured_net` basis). Combines each edge's TRUE per-symbol
    `(weight, return)` pairs (the reconciliation invariant `Σ wᵢ·rᵢ == sleeve GROSS`).

ROOT CAUSE (the author's "missing 1/n → 2.6x exposure" hypothesis was FALSIFIED):
  1. BOTH paths apply the SAME `1/n` and the SAME per-symbol weights — NOT exposure, NOT universe.
  2. The reconstruction is, by construction, the eq-wt book of the 4 edges' TRUE gross returns:
     `gross[d] == (1/n)·Σ_sleeve sleeve_gross[d]` (each edge's own convention: carry for xsfunding,
     short_leg_pnl for unlock, price for tstrend/positioning).
  3. The LIVE book USED to mark EVERY sleeve to RAW PERP PRICE (no funding/carry/hedge args) — DROPPING
     the carry sleeve's funding (the literal edge) and the unlock hedge, and lagging every sleeve a day.

THE FIX (this branch): the live book now books each sleeve's TRUE per-symbol return via
`persist_paper_book`'s return-defined mode (`return_by_sleeve_symbol`), CONTEMPORANEOUSLY on the day's
weights — so the daily book return == the reconstruction's `(1/n)·Σ sleeve_true_gross`. The live book and
the reconstruction now RECONCILE (test_live_book_reconciles_to_reconstruction), with the funding edge
booked (test_*_books_the_funding_edge). The convention is SPOT+PERP delta-neutral carry — exactly what the
edge definition, the Backtest, and the reconstruction already assume. NO network (in-memory store + repo).
"""
from __future__ import annotations

import datetime as dt
import math

from fxhnt.adapters.persistence.paper_repo import PaperRepo
from fxhnt.adapters.warehouse.timescale_feature_store import TimescaleFeatureStore
from fxhnt.application.bybit_book_eval import _DEFAULT_BYBIT_SLEEVES
from fxhnt.application.bybit_liquidity_sweep import per_coin_book_returns
from fxhnt.application.bybit_paper_backfill import (
    backfill_bybit_paper_book,
    combined_symbol_weights_by_day,
)
from fxhnt.application.bybit_paper_weights import (
    reconciliation_series,
    sleeve_weights_and_contributions,
)

_DAY = 86_400
_AT = dt.datetime(2026, 6, 27, 0, 0, tzinfo=dt.UTC)
_CAPITAL = 100_000.0
_N_SLEEVES = len(_DEFAULT_BYBIT_SLEEVES)   # 4


# --- fixtures ------------------------------------------------------------------------------------

def _seed_multi(store: TimescaleFeatureStore, *, days: int = 220) -> list[dict]:
    """BTC + 4 alts with close/funding/spot/long_ratio/high/low — enough for all 4 sleeves to fire.
    Mirrors the existing reconciliation/backfill seeds so the edges produce non-trivial weights."""
    import math as _m
    syms = ["BTCUSDT", "AAAUSDT", "BBBUSDT", "CCCUSDT", "DDDUSDT"]
    px = {s: 100.0 for s in syms}
    px["BTCUSDT"] = 50_000.0
    for d in range(days):
        crowd = (d % 3 == 0)
        for s in syms:
            drift = {"BTCUSDT": 0.001, "AAAUSDT": 0.002, "BBBUSDT": -0.001,
                     "CCCUSDT": 0.0015, "DDDUSDT": -0.0008}[s]
            wobble = 0.004 * _m.sin(d / 5.0 + hash(s) % 7)
            px[s] *= (1.0 + drift + wobble)
            funding = 0.0005 if s != "BTCUSDT" else 0.0001
            spot = px[s] * (1.0 + 0.0002)
            ratio = (0.70 if crowd else 0.30) if s == "AAAUSDT" else \
                    (0.30 if crowd else 0.70) if s == "BBBUSDT" else 0.50
            store.write_features(s, [(d * _DAY, {
                "close": px[s], "funding": funding, "spot_close": spot,
                "long_ratio": ratio, "high": px[s] * 1.001, "low": px[s] * 0.999})])
    return [{"sym": "AAA", "cliff_day": days - 5, "n": 5_000_000.0, "cat": "insiders"}]


def _seed_flat_funding(store: TimescaleFeatureStore, *, days: int = 120) -> None:
    """FLAT perp price, spot == perp (zero basis), nonzero cross-sectional funding. The carry edge's
    ONLY source of return here is FUNDING — so the funding-drop is detectable in isolation."""
    funding = {"AAAUSDT": 0.002, "BBBUSDT": 0.002, "CCCUSDT": 0.002,
               "XXXUSDT": -0.002, "YYYUSDT": -0.002, "ZZZUSDT": -0.002}
    for sym, f in funding.items():
        store.write_features(sym, [(d * _DAY, {
            "close": 100.0, "spot_close": 100.0, "funding": f,
            "high": 100.0, "low": 100.0}) for d in range(days)])


def _nav_daily_returns(navpts: list) -> dict[str, float]:
    out: dict[str, float] = {}
    prev = None
    for p in navpts:
        if prev not in (None, 0.0):
            out[p.run_date] = p.equity / prev - 1.0
        prev = p.equity
    return out


def _iso(day: int) -> str:
    return (dt.date(1970, 1, 1) + dt.timedelta(days=int(day))).isoformat()


def _cumulative(series) -> float:
    eq = 1.0
    for r in series:
        eq *= (1.0 + r)
    return eq - 1.0


# --- 1) BOTH paths apply the SAME 1/n + the SAME per-symbol weights -------------------------------

def test_both_paths_apply_equal_weight_one_over_n_on_identical_weights() -> None:
    """The author's hypothesis (the reconstruction omits the `1/n` and runs at ~2.6x gross exposure) is
    FALSE: the live path's combined per-coin weight on each day equals `(1/n)·Σ_sleeve engine_weight`,
    which is EXACTLY the per-coin weight the reconstruction combines (same source, same 1/n). So the
    divergence is NOT in normalization / gross exposure / universe."""
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    events = _seed_multi(store)
    # The live path's per-day per-sleeve per-symbol combined weights.
    live_combined = combined_symbol_weights_by_day(store, unlock_events=events)
    # The raw engine weights each path consumes.
    per_sleeve = {sl: sleeve_weights_and_contributions(store, sl, unlock_events=events)
                  for sl in _DEFAULT_BYBIT_SLEEVES}
    store.close()

    checked = 0
    for day, by_sleeve in live_combined.items():
        for sleeve, sym_w in by_sleeve.items():
            for sym, w in sym_w.items():
                # live combined weight == (1/n)·engine weight (the same 1/n the reconstruction applies).
                assert math.isclose(w, (1.0 / _N_SLEEVES) * per_sleeve[sleeve][day][sym][0],
                                    rel_tol=1e-12, abs_tol=1e-15)
                checked += 1
    assert checked > 0


# --- 2) the RECONSTRUCTION is the eq-wt book of the 4 edges' TRUE gross returns -------------------

def test_reconstruction_is_eqwt_book_of_true_edge_returns() -> None:
    """`per_coin_book_returns.gross[d] == (1/n)·Σ_sleeve sleeve_gross[d]` to float tolerance, where each
    `sleeve_gross` is the edge's TRUE return (the reconciliation invariant). i.e. the reconstruction is,
    by construction, the honest naive eq-wt book of the 4 edges — no look-ahead, no double-count."""
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    events = _seed_multi(store)
    pc = per_coin_book_returns(store, universe=None, unlock_events=events)
    gross = pc["gross_series"]
    # The four edges' TRUE gross daily return series (Σ w·r == sleeve gross, the invariant).
    sleeve_gross = {sl: reconciliation_series(
        sleeve_weights_and_contributions(store, sl, unlock_events=events))
        for sl in _DEFAULT_BYBIT_SLEEVES}
    store.close()

    assert gross
    for day, g in gross.items():
        expected = sum(sleeve_gross[sl].get(day, 0.0) for sl in _DEFAULT_BYBIT_SLEEVES) / _N_SLEEVES
        assert math.isclose(g, expected, rel_tol=1e-9, abs_tol=1e-12), (day, g, expected)


# --- 3) the LIVE book drops the funding edge (the proven root cause) ------------------------------

def test_live_book_books_the_funding_edge() -> None:
    """DECISIVE: flat perp price + nonzero funding ⇒ the carry edge's whole return IS the funding. BOTH the
    reconstruction AND the (fixed) live book now book it (mean daily ≈ the funding rate). The live book used
    to mark the carry sleeve to raw perp price and book ~0 — the funding-dropping bug — now repaired by the
    return-defined book (carry return incl. funding)."""
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_flat_funding(store)

    pc = per_coin_book_returns(store, universe=None, sleeves=["xsfunding"], unlock_events=[])
    recon = pc["gross_series"]
    recon_mean = sum(recon.values()) / len(recon)

    repo = PaperRepo("sqlite://")
    repo.migrate()
    # cost_rate=0 so the live GROSS book return is compared (the reconstruction gross has no cost).
    backfill_bybit_paper_book(repo, store, capital=_CAPITAL, at=_AT,
                              unlock_events=[], sleeves=("xsfunding",), cost_rate=0.0)
    live = _nav_daily_returns(repo.nav_history())
    store.close()

    live_mean = sum(live.values()) / len(live) if live else 0.0
    # Both capture the funding edge (~+0.002/day); the live book no longer drops it.
    assert recon_mean > 1.5e-3, recon_mean
    assert math.isclose(live_mean, recon_mean, rel_tol=1e-9, abs_tol=1e-12), (live_mean, recon_mean)


def test_live_book_reconciles_to_reconstruction() -> None:
    """THE HEADLINE GUARD: on the full 4-edge shared fixture, the live paper book's daily GROSS return ==
    the reconstruction's `(1/n)·Σ sleeve_true_gross` for EVERY day (to float tolerance). i.e. the live book
    and the cockpit Backtest are now the SAME book — no funding-drop, no hedge mis-mark, no 1-day lag.

    Compared GROSS (live backfilled with cost_rate=0) because the reconstruction's `gross_series` carries no
    cost; the live book's flat-5.5bp turnover haircut and the Backtest's measured per-coin spread are
    SEPARATE, legitimately-different cost models layered on the SAME gross."""
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    events = _seed_multi(store)
    recon = per_coin_book_returns(store, universe=None, unlock_events=events)["gross_series"]

    repo = PaperRepo("sqlite://")
    repo.migrate()
    backfill_bybit_paper_book(repo, store, capital=_CAPITAL, at=_AT, unlock_events=events, cost_rate=0.0)
    nav = repo.nav_history()
    store.close()

    # Live per-day GROSS return keyed by epoch day. The FIRST nav row is relative to the seed capital, so
    # nav[d1]/capital - 1 == recon[d1]; every later row compounds, so nav[d]/nav[d-1] - 1 == recon[d].
    live: dict[int, float] = {}
    prev_eq = _CAPITAL
    for p in nav:
        d = (dt.date.fromisoformat(p.run_date) - dt.date(1970, 1, 1)).days
        live[d] = p.equity / prev_eq - 1.0
        prev_eq = p.equity

    common = sorted(set(live) & set(recon))
    assert len(common) >= int(0.95 * len(recon)), (len(common), len(recon))
    for d in common:
        assert math.isclose(live[d], recon[d], rel_tol=1e-9, abs_tol=1e-12), (d, live[d], recon[d])
    # And the compounded curves tie out end-to-end (the actual NAV vs the reconstruction).
    assert math.isclose(_cumulative(live[d] for d in common),
                        _cumulative(recon[d] for d in common), rel_tol=1e-9), "cumulative mismatch"


# --- 4) the LIVE NET (real per-coin cost) == the cockpit Backtest NET -----------------------------

def _net_seq_from_equity(equity: list[float], capital: float) -> list[float]:
    """Per-day NET return sequence from a compounding equity curve seeded at `capital`."""
    out: list[float] = []
    prev = capital
    for e in equity:
        out.append(e / prev - 1.0)
        prev = e
    return out


def test_live_book_net_equals_backtest_net_with_real_spread() -> None:
    """THE COST GUARD: with the REAL per-coin measured cost (`real_spread_by_coin`), the live paper book's
    NET equity curve == `_bybit_measured_net`'s NET (the cockpit Backtest) day-for-day — BY CONSTRUCTION, not
    coincidence: the live book charges `cost_frac[d] = gross[d] − net[d]` from the SAME per-coin cost path
    (`per_coin_book_returns` + `recost_net_series` at the all-taker operating point, taker_fee + 0.5·real
    spread). So `/paper` NET == Backtest NET; the only prior gap (flat 5.5bp vs measured spread) is closed."""
    from fxhnt.application.compare_measured_cost import _bybit_measured_net

    real_spread = {"BTCUSDT": 1.0, "AAAUSDT": 10.0, "BBBUSDT": 20.0, "CCCUSDT": 5.0, "DDDUSDT": 15.0}

    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    events = _seed_multi(store)
    # Backtest NET (the cockpit number) — per_coin_book_returns re-costed with the real L1 spread.
    bt = _bybit_measured_net(store, capital=_CAPITAL, win_lo=None, win_hi=None, taker_fee_bps=5.5,
                             unlock_events=events, real_spread_by_coin=real_spread)
    bt_net = _net_seq_from_equity(bt["equity"], _CAPITAL)

    # Live book NET — same real per-coin cost charged in the backfill.
    repo = PaperRepo("sqlite://")
    repo.migrate()
    backfill_bybit_paper_book(repo, store, capital=_CAPITAL, at=_AT, unlock_events=events,
                              real_spread_by_coin=real_spread)
    live_net = _net_seq_from_equity([p.equity for p in repo.nav_history()], _CAPITAL)
    store.close()

    assert bt["used_real_spread"] is True
    assert len(live_net) == len(bt_net) > 2, (len(live_net), len(bt_net))
    for i, (a, b) in enumerate(zip(live_net, bt_net, strict=True)):
        assert math.isclose(a, b, rel_tol=1e-9, abs_tol=1e-12), (i, a, b)
    # End-to-end: /paper final NAV == the Backtest final equity (same start capital, same NET series).
    assert math.isclose(_CAPITAL * math.prod(1.0 + r for r in live_net),
                        _CAPITAL * math.prod(1.0 + r for r in bt_net), rel_tol=1e-9)
