"""TRUSTWORTHY Bybit carry re-validation — each de-artifacting knob is proven to BITE:
  * worst_basis_proxy: hand-checked values, 0 when no adverse basis, conservative on a squeeze day;
  * per_coin_cost_bps: liquid ≈ base, thin charged more (capped);
  * carry_metrics_from_store knobs: universe restriction lowers breadth; squeeze-tail lowers Sharpe/raises
    DD; per-coin cost lowers return vs flat;
  * a regression test: a noise-coin-padded universe is Sharpe-inflated vs the liquid subset (the artifact).
NO network — in-memory SQLite store.
"""
from __future__ import annotations

import math

from fxhnt.adapters.warehouse.timescale_feature_store import TimescaleFeatureStore
from fxhnt.application.bybit_carry_revalidate import (
    carry_metrics_from_store,
    per_coin_cost_bps,
    trustworthy_revalidate,
    worst_basis_proxy,
)
from fxhnt.application.bybit_liquidity import liquid_universe

_DAY = 86_400


# --- worst_basis_proxy ---------------------------------------------------------------------------
def test_worst_basis_proxy_hand_checked() -> None:
    # spot_low=98, perp_high=100 → wb = 98/100 - 1 = -0.02 → charge 0.02.
    assert math.isclose(
        worst_basis_proxy(perp_high=100.0, perp_low=99.0, spot_high=101.0, spot_low=98.0), 0.02)


def test_worst_basis_proxy_zero_when_no_adverse_basis() -> None:
    # spot_low > perp_high → wb positive → min(0, wb)=0 → no charge.
    assert worst_basis_proxy(perp_high=100.0, perp_low=99.0, spot_high=110.0, spot_low=105.0) == 0.0


def test_worst_basis_proxy_conservative_vs_close_move() -> None:
    # On a squeeze day the proxy (worst HIGH/LOW alignment) must be >= the close-to-close basis move.
    # close basis: spot 100->101 (+1%), perp 100->105 (+5%) → close basis = +1% - 5% = -4% adverse 0.04.
    perp_high, perp_low = 106.0, 100.0      # perp ranged up to 106 intraday
    spot_high, spot_low = 101.0, 95.0       # spot dipped to 95 intraday
    proxy = worst_basis_proxy(perp_high, perp_low, spot_high, spot_low)
    close_basis_adverse = abs((101.0 / 100.0 - 1.0) - (105.0 / 100.0 - 1.0))  # 0.04
    assert proxy >= close_basis_adverse
    assert math.isclose(proxy, 1.0 - 95.0 / 106.0)   # spot_low / perp_high - 1


def test_worst_basis_proxy_degenerate_prices() -> None:
    assert worst_basis_proxy(perp_high=0.0, perp_low=0.0, spot_high=0.0, spot_low=0.0) == 0.0


# --- per_coin_cost_bps ---------------------------------------------------------------------------
def test_per_coin_cost_liquid_near_base_thin_charged_more() -> None:
    base = 5.5
    liquid = per_coin_cost_bps(1_000_000_000.0, base_bps=base)     # very deep
    thin = per_coin_cost_bps(2_000_000.0, base_bps=base)           # thin
    assert math.isclose(liquid, base, abs_tol=0.1)                 # add-on ~0 at huge depth
    assert thin > liquid                                           # thin coin pays more
    assert per_coin_cost_bps(0.0, base_bps=base) == 50.0           # no liquidity → cap
    assert per_coin_cost_bps(1.0, base_bps=base, cap_bps=50.0) == 50.0  # blows up → capped


# --- carry_metrics_from_store knobs --------------------------------------------------------------
def _seed_carry(store: TimescaleFeatureStore, sym: str, *, days: int, funding: float,
                turnover: float, basis_drift: float = 0.0, range_frac: float = 0.0,
                worst_basis: dict[int, float] | None = None,
                wobble: float = 0.0002, idx: int = 0) -> None:
    """Seed one coin's funding + perp/spot OHLC + turnover. `basis_drift` makes spot/perp diverge a touch;
    `range_frac` sets the intraday high/low band; `worst_basis` (optional) stores the REAL 1m squeeze-tail
    series the override consumes."""
    rows = []
    perp_px = 100.0
    spot_px = 100.0
    for d in range(days):
        f = funding + wobble * math.cos(0.5 * d + idx)
        perp_px *= (1.0 + 0.0)
        spot_px *= (1.0 + basis_drift)
        ph = perp_px * (1.0 + range_frac)
        pl = perp_px * (1.0 - range_frac)
        sh = spot_px * (1.0 + range_frac)
        sl = spot_px * (1.0 - range_frac)
        feats = {
            "funding": f, "close": perp_px, "spot_close": spot_px,
            "high": ph, "low": pl, "spot_high": sh, "spot_low": sl,
            "turnover": turnover,
        }
        if worst_basis is not None and d in worst_basis:
            feats["worst_basis"] = worst_basis[d]
        rows.append((d * _DAY, feats))
    store.write_features(sym, rows)


def _seed_dispersion(store: TimescaleFeatureStore, *, days: int = 50, turnover: float = 50_000_000.0,
                     range_frac: float = 0.0, squeeze: tuple[int, float] | None = None) -> None:
    coins = {"AAAUSDT": 0.001, "BBBUSDT": 0.001, "CCCUSDT": 0.001,
             "XXXUSDT": -0.001, "YYYUSDT": -0.001, "ZZZUSDT": -0.001}
    long_side = {"AAAUSDT", "BBBUSDT", "CCCUSDT"}
    for i, (sym, f) in enumerate(coins.items()):
        # Asymmetric squeeze on the long legs only (so it does NOT cancel in the dollar-neutral book).
        wb = ({squeeze[0]: squeeze[1]} if (squeeze is not None and sym in long_side) else None)
        _seed_carry(store, sym, days=days, funding=f, turnover=turnover, range_frac=range_frac,
                    worst_basis=wb, idx=i)


def test_universe_restriction_changes_breadth_metrics() -> None:
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_dispersion(store)
    full = carry_metrics_from_store(store, cost_bps=0.0)
    subset = {"AAAUSDT", "XXXUSDT", "BBBUSDT"}
    restricted = carry_metrics_from_store(store, cost_bps=0.0, universe=subset)
    store.close()
    assert full["symbols"] == 6
    assert restricted["symbols"] == 3
    # Different breadth → different metrics (the curves are not identical).
    assert restricted["sharpe"] != full["sharpe"] or restricted["total_return"] != full["total_return"]


def test_squeeze_tail_lowers_sharpe_raises_dd() -> None:
    # The squeeze-tail OVERRIDE: a stored worst_basis (a real intraday squeeze on the long legs on one day)
    # replaces the daily-close basis only on that symbol/day. NOT the old additive high/low charge.
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    # squeeze on day 40 (the XS book warms up ~day 29, so the held weights actually carry the hit).
    _seed_dispersion(store, days=60, squeeze=(40, -0.15))   # one deep squeeze day on the long legs
    no_tail = carry_metrics_from_store(store, cost_bps=0.0, charge_squeeze_tail=False)
    with_tail = carry_metrics_from_store(store, cost_bps=0.0, charge_squeeze_tail=True)
    store.close()
    assert with_tail["sharpe"] < no_tail["sharpe"]
    assert with_tail["total_return"] < no_tail["total_return"]
    assert with_tail["max_dd"] <= no_tail["max_dd"]   # deeper (more negative) drawdown


def test_per_coin_cost_lowers_return_vs_flat() -> None:
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    # All coins THIN (2M turnover) → per-coin cost >> the flat 5.5bp.
    coins = {"AAAUSDT": 0.001, "BBBUSDT": 0.001, "CCCUSDT": 0.001,
             "XXXUSDT": -0.001, "YYYUSDT": -0.001, "ZZZUSDT": -0.001}
    for i, (sym, f) in enumerate(coins.items()):
        _seed_carry(store, sym, days=50, funding=f, turnover=2_000_000.0, idx=i)
    flat = carry_metrics_from_store(store, cost_bps=5.5, per_coin_cost=False)
    per_coin = carry_metrics_from_store(store, per_coin_cost=True, base_cost_bps=5.5)
    store.close()
    assert per_coin["total_return"] < flat["total_return"]


def test_regression_noise_coins_inflate_full_universe_sharpe() -> None:
    """The de-artifacting demonstration: a few REAL liquid dispersion coins + many TINY illiquid noise coins.
    The full-universe carry Sharpe is INFLATED by the over-diversification across the noise coins; restricting
    to the liquid subset gives a LOWER, more believable Sharpe."""
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    # 6 real, deep, dispersed coins.
    real = {"AAAUSDT": 0.001, "BBBUSDT": 0.001, "CCCUSDT": 0.001,
            "XXXUSDT": -0.001, "YYYUSDT": -0.001, "ZZZUSDT": -0.001}
    for i, (sym, f) in enumerate(real.items()):
        _seed_carry(store, sym, days=60, funding=f, turnover=50_000_000.0, idx=i)
    # 40 tiny illiquid noise coins: tiny consistent funding (so XS book over-diversifies), low turnover.
    for j in range(40):
        sign = 1.0 if j % 2 == 0 else -1.0
        _seed_carry(store, f"N{j:02d}USDT", days=60, funding=sign * 0.0008,
                    turnover=500_000.0, idx=100 + j, wobble=0.00005)

    universe = liquid_universe(store, min_dollar_vol=10_000_000.0, lookback_days=90)
    full = carry_metrics_from_store(store, cost_bps=0.0)
    liquid_only = carry_metrics_from_store(store, cost_bps=0.0, universe=universe)
    store.close()

    assert universe == set(real), "only the 6 deep coins clear the liquidity floor"
    assert full["symbols"] == 46
    assert liquid_only["symbols"] == 6
    # Over-diversification across the noise coins inflates the full-universe Sharpe above the liquid subset.
    assert full["sharpe"] > liquid_only["sharpe"], (
        f"full {full['sharpe']:.2f} should be inflated vs liquid {liquid_only['sharpe']:.2f}")


def test_trustworthy_revalidate_two_row_report() -> None:
    bybit = TimescaleFeatureStore("sqlite://", table="bybit_features")
    # 6 deep dispersion coins + noise.
    real = {"AAAUSDT": 0.001, "BBBUSDT": 0.001, "CCCUSDT": 0.001,
            "XXXUSDT": -0.001, "YYYUSDT": -0.001, "ZZZUSDT": -0.001}
    for i, (sym, f) in enumerate(real.items()):
        _seed_carry(bybit, sym, days=60, funding=f, turnover=50_000_000.0, range_frac=0.01, idx=i)
    for j in range(20):
        sign = 1.0 if j % 2 == 0 else -1.0
        _seed_carry(bybit, f"N{j:02d}USDT", days=60, funding=sign * 0.0008,
                    turnover=500_000.0, idx=100 + j, wobble=0.00005)

    out = trustworthy_revalidate(bybit, bybit_cost_bps=5.5)
    bybit.close()

    assert set(out) >= {"bybit_naive", "bybit_trustworthy", "universe_size", "verdict"}
    assert out["bybit_naive"]["symbols"] == 26
    assert out["bybit_trustworthy"]["symbols"] == 6      # liquid subset only
    assert out["universe_size"] == 6
    # The trustworthy Sharpe is below the naive (de-artifacted): liquid subset + tail charge + per-coin cost.
    assert out["bybit_trustworthy"]["sharpe"] < out["bybit_naive"]["sharpe"]
    assert out["verdict"]["holds"] in (True, False)
    assert out["verdict"]["rule"] == "bybit_trustworthy.sharpe > 0"


def test_trustworthy_revalidate_insufficient_data() -> None:
    bybit = TimescaleFeatureStore("sqlite://", table="bybit_features")   # empty
    out = trustworthy_revalidate(bybit)
    bybit.close()
    assert out["bybit_trustworthy"]["days"] == 0
    assert out["verdict"]["holds"] is None
    assert out["verdict"]["reason"] == "insufficient data"
