"""TRUE Bybit L1 quoted-spread fetcher + re-costing the maker grid with the REAL spread.

The maker-recost grid's load-bearing input is a 143.6bp median per-coin spread estimated by Corwin–Schultz
from DAILY HIGH/LOW — which captures intraday RANGE, not the quoted bid/ask, so it MASSIVELY over-states the
true L1 spread on liquid coins (BTC/ETH quote spreads ≈ 1bp). These tests prove:

Part A (fetcher, ALL HTTP injected — NO network):
  * the tickers endpoint rows -> correct (ask−bid)/mid·1e4 spread bps;
  * missing / zero / crossed quotes are SKIPPED (defensive — a malformed row never raises);
  * the snapshot is clock-deterministic (stamped from the INJECTED clock, no bare time.time);
  * a universe filter restricts the returned symbols.

Part B (re-cost with the REAL spread):
  * f=0 + a real_spread override ties out to the EXACT cost formula taker_fee + 0.5·real_spread (the same
    blended_cost_bps math, only the spread input changes);
  * a real_spread override reproduces the maker grid math but with the injected spreads;
  * DIRECTION: a fixture where real_spread ≪ CS → the all-taker baseline Sharpe is HIGHER under REAL AND the
    maker lift is SMALLER (tighter real spread → less to save);
  * compare_spreads reports the over-estimate ratio + the fallback count.
"""
from __future__ import annotations

import math

from fxhnt.adapters.data.bybit_true_spread import BybitTrueSpread, spread_bps
from fxhnt.adapters.warehouse.timescale_feature_store import TimescaleFeatureStore
from fxhnt.application.bybit_liquidity_sweep import per_coin_book_returns
from fxhnt.application.bybit_maker_recost import (
    blended_cost_bps,
    compare_spreads,
    maker_recost_sweep,
    recost_net_series,
    true_spread_report,
)

_DAY = 86_400
_NOW = 1780315200.0  # noon 2026-06-01 UTC


def _FIXED_CLOCK() -> float:  # noqa: N802 — constant-like injectable clock
    return _NOW


# --- a tickers payload -------------------------------------------------------------------------------

def _ticker(symbol: str, bid: str | float | None, ask: str | float | None,
            last: str | float = "100") -> dict:
    return {"symbol": symbol, "bid1Price": bid, "ask1Price": ask, "lastPrice": last}


def _tickers_payload(rows: list[dict]) -> dict:
    return {"retCode": 0, "result": {"list": rows}}


# --- Part A: the pure spread_bps helper --------------------------------------------------------------

def test_spread_bps_basic() -> None:
    # bid 99.99, ask 100.01 → spread 0.02 over mid 100.0 → 2.0 bps.
    assert abs(spread_bps("99.99", "100.01") - 2.0) < 1e-9


def test_spread_bps_skips_missing_and_zero() -> None:
    assert spread_bps(None, "100.0") is None
    assert spread_bps("0", "100.0") is None
    assert spread_bps("100.0", "0") is None
    assert spread_bps("abc", "100.0") is None


def test_spread_bps_skips_crossed_book() -> None:
    # ask < bid is a crossed/garbage book → skip, do NOT return a negative spread.
    assert spread_bps("100.5", "100.0") is None


def test_spread_bps_tight_book_is_zero_not_skipped() -> None:
    assert spread_bps("100.0", "100.0") == 0.0


# --- Part A: the fetcher -----------------------------------------------------------------------------

def test_fetcher_parses_spreads_from_fixture() -> None:
    payload = _tickers_payload([
        _ticker("BTCUSDT", "99999.5", "100000.5"),   # ~0.1 bps
        _ticker("ETHUSDT", "1999.98", "2000.02"),     # ~0.2 bps
    ])
    fetch = lambda _url: payload  # noqa: E731
    out = BybitTrueSpread(fetch=fetch, sleep=lambda _: None).real_spread_bps()
    assert set(out) == {"BTCUSDT", "ETHUSDT"}
    assert abs(out["BTCUSDT"] - (1.0 / 100000.0 * 1e4)) < 1e-6
    assert abs(out["ETHUSDT"] - (0.04 / 2000.0 * 1e4)) < 1e-6


def test_fetcher_skips_malformed_rows_without_crashing() -> None:
    payload = _tickers_payload([
        _ticker("BTCUSDT", "99999.5", "100000.5"),
        _ticker("DEADUSDT", "0", "0"),               # zero quotes → skip
        _ticker("CROSSUSDT", "101", "100"),          # crossed → skip
        {"symbol": "NOQUOTE"},                         # missing fields → skip
        {"bid1Price": "1", "ask1Price": "2"},         # missing symbol → skip
        "not-a-dict",                                  # garbage row → skip
    ])
    out = BybitTrueSpread(fetch=lambda _u: payload, sleep=lambda _: None).real_spread_bps()
    assert set(out) == {"BTCUSDT"}


def test_fetcher_universe_filter() -> None:
    payload = _tickers_payload([
        _ticker("BTCUSDT", "99999.5", "100000.5"),
        _ticker("ETHUSDT", "1999.98", "2000.02"),
        _ticker("SOLUSDT", "149.99", "150.01"),
    ])
    out = BybitTrueSpread(fetch=lambda _u: payload, sleep=lambda _: None).real_spread_bps(
        universe={"BTCUSDT", "SOLUSDT"})
    assert set(out) == {"BTCUSDT", "SOLUSDT"}


def test_fetcher_snapshot_is_clock_deterministic() -> None:
    payload = _tickers_payload([_ticker("BTCUSDT", "99999.5", "100000.5")])
    snap = BybitTrueSpread(fetch=lambda _u: payload, sleep=lambda _: None,
                           clock=_FIXED_CLOCK).snapshot()
    assert snap["fetched_at"] == _NOW          # stamped from the injected clock, not wall clock
    assert snap["n"] == 1
    assert "BTCUSDT" in snap["spreads"] and "BTCUSDT" in snap["last_price"]


def test_fetcher_empty_list_is_empty() -> None:
    out = BybitTrueSpread(fetch=lambda _u: _tickers_payload([]), sleep=lambda _: None).real_spread_bps()
    assert out == {}


# --- Part B: fixtures (mirror the maker-recost seeds) ------------------------------------------------

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


def _seed_wide_spread_book(store: TimescaleFeatureStore, *, days: int = 400) -> None:
    """A carry book whose coins carry a WIDE measured (CS) spread — so the maker leg has a lot to save AND a
    tiny REAL quote has a lot LESS to save (the direction the whole exercise is about)."""
    funding = {"AAAUSDT": 0.0012, "BBBUSDT": 0.0012, "CCCUSDT": 0.0012,
               "XXXUSDT": -0.0012, "YYYUSDT": -0.0012, "ZZZUSDT": -0.0012}
    wide = 0.02  # 200 bps measured CS spread
    for idx, (sym, fund) in enumerate(funding.items()):
        rows = []
        px = 100.0
        for d in range(days):
            px *= (1.0 + 0.0005 * math.sin(0.3 * d + idx))
            high, low = _ohlc_from_close(px, wide, vol_frac=0.0005, phase=0.3 * d + idx)
            rows.append((d * _DAY, {"funding": fund + 0.0005 * math.cos(0.5 * d + idx),
                                    "close": px, "spot_close": 100.0, "high": high, "low": low}))
        store.write_features(sym, rows)
    for sym in funding:
        store.write_features(sym, [(d * _DAY, {"turnover": 50_000_000.0}) for d in range(days)])


def _wide_book_detail() -> dict:
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_wide_spread_book(store)
    pc = per_coin_book_returns(store, universe=None, sleeves=["xsfunding"], unlock_events=[],
                               return_detail=True)
    store.close()
    assert pc["available"], pc
    return pc


# --- Part B: the cost-model override ----------------------------------------------------------------

def test_f0_real_spread_ties_to_taker_plus_half_real() -> None:
    """f=0 + a real_spread override reproduces the live all-taker cost formula but with the REAL spread:
    cost_drag == taker_fee + 0.5·real_spread (since every coin shares the same injected real spread)."""
    pc = _wide_book_detail()
    real = {c: 3.0 for c in pc["spread_by_coin"]}  # 3bp real quote on every book coin
    r = recost_net_series(pc, fill_fraction=0.0, adverse_sel=0.0, real_spread_by_coin=real)
    assert abs(r["cost_drag_bps"] - (5.5 + 0.5 * 3.0)) < 1e-9


def test_real_override_reproduces_blended_math() -> None:
    """A real_spread override drives the SAME blended_cost_bps math — just with the injected spread per coin."""
    pc = _wide_book_detail()
    real = {c: 4.0 for c in pc["spread_by_coin"]}
    r = recost_net_series(pc, fill_fraction=0.7, adverse_sel=0.25, real_spread_by_coin=real)
    expected = blended_cost_bps(spread_bps=4.0, fill_fraction=0.7, adverse_sel=0.25)
    assert abs(r["cost_drag_bps"] - expected) < 1e-9


def test_fallback_to_cs_for_unquoted_coins() -> None:
    """A coin with NO live quote falls back to its CS estimate; a quoted coin uses the real spread."""
    pc = _wide_book_detail()
    coins = sorted(pc["spread_by_coin"])
    real = {coins[0]: 2.0}  # only one coin has a live quote; the rest fall back to ~200bp CS
    full_real = {c: 2.0 for c in coins}
    partial = recost_net_series(pc, fill_fraction=0.0, adverse_sel=0.0, real_spread_by_coin=real)
    all_real = recost_net_series(pc, fill_fraction=0.0, adverse_sel=0.0, real_spread_by_coin=full_real)
    cs_only = recost_net_series(pc, fill_fraction=0.0, adverse_sel=0.0)
    # partial (1 real, rest CS) sits strictly between all-real (cheap) and all-CS (expensive).
    assert all_real["cost_drag_bps"] < partial["cost_drag_bps"] < cs_only["cost_drag_bps"]


# --- Part B: the DIRECTION (tighter real spread → less to save) -------------------------------------

def test_real_spread_lifts_baseline_and_shrinks_maker_lift() -> None:
    """The core finding's direction: with a real spread MUCH tighter than the CS proxy, the all-taker baseline
    is CHEAPER (higher Sharpe, lower cost-drag) AND the maker lift is SMALLER — there is less spread left to
    save. The lift is measured in COST-DRAG bps (the mathematically clean measure: maker saves at most
    (0.5−adverse)·spread per unit turnover, so a tiny real spread caps the saving at a tiny number, whereas the
    inflated CS spread leaves tens of bps to save). ΔSharpe is a nonlinear function of cost and is NOT a
    reliable direction proxy on a synthetic near-zero-mean book, so the cost-drag saving is asserted here."""
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_wide_spread_book(store)
    pc = per_coin_book_returns(store, universe=None, sleeves=["xsfunding"], unlock_events=[],
                               return_detail=True)
    real = {c: 2.0 for c in pc["spread_by_coin"]}  # 2bp real vs ~200bp CS
    rep = maker_recost_sweep(store, universe=None, sleeves=["xsfunding"], capital=100_000.0,
                             unlock_events=[], real_spread_by_coin=real)
    store.close()

    assert "real" in rep
    # 1) baseline (all-taker) is CHEAPER under REAL → higher Sharpe, lower cost-drag.
    assert rep["real"]["baseline"]["sharpe"] > rep["baseline"]["sharpe"]
    assert rep["real"]["baseline"]["cost_drag_bps"] < rep["baseline"]["cost_drag_bps"]
    # 2) the maker lift (cost-drag REDUCTION the maker leg buys vs the all-taker baseline) is far SMALLER under
    #    REAL — tighter real spread → less to save.
    cs_saving = abs(rep["operating_point"]["delta_cost_drag_bps"])
    real_saving = abs(rep["real"]["operating_point"]["delta_cost_drag_bps"])
    assert real_saving < cs_saving


def test_sweep_without_real_override_unchanged() -> None:
    """When no real override is given, the sweep result has NO `real` key (back-compat — the CS grid alone)."""
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_wide_spread_book(store)
    rep = maker_recost_sweep(store, universe=None, sleeves=["xsfunding"], capital=100_000.0, unlock_events=[])
    store.close()
    assert rep["available"] is True
    assert "real" not in rep


# --- Part B: compare_spreads (the over-estimate factor) ---------------------------------------------

def test_compare_spreads_reports_ratio_and_fallback() -> None:
    cs = {"BTCUSDT": 144.0, "ETHUSDT": 140.0, "SOLUSDT": 160.0, "THINUSDT": 300.0}
    real = {"BTCUSDT": 1.0, "ETHUSDT": 2.0, "SOLUSDT": 3.0}  # THINUSDT has no live quote
    cmp = compare_spreads(cs, real)
    assert cmp["n_coins"] == 4
    assert cmp["n_real"] == 3 and cmp["n_fallback"] == 1
    assert abs(cmp["median_real_spread_bps"] - 2.0) < 1e-9
    assert abs(cmp["median_cs_spread_bps"] - 152.0) < 1e-9  # median(140,144,160,300)
    assert cmp["cs_over_real_ratio"] > 50.0                  # CS massively over-states the real quote


# --- CLI smoke (mocked store + mocked fetcher, NO network) ------------------------------------------

def _seed_for_cli(store: TimescaleFeatureStore) -> None:
    _seed_wide_spread_book(store, days=400)


class _FakeTrueSpread:
    """A BybitTrueSpread stand-in that serves a tight live quote for the book coins — NO network."""

    def __init__(self, *_a, **_k) -> None:  # noqa: ANN002, ANN003 — match the real ctor signature
        pass

    def real_spread_bps(self, *, universe=None):  # noqa: ANN001, ANN202
        coins = universe or {"AAAUSDT", "BBBUSDT", "CCCUSDT", "XXXUSDT", "YYYUSDT", "ZZZUSDT"}
        return {c: 2.0 for c in coins}


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

    import fxhnt.cli as cli

    seeded = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_for_cli(seeded)
    monkeypatch.setattr(
        "fxhnt.adapters.warehouse.timescale_feature_store.TimescaleFeatureStore",
        lambda *a, **k: seeded, raising=False)
    monkeypatch.setattr("fxhnt.adapters.data.bybit_true_spread.BybitTrueSpread",
                        _FakeTrueSpread, raising=False)

    result = CliRunner().invoke(cli.app, ["bybit-true-spread", "--min-dollar-vol", "0"])
    seeded.close()
    assert result.exit_code == 0, result.output
    out = result.output.lower()
    assert "real quoted spread" in out
    assert "corwin" in out or "high/low" in out
    assert "over-states" in out


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

    import fxhnt.cli as cli

    seeded = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_for_cli(seeded)
    monkeypatch.setattr(
        "fxhnt.adapters.warehouse.timescale_feature_store.TimescaleFeatureStore",
        lambda *a, **k: seeded, raising=False)
    monkeypatch.setattr("fxhnt.adapters.data.bybit_true_spread.BybitTrueSpread",
                        _FakeTrueSpread, raising=False)

    result = CliRunner().invoke(cli.app, ["maker-recost", "--min-dollar-vol", "0", "--real-spread"])
    seeded.close()
    assert result.exit_code == 0, result.output
    out = result.output.lower()
    assert "cs spread" in out and "real spread" in out
    assert "caveat" in out
    assert "current snapshot" in out


def test_true_spread_report_over_book_coins() -> None:
    """Part-A report: builds the book, compares its coins' REAL vs CS spread, returns the ratio + fallbacks."""
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_wide_spread_book(store)
    pc = per_coin_book_returns(store, universe=None, sleeves=["xsfunding"], unlock_events=[],
                               return_detail=True)
    real = {c: 2.0 for c in pc["spread_by_coin"]}
    store2 = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_wide_spread_book(store2)
    rep = true_spread_report(store2, universe=None, sleeves=["xsfunding"], real_spread_by_coin=real,
                             unlock_events=[])
    store.close()
    store2.close()
    assert rep["available"] is True
    assert abs(rep["median_real_spread_bps"] - 2.0) < 1e-9
    assert rep["median_cs_spread_bps"] > 100.0   # the inflated high/low proxy
    assert rep["cs_over_real_ratio"] > 10.0
