"""Unit tests for libs/backtest/snapshot_store.py."""
from __future__ import annotations

import datetime as dt
import json
from pathlib import Path

import pyarrow as pa
import pyarrow.parquet as pq
import pytest


def _build_store_from_fixture(tmp_path: Path) -> object:
    """Build a SnapshotStore directly from test data (no DB/HTTP)."""
    from libs.backtest.snapshot_store import SnapshotStore

    candidates = {
        dt.date(2026, 1, 6): [
            {
                "event_id": "EVT::TEST::001",
                "symbol": "AAPL",
                "execution_date": dt.date(2026, 1, 6),
                "entry_date": "2026-01-06",
                "entry_price": 150.0,
                "score": 0.8,
                "sector": "Technology",
                "event_type": "earnings",
                "event_timestamp": "2026-01-05T21:00:00+00:00",
                "filing_time_bucket": "post_market",
                "reaction_date": "2026-01-05",
                "avg_dollar_volume": 5_000_000.0,
                "atr_14": 3.0,
            }
        ],
        dt.date(2026, 1, 7): [
            {
                "event_id": "EVT::TEST::002",
                "symbol": "MSFT",
                "execution_date": dt.date(2026, 1, 7),
                "entry_date": "2026-01-07",
                "entry_price": 300.0,
                "score": 0.6,
                "sector": "Technology",
                "event_type": "guidance",
                "event_timestamp": "2026-01-06T20:00:00+00:00",
                "filing_time_bucket": "post_market",
                "reaction_date": "2026-01-06",
                "avg_dollar_volume": 10_000_000.0,
                "atr_14": 5.0,
            }
        ],
    }

    bars = {
        "AAPL": {
            dt.date(2026, 1, 6): {"date": dt.date(2026, 1, 6), "open": 150.0, "high": 155.0, "low": 148.0, "close": 152.0, "volume": 1_000_000},
            dt.date(2026, 1, 7): {"date": dt.date(2026, 1, 7), "open": 152.0, "high": 162.0, "low": 150.0, "close": 159.0, "volume": 900_000},
        },
        "MSFT": {
            dt.date(2026, 1, 7): {"date": dt.date(2026, 1, 7), "open": 300.0, "high": 310.0, "low": 295.0, "close": 305.0, "volume": 500_000},
        },
    }

    return SnapshotStore(
        candidates_by_exec_date=candidates,
        bars_by_symbol_date=bars,
    )


class TestSnapshotStoreQuery:
    def test_get_candidates_for_date(self, tmp_path):
        store = _build_store_from_fixture(tmp_path)
        rows = store.get_candidates_for_date(dt.date(2026, 1, 6))
        assert len(rows) == 1
        assert rows[0]["symbol"] == "AAPL"

    def test_get_candidates_empty_date(self, tmp_path):
        store = _build_store_from_fixture(tmp_path)
        rows = store.get_candidates_for_date(dt.date(2026, 1, 1))
        assert rows == []

    def test_get_candidates_for_reaction_date(self, tmp_path):
        store = _build_store_from_fixture(tmp_path)
        rows = store.get_candidates_for_reaction_date(dt.date(2026, 1, 5))
        assert len(rows) == 1
        assert rows[0]["symbol"] == "AAPL"

    def test_no_lookahead(self, tmp_path):
        """Candidates for Jan 7 should NOT appear when querying Jan 6."""
        store = _build_store_from_fixture(tmp_path)
        rows = store.get_candidates_for_date(dt.date(2026, 1, 6))
        symbols = [r["symbol"] for r in rows]
        assert "MSFT" not in symbols

    def test_get_bar_exists(self, tmp_path):
        store = _build_store_from_fixture(tmp_path)
        bar = store.get_bar("AAPL", dt.date(2026, 1, 6))
        assert bar is not None
        assert bar["open"] == 150.0
        assert bar["close"] == 152.0

    def test_get_bar_missing_returns_none(self, tmp_path):
        store = _build_store_from_fixture(tmp_path)
        bar = store.get_bar("AAPL", dt.date(2025, 12, 31))
        assert bar is None

    def test_get_bar_unknown_symbol_returns_none(self, tmp_path):
        store = _build_store_from_fixture(tmp_path)
        assert store.get_bar("UNKNOWN", dt.date(2026, 1, 6)) is None

    def test_all_execution_dates_sorted(self, tmp_path):
        store = _build_store_from_fixture(tmp_path)
        dates = store.all_execution_dates()
        assert dates == sorted(dates)
        assert dt.date(2026, 1, 6) in dates
        assert dt.date(2026, 1, 7) in dates

    def test_all_reaction_dates_sorted(self, tmp_path):
        store = _build_store_from_fixture(tmp_path)
        dates = store.all_reaction_dates()
        assert dates == [dt.date(2026, 1, 5), dt.date(2026, 1, 6)]

    def test_macro_default_empty(self, tmp_path):
        store = _build_store_from_fixture(tmp_path)
        macro = store.get_macro_for_date(dt.date(2026, 1, 6))
        assert macro == {}

    def test_candidates_copy_returned(self, tmp_path):
        """Modifying returned list should not affect internal state."""
        store = _build_store_from_fixture(tmp_path)
        rows1 = store.get_candidates_for_date(dt.date(2026, 1, 6))
        rows1.append({"extra": "data"})
        rows2 = store.get_candidates_for_date(dt.date(2026, 1, 6))
        assert len(rows2) == 1  # unchanged


class TestSnapshotStoreLoadGuard:
    def test_raises_in_running_event_loop(self, tmp_path):
        """load() should raise RuntimeError if called from a running event loop."""
        import asyncio

        from libs.backtest.snapshot_store import SnapshotStore

        async def _test():
            with pytest.raises(RuntimeError, match="running event loop"):
                SnapshotStore.load(tmp_path, "train", "http://localhost", "postgres://")

        asyncio.run(_test())


class TestSnapshotStoreFromParquet:
    def test_compute_date_range(self, tmp_path):
        from libs.backtest.snapshot_store import SnapshotStore

        rows = [
            {"entry_date": "2026-01-05"},
            {"entry_date": "2026-01-10"},
            {"entry_date": "2026-01-07"},
            {"reaction_date": "2026-01-04"},
        ]
        result = SnapshotStore._compute_date_range(rows)
        assert result == (dt.date(2026, 1, 4), dt.date(2026, 1, 10))

    def test_compute_date_range_empty(self, tmp_path):
        from libs.backtest.snapshot_store import SnapshotStore

        assert SnapshotStore._compute_date_range([]) is None
