"""The cockpit Backtest charges a per-COIN MEASURED cost on the Bybit single book by DEFAULT (/paper/sim).

    cost_bps(coin) = taker_fee_bps + 0.5 · effective_spread_bps(coin)     (NO assumed market impact)

The heavy per-coin compute runs in the own-memory `fxhnt compare-measured-precompute` Job (it OOM-killed the
4Gi dashboard live); the cockpit ONLY reads the small precomputed per-book artifacts and falls back to the
flat slider when absent (never a 500).

(Phase 0b venue consolidation, Task 7b: the Binance combined-book leg + the Binance-vs-Bybit COMPARE mode
this file used to also test were retired — Bybit is fxhnt's sole crypto venue. The surviving tests below are
unchanged in substance, just Bybit-only.)

Tests (TDD):
  * `_bybit_measured_net` — thin-spread fixture → cost drag near the fee floor; wide-spread → much higher;
    deterministic;
  * precompute persists the single-book measured curves (naive + levered); round-trips (cached == direct
    compute on a fixture);
  * Backtest measured DEFAULT — bybit_4edge measured = its real costed number; fallback to flat when the
    cache is absent (no 500).
NO network — in-memory SQLite repos + feature stores, seeded directly.
"""
from __future__ import annotations

import datetime as dt
import math
import re

from fastapi.testclient import TestClient

from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo
from fxhnt.adapters.persistence.paper_repo import PaperRepo
from fxhnt.adapters.warehouse.timescale_feature_store import TimescaleFeatureStore
from fxhnt.adapters.web.app import create_app
from fxhnt.application.compare_measured_cost import (
    SINGLE_CACHE_KEYS,
    _bybit_measured_net,
    precompute_single_book_measured,
    read_cached_single_measured,
)

_AT = dt.datetime(2026, 6, 24, tzinfo=dt.UTC)
_DAY = 86_400


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

def _ohlc(close: float, spread_frac: float, phase: float) -> tuple[float, float]:
    """(high, low) bracketing `close` with a KNOWN proportional half-spread + a decorrelated wobble so the
    Corwin–Schultz estimator (which cancels volatility across independent days) recovers the spread."""
    vol = 0.0005 * (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_bybit_features(store: TimescaleFeatureStore, *, days: int = 200,
                         spread_frac: float = 0.0005) -> list[str]:
    """Carry (xsfunding) coins on bybit_features with a KNOWN measured spread. A NEAR-CONSTANT close (px=100)
    is used on purpose so the Corwin–Schultz estimator (which cancels day-to-day vol) recovers the injected
    spread cleanly; the carry sleeve trades off the funding cross-section, which day-to-day creates turnover
    so the per-coin measured cost is actually charged. Mirrors the bybit liquidity-sweep carry fixture."""
    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.0005 * math.cos(0.5 * d + idx)
            high, low = _ohlc(100.0, spread_frac, phase=0.5 * d + idx)
            rows.append((d * _DAY, {"funding": f, "close": 100.0, "spot_close": 100.0,
                                    "high": high, "low": low, "turnover": 50_000_000.0}))
        store.write_features(sym, rows)
    return list(funding)


def _seed_bybit_sleeve_ret(repo: PaperRepo, *, start: dt.date, days: int, at: dt.datetime) -> None:
    sleeves = {"crypto_tstrend": 0.004, "unlock": 0.0025, "xsfunding": 0.001, "positioning": 0.0018}
    for i in range(days):
        d = (start + dt.timedelta(days=i)).isoformat()
        for sleeve, amp in sleeves.items():
            repo.upsert_bybit_sleeve_ret(d, sleeve, amp * (1.0 if (i + hash(sleeve)) % 3 else -0.8), at=at)


def _bybit_store(spread_frac: float) -> TimescaleFeatureStore:
    s = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_bybit_features(s, spread_frac=spread_frac)
    return s


# --- 1) _bybit_measured_net: thin vs wide measured spread ----------------------------------------

def test_bybit_measured_net_thin_spread_near_fee() -> None:
    """A deep (thin-spread) Bybit universe → the per-coin measured cost drag sits near the published taker
    fee floor (fee + half a tiny measured spread)."""
    store = _bybit_store(spread_frac=0.0004)
    view = _bybit_measured_net(store, capital=100_000.0, win_lo=None, win_hi=None,
                               taker_fee_bps=5.5, sleeves=["xsfunding"], unlock_events=[])
    store.close()
    assert view["dates"], view
    assert view["cost_drag_bps"] < 15.0, view["cost_drag_bps"]   # ~fee + a few bp


def test_bybit_measured_net_wide_spread_much_higher() -> None:
    """A thin (wide-spread) Bybit universe → the measured cost drag is FAR above the fee floor (half the wide
    Corwin–Schultz spread dominates), strictly more than the deep universe's."""
    deep = _bybit_store(spread_frac=0.0004)
    wide = _bybit_store(spread_frac=0.02)   # 200 bps measured spread
    deep_view = _bybit_measured_net(deep, capital=100_000.0, win_lo=None, win_hi=None,
                                    taker_fee_bps=5.5, sleeves=["xsfunding"], unlock_events=[])
    wide_view = _bybit_measured_net(wide, capital=100_000.0, win_lo=None, win_hi=None,
                                    taker_fee_bps=5.5, sleeves=["xsfunding"], unlock_events=[])
    deep.close()
    wide.close()
    assert wide_view["cost_drag_bps"] > 50.0, wide_view["cost_drag_bps"]
    assert wide_view["cost_drag_bps"] > deep_view["cost_drag_bps"] * 3, (
        wide_view["cost_drag_bps"], deep_view["cost_drag_bps"])


def test_bybit_measured_net_is_deterministic() -> None:
    store = _bybit_store(spread_frac=0.01)
    a = _bybit_measured_net(store, capital=100_000.0, win_lo=None, win_hi=None,
                            taker_fee_bps=5.5, sleeves=["xsfunding"], unlock_events=[])
    b = _bybit_measured_net(store, capital=100_000.0, win_lo=None, win_hi=None,
                            taker_fee_bps=5.5, sleeves=["xsfunding"], unlock_events=[])
    store.close()
    assert a["equity"] == b["equity"]
    assert a["cost_drag_bps"] == b["cost_drag_bps"]


def test_bybit_measured_dates_render_as_real_years_not_0057() -> None:
    """Regression: `per_coin_book_returns` keys days by UNIX epoch-day (~20627 for 2026), but the cockpit
    renders dates via `date.fromordinal` — so `_bybit_measured_net` MUST emit proleptic ORDINALS, else 2026
    shows as year 0057 (the ~1969-year Unix-epoch shift). Seed REALISTIC 2026 epoch-days and assert the
    rendered years are {2026}; without the conversion they'd be {56, 57}."""
    base = (dt.date(2026, 1, 1) - dt.date(1970, 1, 1)).days   # Unix epoch-day of 2026-01-01 (~20454)
    s = TimescaleFeatureStore("sqlite://", table="bybit_features")
    for idx, (sym, fund) in enumerate({"AAAUSDT": 0.001, "XXXUSDT": -0.001}.items()):
        rows = []
        for k in range(120):                                  # 2026-01-01 .. ~2026-04-30 (all in 2026)
            d = base + k
            high, low = _ohlc(100.0, 0.01, phase=0.5 * k + idx)
            rows.append((d * _DAY, {"funding": fund + 0.0005 * math.cos(0.5 * k + idx), "close": 100.0,
                                    "spot_close": 100.0, "high": high, "low": low, "turnover": 50_000_000.0}))
        s.write_features(sym, rows)
    view = _bybit_measured_net(s, capital=100_000.0, win_lo=None, win_hi=None,
                               taker_fee_bps=5.5, sleeves=["xsfunding"], unlock_events=[])
    s.close()
    assert view["dates"], view
    years = {dt.date.fromordinal(d).year for d in view["dates"]}   # the render path uses fromordinal
    assert years == {2026}, years                                 # NOT {56, 57} — the 0057 bug


# --- 2) precompute persists the single-book measured curves + round-trips ------------------------

def _seeded_all(spread_frac: float = 0.02) -> tuple[PaperRepo, TimescaleFeatureStore]:
    repo = PaperRepo("sqlite://")
    repo.migrate()
    bybit = _bybit_store(spread_frac=spread_frac)
    _seed_bybit_sleeve_ret(repo, start=dt.date(2021, 3, 1), days=120, at=_AT)
    return repo, bybit


def test_precompute_persists_single_book_curves() -> None:
    repo, bybit = _seeded_all()
    out = precompute_single_book_measured(repo, bybit, capital=100_000.0, at=_AT, unlock_events=[])
    bybit.close()
    assert set(out) == {"bybit_4edge", "bybit_4edge_levered"}
    payload = read_cached_single_measured(repo, "bybit_4edge")
    assert payload is not None
    assert payload["book"] == "bybit_4edge"
    assert payload["dates_iso"] and payload["equity"]
    assert len(payload["equity"]) == len(payload["dates_iso"]) == len(payload["leverage"])
    assert SINGLE_CACHE_KEYS["bybit_4edge"] != SINGLE_CACHE_KEYS["bybit_4edge_levered"]


def test_single_book_payload_round_trips_direct_compute() -> None:
    """The cached single-book metrics == computing `_bybit_measured_net` directly on the same fixture (the
    precompute is a faithful cache, no drift)."""
    repo, bybit = _seeded_all()
    by_direct = _bybit_measured_net(bybit, capital=100_000.0, win_lo=None, win_hi=None,
                                    taker_fee_bps=5.5, unlock_events=[])
    precompute_single_book_measured(repo, bybit, capital=100_000.0, at=_AT, unlock_events=[])
    bybit.close()

    by_cached = read_cached_single_measured(repo, "bybit_4edge")
    assert by_cached["equity"] == by_direct["equity"]
    for k in ("sharpe", "cagr", "total_return", "max_dd", "end_value"):
        assert by_cached["metrics"][k] == by_direct["metrics"][k], k
    assert by_cached["cost_drag_bps"] == by_direct["cost_drag_bps"]


def test_precompute_single_book_is_idempotent() -> None:
    repo, bybit = _seeded_all()
    p1 = precompute_single_book_measured(repo, bybit, capital=100_000.0, at=_AT, unlock_events=[])
    p2 = precompute_single_book_measured(repo, bybit, capital=100_000.0, at=_AT, unlock_events=[])
    bybit.close()
    assert p1["bybit_4edge"]["equity"] == p2["bybit_4edge"]["equity"]
    assert read_cached_single_measured(repo, "bybit_4edge")["equity"] == p1["bybit_4edge"]["equity"]


def test_precompute_also_writes_the_levered_book_measured_curve() -> None:
    """The precompute ALSO caches the LEVERED shadow book at the SAME measured cost model — a distinct
    'bybit_4edge_levered' entry (carry-weighted, gross-capped) whose curve differs from the naive book. This is
    how the cockpit Backtest can show the levered curve at the honest measured cost (never flat)."""
    from fxhnt.application.bybit_forward_track import _LEVERED_SLEEVE_SHAPE, MAX_BOOK_GROSS

    repo, bybit = _seeded_all()
    lev_direct = _bybit_measured_net(bybit, capital=100_000.0, win_lo=None, win_hi=None, taker_fee_bps=5.5,
                                     unlock_events=[], leverage_shape=_LEVERED_SLEEVE_SHAPE,
                                     max_gross=MAX_BOOK_GROSS)
    out = precompute_single_book_measured(repo, bybit, capital=100_000.0, at=_AT, unlock_events=[])
    bybit.close()

    lev_cached = read_cached_single_measured(repo, "bybit_4edge_levered")
    assert lev_cached is not None
    assert lev_cached["equity"] == lev_direct["equity"]
    assert "bybit_4edge_levered" in out
    assert lev_cached["equity"] != out["bybit_4edge"]["equity"]   # distinct from the naive book


# --- 3) the Backtest measured default / flat what-if / fallback ----------------------------------

def _client(repo: PaperRepo) -> TestClient:
    fwd = ForwardNavRepo("sqlite://")
    fwd.migrate()
    return TestClient(create_app(fwd, paper_repo=repo))


# --- synthetic measured-cache seeder (the honest curve the Backtest reads) ------------------------
# The Backtest page reads ONLY the precomputed per-coin measured curve. For the UI tests (capital-scale,
# period-window, no-controls) we seed a small deterministic payload directly — exactly the shape
# `precompute_single_book_measured` persists — so we don't run the heavy per-coin compute on the request path.

def _seed_measured_cache(repo: PaperRepo, book: str, *, start: dt.date = dt.date(2022, 1, 1),
                         days: int = 400, capital: float = 100_000.0) -> dict:
    """Persist a synthetic single-book MEASURED-cost payload (the cockpit's honest curve) with a known,
    multi-year, drawdown-bearing equity path so window/scale assertions are exact."""
    from fxhnt.application.compare_measured_cost import SINGLE_CACHE_KEYS
    from fxhnt.application.paper_sim import _metrics

    iso, equity, drawdown, net_rets, epoch = [], [], [], [], []
    eq, peak = capital, capital
    for i in range(days):
        d = start + dt.timedelta(days=i)
        # A deterministic oscillation that compounds up overall but has a deep local dip mid-series.
        r = 0.004 if (i % 5) else -0.05
        eq *= (1.0 + r)
        peak = max(peak, eq)
        iso.append(d.isoformat())
        equity.append(eq)
        net_rets.append(r)
        drawdown.append((eq - peak) / peak if peak > 0 else 0.0)
        epoch.append((d - dt.date(1970, 1, 1)).days)
    metrics = _metrics(epoch, equity, net_rets, drawdown, capital)
    payload = {
        "book": book, "dates_iso": iso, "equity": equity, "drawdown": drawdown,
        "leverage": [1.0] * days, "killed": [False] * days, "n_active": [4] * days,
        "metrics": metrics, "median_spread_bps": 1.0, "cost_drag_bps": 6.5,
        "used_real_spread": book == "bybit_4edge", "capital": capital, "taker_fee_bps": 5.5,
    }
    repo.upsert_compare_measured_cache(SINGLE_CACHE_KEYS[book], payload, at=_AT)
    return payload


def _equity(html: str) -> list[float]:
    m = re.search(r'"equity":\s*\[([-0-9.,\s]+)\]', html)
    assert m, html[:500]
    return [float(x) for x in m.group(1).split(",")]


def test_capital_scales_the_measured_curve() -> None:
    """The Backtest reduces to book/capital/period. Capital rescales the cached curve LINEARLY (per-bp cost is
    capital-robust): 2x capital -> 2x equity, point for point."""
    repo = PaperRepo("sqlite://")
    repo.migrate()
    _seed_measured_cache(repo, "bybit_4edge", capital=100_000.0)
    c = _client(repo)
    one = c.get("/paper/sim/run", params={"book": "bybit_4edge", "capital": 100_000})
    two = c.get("/paper/sim/run", params={"book": "bybit_4edge", "capital": 200_000})
    assert one.status_code == 200 and two.status_code == 200
    e1, e2 = _equity(one.text), _equity(two.text)
    assert len(e1) == len(e2) and len(e1) > 0
    for a, b in zip(e1, e2, strict=True):
        assert abs(b - 2.0 * a) < 1.0, (a, b)


def test_period_windows_the_curve_and_recomputes_metrics() -> None:
    """Period start/end WINDOWS the cached curve (fewer points) and the metrics (CAGR/Sharpe/maxDD) are
    RECOMPUTED on the window — not the full-history numbers sliced. The full curve has a deep mid-series dip;
    a window AFTER the dip must show a shallower max DD than the full history."""
    repo = PaperRepo("sqlite://")
    repo.migrate()
    _seed_measured_cache(repo, "bybit_4edge", start=dt.date(2022, 1, 1), days=400, capital=100_000.0)
    c = _client(repo)
    full = c.get("/paper/sim/run", params={"book": "bybit_4edge", "capital": 100_000})
    win = c.get("/paper/sim/run", params={"book": "bybit_4edge", "capital": 100_000,
                                          "start": "2022-11-01", "end": "2022-12-15"})
    assert full.status_code == 200 and win.status_code == 200
    n_full, n_win = len(_equity(full.text)), len(_equity(win.text))
    assert 0 < n_win < n_full                                   # the window has strictly fewer points

    def _maxdd(html: str) -> float:
        m = re.search(r'"drawdown":\s*\[([-0-9.,\s eE]+)\]', html)
        assert m, html[:500]
        return min(float(x) for x in m.group(1).split(","))

    # the window resets its drawdown peak at the window start, so its worst DD is shallower than the full
    # history's (which includes the deep mid-series dip) — proof the metrics are recomputed on the window.
    assert _maxdd(win.text) > _maxdd(full.text)
    # and the rendered window dates are bounded by the requested period.
    data = _sim_data_dates(win.text)
    assert data[0] >= "2022-11-01" and data[-1] <= "2022-12-15"


def test_windowed_backtest_rebases_to_capital_not_the_cumulative_tail() -> None:
    """The bug the user caught: with $350k from a LATE start date, the equity must BEGIN at ~$350k on that date
    (a fresh backtest), NOT the tail of the full cumulative curve. The buggy behaviour entered the window at the
    2021->date compounded value scaled by capital — e.g. ~$1.6M on $350k, which is impossible. A window REBASES
    to `capital`; equity[0] is one day's return from capital, never the multi-year compounded value."""
    repo = PaperRepo("sqlite://")
    repo.migrate()
    seeded = _seed_measured_cache(repo, "bybit_4edge", start=dt.date(2022, 1, 1), days=400, capital=100_000.0)
    lo = next(i for i, d in enumerate(seeded["dates_iso"]) if d >= "2022-11-01")
    old_rendered = seeded["equity"][lo] * (350_000 / 100_000)   # what the OLD buggy code spliced in as eq[0]
    c = _client(repo)
    win = c.get("/paper/sim/run", params={"book": "bybit_4edge", "capital": 350_000,
                                          "start": "2022-11-01", "end": "2022-12-15"})
    assert win.status_code == 200
    eq = _equity(win.text)
    assert len(eq) > 0
    # equity STARTS at ~capital (within one day's return) — a fresh backtest from the start date.
    assert abs(eq[0] - 350_000) < 350_000 * 0.06, eq[0]
    # and it is CLEARLY NOT the old spliced-in cumulative-tail value (the $1.6M-on-$350k class of bug).
    assert abs(eq[0] - old_rendered) > 350_000 * 0.4, (eq[0], old_rendered)


def _sim_data_dates(html: str) -> list[str]:
    import json
    m = re.search(r'<script type="application/json" id="sim-data">(.*?)</script>', html, re.DOTALL)
    assert m
    return json.loads(m.group(1))["dates"]


def test_backtest_page_has_no_cost_flat_sleeve_kelly_controller_controls() -> None:
    """The honest Backtest is book/capital/period ONLY. The configurable-cost sandbox is GONE: no cost (bps)
    slider, no measured/flat cost-mode select, no sleeve-composition presets/checkboxes, no Kelly slider, no
    controller select. Drag-to-0-cost can no longer resurrect the +7097% mirage."""
    repo = PaperRepo("sqlite://")
    repo.migrate()
    _seed_measured_cache(repo, "bybit_4edge")
    c = _client(repo)
    page = c.get("/paper/sim").text
    assert 'name="cost_bps"' not in page and "cost (bps)" not in page     # no cost slider
    assert 'name="cost_mode"' not in page and "flat (what-if)" not in page  # no cost-mode select
    assert 'name="sleeve"' not in page and "sim-preset" not in page       # no sleeve presets/checkboxes
    assert 'name="kelly"' not in page and 'name="controller"' not in page  # no Kelly / controller
    assert 'type="range"' not in page                                     # no sliders at all
    # the honest knobs remain.
    assert 'name="capital"' in page and 'name="start"' in page and 'name="end"' in page
    # the curve fragment carries the measured marker (the honest real-cost view).
    run = c.get("/paper/sim/run", params={"book": "bybit_4edge", "capital": 100_000})
    assert 'data-cost-mode="measured"' in run.text


def test_cache_absent_shows_pending_note_not_flat_not_500() -> None:
    """With NO precomputed measured artifact the Backtest degrades to a short 'precomputing' note — NOT a flat
    slider, NEVER a 500. (Previously this fell through to the flat what-if; that path is removed.) A stale
    book=binance_combined param (the venue Task 7a removed) degrades to the same bybit rendering."""
    repo = PaperRepo("sqlite://")
    repo.migrate()   # nothing seeded
    c = _client(repo)
    for book in ("bybit_4edge", "binance_combined"):
        r = c.get("/paper/sim/run", params={"book": book, "capital": 100_000})
        assert r.status_code == 200, (book, r.text[:300])
        assert 'data-cost-mode="flat"' not in r.text          # NOT the flat what-if
        assert "precomputing" in r.text.lower()               # the graceful pending note
        assert "nightly" in r.text.lower()


def test_bybit_measured_default_shows_real_costed_number() -> None:
    """book=bybit_4edge in MEASURED mode (default) renders the precomputed per-coin measured curve (its real
    costed number), with the measured cost-mode indicator."""
    repo, bybit = _seeded_all()
    precompute_single_book_measured(repo, bybit, capital=100_000.0, at=_AT, unlock_events=[])
    bybit.close()
    by_cached = read_cached_single_measured(repo, "bybit_4edge")
    assert by_cached is not None and by_cached["equity"]

    c = _client(repo)
    r = c.get("/paper/sim/run", params={"book": "bybit_4edge", "capital": 100000})
    assert r.status_code == 200
    assert 'data-cost-mode="measured"' in r.text
    # the rendered curve's final equity == the precomputed measured curve (at the same capital).
    m = re.search(r'"equity":\s*\[([-0-9.,\s]+)\]', r.text)
    assert m
    rendered_final = float(m.group(1).split(",")[-1])
    assert abs(rendered_final - by_cached["equity"][-1]) < 1.0, (rendered_final, by_cached["equity"][-1])


# --- 4) REAL L1 quoted-spread override (the core cost-model fix) ----------------------------------
# The Bybit liquid book was costed off a daily-high/low Corwin–Schultz proxy that over-states the true quoted
# spread ~104× (1.4bp real vs 143.6bp CS). With the REAL spread the cost drag drops and the net Sharpe rises.

_BYBIT_BOOK_COINS = ["AAAUSDT", "BBBUSDT", "CCCUSDT", "XXXUSDT", "YYYUSDT", "ZZZUSDT"]


def test_bybit_measured_real_spread_lowers_cost_and_lifts_sharpe() -> None:
    """real_spread override (2bp) ≪ the ~200bp CS proxy → lower cost drag AND higher net Sharpe than CS-only
    (the core fix). The GROSS book is unchanged — only the per-bp cost model's spread input changes."""
    cs_store = _bybit_store(spread_frac=0.02)
    real_store = _bybit_store(spread_frac=0.02)
    cs = _bybit_measured_net(cs_store, capital=100_000.0, win_lo=None, win_hi=None, taker_fee_bps=5.5,
                             sleeves=["xsfunding"], unlock_events=[])
    real = _bybit_measured_net(real_store, capital=100_000.0, win_lo=None, win_hi=None, taker_fee_bps=5.5,
                               sleeves=["xsfunding"], unlock_events=[],
                               real_spread_by_coin={c: 2.0 for c in _BYBIT_BOOK_COINS})
    cs_store.close()
    real_store.close()
    assert cs["used_real_spread"] is False and real["used_real_spread"] is True
    assert real["cost_drag_bps"] < cs["cost_drag_bps"], (real["cost_drag_bps"], cs["cost_drag_bps"])
    assert real["metrics"]["sharpe"] > cs["metrics"]["sharpe"], (
        real["metrics"]["sharpe"], cs["metrics"]["sharpe"])
    # GROSS is untouched: the real cost drag still sits at the exact taker_fee + 0.5·real_spread floor.
    assert abs(real["cost_drag_bps"] - (5.5 + 0.5 * 2.0)) < 1e-6, real["cost_drag_bps"]


def test_bybit_measured_partial_override_falls_back_to_cs() -> None:
    """A coin with NO stored real spread falls back to its CS estimate: partial (1 real, rest CS) cost drag
    sits strictly between all-real (cheap) and all-CS (expensive)."""
    full_store = _bybit_store(spread_frac=0.02)
    part_store = _bybit_store(spread_frac=0.02)
    cs_store = _bybit_store(spread_frac=0.02)
    full = _bybit_measured_net(full_store, capital=100_000.0, win_lo=None, win_hi=None, taker_fee_bps=5.5,
                               sleeves=["xsfunding"], unlock_events=[],
                               real_spread_by_coin={c: 2.0 for c in _BYBIT_BOOK_COINS})
    part = _bybit_measured_net(part_store, capital=100_000.0, win_lo=None, win_hi=None, taker_fee_bps=5.5,
                               sleeves=["xsfunding"], unlock_events=[],
                               real_spread_by_coin={_BYBIT_BOOK_COINS[0]: 2.0})
    cs = _bybit_measured_net(cs_store, capital=100_000.0, win_lo=None, win_hi=None, taker_fee_bps=5.5,
                             sleeves=["xsfunding"], unlock_events=[])
    full_store.close()
    part_store.close()
    cs_store.close()
    assert full["cost_drag_bps"] < part["cost_drag_bps"] < cs["cost_drag_bps"], (
        full["cost_drag_bps"], part["cost_drag_bps"], cs["cost_drag_bps"])


def test_precompute_threads_real_spread_for_bybit() -> None:
    """precompute passes the real spread through for bybit_4edge — the cost drag drops and the override flag
    is set."""
    repo1, bybit1 = _seeded_all(spread_frac=0.02)
    base = precompute_single_book_measured(repo1, bybit1, capital=100_000.0, at=_AT, unlock_events=[])
    bybit1.close()

    repo2, bybit2 = _seeded_all(spread_frac=0.02)
    withreal = precompute_single_book_measured(repo2, bybit2, capital=100_000.0, at=_AT,
                                               unlock_events=[],
                                               bybit_real_spread_by_coin={c: 2.0 for c in _BYBIT_BOOK_COINS})
    bybit2.close()

    # bybit cost drag DROPS with the real spread + flags it.
    assert withreal["bybit_4edge"]["used_real_spread"] is True
    assert base["bybit_4edge"]["used_real_spread"] is False
    assert withreal["bybit_4edge"]["cost_drag_bps"] < base["bybit_4edge"]["cost_drag_bps"]


def test_bybit_caption_shows_real_spread_and_keeps_measured_marker() -> None:
    """When the bybit book used real spreads the caption says so (forward-realistic / current-snapshot caveat
    NOT hidden) while the existing data-cost-mode="measured" marker stays intact."""
    repo, bybit = _seeded_all(spread_frac=0.02)
    precompute_single_book_measured(repo, bybit, capital=100_000.0, at=_AT, unlock_events=[],
                                    bybit_real_spread_by_coin={c: 2.0 for c in _BYBIT_BOOK_COINS})
    bybit.close()
    c = _client(repo)

    r = c.get("/paper/sim/run", params={"book": "bybit_4edge", "capital": 100000})
    assert r.status_code == 200
    assert 'data-cost-mode="measured"' in r.text                 # marker preserved
    assert "real L1 quoted spread" in r.text                      # (now in the hover tooltip, not the visible caption)
    # the current-snapshot caveat is NOT hidden — it stays in the tooltip (plain wording after the jargon cleanup)
    assert "current snapshot applied across history" in r.text and "earlier-year spreads were wider" in r.text


# NB: the old `test_measured_default_falls_back_to_flat_when_cache_absent` (cache-absent => flat slider) is
# replaced by `test_cache_absent_shows_pending_note_not_flat_not_500` above — the flat what-if is no longer a
# user path; an absent measured cache now degrades to a "precomputing" note, never the flat slider.
