"""Tests for yesterday priority queue in kintsugi (Phase 02, Plan 01).

Covers:
- clear_yesterday_signal: deletion and no-op behavior
- _load_yesterday_priority_shards: signal parsing, shard creation, staleness
"""

from __future__ import annotations

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

# ---------------------------------------------------------------------------
# clear_yesterday_signal tests
# ---------------------------------------------------------------------------


class TestClearYesterdaySignal:
    """clear_yesterday_signal deletes signal file or no-ops safely."""

    def test_deletes_existing_file(self, tmp_path, monkeypatch):
        import opendeviationbar.sidecar.repair_signal as sig_mod

        sig_path = str(tmp_path / "yesterday-repair.json")
        monkeypatch.setattr(sig_mod, "SIGNAL_FILE_PATH", sig_path)

        # Write a signal file
        Path(sig_path).write_text('{"symbols": {}}')
        assert Path(sig_path).exists()

        sig_mod.clear_yesterday_signal()
        assert not Path(sig_path).exists()

    def test_noop_when_missing(self, tmp_path, monkeypatch):
        import opendeviationbar.sidecar.repair_signal as sig_mod

        sig_path = str(tmp_path / "nonexistent.json")
        monkeypatch.setattr(sig_mod, "SIGNAL_FILE_PATH", sig_path)

        # Should not raise
        sig_mod.clear_yesterday_signal()


# ---------------------------------------------------------------------------
# _load_yesterday_priority_shards tests
# ---------------------------------------------------------------------------


class TestLoadYesterdayPriorityShards:
    """_load_yesterday_priority_shards returns Shard objects from signal file."""

    def _write_signal(self, sig_path: str, symbols: dict[str, str], age_hours: float = 0.0):
        """Helper: write signal file with controllable timestamp."""
        ts = datetime.now(UTC) - timedelta(hours=age_hours)
        data = {
            "symbols": symbols,
            "timestamp": ts.isoformat(),
            "sidecar_pid": 1234,
        }
        Path(sig_path).write_text(json.dumps(data))

    def test_returns_empty_when_no_signal(self, tmp_path, monkeypatch):
        import opendeviationbar.sidecar.repair_signal as sig_mod

        sig_path = str(tmp_path / "nonexistent.json")
        monkeypatch.setattr(sig_mod, "SIGNAL_FILE_PATH", sig_path)

        from opendeviationbar.kintsugi.orchestrator import (
            _load_yesterday_priority_shards,
        )

        result = _load_yesterday_priority_shards()
        assert result == []

    def test_returns_shards_for_each_symbol_threshold(self, tmp_path, monkeypatch):
        import opendeviationbar.sidecar.repair_signal as sig_mod

        sig_path = str(tmp_path / "yesterday-repair.json")
        monkeypatch.setattr(sig_mod, "SIGNAL_FILE_PATH", sig_path)
        self._write_signal(sig_path, {"BTCUSDT": "stathera_gap"})

        with (
            patch(
                "opendeviationbar.kintsugi.orchestrator.get_registered_symbols",
                return_value=("BTCUSDT", "ETHUSDT"),
            ),
            patch(
                "opendeviationbar.kintsugi.orchestrator.get_thresholds_for_symbol",
                return_value=(250, 500, 750, 1000),
            ),
        ):
            from opendeviationbar.kintsugi.orchestrator import (
                _load_yesterday_priority_shards,
            )

            shards = _load_yesterday_priority_shards()

        assert len(shards) == 4  # 1 symbol x 4 thresholds
        symbols = {s.symbol for s in shards}
        thresholds = {s.threshold_decimal_bps for s in shards}
        assert symbols == {"BTCUSDT"}
        assert thresholds == {250, 500, 750, 1000}

    def test_yesterday_date_range(self, tmp_path, monkeypatch):
        import opendeviationbar.sidecar.repair_signal as sig_mod

        sig_path = str(tmp_path / "yesterday-repair.json")
        monkeypatch.setattr(sig_mod, "SIGNAL_FILE_PATH", sig_path)
        self._write_signal(sig_path, {"BTCUSDT": "stathera_gap"})

        with (
            patch(
                "opendeviationbar.kintsugi.orchestrator.get_registered_symbols",
                return_value=("BTCUSDT",),
            ),
            patch(
                "opendeviationbar.kintsugi.orchestrator.get_thresholds_for_symbol",
                return_value=(250,),
            ),
        ):
            from opendeviationbar.kintsugi.orchestrator import (
                _load_yesterday_priority_shards,
            )

            shards = _load_yesterday_priority_shards()

        assert len(shards) == 1
        shard = shards[0]
        # gap_start_ms is actually µs (field name is legacy)
        yesterday = datetime.now(UTC).date() - timedelta(days=1)
        expected_start_us = int(
            datetime.combine(yesterday, datetime.min.time(), UTC).timestamp() * 1_000_000
        )
        expected_end_us = expected_start_us + 86_400_000_000 - 1
        assert shard.gap_start_ms == expected_start_us
        assert shard.gap_end_ms == expected_end_us

    def test_discards_stale_signal(self, tmp_path, monkeypatch, caplog):
        import opendeviationbar.sidecar.repair_signal as sig_mod

        sig_path = str(tmp_path / "yesterday-repair.json")
        monkeypatch.setattr(sig_mod, "SIGNAL_FILE_PATH", sig_path)
        # Signal older than 24h
        self._write_signal(sig_path, {"BTCUSDT": "stathera_gap"}, age_hours=25.0)

        import logging

        with caplog.at_level(logging.WARNING):
            from opendeviationbar.kintsugi.orchestrator import (
                _load_yesterday_priority_shards,
            )

            shards = _load_yesterday_priority_shards()

        assert shards == []
        assert "stale" in caplog.text.lower()
        # File should be deleted
        assert not Path(sig_path).exists()

    def test_skips_unregistered_symbols(self, tmp_path, monkeypatch):
        import opendeviationbar.sidecar.repair_signal as sig_mod

        sig_path = str(tmp_path / "yesterday-repair.json")
        monkeypatch.setattr(sig_mod, "SIGNAL_FILE_PATH", sig_path)
        self._write_signal(sig_path, {"BTCUSDT": "stathera_gap", "FAKEUSDT": "stathera_gap"})

        with (
            patch(
                "opendeviationbar.kintsugi.orchestrator.get_registered_symbols",
                return_value=("BTCUSDT",),  # FAKEUSDT not registered
            ),
            patch(
                "opendeviationbar.kintsugi.orchestrator.get_thresholds_for_symbol",
                return_value=(250,),
            ),
        ):
            from opendeviationbar.kintsugi.orchestrator import (
                _load_yesterday_priority_shards,
            )

            shards = _load_yesterday_priority_shards()

        assert len(shards) == 1
        assert shards[0].symbol == "BTCUSDT"

    def test_shard_default_attributes(self, tmp_path, monkeypatch):
        import opendeviationbar.sidecar.repair_signal as sig_mod

        sig_path = str(tmp_path / "yesterday-repair.json")
        monkeypatch.setattr(sig_mod, "SIGNAL_FILE_PATH", sig_path)
        self._write_signal(sig_path, {"BTCUSDT": "stathera_gap"})

        with (
            patch(
                "opendeviationbar.kintsugi.orchestrator.get_registered_symbols",
                return_value=("BTCUSDT",),
            ),
            patch(
                "opendeviationbar.kintsugi.orchestrator.get_thresholds_for_symbol",
                return_value=(250,),
            ),
        ):
            from opendeviationbar.kintsugi.orchestrator import (
                _load_yesterday_priority_shards,
            )

            shards = _load_yesterday_priority_shards()

        shard = shards[0]
        assert shard.is_overlap is False
        assert shard.is_genesis is True  # full-day recompute, not interior gap
        assert shard.anchor_last_tid is None
        assert shard.resume_first_tid is None
        assert shard.missing_trade_count == 0
        assert shard.gap_hours == 24.0


# ---------------------------------------------------------------------------
# kintsugi_pass integration tests (Task 2)
# ---------------------------------------------------------------------------


def _make_shard(symbol: str, threshold: int, gap_end_ms: int = 1000):
    """Helper to create a Shard for testing."""
    from opendeviationbar.kintsugi.discovery import Shard

    return Shard(
        symbol=symbol,
        threshold_decimal_bps=threshold,
        gap_start_ms=0,
        gap_end_ms=gap_end_ms,
        gap_hours=1.0,
    )


def _make_result(shard, healed: bool = True):
    """Helper to create a KintsugiResult for testing."""
    from opendeviationbar.kintsugi.discovery import KintsugiResult

    return KintsugiResult(
        shard=shard,
        healed=healed,
        bars_written=10 if healed else 0,
        duration_seconds=1.0,
        had_anchor_tid=False,
        ouroboros_splits=0,
    )


class TestKintsugiPassYesterdayIntegration:
    """kintsugi_pass wires yesterday priority queue correctly."""

    def _common_patches(self):
        """Return a dict of common patches for kintsugi_pass tests."""
        return {
            "discover_shards": patch(
                "opendeviationbar.kintsugi.orchestrator.discover_shards",
                return_value=[],
            ),
            "repair_shard": patch(
                "opendeviationbar.kintsugi.orchestrator.repair_shard",
                side_effect=lambda shard, **_kw: _make_result(shard),
            ),
            "verify_repair": patch(
                "opendeviationbar.kintsugi.orchestrator.verify_repair",
                return_value=True,
            ),
            "select_candidates": patch(
                "opendeviationbar.kintsugi.orchestrator._select_repair_candidates",
                side_effect=lambda shards, max_repairs: shards[:max_repairs],
            ),
            "emit_telemetry": patch(
                "opendeviationbar.kintsugi.orchestrator._emit_telemetry",
            ),
            "sd_notify": patch(
                "opendeviationbar.kintsugi.orchestrator._sd_notify",
            ),
            "maybe_log": patch(
                "opendeviationbar.kintsugi.orchestrator._maybe_log_repair",
            ),
            "notify_failures": patch(
                "opendeviationbar.kintsugi.orchestrator._notify_kintsugi_failures",
            ),
            "kintsugi_cfg": patch(
                "opendeviationbar.kintsugi.orchestrator._kintsugi_cfg",
            ),
            "parquet_cached": patch(
                "opendeviationbar.kintsugi.orchestrator._shard_is_parquet_cached",
                return_value=False,
            ),
            "vision_available": patch(
                "opendeviationbar.kintsugi.orchestrator._shard_has_vision_available",
                return_value=False,
            ),
        }

    def _enter_patches(self, patches):
        """Enter all patches and return mocks dict."""
        mocks = {}
        for name, p in patches.items():
            mocks[name] = p.start()
        # Configure kintsugi_cfg mock
        cfg = mocks["kintsugi_cfg"].return_value
        cfg.max_repairs_per_pass = 100
        cfg.max_concurrent_repairs = 1
        cfg.max_concurrent_parquet = 1
        cfg.max_concurrent_rest = 1
        cfg.max_healed_details = 5
        return mocks

    def _exit_patches(self):
        from unittest.mock import patch

        patch.stopall()

    def test_injects_yesterday_shards_before_normal(self, tmp_path, monkeypatch):
        """Yesterday shards appear before normal discovery shards."""
        import opendeviationbar.sidecar.repair_signal as sig_mod

        sig_path = str(tmp_path / "yesterday-repair.json")
        monkeypatch.setattr(sig_mod, "SIGNAL_FILE_PATH", sig_path)

        # Write signal
        import json

        ts = datetime.now(UTC).isoformat()
        Path(sig_path).write_text(
            json.dumps({"symbols": {"BTCUSDT": "stathera_gap"}, "timestamp": ts, "sidecar_pid": 1})
        )

        normal_shard = _make_shard("ETHUSDT", 250, gap_end_ms=5000)

        patches = self._common_patches()
        patches["discover_shards"] = patch(
            "opendeviationbar.kintsugi.orchestrator.discover_shards",
            return_value=[normal_shard],
        )
        patches["load_yesterday"] = patch(
            "opendeviationbar.kintsugi.orchestrator._load_yesterday_priority_shards",
            return_value=[_make_shard("BTCUSDT", 250)],
        )

        mocks = self._enter_patches(patches)
        try:
            from opendeviationbar.kintsugi.orchestrator import kintsugi_pass

            results = kintsugi_pass()
            # Both shards should be processed
            assert len(results) == 2
            # BTCUSDT (yesterday) should appear in results
            symbols = [r.shard.symbol for r in results]
            assert "BTCUSDT" in symbols
            assert "ETHUSDT" in symbols
        finally:
            self._exit_patches()

    def test_clears_signal_after_all_processed(self, tmp_path, monkeypatch):
        """Signal file cleared after all yesterday shards processed."""
        import opendeviationbar.sidecar.repair_signal as sig_mod

        sig_path = str(tmp_path / "yesterday-repair.json")
        monkeypatch.setattr(sig_mod, "SIGNAL_FILE_PATH", sig_path)

        yesterday_shard = _make_shard("BTCUSDT", 250)
        patches = self._common_patches()
        patches["load_yesterday"] = patch(
            "opendeviationbar.kintsugi.orchestrator._load_yesterday_priority_shards",
            return_value=[yesterday_shard],
        )

        clear_calls = []
        patches["clear_signal"] = patch(
            "opendeviationbar.kintsugi.orchestrator.clear_yesterday_signal",
            side_effect=lambda: clear_calls.append(1),
        )

        mocks = self._enter_patches(patches)
        try:
            from opendeviationbar.kintsugi.orchestrator import kintsugi_pass

            kintsugi_pass()
            assert len(clear_calls) == 1
        finally:
            self._exit_patches()

    def test_keeps_signal_when_budget_exhausted(self, tmp_path, monkeypatch):
        """Signal file preserved when some yesterday shards not processed (budget)."""
        import opendeviationbar.sidecar.repair_signal as sig_mod

        sig_path = str(tmp_path / "yesterday-repair.json")
        monkeypatch.setattr(sig_mod, "SIGNAL_FILE_PATH", sig_path)

        yesterday_shards = [
            _make_shard("BTCUSDT", 250),
            _make_shard("BTCUSDT", 500),
        ]
        patches = self._common_patches()
        patches["load_yesterday"] = patch(
            "opendeviationbar.kintsugi.orchestrator._load_yesterday_priority_shards",
            return_value=yesterday_shards,
        )
        # Budget only allows 1 repair
        patches["select_candidates"] = patch(
            "opendeviationbar.kintsugi.orchestrator._select_repair_candidates",
            side_effect=lambda shards, _max_repairs: shards[:1],
        )

        clear_calls = []
        patches["clear_signal"] = patch(
            "opendeviationbar.kintsugi.orchestrator.clear_yesterday_signal",
            side_effect=lambda: clear_calls.append(1),
        )

        mocks = self._enter_patches(patches)
        try:
            from opendeviationbar.kintsugi.orchestrator import kintsugi_pass

            kintsugi_pass()
            # Signal should NOT be cleared (only 1 of 2 shards processed)
            assert len(clear_calls) == 0
        finally:
            self._exit_patches()

    def test_empty_discovery_with_yesterday_still_processes(self, tmp_path, monkeypatch):
        """If normal discovery finds nothing but signal file exists, still processes."""
        import opendeviationbar.sidecar.repair_signal as sig_mod

        sig_path = str(tmp_path / "yesterday-repair.json")
        monkeypatch.setattr(sig_mod, "SIGNAL_FILE_PATH", sig_path)

        yesterday_shard = _make_shard("BTCUSDT", 250)
        patches = self._common_patches()
        # Normal discovery returns nothing
        patches["discover_shards"] = patch(
            "opendeviationbar.kintsugi.orchestrator.discover_shards",
            return_value=[],
        )
        patches["load_yesterday"] = patch(
            "opendeviationbar.kintsugi.orchestrator._load_yesterday_priority_shards",
            return_value=[yesterday_shard],
        )

        mocks = self._enter_patches(patches)
        try:
            from opendeviationbar.kintsugi.orchestrator import kintsugi_pass

            results = kintsugi_pass()
            assert len(results) == 1
            assert results[0].shard.symbol == "BTCUSDT"
        finally:
            self._exit_patches()

    def test_deduplication(self, tmp_path, monkeypatch):
        """Normal discovery shard for same (symbol, threshold) removed when yesterday exists."""
        import opendeviationbar.sidecar.repair_signal as sig_mod

        sig_path = str(tmp_path / "yesterday-repair.json")
        monkeypatch.setattr(sig_mod, "SIGNAL_FILE_PATH", sig_path)

        yesterday_shard = _make_shard("BTCUSDT", 250)
        normal_shard_dup = _make_shard("BTCUSDT", 250, gap_end_ms=5000)
        normal_shard_other = _make_shard("ETHUSDT", 250, gap_end_ms=5000)

        patches = self._common_patches()
        patches["discover_shards"] = patch(
            "opendeviationbar.kintsugi.orchestrator.discover_shards",
            return_value=[normal_shard_dup, normal_shard_other],
        )
        patches["load_yesterday"] = patch(
            "opendeviationbar.kintsugi.orchestrator._load_yesterday_priority_shards",
            return_value=[yesterday_shard],
        )

        mocks = self._enter_patches(patches)
        try:
            from opendeviationbar.kintsugi.orchestrator import kintsugi_pass

            results = kintsugi_pass()
            # Should have 2: yesterday BTCUSDT@250 + normal ETHUSDT@250
            # NOT 3 (duplicate BTCUSDT@250 from discovery removed)
            assert len(results) == 2
            result_keys = {(r.shard.symbol, r.shard.threshold_decimal_bps) for r in results}
            assert result_keys == {("BTCUSDT", 250), ("ETHUSDT", 250)}
        finally:
            self._exit_patches()
