"""Live FORWARD PAPER TRACK for the 4-edge Bybit book — the out-of-sample record.

Asserts (mirroring the existing live paper-track + bybit_book_eval conventions):
  * the snapshot computes the naive eq-wt 4-sleeve book NAV from a synthetic `bybit_features` store and
    recomputes it through the deterministic engine (DB anchor + recompute); idempotent (re-run same data →
    no dup, NAV updated in place);
  * the gate: reconciliation gate (~2wk min window), status WAIT while the forward record is still building;
  * the forward NAV read returns the `bybit_4edge` series from the cockpit forward_nav table;
  * read paths don't clobber the live Binance paper tables (separate track label `bybit_4edge`);
  * the incremental refresh calls the ingests in RESUME mode (only new days) — mocked adapters, no network.
NO network — the store is in-memory SQLite; external inputs are injected.
"""
from __future__ import annotations

import datetime as dt
import math

from fxhnt.adapters.orchestration.assets import (
    build_bybit_4edge_forward_nav,
    refresh_bybit_warehouse,
)
from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo
from fxhnt.adapters.warehouse.timescale_feature_store import TimescaleFeatureStore
from fxhnt.application.bybit_forward_track import (
    BybitFourEdgeStrategy,
    naive_eqwt_daily_returns,
)
from fxhnt.registry import STRATEGY_REGISTRY

_DAY = 86_400
_EPOCH = dt.date(1970, 1, 1)
_AT = dt.datetime(2026, 1, 1)


def _repo(tmp_path) -> ForwardNavRepo:  # noqa: ANN001
    repo = ForwardNavRepo(f"sqlite:///{tmp_path}/x.db")
    repo.migrate()
    return repo


def _inception_t0(store: TimescaleFeatureStore, **kwargs) -> str:  # noqa: ANN003
    """The strategy's OWN latest known date at inception time — passed as `today` on the first `run_track`
    call so the anchor's T0 matches what the old JSON `ForwardTracker` would have frozen as inception."""
    rows, _ = BybitFourEdgeStrategy(store, **kwargs).advance(None, {})
    return rows[-1][0]


def _seed_momentum(store: TimescaleFeatureStore, *, n_symbols: int = 8, days: int = 260,
                   drift: float = 0.01) -> None:
    for idx in range(n_symbols):
        sign = 1.0 if idx % 2 == 0 else -1.0
        px = 100.0
        rows = []
        for d in range(days):
            wobble = 0.003 * math.cos(0.4 * d + idx)
            px *= (1.0 + sign * drift + wobble)
            rows.append((d * _DAY, {"close": px}))
        store.write_features(f"SYM{idx:02d}USDT", rows)


def _seed_carry(store: TimescaleFeatureStore, *, days: int = 260) -> None:
    funding = {"AAAUSDT": 0.001, "BBBUSDT": 0.001, "CCCUSDT": 0.001,
               "XXXUSDT": -0.001, "YYYUSDT": -0.001, "ZZZUSDT": -0.001}
    for idx, (sym, fund) in enumerate(funding.items()):
        rows = []
        for d in range(days):
            f = fund + 0.0002 * math.cos(0.5 * d + idx)
            rows.append((d * _DAY, {"funding": f, "close": 100.0, "spot_close": 100.0}))
        store.write_features(sym, rows)


# --- naive eq-wt curve --------------------------------------------------------------------------

def test_bybit_4edge_backtest_summary_reproduces_the_forward_book_cagr() -> None:
    """The backtest reference the recon gate reconciles against = the naive eq-wt funding-net 4-edge book's
    full-history CAGR under strategy_id 'bybit_4edge'. It must be the SAME book series the forward track
    records, so the gate compares like-for-like (else it degrades to a bare 21-day timer)."""
    import numpy as np

    from fxhnt.application.bybit_forward_track import bybit_4edge_backtest_summary
    from fxhnt.domain.backtest import compute_stats

    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_momentum(store, n_symbols=8, days=260)
    _seed_carry(store, days=260)
    rows, _ = BybitFourEdgeStrategy(store, cost_bps=0.0).advance(None, {})   # the exact forward-book series
    expected = compute_stats(np.array([r for _, r in rows]), periods_per_year=365).cagr
    sm = bybit_4edge_backtest_summary(store, cost_bps=0.0)
    store.close()

    assert sm.strategy_id == "bybit_4edge"
    assert math.isfinite(sm.cagr) and sm.cagr != 0.0
    assert math.isclose(sm.cagr, expected, rel_tol=1e-9)
    assert math.isfinite(sm.sharpe) and math.isfinite(sm.max_drawdown)


def test_backtest_summary_from_rows_is_basis_of_the_rows_given() -> None:
    """`backtest_summary_from_rows` builds the gate reference from WHATEVER inception series it is handed — the
    per-track ref path uses this so a track's ref comes from its OWN strategy's series (correct basis), not the
    raw sleeve series. Metrics must match compute_stats on those exact rows; as_of = last row's date; a <2-row
    series returns the zeroed guard (so the gate withholds PASS rather than reconciling against noise)."""
    import numpy as np

    from fxhnt.application.bybit_forward_track import backtest_summary_from_rows
    from fxhnt.domain.backtest import compute_stats

    rows = [(f"2026-01-{i:02d}", r) for i, r in enumerate(
        [0.012, -0.004, 0.020, -0.011, 0.015, 0.003, -0.008, 0.017, 0.006, -0.002], start=1)]
    sm = backtest_summary_from_rows("crypto_tstrend", rows)
    expected_cagr = compute_stats(np.array([r for _, r in rows]), periods_per_year=365).cagr
    assert sm.strategy_id == "crypto_tstrend"
    assert sm.as_of == "2026-01-10"
    assert math.isclose(sm.cagr, expected_cagr, rel_tol=1e-9)
    assert math.isfinite(sm.sharpe) and math.isfinite(sm.max_drawdown)

    guard = backtest_summary_from_rows("crypto_tstrend", [("2026-01-01", 0.01)])
    assert guard.cagr == 0.0 and guard.passed is False and guard.as_of == ""


def test_backtest_summary_from_rows_matches_bybit_4edge_path() -> None:
    """The refactor is behaviour-preserving: `bybit_4edge_backtest_summary` == `backtest_summary_from_rows`
    fed the same book series (so the shared helper did not change the book ref the gate already relies on)."""
    from fxhnt.application.bybit_forward_track import backtest_summary_from_rows, bybit_4edge_backtest_summary

    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_momentum(store, n_symbols=8, days=260)
    _seed_carry(store, days=260)
    rows, _ = BybitFourEdgeStrategy(store, cost_bps=0.0).advance(None, {})
    via_helper = backtest_summary_from_rows("bybit_4edge", rows)
    via_fn = bybit_4edge_backtest_summary(store, cost_bps=0.0)
    store.close()
    assert math.isclose(via_helper.cagr, via_fn.cagr, rel_tol=1e-12)
    assert math.isclose(via_helper.sharpe, via_fn.sharpe, rel_tol=1e-12)
    assert via_helper.as_of == via_fn.as_of


def test_levered_shape_is_tail_budgeted_carry_40pct_of_gross() -> None:
    """The levered shape is TAIL-BUDGETED: carry's effective weight is ~40% of the 1.5x book gross — so a
    plausible -20% carry dislocation is bounded to ~-8% of the book — NOT the fat-tailed ~53% Sharpe-max
    weight. Probe: run each sleeve as a lone +1.0 day through the real combine; the booked value on that day
    IS that sleeve's effective weight."""
    from fxhnt.application.bybit_forward_track import _LEVERED_SLEEVE_SHAPE, MAX_BOOK_GROSS, levered_eqwt_daily_returns

    sleeves = list(_LEVERED_SLEEVE_SHAPE)
    rbs = {s: {i: (1.0 if s == t else 0.0) for i, t in enumerate(sleeves)} for s in sleeves}
    daily = dict(levered_eqwt_daily_returns(rbs, sleeves, _LEVERED_SLEEVE_SHAPE,
                                            max_gross=MAX_BOOK_GROSS, cost_bps=0.0))
    eff = {sleeves[i]: daily[i] for i in range(len(sleeves))}
    gross = sum(abs(v) for v in eff.values())
    assert abs(gross - MAX_BOOK_GROSS) < 1e-9            # the book gross hits the 1.5x cap
    assert abs(eff["xsfunding"] / gross - 0.40) < 0.02   # carry ~40% of gross (tail-budgeted, not ~53%)


def test_backtest_summary_reuses_precomputed_sleeve_returns_equivalently() -> None:
    """The compute-once optimization is behavior-neutral: passing precomputed `sleeve_returns` yields the
    IDENTICAL backtest summary as recomputing each sleeve from the store (same cagr/sharpe/maxDD). This lets
    the persist Job compute the 4 sleeves ONCE and reuse them for both the naive and levered references
    instead of recomputing the heavy backtest 3x."""
    from fxhnt.application.bybit_book_eval import _DEFAULT_BYBIT_SLEEVES, sleeve_returns_from_store
    from fxhnt.application.bybit_forward_track import _LEVERED_SLEEVE_SHAPE, bybit_4edge_backtest_summary

    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_momentum(store, n_symbols=8, days=260)
    _seed_carry(store, days=260)
    rbs = {s: sleeve_returns_from_store(store, s, cost_bps=0.0) for s in _DEFAULT_BYBIT_SLEEVES}
    recomputed = bybit_4edge_backtest_summary(store, cost_bps=0.0)
    reused = bybit_4edge_backtest_summary(store, cost_bps=0.0, sleeve_returns=rbs)
    rec_lev = bybit_4edge_backtest_summary(store, cost_bps=0.0, leverage_shape=_LEVERED_SLEEVE_SHAPE,
                                           strategy_id="bybit_4edge_levered")
    reu_lev = bybit_4edge_backtest_summary(store, cost_bps=0.0, leverage_shape=_LEVERED_SLEEVE_SHAPE,
                                           strategy_id="bybit_4edge_levered", sleeve_returns=rbs)
    store.close()
    assert reused.cagr == recomputed.cagr and reused.sharpe == recomputed.sharpe
    assert reused.max_drawdown == recomputed.max_drawdown
    assert reu_lev.cagr == rec_lev.cagr and reu_lev.strategy_id == "bybit_4edge_levered"


def test_backtest_summary_gives_recon_gate_a_bybit_4edge_reference() -> None:
    """After persisting the bybit_4edge backtest summary, the cockpit ref provider returns a REAL
    expected_window_return for it — so the recon gate's divergence band is ACTIVE, not the degraded
    no-reference path that PASSes on a bare 21-day timer."""
    from fxhnt.application.bybit_forward_track import bybit_4edge_backtest_summary
    from fxhnt.application.forward_ingest import CockpitBacktestRefProvider

    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_momentum(store, n_symbols=8, days=260)
    _seed_carry(store, days=260)
    sm = bybit_4edge_backtest_summary(store, cost_bps=0.0)
    store.close()

    repo = ForwardNavRepo("sqlite://")
    repo.migrate()
    repo.upsert_backtest_summary(sm, at=dt.datetime(2026, 7, 3))
    ref = CockpitBacktestRefProvider(repo).reference("bybit_4edge", 21)
    assert ref is not None, "no bybit_4edge reference -> recon gate silently degrades to a 21-day timer"
    assert ref.expected_window_return != 0.0


def test_naive_eqwt_daily_returns_is_equal_weight_blend() -> None:
    rbs = {"a": {0: 0.10, 1: 0.20}, "b": {0: 0.30, 1: -0.10}}
    daily = naive_eqwt_daily_returns(rbs, ["a", "b"], cost_bps=0.0)
    assert daily == [(0, 0.20), (1, 0.05)]   # (0.1+0.3)/2, (0.2-0.1)/2


def test_naive_eqwt_matches_bybit_book_eval_baseline() -> None:
    """The forward curve's per-day series must compound to the SAME naive_equal_weight summary the
    read-only bybit_book_eval reports (same engine, same eq-wt math)."""
    from fxhnt.application.bybit_book_eval import risk_managed_book_from_store

    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_momentum(store, n_symbols=8, days=260)
    _seed_carry(store, days=260)
    book = risk_managed_book_from_store(store, capital=100_000.0, cost_bps=0.0)

    rbs = {s: __import__("fxhnt.application.bybit_book_eval", fromlist=["sleeve_returns_from_store"])
           .sleeve_returns_from_store(store, s, cost_bps=0.0) for s in book["sleeves"]}
    store.close()
    daily = naive_eqwt_daily_returns(rbs, book["sleeves"], cost_bps=0.0)
    nav = 1.0
    for _d, r in daily:
        nav *= (1.0 + r)
    assert math.isclose(nav - 1.0, book["naive_equal_weight"]["total_return"], rel_tol=1e-9, abs_tol=1e-9)


# --- snapshot (ForwardTracker) NAV + idempotency ------------------------------------------------

def test_snapshot_freezes_t0_and_books_the_inception_day(tmp_path) -> None:
    """First run freezes T0 at the strategy's latest known date. Recompute mode books `[T0, today]`
    INCLUSIVE of T0 itself (the anchor's own day is part of the record) — unlike the retired JSON
    `ForwardTracker`, which froze T0 but booked strictly-AFTER days only (day 0 on first run). The real
    invariant preserved here is T0-freeze + a deterministic day count, not the old "0 days" number."""
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_momentum(store, n_symbols=8, days=120)
    _seed_carry(store, days=120)
    t0 = _inception_t0(store, cost_bps=0.0)
    repo = _repo(tmp_path)
    out = build_bybit_4edge_forward_nav(repo, store, today=t0, at=_AT, cost_bps=0.0)
    store.close()
    assert out["t0"] == t0 == out["last_date"]
    assert out["days"] == 1
    assert out["reinception"] is True


def test_snapshot_books_forward_days_and_is_idempotent(tmp_path) -> None:
    """Two stores: the 2nd has the SAME data plus extra forward days → recompute now covers more days;
    re-running on the identical 2nd store changes nothing (idempotent — no dup, updated in place). T0 stays
    frozen at the anchor set on the first run regardless of the `today` passed on later calls."""
    repo = _repo(tmp_path)

    early = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_momentum(early, n_symbols=8, days=120)
    _seed_carry(early, days=120)
    t0 = _inception_t0(early, cost_bps=0.0)
    out0 = build_bybit_4edge_forward_nav(repo, early, today=t0, at=_AT, cost_bps=0.0)   # freeze T0
    early.close()

    later = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_momentum(later, n_symbols=8, days=130)
    _seed_carry(later, days=130)
    out1 = build_bybit_4edge_forward_nav(repo, later, today=t0, at=_AT, cost_bps=0.0)
    assert out1["t0"] == t0 == out0["t0"]        # T0 never moves
    assert out1["reinception"] is False
    assert out1["days"] > out0["days"]           # the extra days got recomputed forward

    out2 = build_bybit_4edge_forward_nav(repo, later, today=t0, at=_AT, cost_bps=0.0)   # re-run, SAME data
    later.close()
    assert out2["days"] == out1["days"]          # idempotent: no extra rows
    assert out2["last_date"] == out1["last_date"]
    assert math.isclose(out2["total_return"], out1["total_return"], rel_tol=1e-12)


# --- gate: reconciliation gate (~2wk window), replacing the blind 60-day calendar wait ----------

def test_gate_is_reconciliation_and_waits_while_building(tmp_path) -> None:
    repo = _repo(tmp_path)
    early = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_momentum(early, n_symbols=8, days=120)
    _seed_carry(early, days=120)
    t0 = _inception_t0(early, cost_bps=0.0)
    build_bybit_4edge_forward_nav(repo, early, today=t0, at=_AT, cost_bps=0.0)
    early.close()
    later = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_momentum(later, n_symbols=8, days=130)
    _seed_carry(later, days=130)
    out = build_bybit_4edge_forward_nav(repo, later, today=t0, at=_AT, cost_bps=0.0)
    later.close()

    from fxhnt.application.forward_models import ForwardSummary
    from fxhnt.application.gate_policy import POLICY
    from fxhnt.application.gate_policy_resolve import resolve_policy
    from fxhnt.application.reconciliation_gate import evaluate_reconciliation_gate
    summary = ForwardSummary(strategy_id="bybit_4edge", as_of=out["last_date"], days=out["days"],
                             nav=out["nav"], total_return=out["total_return"], sharpe=out["sharpe"],
                             maxdd=out["maxdd"])
    rows = repo.nav_history("bybit_4edge")
    spec = STRATEGY_REGISTRY["bybit_4edge"]["gate_spec"]
    policy = resolve_policy(POLICY, spec)
    # the blind 60-day calendar wait is gone — this is a reconciliation gate with a ~2wk min window.
    assert spec["gate_type"] == "reconciliation"
    assert "min_days" not in spec
    # bybit_4edge uses a longer 21-day window (lumpy edge; median ~17d between spikes), not the 14-day default.
    assert policy.min_forward_days == 21
    # only ~10 forward days booked (120→130) → still building the OOS record → WAIT.
    verdict = evaluate_reconciliation_gate(summary, rows, None, policy)
    assert verdict.status == "WAIT" and "building" in verdict.reason.lower()
    # the nav rows carry per-date NAV (one row per booked forward date).
    assert rows and all(r.strategy_id == "bybit_4edge" for r in rows)


# --- cockpit forward_nav read returns the bybit_4edge series; no Binance clobber -----------------

def test_forward_nav_read_returns_bybit_4edge_series_without_clobbering_binance(tmp_path) -> None:
    """`run_track` now writes DIRECTLY into forward_nav (no JSON→DB ingest hop needed for this track); the
    invariant that matters is that recording bybit_4edge into the shared cockpit repo never touches a
    differently-labeled (Binance) track's rows."""
    repo = _repo(tmp_path)
    # Seed a Binance live track row FIRST, to prove the bybit_4edge run doesn't clobber it.
    from fxhnt.application.forward_models import ForwardNavRow as NavRowDTO
    repo.upsert_rows([NavRowDTO(strategy_id="xsfunding", date="2025-01-01", ret=0.0, nav=1.0)],
                     at=dt.datetime(2025, 1, 1))

    early = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_momentum(early, n_symbols=8, days=120)
    _seed_carry(early, days=120)
    t0 = _inception_t0(early, cost_bps=0.0)
    build_bybit_4edge_forward_nav(repo, early, today=t0, at=dt.datetime(2025, 1, 2), cost_bps=0.0)
    early.close()
    later = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_momentum(later, n_symbols=8, days=130)
    _seed_carry(later, days=130)
    build_bybit_4edge_forward_nav(repo, later, today=t0, at=dt.datetime(2025, 1, 3), cost_bps=0.0)
    later.close()

    series = repo.nav_history("bybit_4edge")
    assert series, "expected a bybit_4edge forward NAV series in the cockpit"
    assert all(r.strategy_id == "bybit_4edge" for r in series)
    # The Binance xsfunding live track is untouched (separate label).
    binance = repo.nav_history("xsfunding")
    assert any(r.date == "2025-01-01" and r.nav == 1.0 for r in binance)
    # bybit_4edge is in the registry-driven fleet (cockpit visibility).
    assert any(r.strategy_id == "bybit_4edge" for r in repo.registry())


# --- incremental refresh calls ingests in RESUME mode (only new days) ---------------------------

class _FakeFunding:
    def __init__(self) -> None:
        self.calls: list[str] = []

    def funding_history(self, symbol, *, start_ms=None):  # noqa: ANN001, ANN201
        self.calls.append(symbol)
        return {19000: 0.001, 19001: 0.001}


class _FakeKlines:
    def __init__(self) -> None:
        self.calls: list[tuple[str, str]] = []   # (symbol, category) — to distinguish perp vs spot fetches

    def daily_ohlcv(self, symbol, *, category, start_ms=None):  # noqa: ANN001, ANN201 — _BybitKlines shape
        self.calls.append((symbol, category))
        bar = {"close": 100.0, "high": 101.0, "low": 99.0, "turnover": 50_000_000.0}
        return {19000: dict(bar), 19001: dict(bar)}


class _FakeAccountRatio:
    def __init__(self) -> None:
        self.calls: list[str] = []

    def long_ratio_history(self, symbol, *, start_ms=None):  # noqa: ANN001, ANN201
        self.calls.append(symbol)
        return {19000: 0.5, 19001: 0.5}


def test_refresh_all_legs_date_resume_including_perp() -> None:
    """Uniform DATE-RESUME contract after the perp-leg migration (FIX 1): EVERY per-symbol leg — the PERP
    (linear) klines leg included — now resumes from its last persisted day instead of symbol-skipping a symbol
    that already carries `turnover`. So a done symbol is still fetched for ALL legs (from its last day) and
    advances every run; a brand-new symbol is also fetched (its floor is None → full back-fill)."""
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    # Pre-seed DONE (perp turnover + funding + long_ratio) but NO spot_close.
    store.write_features("BTCUSDT", [(19000 * _DAY, {"close": 100.0, "turnover": 9e9, "funding": 0.001,
                                                     "long_ratio": 0.5})])

    funding, klines, ratio = _FakeFunding(), _FakeKlines(), _FakeAccountRatio()
    universe = ["BTCUSDT", "ETHUSDT"]
    summary = refresh_bybit_warehouse(
        store, funding=funding, klines=klines, account_ratio=ratio,
        universe=universe, min_dollar_vol=1_000_000.0)
    store.close()

    # PERP leg now DATE-RESUMES → the already-done BTCUSDT IS re-fetched (advance, not skip); ETHUSDT too.
    assert ("BTCUSDT", "linear") in klines.calls
    assert ("ETHUSDT", "linear") in klines.calls
    # spot + funding DATE-RESUME → the done symbol IS fetched for those legs (advance, not skip).
    assert ("BTCUSDT", "spot") in klines.calls
    assert "BTCUSDT" in funding.calls
    assert "ETHUSDT" in funding.calls
    assert summary["klines"]["symbols"] >= 1


def test_refresh_advances_spot_leg_for_already_perp_done_symbol() -> None:
    """NIGHTLY freeze fix: the spot leg (spot_close/high/low) advances from its last persisted date EVEN for a
    symbol whose perp leg is already done (carries turnover). Before the dedicated spot refresh, the symbol-skip
    resume on 'turnover' left the spot leg frozen at the last manual ingest → the carry sleeve + Bybit forward
    gate stalled."""
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    # Perp leg done THROUGH 19001; spot leg only THROUGH 19000 (the freeze: spot lags perp).
    store.write_features("BTCUSDT", [
        (19000 * _DAY, {"close": 100.0, "turnover": 9e9, "funding": 0.001, "long_ratio": 0.5,
                        "spot_close": 99.0, "spot_high": 99.5, "spot_low": 98.5}),
        (19001 * _DAY, {"close": 101.0, "turnover": 9e9, "funding": 0.001, "long_ratio": 0.5}),
    ])

    funding, klines, ratio = _FakeFunding(), _FakeKlines(), _FakeAccountRatio()
    summary = refresh_bybit_warehouse(
        store, funding=funding, klines=klines, account_ratio=ratio,
        universe=["BTCUSDT"], min_dollar_vol=1_000_000.0)
    spot = store.crypto_spot_panel()
    store.close()

    # _FakeKlines returns spot bars for 19000 AND 19001 → the spot leg now reaches 19001 (advanced past 19000).
    assert 19000 in spot["BTCUSDT"] and 19001 in spot["BTCUSDT"], "spot leg must advance in the nightly refresh"
    assert ("BTCUSDT", "spot") in klines.calls
    assert summary["spot"]["symbols"] >= 1


def test_refresh_advances_perp_leg_for_already_perp_done_symbol() -> None:
    """NIGHTLY freeze ROOT-CAUSE fix (FIX 1): the PERP leg (close/turnover/high/low) advances from its last
    persisted day EVEN for a symbol that already carries `turnover`. The old symbol-skip resume on `turnover`
    froze close/turnover at the last manual ingest for EVERY existing symbol (only brand-new symbols advanced)
    → the whole 4-edge book's cross-section (all sleeves mark on `close`) + the Bybit forward gate stalled."""
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    # Perp leg present only THROUGH 19000 (the freeze) — new days 19001+ must be re-fetched despite `turnover`.
    store.write_features("BTCUSDT", [
        (19000 * _DAY, {"close": 100.0, "turnover": 9e9, "high": 101.0, "low": 99.0}),
    ])

    funding, klines, ratio = _FakeFunding(), _FakeKlines(), _FakeAccountRatio()
    summary = refresh_bybit_warehouse(
        store, funding=funding, klines=klines, account_ratio=ratio,
        universe=["BTCUSDT"], min_dollar_vol=1_000_000.0)
    close_panel = store.crypto_close_panel()
    store.close()

    # _FakeKlines returns perp bars for 19000 AND 19001 → the perp close reaches 19001 (advanced past 19000).
    assert 19000 in close_panel["BTCUSDT"] and 19001 in close_panel["BTCUSDT"], \
        "perp close leg must advance in the nightly refresh"
    assert ("BTCUSDT", "linear") in klines.calls, "the already-done symbol must be RE-FETCHED (date-resume)"
    assert summary["klines"]["symbols"] >= 1


def test_refresh_logs_loud_error_when_close_cross_section_is_stale() -> None:
    """Data-freshness guard (FIX 2): when the `close` cross-section's freshest healthy day lags `today` by more
    than the tolerance, the refresh emits a LOUD error (the data-layer analog of the tracker's re-inception
    guard) so a future ingest stall is VISIBLE, not a silent book freeze. It does NOT hard-crash the run."""
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    funding, klines, ratio = _FakeFunding(), _FakeKlines(), _FakeAccountRatio()
    errors: list[str] = []
    # _FakeKlines lands close through day 19001; today=19010 → 9 days behind (> 2-day tolerance) → loud error.
    summary = refresh_bybit_warehouse(
        store, funding=funding, klines=klines, account_ratio=ratio,
        universe=["BTCUSDT"], min_dollar_vol=1_000_000.0,
        today_day=19_010, log_error=errors.append)
    store.close()

    assert errors, "a stale close cross-section must emit a loud error"
    assert "stale" in errors[0].lower() and "19001" in errors[0]
    assert summary["klines"]["symbols"] >= 1   # run still completes (no hard crash)


def test_refresh_no_error_when_close_cross_section_is_fresh() -> None:
    """The freshness guard is SILENT when the close cross-section is within tolerance (no false alarms)."""
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    funding, klines, ratio = _FakeFunding(), _FakeKlines(), _FakeAccountRatio()
    errors: list[str] = []
    # close lands through day 19001; today=19002 → 1 day behind (<= 2-day tolerance) → no error.
    refresh_bybit_warehouse(
        store, funding=funding, klines=klines, account_ratio=ratio,
        universe=["BTCUSDT"], min_dollar_vol=1_000_000.0,
        today_day=19_002, log_error=errors.append)
    store.close()

    assert errors == []


# --- strategy advance is pure + extends forward only --------------------------------------------

def test_strategy_advance_books_only_after_last_date() -> None:
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_momentum(store, n_symbols=8, days=130)
    _seed_carry(store, days=130)
    strat = BybitFourEdgeStrategy(store, cost_bps=0.0)
    rows, _extra = strat.advance(last_date=None, extra={})
    store.close()
    assert rows
    dates = [d for d, _ in rows]
    assert dates == sorted(dates)            # chronological
    assert len(set(dates)) == len(dates)     # one row per date


# --- strategy with leverage_shape parameter: levered vs naive book selection ----------------------

def test_bybit_four_edge_strategy_levered_differs_from_naive() -> None:
    """With a leverage_shape the strategy books the levered book (carry-weighted, gross-capped); without it,
    the naive book — same days, different returns."""
    from fxhnt.application.bybit_forward_track import _LEVERED_SLEEVE_SHAPE, BybitFourEdgeStrategy
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_momentum(store, n_symbols=8, days=260)
    _seed_carry(store, days=260)
    naive, _ = BybitFourEdgeStrategy(store, cost_bps=0.0).advance(None, {})
    lev, _ = BybitFourEdgeStrategy(store, cost_bps=0.0, leverage_shape=_LEVERED_SLEEVE_SHAPE).advance(None, {})
    store.close()
    assert [d for d, _ in lev] == [d for d, _ in naive]          # same day spine
    assert [r for _, r in lev] != [r for _, r in naive]          # different (levered) returns


# --- levered eq-wt combine (gross-capped levered sleeve weighting) --------------------------------

def test_levered_eqwt_uniform_shape_equals_naive() -> None:
    """With a uniform shape (all 1.0) and max_gross >= 1, the levered combine reduces EXACTLY to the naive
    equal-weight book — same weights, same gross 1.0."""
    from fxhnt.application.bybit_forward_track import levered_eqwt_daily_returns, naive_eqwt_daily_returns
    rbs = {"a": {1: 0.01, 2: -0.02, 3: 0.03}, "b": {1: 0.00, 2: 0.04, 3: -0.01}}
    naive = naive_eqwt_daily_returns(rbs, ["a", "b"], cost_bps=0.0)
    lev = levered_eqwt_daily_returns(rbs, ["a", "b"], {"a": 1.0, "b": 1.0}, max_gross=1.5, cost_bps=0.0)
    assert lev == naive


def test_levered_eqwt_carry_weighted_and_gross_capped() -> None:
    """A carry-heavy shape puts more weight on the high-shape sleeve, and the book gross is capped at
    max_gross (the summed absolute weight never exceeds it)."""
    from fxhnt.application.bybit_forward_track import levered_eqwt_daily_returns
    rbs = {"trend": {1: 0.02, 2: 0.02}, "carry": {1: 0.02, 2: 0.02}}
    lev = levered_eqwt_daily_returns(rbs, ["trend", "carry"], {"trend": 1.0, "carry": 5.0},
                                     max_gross=1.5, cost_bps=0.0)
    # both sleeves move +2%; shape 1:5 -> carry weight 5x trend; scaled so gross = 1.5.
    # weights: trend=1/2, carry=5/2 -> gross=3.0 -> scale 0.5 -> trend=0.25, carry=1.25 (gross 1.5)
    # day return = 0.25*0.02 + 1.25*0.02 = 0.03
    assert len(lev) == 2
    assert abs(lev[0][1] - 0.03) < 1e-12
    assert abs(lev[1][1] - 0.03) < 1e-12


def test_levered_backtest_summary_uses_levered_strategy_id_and_differs() -> None:
    """The levered backtest reference carries strategy_id 'bybit_4edge_levered' and a DIFFERENT cagr from the
    naive one (leverage scales the book), so the levered recon gate reconciles against the levered backtest."""
    from fxhnt.application.bybit_forward_track import _LEVERED_SLEEVE_SHAPE, bybit_4edge_backtest_summary
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_momentum(store, n_symbols=8, days=260)
    _seed_carry(store, days=260)
    naive = bybit_4edge_backtest_summary(store, cost_bps=0.0)
    lev = bybit_4edge_backtest_summary(store, cost_bps=0.0, leverage_shape=_LEVERED_SLEEVE_SHAPE,
                                       strategy_id="bybit_4edge_levered")
    store.close()
    assert lev.strategy_id == "bybit_4edge_levered"
    assert lev.cagr != naive.cagr


# --- nightly builder: build_bybit_4edge_levered_forward_nav (levered shadow, own strategy_id) ----

def test_build_bybit_4edge_levered_forward_nav_books_the_levered_book(tmp_path) -> None:
    """The levered forward builder freezes T0 and records the levered (not naive) book under its OWN
    strategy_id `bybit_4edge_levered` in the DB (separate anchor + nav series from `bybit_4edge`)."""
    from fxhnt.adapters.orchestration.assets import build_bybit_4edge_levered_forward_nav
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_momentum(store, n_symbols=8, days=130)
    _seed_carry(store, days=130)
    t0 = _inception_t0(store, cost_bps=0.0)
    repo = _repo(tmp_path)
    out = build_bybit_4edge_levered_forward_nav(repo, store, today=t0, at=_AT, cost_bps=0.0)
    store.close()
    assert out["t0"] is not None
    assert repo.active_anchor("bybit_4edge_levered") is not None
    assert repo.nav_history("bybit_4edge_levered")


# --- registry: bybit_4edge_levered shadow track registration -------------------------------------------------------

def test_registry_has_bybit_4edge_levered_shadow() -> None:
    """The levered shadow track is registered in STRATEGY_REGISTRY so the nightly forward track + reconciliation
    gate pick it up automatically (OBSERVE-ONLY: records what the levered book WOULD do, validated OOS before
    capital rides it)."""
    from fxhnt.registry import STRATEGY_REGISTRY
    e = STRATEGY_REGISTRY["bybit_4edge_levered"]
    assert e["venue"] == "bybit-perp"
    assert e["gate_spec"] == {"gate_type": "reconciliation", "min_forward_days": 21}


# --- levered backtest reference (the persist Job wires it in) ----------------------------------------

def test_levered_backtest_ref_activates_the_levered_gate() -> None:
    """Persisting the levered summary gives the cockpit ref provider a real reference for
    'bybit_4edge_levered' — the levered gate's band is active, not the degraded no-reference timer."""
    from fxhnt.application.bybit_forward_track import _LEVERED_SLEEVE_SHAPE, bybit_4edge_backtest_summary
    from fxhnt.application.forward_ingest import CockpitBacktestRefProvider
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_momentum(store, n_symbols=8, days=260)
    _seed_carry(store, days=260)
    sm = bybit_4edge_backtest_summary(store, cost_bps=0.0, leverage_shape=_LEVERED_SLEEVE_SHAPE,
                                      strategy_id="bybit_4edge_levered")
    store.close()
    repo = ForwardNavRepo("sqlite://")
    repo.migrate()
    repo.upsert_backtest_summary(sm, at=dt.datetime(2026, 7, 3))
    ref = CockpitBacktestRefProvider(repo).reference("bybit_4edge_levered", 21)
    assert ref is not None and ref.expected_window_return != 0.0
