"""Tests for day-boundary backfill in tick_cache_seeder (SEED-01, SEED-02)."""

from __future__ import annotations

import sys
from datetime import UTC, datetime, timedelta
from pathlib import Path
from unittest.mock import MagicMock, patch

import polars as pl

# Add scripts dir to path so we can import tick_cache_seeder
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))


def _make_trades_df(from_id: int, to_id: int) -> pl.DataFrame:
    """Build a minimal 7-column trades DataFrame for testing."""
    ids = list(range(from_id, to_id + 1))
    return pl.DataFrame({
        "agg_trade_id": ids,
        "price": [100.0] * len(ids),
        "quantity": [1.0] * len(ids),
        "first_trade_id": ids,
        "last_trade_id": ids,
        "timestamp": [1700000000000 + i for i in ids],
        "is_buyer_maker": [False] * len(ids),
    })


class TestBackfillYesterdayBoundary:
    """Tests for _backfill_yesterday_boundary()."""

    @patch("opendeviationbar._core.fetch_aggtrades_parallel")
    @patch("opendeviationbar.crossover.get_crossover_tid")
    def test_detects_incomplete_and_backfills(
        self, mock_crossover, mock_fetch, tmp_path: Path
    ):
        """Test 1: Incomplete file (max TID < crossover_tid - 1) triggers backfill."""
        from tick_cache_seeder import _backfill_yesterday_boundary

        today = datetime.now(UTC).date()
        yesterday = today - timedelta(days=1)
        yesterday_str = yesterday.isoformat()

        # Set up storage mock with existing Parquet file
        storage = MagicMock()
        parquet_path = tmp_path / f"{yesterday_str}.parquet"
        existing_df = _make_trades_df(1000, 1090)
        existing_df.write_parquet(parquet_path)
        storage._get_parquet_path.return_value = parquet_path

        # write_ticks must actually update the file so post-write verify sees new max
        def _fake_write(sym, df):
            combined = pl.concat([pl.read_parquet(parquet_path), df])
            combined.write_parquet(parquet_path)
        storage.write_ticks.side_effect = _fake_write

        # Crossover TID = 1101 -> expected max = 1100
        mock_crossover.return_value = 1101
        # Fetch returns trades 1091-1100
        mock_fetch.return_value = _make_trades_df(1091, 1100).to_dicts()

        state_dir = tmp_path / "state"
        state_dir.mkdir()

        result = _backfill_yesterday_boundary(
            "BTCUSDT", storage, "BTCUSDT", state_dir
        )

        assert result == "backfilled"
        mock_fetch.assert_called_once()
        storage.write_ticks.assert_called_once()

    @patch("opendeviationbar.crossover.get_crossover_tid")
    def test_skips_complete_file(self, mock_crossover, tmp_path: Path):
        """Test 2: Complete file (max TID == crossover_tid - 1) is skipped."""
        from tick_cache_seeder import _backfill_yesterday_boundary

        today = datetime.now(UTC).date()
        yesterday = today - timedelta(days=1)
        yesterday_str = yesterday.isoformat()

        storage = MagicMock()
        parquet_path = tmp_path / f"{yesterday_str}.parquet"
        existing_df = _make_trades_df(1000, 1100)
        existing_df.write_parquet(parquet_path)
        storage._get_parquet_path.return_value = parquet_path

        # Crossover TID = 1101 -> expected max = 1100, current max = 1100
        mock_crossover.return_value = 1101

        state_dir = tmp_path / "state"
        state_dir.mkdir()

        result = _backfill_yesterday_boundary(
            "BTCUSDT", storage, "BTCUSDT", state_dir
        )

        assert result == "complete"
        storage.write_ticks.assert_not_called()

    @patch("opendeviationbar.crossover.get_crossover_tid")
    def test_skips_when_crossover_fails(self, mock_crossover, tmp_path: Path):
        """Test 3: Crossover lookup returns None -> skip."""
        from tick_cache_seeder import _backfill_yesterday_boundary

        today = datetime.now(UTC).date()
        yesterday = today - timedelta(days=1)
        yesterday_str = yesterday.isoformat()

        storage = MagicMock()
        parquet_path = tmp_path / f"{yesterday_str}.parquet"
        existing_df = _make_trades_df(1000, 1050)
        existing_df.write_parquet(parquet_path)
        storage._get_parquet_path.return_value = parquet_path

        mock_crossover.return_value = None

        state_dir = tmp_path / "state"
        state_dir.mkdir()

        result = _backfill_yesterday_boundary(
            "BTCUSDT", storage, "BTCUSDT", state_dir
        )

        assert result == "skipped"

    def test_skips_when_no_parquet_file(self, tmp_path: Path):
        """Test 4: No yesterday Parquet file -> skip (nothing to complete)."""
        from tick_cache_seeder import _backfill_yesterday_boundary

        storage = MagicMock()
        # Point to a non-existent path
        storage._get_parquet_path.return_value = tmp_path / "nonexistent.parquet"

        state_dir = tmp_path / "state"
        state_dir.mkdir()

        result = _backfill_yesterday_boundary(
            "BTCUSDT", storage, "BTCUSDT", state_dir
        )

        assert result == "skipped"

    @patch("opendeviationbar._core.fetch_aggtrades_parallel")
    @patch("opendeviationbar.crossover.get_crossover_tid")
    def test_state_marker_prevents_rerun(
        self, mock_crossover, mock_fetch, tmp_path: Path
    ):
        """Test 5: State marker after backfill prevents re-run on next invocation."""
        from tick_cache_seeder import _backfill_yesterday_boundary

        today = datetime.now(UTC).date()
        yesterday = today - timedelta(days=1)
        yesterday_str = yesterday.isoformat()

        storage = MagicMock()
        parquet_path = tmp_path / f"{yesterday_str}.parquet"
        existing_df = _make_trades_df(1000, 1090)
        existing_df.write_parquet(parquet_path)
        storage._get_parquet_path.return_value = parquet_path

        # write_ticks must actually update the file so post-write verify sees new max
        def _fake_write(sym, df):
            combined = pl.concat([pl.read_parquet(parquet_path), df])
            combined.write_parquet(parquet_path)
        storage.write_ticks.side_effect = _fake_write

        mock_crossover.return_value = 1101
        mock_fetch.return_value = _make_trades_df(1091, 1100).to_dicts()

        state_dir = tmp_path / "state"
        state_dir.mkdir()

        # First call: should backfill
        result1 = _backfill_yesterday_boundary(
            "BTCUSDT", storage, "BTCUSDT", state_dir
        )
        assert result1 == "backfilled"

        # Second call: state marker should prevent re-run
        result2 = _backfill_yesterday_boundary(
            "BTCUSDT", storage, "BTCUSDT", state_dir
        )
        assert result2 == "skipped"
        # fetch should have been called only once (first invocation)
        assert mock_fetch.call_count == 1

    @patch("opendeviationbar._core.fetch_aggtrades_parallel")
    @patch("opendeviationbar.crossover.get_crossover_tid")
    def test_partial_backfill_no_marker_written(
        self, mock_crossover, mock_fetch, tmp_path: Path
    ):
        """Partial backfill (verify fails) does NOT write .done marker — allows retry."""
        from tick_cache_seeder import _backfill_yesterday_boundary

        today = datetime.now(UTC).date()
        yesterday = today - timedelta(days=1)
        yesterday_str = yesterday.isoformat()

        storage = MagicMock()
        parquet_path = tmp_path / f"{yesterday_str}.parquet"
        existing_df = _make_trades_df(1000, 1090)
        existing_df.write_parquet(parquet_path)
        storage._get_parquet_path.return_value = parquet_path

        # Crossover TID = 1101 -> expected max = 1100
        mock_crossover.return_value = 1101
        # Fetch returns only 1091-1095 (partial — missing 1096-1100)
        mock_fetch.return_value = _make_trades_df(1091, 1095).to_dicts()

        state_dir = tmp_path / "state"
        state_dir.mkdir()

        result = _backfill_yesterday_boundary(
            "BTCUSDT", storage, "BTCUSDT", state_dir
        )

        assert result == "partial"
        # Marker must NOT exist — next cycle should retry
        marker = state_dir / f"BTCUSDT_boundary_{yesterday_str}.done"
        assert not marker.exists()

    @patch("opendeviationbar._core.fetch_aggtrades_parallel")
    @patch("opendeviationbar.crossover.get_crossover_tid")
    def test_partial_backfill_retries_on_next_cycle(
        self, mock_crossover, mock_fetch, tmp_path: Path
    ):
        """After partial backfill (no marker), the next call re-attempts the backfill."""
        from tick_cache_seeder import _backfill_yesterday_boundary

        today = datetime.now(UTC).date()
        yesterday = today - timedelta(days=1)
        yesterday_str = yesterday.isoformat()

        storage = MagicMock()
        parquet_path = tmp_path / f"{yesterday_str}.parquet"
        existing_df = _make_trades_df(1000, 1090)
        existing_df.write_parquet(parquet_path)
        storage._get_parquet_path.return_value = parquet_path

        mock_crossover.return_value = 1101
        # First call: partial (only 1091-1095)
        mock_fetch.return_value = _make_trades_df(1091, 1095).to_dicts()

        state_dir = tmp_path / "state"
        state_dir.mkdir()

        result1 = _backfill_yesterday_boundary(
            "BTCUSDT", storage, "BTCUSDT", state_dir
        )
        assert result1 == "partial"

        # Second call: should re-attempt (no marker blocking it)
        mock_fetch.return_value = _make_trades_df(1091, 1100).to_dicts()
        result2 = _backfill_yesterday_boundary(
            "BTCUSDT", storage, "BTCUSDT", state_dir
        )
        # fetch called twice (once per cycle)
        assert mock_fetch.call_count == 2
