"""WALK-FORWARD validation of the deploy-book question: is a BROAD universe + spread-aware overlay an UPGRADE
over the current liquid-61 ($10M) Bybit book? READ-ONLY, SHADOW eval (never mutates the live book).

Step 3 (`spread_overlay`) hinted broad+overlay BEAT the live book on ONE OOS split with TRAIN-tuned k/λ — the
exact place overfitting hides. This module re-tests it with WALK-FORWARD folds + a capacity check, so the
verdict is UPGRADE / CAPACITY-TRADEOFF / OVERFIT / NO.

Tests (TDD):
  * walk-forward splits are correct — train strictly precedes test (no leakage), test blocks disjoint;
  * baseline (k=0, λ=0, full window) reconciles to the existing naive per-coin book (no engine drift);
  * capacity behaves — thinner coins → lower deployable capital; overlay downweighting a thin coin RAISES it;
  * the verdict reacts to economics (NOT hard-coded): broad wins across folds → UPGRADE; broad wins one fold
    only → OVERFIT; broad wins Sharpe but tanks capacity → CAPACITY-TRADEOFF; no lift → NO;
  * the driver runs on a seeded store with disjoint folds + a head-to-head per fold + an aggregate verdict;
  * READ-ONLY (WriteTripwire) + determinism + CLI smoke.
NO network — the store is in-memory SQLite; external inputs are injected.
"""
from __future__ import annotations

import math

from fxhnt.adapters.warehouse.timescale_feature_store import TimescaleFeatureStore
from fxhnt.application.broaden_overlay_validate import (
    _eval_window,
    book_capacity,
    broaden_overlay_validate,
    decide_verdict,
    walk_forward_folds,
)
from fxhnt.application.bybit_liquidity_sweep import per_coin_book_returns
from fxhnt.application.spread_overlay import per_coin_weight_panel

_DAY = 86_400


# --- fixtures (mirror the spread-overlay / liquidity-sweep fixtures) ------------------------------

def _ohlc_from_close(close: float, spread_frac: float, vol_frac: float, phase: float) -> tuple[float, float]:
    vol = vol_frac * (0.5 + 0.5 * math.cos(7.0 * phase))
    half = 0.5 * spread_frac + vol
    return close * (1.0 + half), close * (1.0 - half)


def _seed_carry(store: TimescaleFeatureStore, funding: dict[str, float], spread_by_sym: dict[str, float], *,
                days: int = 600) -> None:
    for idx, (sym, fund) in enumerate(funding.items()):
        sf = spread_by_sym[sym]
        rows = []
        for d in range(days):
            f = fund + 0.0003 * math.cos(0.5 * d + idx)
            px = 100.0
            high, low = _ohlc_from_close(px, sf, vol_frac=0.0005, phase=0.5 * d + idx)
            rows.append((d * _DAY, {"funding": f, "close": px, "spot_close": px, "high": high, "low": low}))
        store.write_features(sym, rows)


def _seed_turnover(store: TimescaleFeatureStore, dv: dict[str, float], *, days: int = 600) -> None:
    for sym, v in dv.items():
        store.write_features(sym, [(d * _DAY, {"turnover": v}) for d in range(days)])


def _seed_two_tier(store: TimescaleFeatureStore, *, days: int = 600) -> None:
    """A deep tier (tight spread, high $vol — clears $10M) + a thin tier (wide spread, low $vol — clears $1M
    but fails $10M). The broad universe pulls in the thin coins the overlay can help with."""
    deep = {"AAAUSDT": 0.001, "BBBUSDT": 0.001, "CCCUSDT": 0.001,
            "XXXUSDT": -0.001, "YYYUSDT": -0.001, "ZZZUSDT": -0.001}
    thin = {"PPPUSDT": 0.0012, "QQQUSDT": 0.0012, "RRRUSDT": 0.0012,
            "SSSUSDT": -0.0012, "TTTUSDT": -0.0012, "UUUUSDT": -0.0012}
    spread = {s: 0.0004 for s in deep}
    spread.update({s: 0.02 for s in thin})        # 200bp measured spread on the thin tier
    _seed_carry(store, {**deep, **thin}, spread, days=days)
    dv = {s: 100_000_000.0 for s in deep}
    dv.update({s: 2_000_000.0 for s in thin})     # thin coins clear $1M, fail $10M
    _seed_turnover(store, dv, days=days)


class _WriteTripwireStore:
    _FORBIDDEN = frozenset({
        "write_features", "write_features_bulk", "upsert_feature_rows", "upsert_membership",
        "replace_positions", "upsert_nav", "upsert_sleeve_ret", "replace_shadow_positions",
        "replace_trades", "_create_schema", "_bulk_upsert",
    })

    def __init__(self, inner: TimescaleFeatureStore) -> None:
        object.__setattr__(self, "_inner", inner)

    def __getattr__(self, name: str):
        if name in _WriteTripwireStore._FORBIDDEN:
            raise AssertionError(f"READ-ONLY violation: broaden-overlay-validate called write method {name!r}")
        return getattr(self._inner, name)


# --- walk-forward fold correctness (no leakage) --------------------------------------------------

def test_walk_forward_folds_no_leakage_and_disjoint() -> None:
    days = list(range(0, 100))
    folds = walk_forward_folds(days, n_folds=4)
    assert len(folds) == 4
    prev_test_hi = -1
    for f in folds:
        # train strictly precedes test (no leakage).
        assert f["train_hi"] < f["test_lo"], f
        assert f["test_lo"] <= f["test_hi"]
        # test blocks are disjoint and ordered (each begins after the previous one ended).
        assert f["test_lo"] > prev_test_hi, f
        prev_test_hi = f["test_hi"]


def test_walk_forward_folds_expands_train() -> None:
    """Expanding window: each fold's train_hi is >= the previous fold's (the train set grows)."""
    folds = walk_forward_folds(list(range(0, 120)), n_folds=3)
    train_his = [f["train_hi"] for f in folds]
    assert train_his == sorted(train_his)
    assert len(set(train_his)) == len(train_his)   # strictly growing


def test_walk_forward_folds_too_few_days_returns_empty() -> None:
    assert walk_forward_folds([1, 2, 3], n_folds=4) == []
    assert walk_forward_folds(list(range(20)), n_folds=0) == []


# --- baseline reconciliation ---------------------------------------------------------------------

def test_baseline_window_reconciles_to_naive_book() -> None:
    """k=0, λ=0 over the FULL window must reproduce the naive eq-wt per-coin book EXACTLY (no engine drift)."""
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    funding = {"AAAUSDT": 0.001, "BBBUSDT": 0.001, "XXXUSDT": -0.001, "YYYUSDT": -0.001}
    spread = {s: 0.0008 for s in funding}
    _seed_carry(store, funding, spread, days=200)
    _seed_turnover(store, {s: 50_000_000.0 for s in funding}, days=200)
    daily, cost, spr = per_coin_weight_panel(store, universe=None, sleeves=["xsfunding"], unlock_events=[])
    ev = _eval_window(daily, cost, spr, k=0.0, lam=0.0, day_lo=None, day_hi=None)
    pc = per_coin_book_returns(store, universe=None, sleeves=["xsfunding"], unlock_events=[])
    store.close()
    common = sorted(set(ev["net_series"]) & set(pc["net_series"]))
    assert common
    for d in common:
        assert abs(ev["net_series"][d] - pc["net_series"][d]) < 1e-12, (d, ev["net_series"][d],
                                                                        pc["net_series"][d])


# --- capacity behaviour --------------------------------------------------------------------------

def _hand_panel(weights: dict[str, float], n_days: int = 40) -> dict[int, dict[str, tuple[float, float]]]:
    """A constant-weight panel (per-unit return 0 — capacity is about weights/volume, not P&L)."""
    return {d: {c: (w, 0.0) for c, w in weights.items()} for d in range(n_days)}


def test_capacity_thinner_coins_lower_capital() -> None:
    """A book that must hold a THIN coin has lower deployable capital than one holding only a DEEP coin."""
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_turnover(store, {"DEEPUSDT": 100_000_000.0, "THINUSDT": 1_000_000.0}, days=40)
    deep_only = book_capacity(store, _hand_panel({"DEEPUSDT": 1.0}), {"DEEPUSDT": 0.0},
                              k=0.0, day_lo=0, day_hi=39 * _DAY)
    with_thin = book_capacity(store, _hand_panel({"DEEPUSDT": 0.5, "THINUSDT": 0.5}),
                              {"DEEPUSDT": 0.0, "THINUSDT": 0.0}, k=0.0, day_lo=0, day_hi=39 * _DAY)
    store.close()
    assert with_thin["bottleneck_capital"] < deep_only["bottleneck_capital"], (with_thin, deep_only)


def test_capacity_overlay_downweighting_raises_capital() -> None:
    """Overlay sizing (k>0) downweights the WIDE-spread thin coin → raises its absorbable bound → lifts the
    book's bottleneck deployable capital vs no overlay (k=0)."""
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_turnover(store, {"DEEPUSDT": 100_000_000.0, "THINUSDT": 1_000_000.0}, days=40)
    panel = _hand_panel({"DEEPUSDT": 0.5, "THINUSDT": 0.5})
    spread = {"DEEPUSDT": 1.0, "THINUSDT": 500.0}     # thin coin has a wide measured spread
    no_overlay = book_capacity(store, panel, spread, k=0.0, day_lo=0, day_hi=39 * _DAY)
    with_overlay = book_capacity(store, panel, spread, k=0.05, day_lo=0, day_hi=39 * _DAY)
    store.close()
    assert with_overlay["bottleneck_capital"] > no_overlay["bottleneck_capital"], (with_overlay, no_overlay)


def test_capacity_ratio_invariant_to_participation_frac() -> None:
    """The capacity RATIO (B/A) is invariant to participation_frac (it cancels), so the verdict does not depend
    on that assumption."""
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_turnover(store, {"DEEPUSDT": 100_000_000.0, "THINUSDT": 1_000_000.0}, days=40)
    panel = _hand_panel({"DEEPUSDT": 0.5, "THINUSDT": 0.5})
    spread = {"DEEPUSDT": 0.0, "THINUSDT": 0.0}
    a = book_capacity(store, panel, spread, k=0.0, day_lo=0, day_hi=39 * _DAY, participation_frac=0.05)
    b = book_capacity(store, panel, spread, k=0.0, day_lo=0, day_hi=39 * _DAY, participation_frac=0.20)
    store.close()
    assert b["bottleneck_capital"] == 4.0 * a["bottleneck_capital"] or abs(
        b["bottleneck_capital"] - 4.0 * a["bottleneck_capital"]) < 1e-6


# --- the verdict reacts to economics (NOT hard-coded) --------------------------------------------

def test_verdict_upgrade_when_broad_wins_across_folds() -> None:
    v = decide_verdict(mean_a=1.0, mean_b=1.4, win_frac=1.0, cap_a=1_000_000.0, cap_b=900_000.0)
    assert v["verdict"] == "UPGRADE", v


def test_verdict_overfit_when_broad_wins_only_one_fold() -> None:
    """A big win on a single fold (mean lift looks positive) but inconsistent across folds → OVERFIT, not
    UPGRADE — the single-split edge did not survive walk-forward."""
    v = decide_verdict(mean_a=1.0, mean_b=1.3, win_frac=0.25, cap_a=1_000_000.0, cap_b=1_000_000.0)
    assert v["verdict"] == "OVERFIT", v


def test_verdict_capacity_tradeoff_when_sharpe_wins_but_capacity_tanks() -> None:
    v = decide_verdict(mean_a=1.0, mean_b=1.4, win_frac=1.0, cap_a=1_000_000.0, cap_b=100_000.0)
    assert v["verdict"] == "CAPACITY-TRADEOFF", v


def test_verdict_no_when_no_lift() -> None:
    v = decide_verdict(mean_a=1.2, mean_b=1.1, win_frac=0.5, cap_a=1_000_000.0, cap_b=1_000_000.0)
    assert v["verdict"] == "NO", v


def test_verdict_overfit_when_lift_small_but_consistent() -> None:
    """A small (<0.20) but consistent lift still shrinks toward liquid-61 → OVERFIT (not a meaningful upgrade)."""
    v = decide_verdict(mean_a=1.0, mean_b=1.1, win_frac=1.0, cap_a=1_000_000.0, cap_b=1_000_000.0)
    assert v["verdict"] == "OVERFIT", v


# --- the driver (walk-forward head-to-head on a seeded store) ------------------------------------

def test_driver_runs_walk_forward_head_to_head() -> None:
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_two_tier(store)
    rep = broaden_overlay_validate(store, broad_bounds=(1_000_000.0,), n_folds=3,
                                   sleeves=["xsfunding"], unlock_events=[])
    store.close()
    assert rep["available"] is True
    assert rep["n_folds"] >= 2
    # disjoint test windows + train precedes test on every fold (no leakage).
    prev_hi = -1
    for f in rep["folds"]:
        assert f["train_hi"] < f["test_lo"]
        assert f["test_lo"] > prev_hi
        prev_hi = f["test_hi"]
        # both books reported per fold.
        assert math.isfinite(f["live"]["sharpe"])
        assert math.isfinite(f["broad"]["sharpe"])
        assert f["cap_live"] >= 0.0 and f["cap_broad"] >= 0.0
    agg = rep["aggregate"]
    assert math.isfinite(agg["mean_live_sharpe"]) and math.isfinite(agg["mean_broad_sharpe"])
    assert 0.0 <= agg["win_frac"] <= 1.0
    assert rep["verdict"]["verdict"] in ("UPGRADE", "CAPACITY-TRADEOFF", "OVERFIT", "NO")


def test_driver_is_read_only() -> None:
    inner = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_two_tier(inner)
    guarded = _WriteTripwireStore(inner)
    rep = broaden_overlay_validate(guarded, broad_bounds=(1_000_000.0,), n_folds=3,
                                   sleeves=["xsfunding"], unlock_events=[])
    inner.close()
    assert rep["available"] is True


def test_driver_is_deterministic() -> None:
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_two_tier(store)
    a = broaden_overlay_validate(store, broad_bounds=(1_000_000.0,), n_folds=3,
                                 sleeves=["xsfunding"], unlock_events=[])
    b = broaden_overlay_validate(store, broad_bounds=(1_000_000.0,), n_folds=3,
                                 sleeves=["xsfunding"], unlock_events=[])
    store.close()
    assert a["aggregate"] == b["aggregate"]
    assert a["verdict"] == b["verdict"]


def test_driver_empty_store_unavailable() -> None:
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    rep = broaden_overlay_validate(store, broad_bounds=(1_000_000.0,), n_folds=3,
                                   sleeves=["xsfunding"], unlock_events=[])
    store.close()
    assert rep["available"] is False


# --- CLI smoke -----------------------------------------------------------------------------------

def test_cli_broaden_overlay_validate_prints(monkeypatch) -> None:
    from typer.testing import CliRunner

    import fxhnt.cli as cli

    seeded = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_two_tier(seeded)

    monkeypatch.setattr(
        "fxhnt.adapters.warehouse.timescale_feature_store.TimescaleFeatureStore",
        lambda *a, **k: seeded, raising=False)

    result = CliRunner().invoke(
        cli.app, ["broaden-overlay-validate", "--min-dollar-vols", "1000000",
                  "--n-folds", "3", "--sleeves", "xsfunding"])
    seeded.close()

    assert result.exit_code == 0, result.output
    out = result.output.lower()
    assert "fold" in out
    assert "verdict" in out
    assert "capacity" in out
