from __future__ import annotations

from datetime import datetime, timedelta, timezone

import polars as pl

from uforia.analytics import (
    build_funding_context_series,
    build_gamma_exposure_series,
    build_options_adaptive_hedge_series,
    build_options_carry_series,
    build_options_dispersion_series,
    build_options_dislocation_series,
    build_options_expiry_flow_series,
    build_options_funding_skew_series,
    build_options_ml_carry_series,
    build_options_positioning_series,
    build_options_relative_value_series,
    build_options_sessionality_series,
    build_options_signal_series,
    build_options_skew_series,
    build_options_surface_snapshot,
    build_options_surface_factor_series,
    build_straddle_breakeven_series,
    build_term_structure_momentum_series,
    build_vol_cone_series,
    build_vol_regime_transition_series,
)
from uforia.storage import DATASET_SPECS, DatasetName, DatasetStore
from tests.support import seed_options_context


def _seed_sessionality_clock_fixture(store: DatasetStore) -> None:
    rows = []
    surface_rows = []
    for ts, spot in (
        (datetime(2026, 3, 6, 8, tzinfo=timezone.utc), 70_000.0),
        (datetime(2026, 3, 7, 8, tzinfo=timezone.utc), 70_500.0),
    ):
        rows.append(
            {
                "ts": ts,
                "date": ts.date(),
                "venue": "deribit",
                "underlying": "BTC",
                "expiry": ts + timedelta(days=7),
                "instrument_name": f"BTC-{ts.isoformat()}-ATM-call",
                "option_type": "call",
                "strike": spot,
                "bid": 0.024,
                "ask": 0.026,
                "mark": 0.025,
                "mid": 0.025,
                "mark_iv": 0.55,
                "delta": 0.5,
                "gamma": 0.02,
                "vega": 0.1,
                "index_price": spot,
                "underlying_price": spot,
                "forward": spot,
                "interest_rate": 0.0,
                "open_interest": 100.0,
                "volume": 10.0,
                "quote_age_ms": 100,
                "quality_flag": "ok",
            }
        )
        surface_rows.append(
            {
                "ts": ts,
                "date": ts.date(),
                "underlying": "BTC",
                "source": "ssvi",
                "atm_iv_7d": 0.55,
                "atm_iv_14d": 0.56,
                "atm_iv_30d": 0.58,
                "atm_iv_60d": 0.60,
                "rr_25_7d": -0.05,
                "rr_25_30d": -0.03,
                "bf_25_7d": 0.02,
                "bf_25_30d": 0.03,
                "term_slope_7_30": -0.03,
                "term_slope_30_60": -0.02,
                "fit_quality": 0.001,
                "coverage_ratio": 0.8,
                "maturity_violations": 0,
                "fit_validity_flag": "ok",
                "quality_flag": "ok",
            }
        )
    store.write(DatasetName.OPTION_CHAIN_SNAPSHOT, pl.DataFrame(rows))
    store.write(DatasetName.SURFACE_FEATURES, pl.DataFrame(surface_rows))


def test_options_family_builders_produce_rows(tmp_path) -> None:
    store = DatasetStore(tmp_path)
    seed_options_context(store)

    surface_snapshot = build_options_surface_snapshot(
        store,
        start=datetime(2026, 3, 1, tzinfo=timezone.utc),
        end=datetime(2026, 3, 3, tzinfo=timezone.utc),
        underlying="BTC",
    )
    carry = build_options_carry_series(
        store,
        start=datetime(2026, 3, 1, tzinfo=timezone.utc),
        end=datetime(2026, 3, 3, tzinfo=timezone.utc),
        underlying="BTC",
    )
    skew = build_options_skew_series(
        store,
        start=datetime(2026, 3, 1, tzinfo=timezone.utc),
        end=datetime(2026, 3, 3, tzinfo=timezone.utc),
        underlying="BTC",
    )
    dislocation = build_options_dislocation_series(
        store,
        start=datetime(2026, 3, 1, tzinfo=timezone.utc),
        end=datetime(2026, 3, 3, tzinfo=timezone.utc),
        underlying="BTC",
    )
    positioning = build_options_positioning_series(
        store,
        start=datetime(2026, 3, 1, tzinfo=timezone.utc),
        end=datetime(2026, 3, 3, tzinfo=timezone.utc),
        underlying="BTC",
    )
    vol_cone = build_vol_cone_series(
        store,
        start=datetime(2026, 3, 1, tzinfo=timezone.utc),
        end=datetime(2026, 3, 3, tzinfo=timezone.utc),
        underlying="BTC",
    )
    term_momentum = build_term_structure_momentum_series(
        store,
        start=datetime(2026, 3, 1, tzinfo=timezone.utc),
        end=datetime(2026, 3, 3, tzinfo=timezone.utc),
        underlying="BTC",
    )
    gamma = build_gamma_exposure_series(
        store,
        start=datetime(2026, 3, 1, tzinfo=timezone.utc),
        end=datetime(2026, 3, 3, tzinfo=timezone.utc),
        underlying="BTC",
    )
    funding = build_funding_context_series(
        store,
        start=datetime(2026, 3, 1, tzinfo=timezone.utc),
        end=datetime(2026, 3, 3, tzinfo=timezone.utc),
        underlying="BTC",
    )
    breakeven = build_straddle_breakeven_series(
        store,
        start=datetime(2026, 3, 1, tzinfo=timezone.utc),
        end=datetime(2026, 3, 3, tzinfo=timezone.utc),
        underlying="BTC",
    )
    surface_factor = build_options_surface_factor_series(
        store,
        start=datetime(2026, 3, 1, tzinfo=timezone.utc),
        end=datetime(2026, 3, 3, tzinfo=timezone.utc),
        underlying="BTC",
    )
    ml_carry = build_options_ml_carry_series(
        store,
        start=datetime(2026, 3, 1, tzinfo=timezone.utc),
        end=datetime(2026, 3, 3, tzinfo=timezone.utc),
        underlying="BTC",
    )
    expiry_flow = build_options_expiry_flow_series(
        store,
        start=datetime(2026, 3, 1, tzinfo=timezone.utc),
        end=datetime(2026, 3, 3, tzinfo=timezone.utc),
        underlying="BTC",
    )
    adaptive = build_options_adaptive_hedge_series(
        store,
        start=datetime(2026, 3, 1, tzinfo=timezone.utc),
        end=datetime(2026, 3, 3, tzinfo=timezone.utc),
        underlying="BTC",
    )
    sessionality = build_options_sessionality_series(
        store,
        start=datetime(2026, 3, 1, tzinfo=timezone.utc),
        end=datetime(2026, 3, 3, tzinfo=timezone.utc),
        underlying="BTC",
    )
    funding_skew = build_options_funding_skew_series(
        store,
        start=datetime(2026, 3, 1, tzinfo=timezone.utc),
        end=datetime(2026, 3, 3, tzinfo=timezone.utc),
        underlying="BTC",
    )

    assert surface_snapshot.height > 0
    assert carry.height > 0
    assert skew.height > 0
    assert dislocation.height > 0
    assert positioning.height > 0
    assert vol_cone.height > 0
    assert term_momentum.height > 0
    assert gamma.height > 0
    assert funding.height > 0
    assert breakeven.height > 0
    assert surface_factor.height > 0
    assert ml_carry.height > 0
    assert expiry_flow.height > 0
    assert adaptive.height > 0
    assert sessionality.height > 0
    assert funding_skew.height > 0
    assert adaptive["route_preference"].null_count() < adaptive.height
    assert adaptive["route_score"].null_count() < adaptive.height

    store.write(DatasetName.OPTIONS_CARRY_SERIES, carry)
    store.write(
        DatasetName.OPTIONS_CARRY_SERIES,
        build_options_carry_series(
            store,
            start=datetime(2026, 3, 1, tzinfo=timezone.utc),
            end=datetime(2026, 3, 3, tzinfo=timezone.utc),
            underlying="ETH",
        ),
    )
    store.write(DatasetName.OPTIONS_SKEW_SERIES, skew)
    store.write(
        DatasetName.OPTIONS_SKEW_SERIES,
        build_options_skew_series(
            store,
            start=datetime(2026, 3, 1, tzinfo=timezone.utc),
            end=datetime(2026, 3, 3, tzinfo=timezone.utc),
            underlying="ETH",
        ),
    )
    store.write(DatasetName.OPTIONS_DISLOCATION_SERIES, dislocation)
    store.write(DatasetName.VOL_CONE_SERIES, vol_cone)
    store.write(DatasetName.TERM_STRUCTURE_MOMENTUM_SERIES, term_momentum)
    store.write(DatasetName.GAMMA_EXPOSURE_SERIES, gamma)
    store.write(DatasetName.FUNDING_CONTEXT_SERIES, funding)
    store.write(DatasetName.STRADDLE_BREAKEVEN_SERIES, breakeven)
    store.write(DatasetName.OPTIONS_SURFACE_FACTOR_SERIES, surface_factor)
    store.write(DatasetName.OPTIONS_ML_CARRY_SERIES, ml_carry)
    store.write(DatasetName.OPTIONS_EXPIRY_FLOW_SERIES, expiry_flow)
    store.write(DatasetName.OPTIONS_ADAPTIVE_HEDGE_SERIES, adaptive)
    store.write(DatasetName.OPTIONS_SESSIONALITY_SERIES, sessionality)
    store.write(DatasetName.OPTIONS_FUNDING_SKEW_SERIES, funding_skew)
    store.write(
        DatasetName.OPTIONS_DISLOCATION_SERIES,
        build_options_dislocation_series(
            store,
            start=datetime(2026, 3, 1, tzinfo=timezone.utc),
            end=datetime(2026, 3, 3, tzinfo=timezone.utc),
            underlying="ETH",
        ),
    )

    relative = build_options_relative_value_series(
        store,
        start=datetime(2026, 3, 1, tzinfo=timezone.utc),
        end=datetime(2026, 3, 3, tzinfo=timezone.utc),
    )
    store.write(DatasetName.OPTIONS_RELATIVE_VALUE_SERIES, relative)
    dispersion = build_options_dispersion_series(
        store,
        start=datetime(2026, 3, 1, tzinfo=timezone.utc),
        end=datetime(2026, 3, 3, tzinfo=timezone.utc),
    )
    store.write(DatasetName.OPTIONS_DISPERSION_SERIES, dispersion)
    signals = build_options_signal_series(
        store,
        start=datetime(2026, 3, 1, tzinfo=timezone.utc),
        end=datetime(2026, 3, 3, tzinfo=timezone.utc),
        underlying="BTC",
    )
    store.write(DatasetName.OPTIONS_SIGNAL_SERIES, signals)
    transitions = build_vol_regime_transition_series(
        store,
        start=datetime(2026, 3, 1, tzinfo=timezone.utc),
        end=datetime(2026, 3, 3, tzinfo=timezone.utc),
        underlying="BTC",
    )

    assert relative.height > 0
    assert dispersion.height > 0
    assert signals.height > 0
    assert transitions.height > 0
    assert "vol_carry_score" in signals.columns
    assert "signal_state" in signals.columns
    assert "funding_vol_context_score" in signals.columns
    assert "surface_factor_score" in signals.columns
    assert "ml_carry_toxicity_score" in signals.columns
    assert "dispersion_score" in signals.columns
    assert "vol_regime_long_threshold" in signals.columns
    assert signals["vol_regime_long_threshold"].null_count() < signals.height
    assert signals["signal_state"].null_count() < signals.height


def test_adaptive_hedge_prefers_microstructure_route_inputs(tmp_path) -> None:
    store = DatasetStore(tmp_path)
    seed_options_context(store)

    btc = build_options_adaptive_hedge_series(
        store,
        start=datetime(2026, 3, 1, tzinfo=timezone.utc),
        end=datetime(2026, 3, 3, tzinfo=timezone.utc),
        underlying="BTC",
    )
    eth = build_options_adaptive_hedge_series(
        store,
        start=datetime(2026, 3, 1, tzinfo=timezone.utc),
        end=datetime(2026, 3, 3, tzinfo=timezone.utc),
        underlying="ETH",
    )

    assert btc.filter(pl.col("route_preference") == "nearby_future").height > 0
    assert eth.filter(pl.col("route_preference") == "perp").height > 0
    assert btc["basis_mismatch_bps"].null_count() < btc.height
    assert eth["route_score"].null_count() < eth.height


def test_ml_carry_uses_forecast_rv_not_raw_carry_zscore(tmp_path) -> None:
    store = DatasetStore(tmp_path)
    seed_options_context(store)

    carry = build_options_carry_series(
        store,
        start=datetime(2026, 3, 1, tzinfo=timezone.utc),
        end=datetime(2026, 3, 3, tzinfo=timezone.utc),
        underlying="BTC",
    )
    store.write(DatasetName.OPTIONS_CARRY_SERIES, carry)

    ml_carry = build_options_ml_carry_series(
        store,
        start=datetime(2026, 3, 1, tzinfo=timezone.utc),
        end=datetime(2026, 3, 3, tzinfo=timezone.utc),
        underlying="BTC",
    )

    joined = ml_carry.join(
        carry.select(["ts", "carry_spread_7d", "realized_vol_7d"]),
        on="ts",
        how="inner",
    ).drop_nulls(["gross_carry_spread", "carry_spread_7d", "forecast_rv_7d", "realized_vol_7d"])

    assert joined.height > 0
    assert (
        joined.filter(
            (
                pl.col("gross_carry_spread")
                - (
                    pl.col("carry_spread_7d")
                    + pl.col("realized_vol_7d")
                    - pl.col("forecast_rv_7d")
                )
            ).abs()
            < 1e-9
        ).height
        > 0
    )


def test_sessionality_uses_correct_weekend_and_expiry_clock_flags(tmp_path) -> None:
    store = DatasetStore(tmp_path)
    _seed_sessionality_clock_fixture(store)

    sessionality = build_options_sessionality_series(
        store,
        start=datetime(2026, 3, 6, tzinfo=timezone.utc),
        end=datetime(2026, 3, 8, tzinfo=timezone.utc),
        underlying="BTC",
    )

    friday = sessionality.filter(pl.col("ts") == datetime(2026, 3, 6, 8, tzinfo=timezone.utc))
    saturday = sessionality.filter(pl.col("ts") == datetime(2026, 3, 7, 8, tzinfo=timezone.utc))

    assert friday.height == 1
    assert saturday.height == 1
    assert friday.item(0, "is_weekend") is False
    assert friday.item(0, "is_expiry_window") is True
    assert saturday.item(0, "is_weekend") is True
    assert saturday.item(0, "is_expiry_window") is False


def test_dispersion_uses_iv_comovement_proxy_not_iv_level_similarity(tmp_path) -> None:
    store = DatasetStore(tmp_path)
    seed_options_context(store)

    eth_surface = store.read(
        DatasetName.SURFACE_FEATURES,
        filters={"underlying": "ETH", "source": "ssvi"},
    ).with_columns(
        (pl.col("atm_iv_7d") * 1.25).alias("atm_iv_7d"),
        (pl.col("atm_iv_14d") * 1.25).alias("atm_iv_14d"),
        (pl.col("atm_iv_30d") * 1.25).alias("atm_iv_30d"),
        (pl.col("atm_iv_60d") * 1.25).alias("atm_iv_60d"),
    )
    store.write(DatasetName.SURFACE_FEATURES, eth_surface)

    dispersion = build_options_dispersion_series(
        store,
        start=datetime(2026, 3, 1, tzinfo=timezone.utc),
        end=datetime(2026, 3, 3, tzinfo=timezone.utc),
    )
    latest = (
        dispersion.filter(pl.col("tenor") == "7d")
        .drop_nulls(["implied_correlation"])
        .sort("ts")
        .tail(1)
    )

    assert latest.height == 1
    old_similarity = 1.0 - (
        abs(latest.item(0, "btc_atm_iv") - latest.item(0, "eth_atm_iv"))
        / (latest.item(0, "btc_atm_iv") + latest.item(0, "eth_atm_iv"))
    )
    assert abs(latest.item(0, "implied_correlation") - old_similarity) > 0.5


def test_expiry_flow_marks_missing_greeks_as_partial_context(tmp_path) -> None:
    store = DatasetStore(tmp_path)
    seed_options_context(store)

    btc_options = store.read(
        DatasetName.OPTION_CHAIN_SNAPSHOT,
        filters={"underlying": "BTC"},
    ).with_columns(
        pl.when(pl.col("option_type") == "put")
        .then(pl.lit(None, dtype=pl.Float64))
        .otherwise(pl.col("gamma"))
        .alias("gamma")
    )
    store.write(DatasetName.OPTION_CHAIN_SNAPSHOT, btc_options)

    expiry_flow = build_options_expiry_flow_series(
        store,
        start=datetime(2026, 3, 1, tzinfo=timezone.utc),
        end=datetime(2026, 3, 3, tzinfo=timezone.utc),
        underlying="BTC",
    )

    assert expiry_flow.height > 0
    assert expiry_flow.filter(pl.col("quality_flag") == "partial_context").height > 0
    assert expiry_flow.filter(pl.col("quality_flag") == "ok").height < expiry_flow.height


def test_read_surface_recovers_missing_latest_rows_from_option_chain(tmp_path) -> None:
    seed_store = DatasetStore(tmp_path / "seed")
    store = DatasetStore(tmp_path / "target")
    seed_options_context(seed_store)

    eth_options = seed_store.read(
        DatasetName.OPTION_CHAIN_SNAPSHOT,
        filters={"underlying": "ETH"},
    )
    eth_surface = (
        seed_store.read(
            DatasetName.SURFACE_FEATURES,
            filters={"underlying": "ETH", "source": "ssvi"},
        )
        .sort("ts")
        .head(70)
    )

    store.write(DatasetName.OPTION_CHAIN_SNAPSHOT, eth_options)
    store.write(DatasetName.SURFACE_FEATURES, eth_surface)

    carry = build_options_carry_series(
        store,
        start=datetime(2026, 3, 1, tzinfo=timezone.utc),
        end=datetime(2026, 3, 4, tzinfo=timezone.utc),
        underlying="ETH",
    )

    assert carry.height > 0
    assert carry.sort("ts").tail(1).item(0, "ts") == eth_options.sort("ts").tail(1).item(0, "ts")


def test_vol_regime_transitions_emit_current_rows_when_signal_state_is_missing(tmp_path) -> None:
    store = DatasetStore(tmp_path)
    spec = DATASET_SPECS[DatasetName.OPTIONS_SIGNAL_SERIES].schema
    rows: list[dict[str, object]] = []
    base = datetime(2026, 3, 1, 0, tzinfo=timezone.utc)
    for hour in range(3):
        ts = base + timedelta(hours=hour)
        row = {column: None for column in spec}
        row.update(
            {
                "ts": ts,
                "date": ts.date(),
                "underlying": "BTC",
                "source": "options_composite",
                "quality_flag": "insufficient_data",
                "signal_state": None,
            }
        )
        rows.append(row)
    store.write(
        DatasetName.OPTIONS_SIGNAL_SERIES,
        pl.DataFrame(rows, schema_overrides=spec),
    )

    transitions = build_vol_regime_transition_series(
        store,
        start=base,
        end=base + timedelta(hours=3),
        underlying="BTC",
    )

    assert transitions.height == 3
    assert transitions.sort("ts").tail(1).item(0, "ts") == base + timedelta(hours=2)
    assert transitions["quality_flag"].to_list() == ["insufficient_data"] * 3


def test_adaptive_hedge_falls_back_without_microstructure_route_inputs(tmp_path) -> None:
    store = DatasetStore(tmp_path)
    seed_options_context(store, include_microstructure=False)

    adaptive = build_options_adaptive_hedge_series(
        store,
        start=datetime(2026, 3, 1, tzinfo=timezone.utc),
        end=datetime(2026, 3, 3, tzinfo=timezone.utc),
        underlying="BTC",
    )

    assert adaptive.height > 0
    assert adaptive["route_preference"].drop_nulls().unique().to_list() == ["perp"]
    assert adaptive["basis_mismatch_bps"].drop_nulls().unique().to_list() == [15.0]
