from __future__ import annotations

from datetime import datetime, timezone

import polars as pl

from uforia.analytics.composite_family import (
    _apply_directional_calibration,
    build_composite_component_series,
    build_composite_signal_series,
    build_composite_transition_series,
    build_market_composite_signal_series,
)
from uforia.storage import DatasetName, DatasetStore

from .support import seed_options_context


def test_composite_builds_asset_and_market_indices(tmp_path) -> None:
    store = DatasetStore(tmp_path)
    seed_options_context(store)
    start = datetime(2026, 3, 2, 0, tzinfo=timezone.utc)
    end = datetime(2026, 3, 3, 23, tzinfo=timezone.utc)

    btc = build_composite_signal_series(store, start, end, asset="BTC")
    eth = build_composite_signal_series(store, start, end, asset="ETH")
    store.write(DatasetName.COMPOSITE_SIGNAL_SERIES, btc)
    store.write(DatasetName.COMPOSITE_SIGNAL_SERIES, eth)
    market = build_market_composite_signal_series(store, start, end)

    assert btc.height > 0
    assert eth.height > 0
    assert market.height > 0
    assert {"sentiment_index", "directional_tilt", "fragility_index", "confidence_score"} <= set(
        btc.columns
    )
    assert market.get_column("asset").unique().to_list() == ["MARKET"]
    assert market.get_column("scope").unique().to_list() == ["market"]


def test_composite_component_contributions_and_regimes(tmp_path) -> None:
    store = DatasetStore(tmp_path)
    seed_options_context(store)
    start = datetime(2026, 3, 2, 0, tzinfo=timezone.utc)
    end = datetime(2026, 3, 3, 23, tzinfo=timezone.utc)

    signals = build_composite_signal_series(store, start, end, asset="BTC")
    store.write(DatasetName.COMPOSITE_SIGNAL_SERIES, signals)
    components = build_composite_component_series(store, start, end, asset="BTC")
    transitions = build_composite_transition_series(store, start, end, asset="BTC")

    assert components.height > 0
    assert transitions.height > 0
    assert set(components.get_column("pillar").unique().to_list()) == {
        "sentiment",
        "directional",
        "fragility",
    }
    assert signals.get_column("sentiment_regime").drop_nulls().len() > 0
    assert signals.get_column("directional_regime").drop_nulls().len() > 0
    assert signals.get_column("fragility_regime").drop_nulls().len() > 0


def test_composite_coverage_shrinks_without_microstructure(tmp_path) -> None:
    store = DatasetStore(tmp_path)
    seed_options_context(store, include_microstructure=False)
    start = datetime(2026, 3, 2, 0, tzinfo=timezone.utc)
    end = datetime(2026, 3, 3, 23, tzinfo=timezone.utc)

    frame = build_composite_signal_series(store, start, end, asset="BTC")

    assert frame.height > 0
    assert frame.get_column("sentiment_index").drop_nulls().len() > 0
    assert frame.get_column("fragility_index").drop_nulls().len() > 0
    assert frame.get_column("coverage_ratio").mean() < 1.0
    assert frame.get_column("confidence_score").mean() < 0.8
    assert set(frame.get_column("quality_flag").unique().to_list()) <= {
        "ok",
        "partial_context",
        "insufficient_data",
    }


def test_composite_sparse_history_publishes_sentiment_and_fragility(tmp_path) -> None:
    store = DatasetStore(tmp_path)
    seed_options_context(store)
    start = datetime(2026, 3, 1, 0, tzinfo=timezone.utc)
    end = datetime(2026, 3, 1, 12, tzinfo=timezone.utc)

    frame = build_composite_signal_series(store, start, end, asset="BTC")

    assert frame.height > 0
    publishable = frame.filter(
        pl.col("sentiment_index").is_not_null() & pl.col("fragility_index").is_not_null()
    )
    assert publishable.height > 0
    assert publishable.get_column("confidence_score").max() < 0.8
    assert set(publishable.get_column("quality_flag").unique().to_list()) <= {
        "insufficient_data",
        "partial_context",
        "ok",
    }


def test_composite_sparse_history_confidence_is_lower_than_full_history(tmp_path) -> None:
    store = DatasetStore(tmp_path)
    seed_options_context(store)
    sparse_start = datetime(2026, 3, 1, 0, tzinfo=timezone.utc)
    sparse_end = datetime(2026, 3, 1, 12, tzinfo=timezone.utc)
    full_start = datetime(2026, 3, 2, 0, tzinfo=timezone.utc)
    full_end = datetime(2026, 3, 3, 23, tzinfo=timezone.utc)

    sparse = build_composite_signal_series(store, sparse_start, sparse_end, asset="BTC")
    full = build_composite_signal_series(store, full_start, full_end, asset="BTC")

    sparse_publishable = sparse.filter(pl.col("sentiment_index").is_not_null())
    full_publishable = full.filter(pl.col("sentiment_index").is_not_null())

    assert sparse_publishable.height > 0
    assert full_publishable.height > 0
    assert sparse_publishable.get_column("confidence_score").mean() < full_publishable.get_column(
        "confidence_score"
    ).mean()


def test_composite_missing_inputs_stay_null(tmp_path) -> None:
    store = DatasetStore(tmp_path)
    start = datetime(2026, 3, 2, 0, tzinfo=timezone.utc)
    end = datetime(2026, 3, 2, 12, tzinfo=timezone.utc)

    frame = build_composite_signal_series(store, start, end, asset="BTC")

    assert frame.is_empty()


def test_directional_calibration_tolerates_missing_micro_columns() -> None:
    ts_values = [datetime(2026, 3, 2, hour, tzinfo=timezone.utc) for hour in range(12)]
    signal_frame = pl.DataFrame(
        {
            "ts": ts_values,
            "directional_tilt": [float(hour) for hour in range(12)],
            "coverage_ratio": [0.8] * 12,
        }
    )
    raw_frame = pl.DataFrame(
        {
            "ts": ts_values,
            "next_return_bps": [float(hour - 6) for hour in range(12)],
        }
    )

    calibrated = _apply_directional_calibration(signal_frame, raw_frame)

    assert calibrated.height == signal_frame.height
    assert "directional_tilt" in calibrated.columns


def test_market_composite_uses_fixed_btc_eth_weights(tmp_path) -> None:
    store = DatasetStore(tmp_path)
    ts = datetime(2026, 3, 3, 12, tzinfo=timezone.utc)
    store.write(
        DatasetName.COMPOSITE_SIGNAL_SERIES,
        pl.DataFrame(
            [
                {
                    "ts": ts,
                    "date": ts.date(),
                    "scope": "asset",
                    "asset": "BTC",
                    "source": "composite_v1",
                    "sentiment_index": 80.0,
                    "directional_tilt": 20.0,
                    "fragility_index": 40.0,
                    "confidence_score": 0.9,
                    "options_sentiment_subindex": 0.6,
                    "perps_sentiment_subindex": 0.5,
                    "micro_sentiment_subindex": 0.2,
                    "micro_direction_subindex": 0.3,
                    "perps_direction_subindex": 0.2,
                    "options_direction_subindex": 0.1,
                    "perps_fragility_subindex": 0.4,
                    "options_fragility_subindex": 0.2,
                    "micro_fragility_subindex": 0.1,
                    "coverage_ratio": 1.0,
                    "freshness_score": 1.0,
                    "calibrator_mode": "fixed_fallback",
                    "sentiment_regime": "greed",
                    "directional_regime": "mild_bullish",
                    "fragility_regime": "watch",
                    "quality_flag": "ok",
                },
                {
                    "ts": ts,
                    "date": ts.date(),
                    "scope": "asset",
                    "asset": "ETH",
                    "source": "composite_v1",
                    "sentiment_index": 20.0,
                    "directional_tilt": -20.0,
                    "fragility_index": 60.0,
                    "confidence_score": 0.5,
                    "options_sentiment_subindex": -0.6,
                    "perps_sentiment_subindex": -0.5,
                    "micro_sentiment_subindex": -0.2,
                    "micro_direction_subindex": -0.3,
                    "perps_direction_subindex": -0.2,
                    "options_direction_subindex": -0.1,
                    "perps_fragility_subindex": 0.6,
                    "options_fragility_subindex": 0.4,
                    "micro_fragility_subindex": 0.2,
                    "coverage_ratio": 1.0,
                    "freshness_score": 1.0,
                    "calibrator_mode": "fixed_fallback",
                    "sentiment_regime": "fear",
                    "directional_regime": "mild_bearish",
                    "fragility_regime": "fragile",
                    "quality_flag": "ok",
                },
            ]
        ),
    )

    market = build_market_composite_signal_series(store, ts, ts)
    row = market.to_dicts()[0]

    assert row["sentiment_index"] == 59.0
    assert row["directional_tilt"] == 6.0
    assert row["fragility_index"] == 47.0
