from __future__ import annotations

from datetime import datetime, timedelta, timezone

import httpx
import polars as pl
from fastapi.testclient import TestClient

from uforia.api.app import create_app
from uforia.api.providers.default import (
    DefaultDatasetProvider,
    DefaultOptionsDashboardProvider,
    DefaultPriceProvider,
    ReadCache,
)
from uforia.api.settings import ApiSettings
from uforia.storage import DatasetName, DatasetStore
from uforia.storage.datasets import empty_frame


def test_api_endpoints_return_signal_and_run_data(tmp_path) -> None:
    store = DatasetStore(tmp_path)
    base = datetime(2026, 3, 1, 12, tzinfo=timezone.utc)
    store.write(
        DatasetName.DVOL_SNAPSHOT,
        pl.DataFrame(
            [
                {
                    "ts": base,
                    "date": base.date(),
                    "venue": "deribit",
                    "underlying": "BTC",
                    "source": "dvol",
                    "dvol_open": 50.0,
                    "dvol_high": 55.0,
                    "dvol_low": 49.0,
                    "dvol_close": 54.0,
                    "quality_flag": "ok",
                }
            ]
        ),
    )
    store.write(
        DatasetName.VVIX_SERIES,
        pl.DataFrame(
            [
                {
                    "ts": base,
                    "date": base.date(),
                    "underlying": "BTC",
                    "source": "dvol",
                    "vol_index_value": 54.0,
                    "rvvix_7d": 90.0,
                    "rvvix_14d": 80.0,
                    "rvvix_30d": 70.0,
                    "qvvix_30d": 65.0,
                    "model_version": "qvvix-var-v1",
                    "quality_flag": "ok",
                }
            ]
        ),
    )
    store.write(
        DatasetName.SURFACE_FEATURES,
        pl.DataFrame(
            [
                {
                    "ts": base,
                    "date": base.date(),
                    "underlying": "BTC",
                    "source": "ssvi",
                    "atm_iv_7d": 0.5,
                    "atm_iv_14d": 0.52,
                    "atm_iv_30d": 0.55,
                    "atm_iv_60d": 0.58,
                    "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.05,
                    "term_slope_30_60": -0.03,
                    "fit_quality": 0.01,
                    "coverage_ratio": 0.8,
                    "maturity_violations": 0,
                    "fit_validity_flag": "ok",
                    "quality_flag": "ok",
                }
            ]
        ),
    )
    store.write(
        DatasetName.INGESTION_RUNS,
        pl.DataFrame(
            [
                {
                    "ts": base,
                    "date": base.date(),
                    "dataset": DatasetName.OPTION_CHAIN_SNAPSHOT.value,
                    "venue": "deribit",
                    "underlying": "BTC",
                    "status": "success",
                    "rows_written_raw": 1,
                    "rows_written_normalized": 2,
                    "error_message": None,
                    "duration_ms": 100,
                }
            ]
        ),
        logical_ts=base,
    )
    app = create_app(settings=ApiSettings(data_root=tmp_path), store=store)
    client = TestClient(app)

    latest = client.get("/api/v1/signals/btc_vvix/latest", params={"source": "dvol"})
    series = client.get(
        "/api/v1/signals/btc_vvix/series",
        params={
            "source": "dvol",
            "start": (base - timedelta(days=1)).isoformat(),
            "end": base.isoformat(),
        },
    )
    runs = client.get("/api/v1/runs")
    health = client.get("/api/v1/system/health")

    assert latest.status_code == 200
    assert latest.json()["data"]["latest_values"]["dvol"] == 54.0
    assert series.status_code == 200
    assert len(series.json()["data"]["points"]) == 1
    assert client.get("/api/v1/signals").status_code == 200
    assert runs.status_code == 200
    assert runs.json()["data"][0]["status"] == "success"
    assert health.status_code == 200
    assert health.json()["data"]["status"] == "ok"


def test_api_websocket_stream_emits_updates(tmp_path) -> None:
    store = DatasetStore(tmp_path)
    base = datetime(2026, 3, 1, 12, tzinfo=timezone.utc)
    store.write(
        DatasetName.VVIX_SERIES,
        pl.DataFrame(
            [
                {
                    "ts": base,
                    "date": base.date(),
                    "underlying": "BTC",
                    "source": "dvol",
                    "vol_index_value": 54.0,
                    "rvvix_7d": 90.0,
                    "rvvix_14d": 80.0,
                    "rvvix_30d": 70.0,
                    "qvvix_30d": 65.0,
                    "model_version": "qvvix-var-v1",
                    "quality_flag": "ok",
                }
            ]
        ),
    )
    store.write(
        DatasetName.DVOL_SNAPSHOT,
        pl.DataFrame(
            [
                {
                    "ts": base,
                    "date": base.date(),
                    "venue": "deribit",
                    "underlying": "BTC",
                    "source": "dvol",
                    "dvol_open": 50.0,
                    "dvol_high": 55.0,
                    "dvol_low": 49.0,
                    "dvol_close": 54.0,
                    "quality_flag": "ok",
                }
            ]
        ),
    )
    app = create_app(
        settings=ApiSettings(data_root=tmp_path, stream_interval_seconds=0.01),
        store=store,
    )
    client = TestClient(app)

    with client.websocket_connect("/api/v1/stream") as websocket:
        message = websocket.receive_json()

    assert message["event"] in {
        "signal.updated",
        "dataset.updated",
        "engine.updated",
        "run.updated",
    }


def test_engines_endpoint_skips_microstructure_reads_when_disabled(tmp_path) -> None:
    store = DatasetStore(tmp_path)
    app = create_app(
        settings=ApiSettings(data_root=tmp_path, microstructure_enabled=False),
        store=store,
    )
    client = TestClient(app)

    engines = client.get("/api/v1/engines")
    health = client.get("/api/v1/system/health")

    assert engines.status_code == 200
    engine_rows = {row["id"]: row for row in engines.json()["data"]}
    assert engine_rows["microstructure_live_engine"]["status"] == "disabled"
    assert health.status_code == 200


def test_options_dashboard_listing_fails_soft_when_one_dashboard_read_breaks(
    tmp_path, monkeypatch
) -> None:
    store = DatasetStore(tmp_path)
    cache = ReadCache(ttl_seconds=1.0)
    provider = DefaultOptionsDashboardProvider(store, cache)
    original = provider.get_latest

    def flaky_get_latest(dashboard_id: str, *, underlying: str | None):  # type: ignore[no-untyped-def]
        if dashboard_id == "vol_regime_transitions":
            raise RuntimeError("boom")
        return original(dashboard_id, underlying=underlying)

    monkeypatch.setattr(provider, "get_latest", flaky_get_latest)

    dashboards = provider.list_dashboards()
    errored = next(item for item in dashboards if item.id == "vol_regime_transitions")

    assert len(dashboards) > 0
    assert errored.latest_quality == "error"
    assert errored.latest_values == {}


def test_options_dashboard_get_latest_uses_latest_logical_slice(tmp_path, monkeypatch) -> None:
    store = DatasetStore(tmp_path)
    base = datetime(2026, 3, 18, 13, tzinfo=timezone.utc)
    cache = ReadCache(ttl_seconds=1.0)
    provider = DefaultOptionsDashboardProvider(store, cache)

    read_calls: list[dict[str, object]] = []

    def fake_read_latest_slice(store_obj, dataset, *, filters=None, columns=None):  # type: ignore[no-untyped-def]
        del store_obj
        read_calls.append({"dataset": dataset, "filters": filters, "columns": columns})
        return pl.DataFrame(
            [
                {
                    "ts": base,
                    "mean_mark_iv": 0.65,
                    "iv_change_24h": -0.02,
                    "option_count": 57.0,
                    "quality_flag": "ok",
                }
            ]
        )

    monkeypatch.setattr(cache, "read_latest_slice", fake_read_latest_slice)

    latest = provider.get_latest("vol_surface", underlying="BTC")

    assert latest.latest_updated_at == base
    assert latest.latest_quality == "ok"
    assert latest.latest_values["mean_mark_iv"] == 0.65
    assert len(read_calls) == 1
    assert read_calls[0]["dataset"] == DatasetName.OPTIONS_SURFACE_SNAPSHOT
    assert read_calls[0]["filters"] == {"source": "surface_monitor", "underlying": "BTC"}
    assert read_calls[0]["columns"] == [
        "ts",
        "quality_flag",
        "mean_mark_iv",
        "iv_change_24h",
        "option_count",
    ]


def test_price_series_falls_back_to_latest_available_bars(tmp_path) -> None:
    store = DatasetStore(tmp_path)
    latest = datetime(2026, 3, 1, 12, tzinfo=timezone.utc)
    earlier = latest - timedelta(minutes=15)
    store.write(
        DatasetName.MICROSTRUCTURE_BAR,
        pl.DataFrame(
            [
                {
                    "ts": earlier,
                    "date": earlier.date(),
                    "asset": "BTC",
                    "venue": "binance_spot",
                    "exchange_symbol": "BTCUSDT",
                    "venue_group": "binance",
                    "market_type": "spot",
                    "is_derivatives": False,
                    "contract_type": "spot",
                    "clock": "event_time",
                    "bar_size_secs": 60,
                    "open": 100.0,
                    "high": 101.0,
                    "low": 99.0,
                    "close": 100.5,
                    "volume": 10.0,
                    "buy_volume": 6.0,
                    "sell_volume": 4.0,
                    "trade_count": 5,
                    "vwap": 100.4,
                    "microprice": 100.45,
                    "spread": 0.5,
                    "depth_bid_5": 1.0,
                    "depth_ask_5": 1.1,
                    "depth_notional_bid_5": 100.5,
                    "depth_notional_ask_5": 110.55,
                    "is_gap": False,
                    "quality_flag": "ok",
                },
                {
                    "ts": latest,
                    "date": latest.date(),
                    "asset": "BTC",
                    "venue": "binance_spot",
                    "exchange_symbol": "BTCUSDT",
                    "venue_group": "binance",
                    "market_type": "spot",
                    "is_derivatives": False,
                    "contract_type": "spot",
                    "clock": "event_time",
                    "bar_size_secs": 60,
                    "open": 100.5,
                    "high": 102.0,
                    "low": 100.0,
                    "close": 101.5,
                    "volume": 12.0,
                    "buy_volume": 7.0,
                    "sell_volume": 5.0,
                    "trade_count": 6,
                    "vwap": 101.2,
                    "microprice": 101.3,
                    "spread": 0.45,
                    "depth_bid_5": 1.2,
                    "depth_ask_5": 1.3,
                    "depth_notional_bid_5": 121.8,
                    "depth_notional_ask_5": 131.95,
                    "is_gap": False,
                    "quality_flag": "ok",
                },
            ]
        ),
    )
    app = create_app(settings=ApiSettings(data_root=tmp_path), store=store)
    client = TestClient(app)

    response = client.get(
        "/api/v1/prices/series",
        params={
            "asset": "BTC",
            "timeframe": "15m",
            "start": "2026-03-05T00:00:00Z",
            "end": "2026-03-05T02:00:00Z",
            "max_points": 10,
        },
    )

    assert response.status_code == 200
    payload = response.json()["data"]
    assert len(payload["points"]) == 2
    assert payload["latest_values"]["close"] == 101.5
    assert payload["context"]["stale"] is True
    assert payload["context"]["latest_available_at"].startswith("2026-03-01T12:00:00")


def test_latest_prices_fall_back_to_microstructure_bars(tmp_path) -> None:
    store = DatasetStore(tmp_path)
    ts = datetime(2026, 3, 1, 12, tzinfo=timezone.utc)
    store.write(
        DatasetName.MICROSTRUCTURE_BAR,
        pl.DataFrame(
            [
                {
                    "ts": ts,
                    "date": ts.date(),
                    "asset": "ETH",
                    "venue": "bybit_spot",
                    "exchange_symbol": "ETHUSDT",
                    "venue_group": "bybit",
                    "market_type": "spot",
                    "is_derivatives": False,
                    "contract_type": "spot",
                    "clock": "event_time",
                    "bar_size_secs": 60,
                    "open": 2000.0,
                    "high": 2010.0,
                    "low": 1995.0,
                    "close": 2005.0,
                    "volume": 3.0,
                    "buy_volume": 2.0,
                    "sell_volume": 1.0,
                    "trade_count": 2,
                    "vwap": 2004.0,
                    "microprice": 2005.1,
                    "spread": 0.3,
                    "depth_bid_5": 2.0,
                    "depth_ask_5": 2.1,
                    "depth_notional_bid_5": 4010.0,
                    "depth_notional_ask_5": 4210.5,
                    "is_gap": False,
                    "quality_flag": "ok",
                }
            ]
        ),
    )
    app = create_app(settings=ApiSettings(data_root=tmp_path), store=store)
    client = TestClient(app)

    response = client.get("/api/v1/prices")

    assert response.status_code == 200
    payload = response.json()["data"]
    assert len(payload) == 1
    assert payload[0]["asset"] == "ETH"
    assert payload[0]["mid_price"] == 2005.0


def test_price_series_reads_latest_logical_bar_rows(tmp_path) -> None:
    store = DatasetStore(tmp_path)
    ts = datetime(2026, 3, 18, 0, 45, tzinfo=timezone.utc)
    first = pl.DataFrame(
        [
            {
                "ts": ts,
                "date": ts.date(),
                "asset": "BTC",
                "venue": "binance_spot",
                "exchange_symbol": "BTCUSDT",
                "venue_group": "binance",
                "market_type": "spot",
                "is_derivatives": False,
                "contract_type": "spot",
                "clock": "event_time",
                "bar_size_secs": 60,
                "open": 100.0,
                "high": 101.0,
                "low": 99.0,
                "close": 100.5,
                "volume": 10.0,
                "buy_volume": 6.0,
                "sell_volume": 4.0,
                "trade_count": 5,
                "vwap": 100.4,
                "microprice": 100.45,
                "spread": 0.5,
                "depth_bid_5": 1.0,
                "depth_ask_5": 1.1,
                "depth_notional_bid_5": 100.5,
                "depth_notional_ask_5": 110.55,
                "is_gap": False,
                "quality_flag": "ok",
            }
        ]
    )
    second = first.with_columns(
        [
            pl.lit(110.0).alias("open"),
            pl.lit(111.0).alias("high"),
            pl.lit(109.0).alias("low"),
            pl.lit(110.5).alias("close"),
            pl.lit(20.0).alias("volume"),
        ]
    )
    store.write(DatasetName.MICROSTRUCTURE_BAR, first, run_ts=ts)
    store.write(DatasetName.MICROSTRUCTURE_BAR, second, run_ts=ts + timedelta(minutes=1))
    app = create_app(settings=ApiSettings(data_root=tmp_path), store=store)
    client = TestClient(app)

    response = client.get(
        "/api/v1/prices/series",
        params={
            "asset": "BTC",
            "venue_scope": "binance",
            "timeframe": "1m",
            "start": ts.isoformat(),
            "end": (ts + timedelta(minutes=1)).isoformat(),
        },
    )

    assert response.status_code == 200
    points = response.json()["data"]["points"]
    assert len(points) == 1
    assert points[0]["open"] == 110.0
    assert points[0]["close"] == 110.5


def test_price_series_backfills_gap_from_exchange_candles(tmp_path, monkeypatch) -> None:
    store = DatasetStore(tmp_path)
    first = datetime(2026, 3, 18, 0, 0, tzinfo=timezone.utc)
    second = first + timedelta(minutes=30)
    store.write(
        DatasetName.MICROSTRUCTURE_BAR,
        pl.DataFrame(
            [
                {
                    "ts": first,
                    "date": first.date(),
                    "asset": "BTC",
                    "venue": "binance_spot",
                    "exchange_symbol": "BTCUSDT",
                    "venue_group": "binance",
                    "market_type": "spot",
                    "is_derivatives": False,
                    "contract_type": "spot",
                    "clock": "event_time",
                    "bar_size_secs": 60,
                    "open": 100.0,
                    "high": 101.0,
                    "low": 99.0,
                    "close": 100.5,
                    "volume": 10.0,
                    "buy_volume": 6.0,
                    "sell_volume": 4.0,
                    "trade_count": 5,
                    "vwap": 100.4,
                    "microprice": 100.45,
                    "spread": 0.5,
                    "depth_bid_5": 1.0,
                    "depth_ask_5": 1.1,
                    "depth_notional_bid_5": 100.5,
                    "depth_notional_ask_5": 110.55,
                    "is_gap": False,
                    "quality_flag": "ok",
                },
                {
                    "ts": second,
                    "date": second.date(),
                    "asset": "BTC",
                    "venue": "binance_spot",
                    "exchange_symbol": "BTCUSDT",
                    "venue_group": "binance",
                    "market_type": "spot",
                    "is_derivatives": False,
                    "contract_type": "spot",
                    "clock": "event_time",
                    "bar_size_secs": 60,
                    "open": 102.0,
                    "high": 103.0,
                    "low": 101.0,
                    "close": 102.5,
                    "volume": 12.0,
                    "buy_volume": 7.0,
                    "sell_volume": 5.0,
                    "trade_count": 6,
                    "vwap": 102.4,
                    "microprice": 102.45,
                    "spread": 0.5,
                    "depth_bid_5": 1.0,
                    "depth_ask_5": 1.1,
                    "depth_notional_bid_5": 102.5,
                    "depth_notional_ask_5": 112.75,
                    "is_gap": False,
                    "quality_flag": "ok",
                },
            ]
        ),
    )

    original_get = httpx.Client.get

    def fake_get(self, url: str, params: dict[str, object], **kwargs: object) -> httpx.Response:
        if "binance.com" not in str(url):
            return original_get(self, url, params=params, **kwargs)
        return httpx.Response(
            200,
            json=[
                [int((first + timedelta(minutes=15)).timestamp() * 1000), "101", "102", "100", "101.5", "5", None, None, 2],
            ],
            request=httpx.Request("GET", url),
        )

    monkeypatch.setattr(httpx.Client, "get", fake_get)
    app = create_app(settings=ApiSettings(data_root=tmp_path), store=store)
    client = TestClient(app)

    response = client.get(
        "/api/v1/prices/series",
        params={
            "asset": "BTC",
            "venue_scope": "binance",
            "timeframe": "15m",
            "start": first.isoformat(),
            "end": second.isoformat(),
        },
    )

    assert response.status_code == 200
    points = response.json()["data"]["points"]
    assert [point["ts"] for point in points] == [
        "2026-03-18T00:00:00Z",
        "2026-03-18T00:15:00Z",
        "2026-03-18T00:30:00Z",
    ]
    assert points[1]["close"] == 101.5


def test_price_series_backfills_gap_for_all_venue_aggregate(tmp_path, monkeypatch) -> None:
    store = DatasetStore(tmp_path)
    first = datetime(2026, 3, 18, 0, 0, tzinfo=timezone.utc)
    second = first + timedelta(minutes=30)
    for venue, venue_group, open_price, close_price in (
        ("binance_spot", "binance", 100.0, 100.5),
        ("bybit_spot", "bybit", 101.0, 101.5),
    ):
        for ts, base_price in ((first, open_price), (second, close_price)):
            store.write(
                DatasetName.MICROSTRUCTURE_BAR,
                pl.DataFrame(
                    [
                        {
                            "ts": ts,
                            "date": ts.date(),
                            "asset": "BTC",
                            "venue": venue,
                            "exchange_symbol": "BTCUSDT",
                            "venue_group": venue_group,
                            "market_type": "spot",
                            "is_derivatives": False,
                            "contract_type": "spot",
                            "clock": "event_time",
                            "bar_size_secs": 60,
                            "open": base_price,
                            "high": base_price + 1.0,
                            "low": base_price - 1.0,
                            "close": base_price + 0.5,
                            "volume": 10.0,
                            "buy_volume": 6.0,
                            "sell_volume": 4.0,
                            "trade_count": 5,
                            "vwap": base_price + 0.4,
                            "microprice": base_price + 0.45,
                            "spread": 0.5,
                            "depth_bid_5": 1.0,
                            "depth_ask_5": 1.1,
                            "depth_notional_bid_5": 100.5,
                            "depth_notional_ask_5": 110.55,
                            "is_gap": False,
                            "quality_flag": "ok",
                        }
                    ]
                ),
            )

    original_get = httpx.Client.get

    def fake_get(self, url: str, params: dict[str, object], **kwargs: object) -> httpx.Response:
        url_str = str(url)
        if "binance.com" in url_str:
            payload = [
                [int((first + timedelta(minutes=15)).timestamp() * 1000), "101", "102", "100", "101.5", "5", None, None, 2],
            ]
            return httpx.Response(200, json=payload, request=httpx.Request("GET", url))
        if "bybit.com" in url_str:
            payload = {
                "result": {
                    "list": [
                        [str(int((first + timedelta(minutes=15)).timestamp() * 1000)), "103", "104", "102", "103.5", "7"],
                    ]
                }
            }
            return httpx.Response(200, json=payload, request=httpx.Request("GET", url))
        return original_get(self, url, params=params, **kwargs)

    monkeypatch.setattr(httpx.Client, "get", fake_get)
    app = create_app(settings=ApiSettings(data_root=tmp_path), store=store)
    client = TestClient(app)

    response = client.get(
        "/api/v1/prices/series",
        params={
            "asset": "BTC",
            "venue_scope": "all",
            "timeframe": "15m",
            "start": first.isoformat(),
            "end": second.isoformat(),
        },
    )

    assert response.status_code == 200
    points = response.json()["data"]["points"]
    assert [point["ts"] for point in points] == [
        "2026-03-18T00:00:00Z",
        "2026-03-18T00:15:00Z",
        "2026-03-18T00:30:00Z",
    ]


def test_price_series_backfills_stale_tail_for_all_venue_aggregate(tmp_path, monkeypatch) -> None:
    store = DatasetStore(tmp_path)
    first = datetime(2026, 3, 18, 0, 0, tzinfo=timezone.utc)
    second = first + timedelta(minutes=15)
    end = first + timedelta(minutes=60)
    for venue, venue_group, open_price, close_price in (
        ("binance_spot", "binance", 100.0, 100.5),
        ("bybit_spot", "bybit", 101.0, 101.5),
    ):
        for ts, base_price in ((first, open_price), (second, close_price)):
            store.write(
                DatasetName.MICROSTRUCTURE_BAR,
                pl.DataFrame(
                    [
                        {
                            "ts": ts,
                            "date": ts.date(),
                            "asset": "BTC",
                            "venue": venue,
                            "exchange_symbol": "BTCUSDT",
                            "venue_group": venue_group,
                            "market_type": "spot",
                            "is_derivatives": False,
                            "contract_type": "spot",
                            "clock": "event_time",
                            "bar_size_secs": 60,
                            "open": base_price,
                            "high": base_price + 1.0,
                            "low": base_price - 1.0,
                            "close": base_price + 0.5,
                            "volume": 10.0,
                            "buy_volume": 6.0,
                            "sell_volume": 4.0,
                            "trade_count": 5,
                            "vwap": base_price + 0.4,
                            "microprice": base_price + 0.45,
                            "spread": 0.5,
                            "depth_bid_5": 1.0,
                            "depth_ask_5": 1.1,
                            "depth_notional_bid_5": 100.5,
                            "depth_notional_ask_5": 110.55,
                            "is_gap": False,
                            "quality_flag": "ok",
                        }
                    ]
                ),
            )

    original_get = httpx.Client.get

    def fake_get(self, url: str, params: dict[str, object], **kwargs: object) -> httpx.Response:
        url_str = str(url)
        if "binance.com" in url_str:
            payload = [
                [int((first + timedelta(minutes=30)).timestamp() * 1000), "102", "103", "101", "102.5", "5", None, None, 2],
                [int((first + timedelta(minutes=45)).timestamp() * 1000), "103", "104", "102", "103.5", "5", None, None, 2],
                [int(end.timestamp() * 1000), "104", "105", "103", "104.5", "5", None, None, 2],
            ]
            return httpx.Response(200, json=payload, request=httpx.Request("GET", url))
        if "bybit.com" in url_str:
            payload = {
                "result": {
                    "list": [
                        [str(int((first + timedelta(minutes=30)).timestamp() * 1000)), "104", "105", "103", "104.5", "7"],
                        [str(int((first + timedelta(minutes=45)).timestamp() * 1000)), "105", "106", "104", "105.5", "7"],
                        [str(int(end.timestamp() * 1000)), "106", "107", "105", "106.5", "7"],
                    ]
                }
            }
            return httpx.Response(200, json=payload, request=httpx.Request("GET", url))
        return original_get(self, url, params=params, **kwargs)

    monkeypatch.setattr(httpx.Client, "get", fake_get)
    app = create_app(settings=ApiSettings(data_root=tmp_path), store=store)
    client = TestClient(app)

    response = client.get(
        "/api/v1/prices/series",
        params={
            "asset": "BTC",
            "venue_scope": "all",
            "timeframe": "15m",
            "start": first.isoformat(),
            "end": end.isoformat(),
        },
    )

    assert response.status_code == 200
    points = response.json()["data"]["points"]
    assert [point["ts"] for point in points] == [
        "2026-03-18T00:00:00Z",
        "2026-03-18T00:15:00Z",
        "2026-03-18T00:30:00Z",
        "2026-03-18T00:45:00Z",
        "2026-03-18T01:00:00Z",
    ]
    assert points[2]["open"] == 103.0
    assert points[2]["close"] == 103.5


def test_latest_prices_reads_recent_book_state_per_asset_without_full_latest_scan() -> None:
    ts = datetime(2026, 3, 17, 13, 0, tzinfo=timezone.utc)

    class StubStore:
        def __init__(self) -> None:
            self.read_calls: list[dict[str, object]] = []

        def read_latest(self, *args: object, **kwargs: object) -> pl.DataFrame:
            raise AssertionError("get_latest_prices should not call read_latest on book state")

        def latest_logical_ts(
            self,
            dataset: DatasetName,
            filters: dict[str, object] | None = None,
        ) -> datetime | None:
            assert dataset == DatasetName.MICROSTRUCTURE_BOOK_STATE
            if filters == {"asset": "BTC"}:
                return ts
            if filters == {"asset": "ETH"}:
                return None
            return None

        def read(
            self,
            dataset: DatasetName,
            *,
            start: datetime | None = None,
            end: datetime | None = None,
            filters: dict[str, object] | None = None,
            read_mode: object | None = None,
            columns: list[str] | tuple[str, ...] | None = None,
        ) -> pl.DataFrame:
            self.read_calls.append(
                {
                    "dataset": dataset,
                    "start": start,
                    "end": end,
                    "filters": filters,
                    "read_mode": read_mode,
                    "columns": columns,
                }
            )
            if dataset == DatasetName.MICROSTRUCTURE_BOOK_STATE and filters == {"asset": "BTC"}:
                return pl.DataFrame(
                    [
                        {
                            "ts": ts,
                            "date": ts.date(),
                            "asset": "BTC",
                            "venue": "binance_spot",
                            "exchange_symbol": "BTCUSDT",
                            "venue_group": "binance",
                            "market_type": "spot",
                            "is_derivatives": False,
                            "contract_type": "spot",
                            "best_bid": 70000.0,
                            "best_ask": 70010.0,
                            "best_bid_size": 1.0,
                            "best_ask_size": 1.5,
                            "mid_price": 70005.0,
                            "microprice": 70005.0,
                            "spread": 10.0,
                            "spread_rel": 10.0 / 70005.0,
                            "depth_bid_5": 5.0,
                            "depth_ask_5": 6.0,
                            "depth_notional_bid_5": 350025.0,
                            "depth_notional_ask_5": 420030.0,
                            "imbalance_5": 0.1,
                            "is_seeded": False,
                            "is_sequence_valid": True,
                            "is_gap_detected": False,
                            "reseed_count": 0,
                            "book_quality": "ok",
                            "quality_flag": "ok",
                            "logical_ts": ts,
                            "run_id": "run-1",
                            "run_ts": ts,
                            "write_mode": "append",
                        }
                    ]
                )
            return empty_frame(dataset)

    provider = DefaultPriceProvider(StubStore(), ReadCache(ttl_seconds=30))

    prices = provider.get_latest_prices()

    assert len(prices) == 1
    assert prices[0].asset == "BTC"
    assert prices[0].mid_price == 70005.0


def test_dataset_summaries_use_store_aggregate_path_when_available() -> None:
    ts = datetime(2026, 3, 17, 12, 0, tzinfo=timezone.utc)

    class StubStore:
        def dataset_summary(self, dataset: DatasetName) -> dict[str, object]:
            assert dataset == DatasetName.DVOL_SNAPSHOT
            return {
                "rows": 42,
                "latest_ts": ts,
                "latest_logical_ts": ts,
            }

        def read(self, *args: object, **kwargs: object) -> pl.DataFrame:
            raise AssertionError("dataset summaries should not materialize dataset frames")

    provider = DefaultDatasetProvider(StubStore(), ReadCache(ttl_seconds=30))

    summary = provider._build_summary(DatasetName.DVOL_SNAPSHOT)

    assert summary.id == "dvol_snapshot"
    assert summary.rows == 42
    assert summary.latest_ts == ts
    assert summary.latest_logical_ts == ts


def test_dataset_list_hides_microstructure_when_disabled(tmp_path) -> None:
    store = DatasetStore(tmp_path)
    app = create_app(
        settings=ApiSettings(data_root=tmp_path, microstructure_enabled=False),
        store=store,
    )
    client = TestClient(app)

    datasets = client.get("/api/v1/datasets")
    health = client.get("/api/v1/system/health")

    assert datasets.status_code == 200
    dataset_ids = [row["id"] for row in datasets.json()["data"]]
    assert dataset_ids
    assert all(not dataset_id.startswith("microstructure_") for dataset_id in dataset_ids)
    assert health.status_code == 200
    assert health.json()["data"]["dataset_count"] == len(dataset_ids)


def test_price_series_uses_robust_cross_venue_aggregation(tmp_path) -> None:
    store = DatasetStore(tmp_path)
    ts = datetime(2026, 3, 1, 12, tzinfo=timezone.utc)
    store.write(
        DatasetName.MICROSTRUCTURE_BAR,
        pl.DataFrame(
            [
                {
                    "ts": ts,
                    "date": ts.date(),
                    "asset": "BTC",
                    "venue": "binance_futures",
                    "exchange_symbol": "BTCUSDT",
                    "venue_group": "binance",
                    "market_type": "perps",
                    "is_derivatives": True,
                    "contract_type": "linear_perpetual",
                    "clock": "event_time",
                    "bar_size_secs": 60,
                    "open": 100.0,
                    "high": 100.0,
                    "low": 100.0,
                    "close": 100.0,
                    "volume": 20.0,
                    "buy_volume": 12.0,
                    "sell_volume": 8.0,
                    "trade_count": 10,
                    "vwap": 100.0,
                    "microprice": 100.0,
                    "spread": 0.2,
                    "depth_bid_5": 1.0,
                    "depth_ask_5": 1.0,
                    "depth_notional_bid_5": 100.0,
                    "depth_notional_ask_5": 100.0,
                    "is_gap": False,
                    "quality_flag": "ok",
                },
                {
                    "ts": ts,
                    "date": ts.date(),
                    "asset": "BTC",
                    "venue": "binance_spot",
                    "exchange_symbol": "BTCUSDT",
                    "venue_group": "binance",
                    "market_type": "spot",
                    "is_derivatives": False,
                    "contract_type": "spot",
                    "clock": "event_time",
                    "bar_size_secs": 60,
                    "open": 110.0,
                    "high": 112.0,
                    "low": 109.0,
                    "close": 111.0,
                    "volume": 30.0,
                    "buy_volume": 17.0,
                    "sell_volume": 13.0,
                    "trade_count": 12,
                    "vwap": 110.5,
                    "microprice": 110.6,
                    "spread": 0.3,
                    "depth_bid_5": 1.4,
                    "depth_ask_5": 1.5,
                    "depth_notional_bid_5": 154.0,
                    "depth_notional_ask_5": 166.5,
                    "is_gap": False,
                    "quality_flag": "ok",
                },
                {
                    "ts": ts,
                    "date": ts.date(),
                    "asset": "BTC",
                    "venue": "bybit_perps",
                    "exchange_symbol": "BTCUSDT",
                    "venue_group": "bybit",
                    "market_type": "perps",
                    "is_derivatives": True,
                    "contract_type": "linear_perpetual",
                    "clock": "event_time",
                    "bar_size_secs": 60,
                    "open": 110.5,
                    "high": 113.0,
                    "low": 108.5,
                    "close": 111.5,
                    "volume": 28.0,
                    "buy_volume": 16.0,
                    "sell_volume": 12.0,
                    "trade_count": 11,
                    "vwap": 111.1,
                    "microprice": 111.0,
                    "spread": 0.25,
                    "depth_bid_5": 1.3,
                    "depth_ask_5": 1.4,
                    "depth_notional_bid_5": 143.65,
                    "depth_notional_ask_5": 155.4,
                    "is_gap": False,
                    "quality_flag": "ok",
                },
                {
                    "ts": ts,
                    "date": ts.date(),
                    "asset": "BTC",
                    "venue": "bybit_spot",
                    "exchange_symbol": "BTCUSDT",
                    "venue_group": "bybit",
                    "market_type": "spot",
                    "is_derivatives": False,
                    "contract_type": "spot",
                    "clock": "event_time",
                    "bar_size_secs": 60,
                    "open": 111.0,
                    "high": 114.0,
                    "low": 109.5,
                    "close": 112.0,
                    "volume": 32.0,
                    "buy_volume": 18.0,
                    "sell_volume": 14.0,
                    "trade_count": 13,
                    "vwap": 112.0,
                    "microprice": 112.1,
                    "spread": 0.35,
                    "depth_bid_5": 1.6,
                    "depth_ask_5": 1.7,
                    "depth_notional_bid_5": 177.6,
                    "depth_notional_ask_5": 190.4,
                    "is_gap": False,
                    "quality_flag": "ok",
                },
            ]
        ),
    )
    app = create_app(settings=ApiSettings(data_root=tmp_path), store=store)
    client = TestClient(app)

    response = client.get(
        "/api/v1/prices/series",
        params={
            "asset": "BTC",
            "timeframe": "1m",
            "start": "2026-03-01T12:00:00Z",
            "end": "2026-03-01T12:01:00Z",
            "max_points": 10,
        },
    )

    assert response.status_code == 200
    payload = response.json()["data"]
    assert len(payload["points"]) == 1
    assert payload["points"][0]["open"] == 110.25
    assert payload["points"][0]["high"] == 112.5
    assert payload["points"][0]["low"] == 108.75
    assert payload["points"][0]["close"] == 111.25
    assert payload["context"]["aggregation_mode"] == "median_across_venues"
    assert payload["context"]["source_venue"] is None


def test_price_series_uses_preferred_binance_venue(tmp_path) -> None:
    store = DatasetStore(tmp_path)
    ts = datetime(2026, 3, 1, 12, tzinfo=timezone.utc)
    store.write(
        DatasetName.MICROSTRUCTURE_BAR,
        pl.DataFrame(
            [
                {
                    "ts": ts,
                    "date": ts.date(),
                    "asset": "BTC",
                    "venue": "binance_futures",
                    "exchange_symbol": "BTCUSDT",
                    "venue_group": "binance",
                    "market_type": "perps",
                    "is_derivatives": True,
                    "contract_type": "perpetual",
                    "clock": "event_time",
                    "bar_size_secs": 60,
                    "open": 100.0,
                    "high": 100.0,
                    "low": 100.0,
                    "close": 100.0,
                    "volume": 25.0,
                    "buy_volume": 13.0,
                    "sell_volume": 12.0,
                    "trade_count": 8,
                    "vwap": 100.0,
                    "microprice": 100.0,
                    "spread": 0.2,
                    "depth_bid_5": 1.0,
                    "depth_ask_5": 1.0,
                    "depth_notional_bid_5": 100.0,
                    "depth_notional_ask_5": 100.0,
                    "is_gap": False,
                    "quality_flag": "ok",
                },
                {
                    "ts": ts,
                    "date": ts.date(),
                    "asset": "BTC",
                    "venue": "binance_spot",
                    "exchange_symbol": "BTCUSDT",
                    "venue_group": "binance",
                    "market_type": "spot",
                    "is_derivatives": False,
                    "contract_type": "spot",
                    "clock": "event_time",
                    "bar_size_secs": 60,
                    "open": 110.0,
                    "high": 113.0,
                    "low": 109.0,
                    "close": 112.0,
                    "volume": 30.0,
                    "buy_volume": 16.0,
                    "sell_volume": 14.0,
                    "trade_count": 10,
                    "vwap": 111.2,
                    "microprice": 111.3,
                    "spread": 0.15,
                    "depth_bid_5": 1.1,
                    "depth_ask_5": 1.2,
                    "depth_notional_bid_5": 122.1,
                    "depth_notional_ask_5": 134.4,
                    "is_gap": False,
                    "quality_flag": "ok",
                },
            ]
        ),
    )
    app = create_app(settings=ApiSettings(data_root=tmp_path), store=store)
    client = TestClient(app)

    response = client.get(
        "/api/v1/prices/series",
        params={
            "asset": "BTC",
            "venue_scope": "binance",
            "timeframe": "1m",
            "start": "2026-03-01T12:00:00Z",
            "end": "2026-03-01T12:01:00Z",
            "max_points": 10,
        },
    )

    assert response.status_code == 200
    payload = response.json()["data"]
    assert len(payload["points"]) == 1
    assert payload["points"][0]["open"] == 110.0
    assert payload["points"][0]["high"] == 113.0
    assert payload["points"][0]["low"] == 109.0
    assert payload["points"][0]["close"] == 112.0
    assert payload["context"]["aggregation_mode"] == "preferred_venue"
    assert payload["context"]["source_venue"] == "binance_spot"


def test_prices_uses_bounded_latest_book_snapshot(tmp_path) -> None:
    store = DatasetStore(tmp_path)
    stale = datetime(2026, 3, 1, 11, 0, tzinfo=timezone.utc)
    latest = datetime(2026, 3, 1, 12, 0, tzinfo=timezone.utc)
    store.write(
        DatasetName.MICROSTRUCTURE_BOOK_STATE,
        pl.DataFrame(
            [
                {
                    "ts": stale,
                    "date": stale.date(),
                    "asset": "BTC",
                    "venue": "binance_spot",
                    "exchange_symbol": "BTCUSDT",
                    "venue_group": "binance",
                    "market_type": "spot",
                    "is_derivatives": False,
                    "contract_type": "spot",
                    "best_bid": 70000.0,
                    "best_ask": 70001.0,
                    "best_bid_size": 1.0,
                    "best_ask_size": 1.2,
                    "mid_price": 70000.5,
                    "microprice": 70000.6,
                    "spread": 1.0,
                    "spread_rel": 1.0 / 70000.5,
                    "depth_bid_5": 2.0,
                    "depth_ask_5": 2.1,
                    "depth_notional_bid_5": 140001.0,
                    "depth_notional_ask_5": 147002.1,
                    "imbalance_5": 0.02,
                    "is_seeded": False,
                    "is_sequence_valid": True,
                    "is_gap_detected": False,
                    "reseed_count": 0,
                    "book_quality": "ok",
                    "quality_flag": "ok",
                },
                {
                    "ts": latest,
                    "date": latest.date(),
                    "asset": "BTC",
                    "venue": "binance_spot",
                    "exchange_symbol": "BTCUSDT",
                    "venue_group": "binance",
                    "market_type": "spot",
                    "is_derivatives": False,
                    "contract_type": "spot",
                    "best_bid": 71000.0,
                    "best_ask": 71001.0,
                    "best_bid_size": 1.5,
                    "best_ask_size": 1.6,
                    "mid_price": 71000.5,
                    "microprice": 71000.55,
                    "spread": 1.0,
                    "spread_rel": 1.0 / 71000.5,
                    "depth_bid_5": 2.5,
                    "depth_ask_5": 2.6,
                    "depth_notional_bid_5": 177501.25,
                    "depth_notional_ask_5": 184602.6,
                    "imbalance_5": 0.03,
                    "is_seeded": False,
                    "is_sequence_valid": True,
                    "is_gap_detected": False,
                    "reseed_count": 0,
                    "book_quality": "ok",
                    "quality_flag": "ok",
                },
                {
                    "ts": latest,
                    "date": latest.date(),
                    "asset": "ETH",
                    "venue": "bybit_spot",
                    "exchange_symbol": "ETHUSDT",
                    "venue_group": "bybit",
                    "market_type": "spot",
                    "is_derivatives": False,
                    "contract_type": "spot",
                    "best_bid": 2000.0,
                    "best_ask": 2001.0,
                    "best_bid_size": 3.0,
                    "best_ask_size": 3.2,
                    "mid_price": 2000.5,
                    "microprice": 2000.55,
                    "spread": 1.0,
                    "spread_rel": 1.0 / 2000.5,
                    "depth_bid_5": 3.5,
                    "depth_ask_5": 3.6,
                    "depth_notional_bid_5": 7001.75,
                    "depth_notional_ask_5": 7201.8,
                    "imbalance_5": 0.04,
                    "is_seeded": False,
                    "is_sequence_valid": True,
                    "is_gap_detected": False,
                    "reseed_count": 0,
                    "book_quality": "ok",
                    "quality_flag": "ok",
                },
            ]
        ),
    )
    app = create_app(settings=ApiSettings(data_root=tmp_path), store=store)
    client = TestClient(app)

    response = client.get("/api/v1/prices")

    assert response.status_code == 200
    payload = {row["asset"]: row for row in response.json()["data"]}
    assert payload["BTC"]["mid_price"] == 71000.5
    assert payload["BTC"]["venue"] == "binance_spot"
    assert payload["ETH"]["mid_price"] == 2000.5


def test_price_series_marks_in_range_data_as_stale(tmp_path) -> None:
    store = DatasetStore(tmp_path)
    latest = datetime(2026, 3, 1, 12, tzinfo=timezone.utc)
    store.write(
        DatasetName.MICROSTRUCTURE_BAR,
        pl.DataFrame(
            [
                {
                    "ts": latest,
                    "date": latest.date(),
                    "asset": "BTC",
                    "venue": "binance_spot",
                    "exchange_symbol": "BTCUSDT",
                    "venue_group": "binance",
                    "market_type": "spot",
                    "is_derivatives": False,
                    "contract_type": "spot",
                    "clock": "event_time",
                    "bar_size_secs": 60,
                    "open": 100.5,
                    "high": 102.0,
                    "low": 100.0,
                    "close": 101.5,
                    "volume": 12.0,
                    "buy_volume": 7.0,
                    "sell_volume": 5.0,
                    "trade_count": 6,
                    "vwap": 101.2,
                    "microprice": 101.3,
                    "spread": 0.45,
                    "depth_bid_5": 1.2,
                    "depth_ask_5": 1.3,
                    "depth_notional_bid_5": 121.8,
                    "depth_notional_ask_5": 131.95,
                    "is_gap": False,
                    "quality_flag": "ok",
                }
            ]
        ),
    )
    app = create_app(settings=ApiSettings(data_root=tmp_path), store=store)
    client = TestClient(app)

    response = client.get(
        "/api/v1/prices/series",
        params={
            "asset": "BTC",
            "venue_scope": "binance",
            "timeframe": "15m",
            "start": "2026-03-01T11:00:00Z",
            "end": "2026-03-01T14:00:00Z",
            "max_points": 10,
        },
    )

    assert response.status_code == 200
    payload = response.json()["data"]
    assert payload["context"]["stale"] is True
    assert payload["context"]["latest_available_at"].startswith("2026-03-01T12:00:00")
    assert payload["context"]["source_venue"] == "binance_spot"


def test_api_exposes_eth_vvix_and_razor_signals(tmp_path) -> None:
    store = DatasetStore(tmp_path)
    base = datetime(2026, 3, 1, 12, tzinfo=timezone.utc)
    store.write(
        DatasetName.DVOL_SNAPSHOT,
        pl.DataFrame(
            [
                {
                    "ts": base,
                    "date": base.date(),
                    "venue": "deribit",
                    "underlying": "ETH",
                    "source": "dvol",
                    "dvol_open": 60.0,
                    "dvol_high": 61.0,
                    "dvol_low": 59.0,
                    "dvol_close": 60.5,
                    "quality_flag": "ok",
                }
            ]
        ),
    )
    store.write(
        DatasetName.VVIX_SERIES,
        pl.DataFrame(
            [
                {
                    "ts": base,
                    "date": base.date(),
                    "underlying": "ETH",
                    "source": "dvol",
                    "vol_index_value": 60.5,
                    "rvvix_7d": 88.0,
                    "rvvix_14d": 77.0,
                    "rvvix_30d": 66.0,
                    "qvvix_30d": None,
                    "model_version": "rvvix-v1",
                    "quality_flag": "ok",
                }
            ]
        ),
    )
    store.write(
        DatasetName.RAZOR_SERIES,
        pl.DataFrame(
            [
                {
                    "ts": base,
                    "date": base.date(),
                    "underlying": "BTC",
                    "source": "synthetic_7d_atm",
                    "reference_tenor_days": 7,
                    "razor_pnl": 12.0,
                    "razor_vrp": 1.5,
                    "buyer_increment": 0.1,
                    "seller_increment": -0.1,
                    "option_reval_pnl": 0.05,
                    "hedge_pnl": 0.03,
                    "roll_pnl": 0.02,
                    "razor_pnl_zscore": 1.4,
                    "razor_vrp_zscore": 0.8,
                    "trade_band": "double_buy",
                    "long_vol_signal": True,
                    "short_vol_signal": False,
                    "quality_flag": "ok",
                }
            ]
        ),
    )

    app = create_app(settings=ApiSettings(data_root=tmp_path), store=store)
    client = TestClient(app)

    signals = client.get("/api/v1/signals")
    eth_latest = client.get("/api/v1/signals/eth_vvix/latest", params={"source": "dvol"})
    razor_series = client.get(
        "/api/v1/signals/btc_razor/series",
        params={"source": "synthetic_7d_atm"},
    )

    assert signals.status_code == 200
    signal_ids = {item["id"] for item in signals.json()["data"]}
    assert {"btc_vvix", "eth_vvix", "btc_razor", "eth_razor"} <= signal_ids
    assert eth_latest.status_code == 200
    assert eth_latest.json()["data"]["symbol"] == "ETH"
    assert eth_latest.json()["data"]["latest_values"]["dvol"] == 60.5
    assert razor_series.status_code == 200
    assert razor_series.json()["data"]["points"][0]["trade_band"] == "double_buy"
