"""READ-ONLY multi-edge evaluator on a warehouse store — run tstrend/unlock/stablecoin/xsfunding on Bybit.

Seeds a synthetic in-memory `TimescaleFeatureStore("sqlite://", table="bybit_features")` and asserts:
  * `edge_metrics_from_store(store, "tstrend")` yields sane non-degenerate metrics on a clear momentum
    pattern, and a `universe` restriction changes the breadth (fewer symbols);
  * the evaluator is READ-ONLY — it never calls any write/persist method on the store or a paper repo
    (asserted via a write-tripwire store proxy);
  * unlock computes with an injected (mock) DefiLlama-style calendar, and reports N/A cleanly when the
    calendar is absent; stablecoin reports N/A (reason) when the Bybit warehouse has no stablecoin spot
    panel, and computes when an explicit spot panel is injected;
  * xsfunding reuses `carry_metrics_from_store`;
  * `evaluate_all_edges` returns a dict keyed by edge with metric dicts plus a combined book.
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.bybit_edges_eval import (
    edge_metrics_from_store,
    evaluate_all_edges,
)

_DAY = 86_400


def _seed_momentum(store: TimescaleFeatureStore, *, n_symbols: int = 8, days: int = 200,
                   drift: float = 0.01) -> None:
    """Write `days` of daily closes for `n_symbols` with a clear, persistent up/down momentum pattern.

    Even-indexed symbols trend UP (compounding +drift/day), odd-indexed trend DOWN, each with a small
    deterministic wobble so realized vol is finite/non-degenerate — a long/flat TSMOM book takes the
    up-trenders, giving sane non-degenerate metrics. >= 200 days clears the 130-close min-history gate."""
    for idx in range(n_symbols):
        sign = 1.0 if idx % 2 == 0 else -1.0
        px = 100.0
        rows = []
        for d in range(days):
            wobble = 0.002 * math.cos(0.4 * d + idx)
            px *= (1.0 + sign * drift + wobble)
            rows.append((d * _DAY, {"close": px}))
        store.write_features(f"SYM{idx:02d}USDT", rows)


def _seed_carry(store: TimescaleFeatureStore, *, days: int = 60) -> None:
    """Funding dispersion + flat perp/spot closes → a hand-checkable positive carry curve for xsfunding."""
    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.0002 * math.cos(0.5 * d + idx)
            rows.append((d * _DAY, {"funding": f, "close": 100.0, "spot_close": 100.0}))
        store.write_features(sym, rows)


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)


# --- tstrend ------------------------------------------------------------------------------------

def test_tstrend_metrics_are_sane_and_non_degenerate() -> None:
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_momentum(store)
    m = edge_metrics_from_store(store, "tstrend", cost_bps=0.0)
    store.close()

    assert m["days"] > 0
    assert m["symbols"] > 0
    assert math.isfinite(m["sharpe"])
    assert math.isfinite(m["cagr"])
    assert m["max_dd"] <= 0.0
    # A persistent up-trend long/flat book should make money gross.
    assert m["total_return"] > 0.0


def test_tstrend_universe_restriction_changes_breadth() -> None:
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_momentum(store, n_symbols=8)
    full = edge_metrics_from_store(store, "tstrend")
    restricted = edge_metrics_from_store(store, "tstrend",
                                         universe={"SYM00USDT", "SYM02USDT"})
    store.close()

    assert restricted["symbols"] < full["symbols"]


def test_tstrend_is_read_only() -> None:
    inner = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_momentum(inner)
    guarded = _WriteTripwireStore(inner)
    # Must NOT raise an AssertionError (no write path touched).
    m = edge_metrics_from_store(guarded, "tstrend")
    inner.close()
    assert m["days"] > 0


# --- unlock -------------------------------------------------------------------------------------

def test_unlock_with_injected_calendar_computes() -> None:
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_momentum(store, n_symbols=6, days=200)
    # An unlock cliff for SYM02USDT (an up-trender we will short into) + BTC as the market hedge leg.
    events = [{"sym": "SYM02USDT", "cliff_day": 190, "n": 5_000_000.0, "cat": "investors"}]
    # Need a market symbol for the hedge leg (BTCUSDT) — seed a simple flat-ish series.
    btc_rows = [(d * _DAY, {"close": 30_000.0 * (1.0 + 0.0005 * d)}) for d in range(200)]
    store.write_features("BTCUSDT", btc_rows)

    m = edge_metrics_from_store(store, "unlock", unlock_events=events)
    store.close()

    assert m["available"] is True
    assert m["days"] >= 0
    assert math.isfinite(m["sharpe"])


def test_unlock_na_when_calendar_absent() -> None:
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_momentum(store, n_symbols=4)
    # No unlock_events injected AND no calendar file loader provided → N/A, not a fabricated number.
    m = edge_metrics_from_store(store, "unlock", unlock_events=None, load_unlock_events=None)
    store.close()

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


# --- stablecoin ---------------------------------------------------------------------------------

def test_stablecoin_na_when_no_spot_panel() -> None:
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_momentum(store, n_symbols=4)   # no stablecoin spot closes in the perp warehouse
    m = edge_metrics_from_store(store, "stablecoin_rotation")
    store.close()

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


def test_stablecoin_with_injected_spot_panel_computes() -> None:
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_momentum(store, n_symbols=4)
    # A stablecoin spot panel with a clear "rich" (>1.0) peg deviation that mean-reverts.
    spot_panel = {
        "FDUSDUSDT": {d: (1.01 if d % 2 == 0 else 1.0) for d in range(40)},
        "TUSDUSDT": {d: (1.008 if d % 2 == 0 else 1.0) for d in range(40)},
    }
    m = edge_metrics_from_store(store, "stablecoin_rotation", stablecoin_spot_panel=spot_panel)
    store.close()

    assert m["available"] is True
    assert m["symbols"] == 2
    assert math.isfinite(m["sharpe"])


# --- xsfunding ----------------------------------------------------------------------------------

def test_xsfunding_reuses_carry_metrics() -> None:
    from fxhnt.application.bybit_carry_revalidate import carry_metrics_from_store

    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_carry(store)
    via_edge = edge_metrics_from_store(store, "xsfunding", cost_bps=5.5)
    via_carry = carry_metrics_from_store(store, cost_bps=5.5)
    store.close()

    assert via_edge["sharpe"] == via_carry["sharpe"]
    assert via_edge["total_return"] == via_carry["total_return"]
    assert via_edge["symbols"] == via_carry["symbols"]


# --- evaluate_all_edges -------------------------------------------------------------------------

def test_evaluate_all_edges_returns_keyed_dict() -> None:
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_momentum(store, n_symbols=6, days=200)
    _seed_carry(store)   # adds AAAUSDT.. with funding+spot_close for xsfunding
    out = evaluate_all_edges(store, cost_bps=5.5)
    store.close()

    for edge in ("tstrend", "unlock", "stablecoin_rotation", "xsfunding", "combined"):
        assert edge in out
        assert isinstance(out[edge], dict)
    # tstrend + xsfunding have data → available; unlock/stablecoin N/A without external inputs.
    assert out["tstrend"]["available"] is True
    assert out["xsfunding"]["available"] is True
    assert out["unlock"]["available"] is False
    assert out["stablecoin_rotation"]["available"] is False
    # The combined book is reported (equal-weight over the available edges).
    assert out["combined"]["available"] is True


def test_evaluate_all_edges_is_read_only() -> None:
    inner = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_momentum(inner, n_symbols=6, days=200)
    _seed_carry(inner)
    guarded = _WriteTripwireStore(inner)
    out = evaluate_all_edges(guarded, cost_bps=5.5)   # must not trip the write tripwire
    inner.close()
    assert "combined" in out


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

def test_cli_bybit_edges_eval_prints_table(monkeypatch) -> None:
    """CLI smoke: a seeded in-memory store stands in for the live Bybit warehouse (no network), and the
    command prints the per-edge table including tstrend + xsfunding + the combined row."""
    from typer.testing import CliRunner

    import fxhnt.cli as cli

    seeded = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_momentum(seeded, n_symbols=6, days=200)
    _seed_carry(seeded)

    # Make the command construct OUR seeded store instead of connecting to the operational DSN, and stub the
    # external loader so NO network is touched (unlock calendar absent → N/A). stablecoin_rotation is N/A
    # unconditionally — Phase 0b venue consolidation retired the CLI's Binance-fed stablecoin spot fetch.
    monkeypatch.setattr(
        "fxhnt.adapters.warehouse.timescale_feature_store.TimescaleFeatureStore",
        lambda *a, **k: seeded, raising=False)

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

    assert result.exit_code == 0, result.output
    assert "Bybit multi-edge evaluation" in result.output
    assert "tstrend" in result.output
    assert "xsfunding" in result.output
    assert "combined" in result.output
