"""Tests for kintsugi Parquet completeness gate (TRUST-01) and orphan bar marking (TRUST-02)."""

from __future__ import annotations

from datetime import date
from unittest.mock import MagicMock, patch

import polars as pl

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

_DAY = date(2025, 6, 15)
_DAY_START_US = 1750032000_000000  # 2025-06-15 00:00:00 UTC in microseconds
_DAY_END_US = 1750118399_999000    # 2025-06-15 23:59:59.999 UTC in microseconds
_SYMBOL = "BTCUSDT"
_THRESHOLD = 250
_NEXT_DAY_CROSSOVER_TID = 500_000


def _make_shard(symbol: str = _SYMBOL, threshold: int = _THRESHOLD) -> MagicMock:
    shard = MagicMock()
    shard.symbol = symbol
    shard.threshold_decimal_bps = threshold
    return shard


def _make_bars_df(n: int = 5) -> pl.DataFrame:
    """Create a minimal bars DataFrame with n rows."""
    return pl.DataFrame({
        "open": [1.0] * n,
        "high": [2.0] * n,
        "low": [0.5] * n,
        "close": [1.5] * n,
        "volume": [100.0] * n,
        "open_time_us": list(range(n)),
        "close_time_us": list(range(n)),
        "first_agg_trade_id": list(range(n)),
        "last_agg_trade_id": list(range(100, 100 + n)),
    })


def _make_ticks_df(max_tid: int, n: int = 100) -> pl.DataFrame:
    """Create a minimal ticks DataFrame with max agg_trade_id = max_tid."""
    return pl.DataFrame({
        "timestamp": list(range(n)),
        "price": [1.0] * n,
        "quantity": [1.0] * n,
        "agg_trade_id": list(range(max_tid - n + 1, max_tid + 1)),
    })


# ---------------------------------------------------------------------------
# TRUST-01: Parquet completeness gate
# ---------------------------------------------------------------------------


class TestParquetCompletenessGate:
    """_try_parquet_day_replace returns 0 when Parquet is incomplete."""

    @patch("opendeviationbar.kintsugi.repair._get_worker_cache")
    @patch("opendeviationbar.kintsugi.repair._stream_bars_with_processor")
    @patch("opendeviationbar.kintsugi.repair.TickStorage")
    def test_returns_0_when_parquet_has_no_ticks(
        self,
        mock_storage_cls,
        mock_stream,
        mock_cache,
    ):
        """Test 1: returns 0 when Parquet has no ticks for the day."""
        storage = mock_storage_cls.return_value
        storage.has_ticks.return_value = False

        from opendeviationbar.kintsugi.repair import _try_parquet_day_replace

        result = _try_parquet_day_replace(
            shard=_make_shard(),
            day_start_us=_DAY_START_US,
            day_end_us=_DAY_END_US,
            include_microstructure=False,
        )

        assert result == 0
        # Should NOT have streamed bars (early return)
        mock_stream.assert_not_called()

    @patch("opendeviationbar.kintsugi.repair._get_worker_cache")
    @patch("opendeviationbar.kintsugi.repair._get_mode_for_symbol", return_value="aion")
    @patch("opendeviationbar.kintsugi.repair._stream_bars_with_processor")
    @patch("opendeviationbar.kintsugi.repair.TickStorage")
    def test_proceeds_when_parquet_has_ticks(
        self,
        mock_storage_cls,
        mock_stream,
        mock_mode,
        mock_cache,
    ):
        """Test 2: proceeds normally when Parquet has ticks for the day."""
        storage = mock_storage_cls.return_value
        storage.has_ticks.return_value = True
        storage.read_ticks.return_value = _make_ticks_df(max_tid=500_000)

        bars = _make_bars_df(3)
        mock_stream.return_value = iter([bars])

        cache = mock_cache.return_value
        cache.store_bars_batch.return_value = 3

        from opendeviationbar.kintsugi.repair import _try_parquet_day_replace

        result = _try_parquet_day_replace(
            shard=_make_shard(),
            day_start_us=_DAY_START_US,
            day_end_us=_DAY_END_US,
            include_microstructure=False,
        )

        assert result == 3
        mock_stream.assert_called_once()


# ---------------------------------------------------------------------------
# TRUST-02: Orphan bar marking
# ---------------------------------------------------------------------------


class TestOrphanBarMarking:
    """Last bar in repair output has is_orphan=True."""

    @patch("opendeviationbar.kintsugi.repair._get_worker_cache")
    @patch("opendeviationbar.kintsugi.repair._get_mode_for_symbol", return_value="aion")
    @patch("opendeviationbar.kintsugi.repair._stream_bars_with_processor")
    @patch("opendeviationbar.kintsugi.repair.TickStorage")
    def test_parquet_path_sets_orphan_on_last_bar(
        self,
        mock_storage_cls,
        mock_stream,
        mock_mode,
        mock_cache,
    ):
        """Test 4: Parquet path sets is_orphan=True on the last bar."""
        storage = mock_storage_cls.return_value
        storage.has_ticks.return_value = True
        storage.read_ticks.return_value = _make_ticks_df(max_tid=500_000)

        bars = _make_bars_df(4)
        mock_stream.return_value = iter([bars])

        cache = mock_cache.return_value
        # Capture the bars_df passed to store_bars_batch
        stored_bars = []

        def capture_store(**kwargs: object) -> int:
            stored_bars.append(kwargs["bars"].clone())
            return len(kwargs["bars"])

        cache.store_bars_batch.side_effect = capture_store

        from opendeviationbar.kintsugi.repair import _try_parquet_day_replace

        result = _try_parquet_day_replace(
            shard=_make_shard(),
            day_start_us=_DAY_START_US,
            day_end_us=_DAY_END_US,
            include_microstructure=False,
        )

        assert result == 4
        assert len(stored_bars) == 1
        df = stored_bars[0]
        assert "is_orphan" in df.columns
        orphan_col = df["is_orphan"].to_list()
        assert orphan_col == [False, False, False, True]

    @patch("opendeviationbar.kintsugi.repair._get_worker_cache")
    @patch("opendeviationbar.kintsugi.repair._stream_bars_with_processor")
    @patch("opendeviationbar.kintsugi.repair.TickStorage")
    def test_vision_path_sets_orphan_on_last_bar(
        self,
        mock_storage_cls,
        mock_stream,
        mock_cache,
    ):
        """Test 5: Vision path sets is_orphan=True on the last bar."""
        # Make Parquet path fail (no ticks) so it falls through to Vision
        storage = mock_storage_cls.return_value
        storage.has_ticks.return_value = False

        bars = _make_bars_df(3)

        # Patch Vision path dependencies at their source modules (local imports
        # inside _try_vision_day_replace bind at call time, not module import time).
        with (
            patch(
                "opendeviationbar.backfill._is_vision_cached_unavailable",
                return_value=False,
            ),
            patch(
                "opendeviationbar.orchestration.helpers._stream_open_deviation_bars_binance",
                return_value=iter([bars]),
            ),
        ):
            stored_bars = []

            def capture_store(**kwargs: object) -> int:
                stored_bars.append(kwargs["bars"].clone())
                return len(kwargs["bars"])

            cache = mock_cache.return_value
            cache.store_bars_batch.side_effect = capture_store

            from opendeviationbar.kintsugi.repair import _try_vision_day_replace

            result = _try_vision_day_replace(
                shard=_make_shard(),
                repair_date=_DAY,
                include_microstructure=False,
            )

            assert result == 3
            assert len(stored_bars) == 1
            df = stored_bars[0]
            assert "is_orphan" in df.columns
            orphan_col = df["is_orphan"].to_list()
            assert orphan_col == [False, False, True]
