"""Tests for yesterday integrity check + signal file (Phase 01, Plan 01).

Covers:
- signal.py: atomic write/read of yesterday-repair signal file
- midnight.py: _check_yesterday_integrity() -- orphan check + Stathera audit
"""

from __future__ import annotations

import json
from pathlib import Path
from unittest.mock import MagicMock, patch

# ---------------------------------------------------------------------------
# Signal file tests
# ---------------------------------------------------------------------------


class TestWriteYesterdaySignal:
    """write_yesterday_signal writes valid JSON atomically."""

    def test_writes_valid_json(self, tmp_path, monkeypatch):
        import opendeviationbar.sidecar.yesterday_signal as sig_mod

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

        sig_mod.write_yesterday_signal({"BTCUSDT": "stathera_gap"})

        assert Path(sig_path).exists()
        data = json.loads(Path(sig_path).read_text())
        assert data["symbols"] == {"BTCUSDT": "stathera_gap"}
        assert "timestamp" in data
        assert "sidecar_pid" in data

    def test_empty_dict_writes_file(self, tmp_path, monkeypatch):
        import opendeviationbar.sidecar.yesterday_signal as sig_mod

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

        sig_mod.write_yesterday_signal({})

        data = json.loads(Path(sig_path).read_text())
        assert data["symbols"] == {}

    def test_custom_pid(self, tmp_path, monkeypatch):
        import opendeviationbar.sidecar.yesterday_signal as sig_mod

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

        sig_mod.write_yesterday_signal({"ETHUSDT": "orphan_misaligned"}, sidecar_pid=12345)

        data = json.loads(Path(sig_path).read_text())
        assert data["sidecar_pid"] == 12345


class TestReadYesterdaySignal:
    """read_yesterday_signal returns parsed dict or None."""

    def test_returns_none_when_missing(self, tmp_path, monkeypatch):
        import opendeviationbar.sidecar.yesterday_signal as sig_mod

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

        assert sig_mod.read_yesterday_signal() is None

    def test_returns_parsed_dict(self, tmp_path, monkeypatch):
        import opendeviationbar.sidecar.yesterday_signal as sig_mod

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

        # Write manually
        data = {"symbols": {"BTCUSDT": "stathera_gap"}, "timestamp": "t", "sidecar_pid": 1}
        Path(sig_path).write_text(json.dumps(data))

        result = sig_mod.read_yesterday_signal()
        assert result is not None
        assert result["symbols"]["BTCUSDT"] == "stathera_gap"


# ---------------------------------------------------------------------------
# _check_yesterday_integrity tests
# ---------------------------------------------------------------------------


class TestCheckYesterdayIntegrity:
    """_check_yesterday_integrity returns {symbol: reason} dict."""

    def _mock_cache(self, orphan_tid_map: dict[str, int | None]):
        """Create a mock cache where get_yesterday_orphan_tid returns from map."""
        cache = MagicMock()
        cache.get_yesterday_orphan_tid = MagicMock(
            side_effect=lambda sym: orphan_tid_map.get(sym)
        )
        # Default: Stathera query returns empty (clean)
        mock_result = MagicMock()
        mock_result.result_rows = []
        cache.client.query = MagicMock(return_value=mock_result)
        return cache

    def _patch_integrity_deps(self):
        """Patch all external deps of _check_yesterday_integrity."""
        return (
            patch(
                "opendeviationbar.clickhouse.cache.OpenDeviationBarCache",
            ),
            patch(
                "opendeviationbar.ouroboros.get_operational_ouroboros_mode",
                return_value="day",
            ),
        )

    def test_all_clean(self):
        from opendeviationbar.sidecar.midnight import _check_yesterday_integrity

        cache = self._mock_cache({"BTCUSDT": 999})  # orphan_tid = crossover - 1 = 999
        p_cache, p_ouroboros = self._patch_integrity_deps()
        with p_cache as mock_cache_cls, p_ouroboros:
            mock_cache_cls.return_value.__enter__ = MagicMock(return_value=cache)
            mock_cache_cls.return_value.__exit__ = MagicMock(return_value=False)

            result = _check_yesterday_integrity(
                symbols=["BTCUSDT"],
                thresholds=[250, 500],
                preflight_verified={"BTCUSDT": 1000},  # crossover_tid
            )
        assert result == {}

    def test_orphan_misaligned(self):
        from opendeviationbar.sidecar.midnight import _check_yesterday_integrity

        # orphan_tid=998 but crossover=1000 so expected=999, mismatch
        cache = self._mock_cache({"AVAXUSDT": 998})
        p_cache, p_ouroboros = self._patch_integrity_deps()
        with p_cache as mock_cache_cls, p_ouroboros:
            mock_cache_cls.return_value.__enter__ = MagicMock(return_value=cache)
            mock_cache_cls.return_value.__exit__ = MagicMock(return_value=False)

            result = _check_yesterday_integrity(
                symbols=["AVAXUSDT"],
                thresholds=[250],
                preflight_verified={"AVAXUSDT": 1000},
            )
        assert result == {"AVAXUSDT": "orphan_misaligned"}

    def test_orphan_missing(self):
        from opendeviationbar.sidecar.midnight import _check_yesterday_integrity

        cache = self._mock_cache({"AVAXUSDT": None})  # No orphan bar
        p_cache, p_ouroboros = self._patch_integrity_deps()
        with p_cache as mock_cache_cls, p_ouroboros:
            mock_cache_cls.return_value.__enter__ = MagicMock(return_value=cache)
            mock_cache_cls.return_value.__exit__ = MagicMock(return_value=False)

            result = _check_yesterday_integrity(
                symbols=["AVAXUSDT"],
                thresholds=[250],
                preflight_verified={"AVAXUSDT": 1000},
            )
        assert result == {"AVAXUSDT": "orphan_missing"}

    def test_stathera_gap(self):
        from opendeviationbar.sidecar.midnight import _check_yesterday_integrity

        cache = self._mock_cache({"BTCUSDT": 999})  # orphan OK
        p_cache, p_ouroboros = self._patch_integrity_deps()
        with p_cache as mock_cache_cls, p_ouroboros:
            mock_cache_cls.return_value.__enter__ = MagicMock(return_value=cache)
            mock_cache_cls.return_value.__exit__ = MagicMock(return_value=False)

            # Stathera query returns a gap row (tid_delta > 0)
            mock_result = MagicMock()
            mock_result.result_rows = [(250, 5)]  # threshold=250, tid_delta=5 (gap)
            cache.client.query = MagicMock(return_value=mock_result)

            result = _check_yesterday_integrity(
                symbols=["BTCUSDT"],
                thresholds=[250, 500],
                preflight_verified={"BTCUSDT": 1000},
            )
        assert result == {"BTCUSDT": "stathera_gap"}

    def test_stathera_overlap(self):
        from opendeviationbar.sidecar.midnight import _check_yesterday_integrity

        cache = self._mock_cache({"ETHUSDT": 999})  # orphan OK
        p_cache, p_ouroboros = self._patch_integrity_deps()
        with p_cache as mock_cache_cls, p_ouroboros:
            mock_cache_cls.return_value.__enter__ = MagicMock(return_value=cache)
            mock_cache_cls.return_value.__exit__ = MagicMock(return_value=False)

            # Stathera query returns an overlap row (tid_delta < 0)
            mock_result = MagicMock()
            mock_result.result_rows = [(500, -3)]  # threshold=500, tid_delta=-3 (overlap)
            cache.client.query = MagicMock(return_value=mock_result)

            result = _check_yesterday_integrity(
                symbols=["ETHUSDT"],
                thresholds=[250, 500],
                preflight_verified={"ETHUSDT": 1000},
            )
        assert result == {"ETHUSDT": "stathera_overlap"}

    def test_ch_unavailable(self):
        from opendeviationbar.sidecar.midnight import _check_yesterday_integrity

        p_cache, p_ouroboros = self._patch_integrity_deps()
        with p_cache as mock_cache_cls, p_ouroboros:
            mock_cache_cls.return_value.__enter__ = MagicMock(
                side_effect=ConnectionError("CH down")
            )
            mock_cache_cls.return_value.__exit__ = MagicMock(return_value=False)

            result = _check_yesterday_integrity(
                symbols=["BTCUSDT", "ETHUSDT"],
                thresholds=[250],
                preflight_verified={"BTCUSDT": 1000, "ETHUSDT": 2000},
            )
        assert result == {"BTCUSDT": "ch_unavailable", "ETHUSDT": "ch_unavailable"}

    def test_aion_symbol_skips_orphan_check(self):
        """Aion-mode symbols skip orphan check entirely -- no CH query for that symbol."""
        from opendeviationbar.sidecar.midnight import _check_yesterday_integrity

        cache = self._mock_cache({})
        p_cache = patch(
            "opendeviationbar.clickhouse.cache.OpenDeviationBarCache",
        )
        # Provide a symbol entry with aion mode
        mock_entry = MagicMock()
        mock_entry.ouroboros_mode = MagicMock()
        mock_entry.ouroboros_mode.value = "aion"

        p_entries = patch(
            "opendeviationbar.symbol_registry.get_symbol_entries",
            return_value={"BTCUSDT": mock_entry},
        )
        with p_cache as mock_cache_cls, p_entries:
            mock_cache_cls.return_value.__enter__ = MagicMock(return_value=cache)
            mock_cache_cls.return_value.__exit__ = MagicMock(return_value=False)

            result = _check_yesterday_integrity(
                symbols=["BTCUSDT"],
                thresholds=[250, 500],
                preflight_verified={"BTCUSDT": 1000},
            )
        # Aion symbol should NOT be flagged -- orphan check skipped entirely
        assert result == {}
        # Should NOT have queried CH for orphan TID
        cache.get_yesterday_orphan_tid.assert_not_called()

    def test_skips_symbols_not_in_preflight(self):
        from opendeviationbar.sidecar.midnight import _check_yesterday_integrity

        cache = self._mock_cache({})
        p_cache, p_ouroboros = self._patch_integrity_deps()
        with p_cache as mock_cache_cls, p_ouroboros:
            mock_cache_cls.return_value.__enter__ = MagicMock(return_value=cache)
            mock_cache_cls.return_value.__exit__ = MagicMock(return_value=False)

            # BTCUSDT in symbols but NOT in preflight_verified -- should be skipped
            result = _check_yesterday_integrity(
                symbols=["BTCUSDT", "ETHUSDT"],
                thresholds=[250],
                preflight_verified={"ETHUSDT": 2000},  # Only ETHUSDT
            )
        # ETHUSDT has no orphan_tid (not in mock map) -> orphan_missing
        assert "BTCUSDT" not in result
        assert result == {"ETHUSDT": "orphan_missing"}
