"""Tests for Parquet-first catchup (PQFIRST-01, PQFIRST-02, PQFIRST-03).

Validates that sidecar startup reads max(agg_trade_id) from today's Parquet
tick cache per symbol, advances REST fill start point, and falls back
gracefully when Parquet is stale or missing.
"""

from __future__ import annotations

import os
import time
from datetime import UTC, datetime
from pathlib import Path
from unittest.mock import MagicMock, patch

import polars as pl
import pytest


@pytest.fixture
def today_str() -> str:
    """Return today's date string in YYYY-MM-DD format."""
    return datetime.now(UTC).strftime("%Y-%m-%d")


@pytest.fixture
def ticks_dir(tmp_path: Path) -> Path:
    """Return a temporary ticks directory."""
    d = tmp_path / "ticks"
    d.mkdir()
    return d


def _write_parquet(ticks_dir: Path, symbol: str, day_key: str, trade_ids: list[int]) -> Path:
    """Helper: write a minimal Parquet file with agg_trade_id column."""
    sym_dir = ticks_dir / symbol
    sym_dir.mkdir(parents=True, exist_ok=True)
    path = sym_dir / f"{day_key}.parquet"
    df = pl.DataFrame({"agg_trade_id": trade_ids})
    df.write_parquet(path)
    return path


class TestReadParquetMaxTids:
    """Tests for _read_parquet_max_tids() function (PQFIRST-01, PQFIRST-03)."""

    def test_fresh_parquet_returns_max_tid(self, ticks_dir: Path, today_str: str) -> None:
        """PQFIRST-01: Fresh Parquet files return max(agg_trade_id) per symbol."""
        _write_parquet(ticks_dir, "BTCUSDT", today_str, [100, 200, 5000])
        _write_parquet(ticks_dir, "ETHUSDT", today_str, [10, 20, 3000])

        from opendeviationbar.sidecar.midnight import _read_parquet_max_tids

        result = _read_parquet_max_tids(["BTCUSDT", "ETHUSDT"], ticks_dir=ticks_dir)

        assert result == {"BTCUSDT": 5000, "ETHUSDT": 3000}

    def test_stale_parquet_omitted(self, ticks_dir: Path, today_str: str) -> None:
        """PQFIRST-03: Parquet files older than 10 min are skipped."""
        path = _write_parquet(ticks_dir, "BTCUSDT", today_str, [100, 5000])
        # Backdate mtime to 15 minutes ago
        old_time = time.time() - 900
        os.utime(path, (old_time, old_time))

        from opendeviationbar.sidecar.midnight import _read_parquet_max_tids

        result = _read_parquet_max_tids(["BTCUSDT"], ticks_dir=ticks_dir)

        assert result == {}

    def test_missing_parquet_returns_empty(self, ticks_dir: Path) -> None:
        """PQFIRST-03: Missing Parquet files result in empty dict."""
        from opendeviationbar.sidecar.midnight import _read_parquet_max_tids

        result = _read_parquet_max_tids(["BTCUSDT", "ETHUSDT"], ticks_dir=ticks_dir)

        assert result == {}

    def test_corrupt_parquet_skipped(self, ticks_dir: Path, today_str: str) -> None:
        """PQFIRST-03: Corrupt Parquet files are skipped without raising."""
        sym_dir = ticks_dir / "BTCUSDT"
        sym_dir.mkdir(parents=True)
        corrupt_path = sym_dir / f"{today_str}.parquet"
        corrupt_path.write_bytes(b"not a parquet file")

        from opendeviationbar.sidecar.midnight import _read_parquet_max_tids

        result = _read_parquet_max_tids(["BTCUSDT"], ticks_dir=ticks_dir)

        assert result == {}


class TestParquetSeedOverride:
    """Tests for Parquet-first seed override integration (PQFIRST-02)."""

    def test_seed_advanced_by_parquet(self, ticks_dir: Path, today_str: str) -> None:
        """PQFIRST-02: seed_trade_id called with Parquet max TID for each (symbol, threshold)."""
        _write_parquet(ticks_dir, "BTCUSDT", today_str, [100, 5000])

        from opendeviationbar.sidecar.midnight import _read_parquet_max_tids

        parquet_tids = _read_parquet_max_tids(["BTCUSDT"], ticks_dir=ticks_dir)
        assert parquet_tids == {"BTCUSDT": 5000}

        # Simulate what sidecar does: call seed_trade_id for each (symbol, threshold)
        engine = MagicMock()
        thresholds = [250, 500]
        for symbol, max_tid in parquet_tids.items():
            for threshold in thresholds:
                engine.seed_trade_id(symbol, threshold, max_tid)

        engine.seed_trade_id.assert_any_call("BTCUSDT", 250, 5000)
        engine.seed_trade_id.assert_any_call("BTCUSDT", 500, 5000)
        assert engine.seed_trade_id.call_count == 2


class TestParquetFirstIntegration:
    """Integration test: Parquet-first block in sidecar startup."""

    def test_sidecar_calls_seed_for_all_pairs(
        self, ticks_dir: Path, today_str: str
    ) -> None:
        """Parquet-first block seeds engine for each (symbol, threshold) pair."""
        _write_parquet(ticks_dir, "BTCUSDT", today_str, [100, 7000])
        _write_parquet(ticks_dir, "ETHUSDT", today_str, [50, 4000])

        engine = MagicMock()
        config_symbols = ["BTCUSDT", "ETHUSDT", "SOLUSDT"]  # SOLUSDT has no Parquet
        config_thresholds = [250, 500, 750]

        with patch(
            "opendeviationbar.sidecar.midnight._read_parquet_max_tids"
        ) as mock_read:
            mock_read.return_value = {"BTCUSDT": 7000, "ETHUSDT": 4000}

            # Execute the integration block (same logic as sidecar __init__.py)
            parquet_tids = mock_read(config_symbols)
            if parquet_tids:
                for symbol, max_tid in parquet_tids.items():
                    for threshold in config_thresholds:
                        engine.seed_trade_id(symbol, threshold, max_tid)

        # 2 symbols x 3 thresholds = 6 calls
        assert engine.seed_trade_id.call_count == 6
        engine.seed_trade_id.assert_any_call("BTCUSDT", 250, 7000)
        engine.seed_trade_id.assert_any_call("BTCUSDT", 750, 7000)
        engine.seed_trade_id.assert_any_call("ETHUSDT", 500, 4000)

    def test_empty_parquet_no_seed_calls(self) -> None:
        """When _read_parquet_max_tids returns empty, no seed_trade_id calls."""
        engine = MagicMock()

        with patch(
            "opendeviationbar.sidecar.midnight._read_parquet_max_tids"
        ) as mock_read:
            mock_read.return_value = {}

            parquet_tids = mock_read(["BTCUSDT"])
            if parquet_tids:
                for symbol, max_tid in parquet_tids.items():
                    for threshold in [250, 500]:
                        engine.seed_trade_id(symbol, threshold, max_tid)

        engine.seed_trade_id.assert_not_called()

    def test_exception_is_non_fatal(self, ticks_dir: Path) -> None:
        """Parquet-first block catches exceptions without propagating."""
        engine = MagicMock()

        with patch(
            "opendeviationbar.sidecar.midnight._read_parquet_max_tids",
            side_effect=RuntimeError("simulated failure"),
        ):
            # Replicate sidecar's try/except pattern
            try:
                from opendeviationbar.sidecar.midnight import _read_parquet_max_tids

                parquet_tids = _read_parquet_max_tids(["BTCUSDT"])
                if parquet_tids:
                    for symbol, max_tid in parquet_tids.items():
                        for threshold in [250]:
                            engine.seed_trade_id(symbol, threshold, max_tid)
            except Exception:
                pass  # Non-fatal fallback

        # No seed calls -- exception intercepted
        engine.seed_trade_id.assert_not_called()
