"""READ-ONLY Phase-1 evaluator: stablecoin peg-reversion on BYBIT's OWN near-$1 stablecoin spot pairs.

Seeds a synthetic in-memory `TimescaleFeatureStore("sqlite://", table="bybit_features")` with `spot_close`
rows and asserts:
  * `bybit_stable_spot_panel` auto-detects ONLY symbols that STAY near $1 (median ∈ [0.98, 1.02] AND >=90%
    of days within ±5% of $1 AND >=60 days history) — excluding a gold-like (~3000), a dead peg (~0.02), a
    normal coin (~100), AND a VOLATILE coin that merely averages ~$1 (SUSHI-like: regression for the bug);
  * `stablecoin_reversion_metrics_from_store` runs the LIVE `StableReversionRunner` UNCHANGED on that panel —
    a rich→revert panel produces a positive (short-rich) curve with sane finite metrics;
  * the evaluator is READ-ONLY (write-tripwire store proxy);
  * the CLI prints the detected pairs + metrics + caveat (mocked store, no network).
NO network — in-memory SQLite; no external inputs.
"""
from __future__ import annotations

import math

from fxhnt.adapters.warehouse.timescale_feature_store import TimescaleFeatureStore
from fxhnt.application.bybit_stablecoin_eval import (
    bybit_stable_spot_panel,
    stablecoin_reversion_metrics_from_store,
)

_DAY = 86_400


def _seed_spot(store: TimescaleFeatureStore, symbol: str, series: dict[int, float]) -> None:
    """Write a {epoch_day: spot_close} series for one symbol."""
    store.write_features(symbol, [(d * _DAY, {"spot_close": px}) for d, px in series.items()])


def _seed_mixed_universe(store: TimescaleFeatureStore, *, days: int = 80) -> None:
    """A realistic mix: two TRUE near-$1 stablecoins (rich→revert, stay near peg), a SUSHI-like volatile coin
    (median ~1 but WANDERS $0.5–$5), a gold-like (~3000), a dead peg (~0.02), and a normal coin (~100). Only
    the two true stablecoins should be auto-detected — the SUSHI-like one is the regression case for the bug.
    `days >= 60` so the true stablecoins clear the min-history requirement."""
    # USDC: persistently slightly rich (above peg) on even days, reverts to peg on odd days. Stays near $1.
    _seed_spot(store, "USDCUSDT", {d: (1.01 if d % 2 == 0 else 1.0) for d in range(days)})
    # USDE (Ethena): same rich→revert shape, slightly smaller deviation. Stays near $1.
    _seed_spot(store, "USDEUSDT", {d: (1.008 if d % 2 == 0 else 1.0) for d in range(days)})
    # SUSHI-like: values cycle 0.5, 0.8, 1.0, 2.5, 4.0 — median ~1 but WANDERS far from peg most days.
    # Loose median-only filter wrongly admitted this (bogus −90% DD); the dispersion filter must EXCLUDE it.
    _wander = [0.5, 0.8, 1.0, 2.5, 4.0]
    _seed_spot(store, "SUSHIUSDT", {d: _wander[d % len(_wander)] for d in range(days)})
    # Gold-proxy — median ~3000, must be EXCLUDED.
    _seed_spot(store, "PAXGUSDT", {d: 3000.0 + d for d in range(days)})
    # Dead peg — median ~0.02, must be EXCLUDED.
    _seed_spot(store, "USTCUSDT", {d: 0.02 for d in range(days)})
    # Normal coin — median ~100, must be EXCLUDED.
    _seed_spot(store, "ETHUSDT", {d: 100.0 + d for d in range(days)})


class _WriteTripwireStore:
    """Read-only proxy over a TimescaleFeatureStore: forwards reads, but ANY write/persist call trips an
    assertion. Proves the evaluator never mutates the warehouse (the READ-ONLY guarantee)."""

    _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: evaluator called write method {name!r}")
        return getattr(self._inner, name)


# --- auto-detect --------------------------------------------------------------------------------

def test_panel_selects_only_near_one_stablecoins() -> None:
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_mixed_universe(store)
    panel = bybit_stable_spot_panel(store)
    store.close()

    assert set(panel) == {"USDCUSDT", "USDEUSDT"}
    # Gold / dead peg / normal coin are NOT in the panel.
    for excluded in ("PAXGUSDT", "USTCUSDT", "ETHUSDT"):
        assert excluded not in panel
    # The selected series are full {epoch_day: close} dicts.
    assert all(isinstance(v, dict) and v for v in panel.values())


def test_panel_excludes_volatile_coin_averaging_one() -> None:
    """REGRESSION for the bug: a SUSHI-like coin whose MEDIAN is ~$1 but which WANDERS $0.5–$5 must NOT be
    treated as a stablecoin — its median passes the old loose filter but it spends most days far from peg, so
    the frac-in-band dispersion filter excludes it. (Admitting it caused a bogus −90% DD.)"""
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_mixed_universe(store)
    panel = bybit_stable_spot_panel(store)
    store.close()

    assert "SUSHIUSDT" not in panel
    assert set(panel) == {"USDCUSDT", "USDEUSDT"}


def test_panel_frac_in_band_boundary() -> None:
    """A coin near-$1 for MOST days but spiking occasionally: frac_in_band just under 0.90 → excluded;
    just over 0.90 → included (boundary around STABLE_MIN_FRAC_IN_BAND)."""
    from fxhnt.application.bybit_stablecoin_eval import STABLE_MIN_FRAC_IN_BAND

    assert STABLE_MIN_FRAC_IN_BAND == 0.90

    # 100 days: 11 spike days (off-peg) → 89% in-band → just UNDER 0.90 → excluded.
    under = {d: (2.0 if d < 11 else 1.0) for d in range(100)}
    # 100 days: 10 spike days → 90% in-band → exactly at threshold → included.
    over = {d: (2.0 if d < 10 else 1.0) for d in range(100)}

    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_spot(store, "UNDERUSDT", under)
    _seed_spot(store, "OVERUSDT", over)
    panel = bybit_stable_spot_panel(store)
    store.close()

    assert "UNDERUSDT" not in panel   # 0.89 < 0.90
    assert "OVERUSDT" in panel        # 0.90 >= 0.90


def test_panel_excludes_short_history_coin() -> None:
    """Min-history: a brand-new coin with only ~5 near-$1 prints must be EXCLUDED (n < STABLE_MIN_HISTORY),
    even though every print is exactly at peg."""
    from fxhnt.application.bybit_stablecoin_eval import STABLE_MIN_HISTORY

    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_spot(store, "NEWUSDT", {d: 1.0 for d in range(5)})
    # A real stablecoin with ample history, for contrast.
    _seed_spot(store, "USDCUSDT", {d: 1.0 for d in range(STABLE_MIN_HISTORY + 5)})
    panel = bybit_stable_spot_panel(store)
    store.close()

    assert "NEWUSDT" not in panel
    assert "USDCUSDT" in panel


def test_panel_empty_when_no_near_one_symbol() -> None:
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_spot(store, "ETHUSDT", {d: 100.0 + d for d in range(80)})
    _seed_spot(store, "PAXGUSDT", {d: 3000.0 for d in range(80)})
    panel = bybit_stable_spot_panel(store)
    store.close()

    assert panel == {}


# --- metrics ------------------------------------------------------------------------------------

def test_metrics_short_rich_is_positive_and_sane() -> None:
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_mixed_universe(store)
    m = stablecoin_reversion_metrics_from_store(store, cost_bps=0.0)
    store.close()

    assert m["available"] is True
    assert m["symbols"] == 2
    assert m["pairs"] == ["USDCUSDT", "USDEUSDT"]
    assert m["days"] > 0
    assert math.isfinite(m["sharpe"])
    assert math.isfinite(m["cagr"])
    assert m["max_dd"] <= 0.0
    # Shorting a rich (above-peg) name that mean-reverts to peg is profitable gross.
    assert m["total_return"] > 0.0


def test_metrics_na_when_no_stable_pair() -> None:
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_spot(store, "ETHUSDT", {d: 100.0 + d for d in range(40)})
    m = stablecoin_reversion_metrics_from_store(store)
    store.close()

    assert m["available"] is False
    assert "reason" in m and m["reason"]
    assert m["pairs"] == []


def test_metrics_is_read_only() -> None:
    inner = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_mixed_universe(inner)
    guarded = _WriteTripwireStore(inner)
    # Must NOT raise an AssertionError (no write path touched).
    m = stablecoin_reversion_metrics_from_store(guarded, cost_bps=0.0)
    inner.close()
    assert m["available"] is True


# --- CLI smoke (mocked store) -------------------------------------------------------------------

def test_cli_bybit_stablecoin_eval_prints_pairs_and_caveat(monkeypatch) -> None:
    """CLI smoke: a seeded in-memory store stands in for the live Bybit warehouse (no network); the command
    prints the detected pairs, the metrics, and the daily-close caveat."""
    from typer.testing import CliRunner

    import fxhnt.cli as cli

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

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

    result = CliRunner().invoke(cli.app, ["bybit-stablecoin-eval"])
    seeded.close()

    assert result.exit_code == 0, result.output
    assert "stablecoin peg-reversion" in result.output
    assert "USDCUSDT" in result.output
    assert "USDEUSDT" in result.output
    # The honesty caveats are surfaced.
    assert "CONSERVATIVE LOWER BOUND" in result.output
    assert "Phase-2" in result.output
