"""Tests for enhanced seeder heartbeat check (D-24, D-25)."""

from __future__ import annotations

import contextlib
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

# ── Fixtures ───────────────────────────────────────────────────────────


@pytest.fixture
def seeder_state_file(tmp_path: Path) -> Path:
    """Create a valid seeder state file with Vision 404 and REST fallback."""
    state_file = tmp_path / "seeder-state.txt"
    state_file.write_text(
        "ok\n"
        "timestamp=2026-03-22T00:00:00+00:00\n"
        "seeded=100\n"
        "elapsed_s=60\n"
        "failed=0\n"
        "vision_404_count=3\n"
        "rest_fallback_count=2\n"
        "BTCUSDT_seeded=50\n"
        "ETHUSDT_seeded=50\n"
    )
    return state_file


# ── Test 1: days_behind=0 when all Parquet files exist ─────────────────


def test_days_behind_zero(seeder_state_file: Path, tmp_path: Path) -> None:
    """When all Parquet files exist for last 7 days, seeder_days_behind = '0'."""
    from datetime import UTC, datetime

    today = datetime.now(UTC).date()
    parquet_dir = tmp_path / "parquet"
    parquet_dir.mkdir()

    def mock_get_path(symbol: str, day_key: str) -> Path:
        p = parquet_dir / f"{symbol}_{day_key}.parquet"
        p.touch()
        return p

    mock_storage = MagicMock()
    mock_storage._get_parquet_path.side_effect = mock_get_path

    results: dict[str, bool | str] = {}
    with (
        _patch_state_path(seeder_state_file),
        _patch_tick_storage(mock_storage),
        _patch_registry(["BTCUSDT", "ETHUSDT"]),
    ):
        from heartbeat.checks.seeder import check_seeder

        check_seeder(results)

    assert results.get("seeder_days_behind") == "0"


# ── Test 2: days_behind=3 when 3 of 7 files missing ───────────────────


def test_days_behind_nonzero(seeder_state_file: Path, tmp_path: Path) -> None:
    """When 3 of last 7 Parquet files are missing, seeder_days_behind = '3'."""
    from datetime import UTC, datetime, timedelta

    today = datetime.now(UTC).date()
    parquet_dir = tmp_path / "parquet"
    parquet_dir.mkdir()

    existing_days = set()
    for d in range(1, 5):  # days 1-4 exist, 5-7 missing
        existing_days.add((today - timedelta(days=d)).isoformat())

    def mock_get_path(symbol: str, day_key: str) -> Path:
        p = parquet_dir / f"{symbol}_{day_key}.parquet"
        if day_key in existing_days:
            p.touch()
        return p

    mock_storage = MagicMock()
    mock_storage._get_parquet_path.side_effect = mock_get_path

    results: dict[str, bool | str] = {}
    with (
        _patch_state_path(seeder_state_file),
        _patch_tick_storage(mock_storage),
        _patch_registry(["BTCUSDT"]),
    ):
        from heartbeat.checks.seeder import check_seeder

        check_seeder(results)

    assert results.get("seeder_days_behind") == "3"


# ── Test 3: vision_404_count parsed from state file ────────────────────


def test_vision_404_count(seeder_state_file: Path) -> None:
    """seeder_vision_404 parsed from state file kv."""
    results: dict[str, bool | str] = {}
    with _patch_state_path(seeder_state_file):
        from heartbeat.checks.seeder import check_seeder

        check_seeder(results)

    assert results.get("seeder_vision_404") == "3"


# ── Test 4: rest_fallback_count parsed from state file ─────────────────


def test_rest_fallback_count(seeder_state_file: Path) -> None:
    """seeder_rest_fallback parsed from state file kv."""
    results: dict[str, bool | str] = {}
    with _patch_state_path(seeder_state_file):
        from heartbeat.checks.seeder import check_seeder

        check_seeder(results)

    assert results.get("seeder_rest_fallback") == "2"


# ── Test 5: budget_used from RateLimitCoordinator ──────────────────────


def test_budget_used(seeder_state_file: Path) -> None:
    """seeder_budget_used reports from RateLimitCoordinator."""
    mock_coord_cls = MagicMock()
    mock_coord_cls.return_value.get_consumer_usage.return_value = 450

    mock_module = MagicMock()
    mock_module.RateLimitCoordinator = mock_coord_cls

    results: dict[str, bool | str] = {}
    old = sys.modules.get("opendeviationbar.ratelimit")
    sys.modules["opendeviationbar.ratelimit"] = mock_module
    try:
        with _patch_state_path(seeder_state_file):
            from heartbeat.checks.seeder import check_seeder

            check_seeder(results)
    finally:
        if old is not None:
            sys.modules["opendeviationbar.ratelimit"] = old
        else:
            sys.modules.pop("opendeviationbar.ratelimit", None)

    assert results.get("seeder_budget_used") == "450/800"


# ── Test 6: days_behind does NOT gate seeder_ok ────────────────────────


def test_days_behind_does_not_gate_ok(seeder_state_file: Path, tmp_path: Path) -> None:
    """seeder_ok remains True even when days_behind > 0."""
    parquet_dir = tmp_path / "parquet"
    parquet_dir.mkdir()

    def mock_get_path(symbol: str, day_key: str) -> Path:
        return parquet_dir / f"{symbol}_{day_key}.parquet"  # Never touched, doesn't exist

    mock_storage = MagicMock()
    mock_storage._get_parquet_path.side_effect = mock_get_path

    results: dict[str, bool | str] = {}
    with (
        _patch_state_path(seeder_state_file),
        _patch_tick_storage(mock_storage),
        _patch_registry(["BTCUSDT"]),
    ):
        from heartbeat.checks.seeder import check_seeder

        check_seeder(results)

    assert int(results.get("seeder_days_behind", "0")) > 0
    assert results["seeder_ok"] is True


# ── Helpers ────────────────────────────────────────────────────────────


@contextlib.contextmanager
def _patch_state_path(state_file: Path):
    """Redirect seeder state path."""
    import heartbeat.checks.seeder as mod

    original_path_cls = mod.Path

    class _MockPath(type(state_file)):
        def __new__(cls, *args: object, **kwargs: object) -> Path:
            if args and str(args[0]) == "/var/tmp/opendeviationbar-seeder-state.txt":
                return state_file
            return original_path_cls(*args, **kwargs)

    with patch.object(mod, "Path", _MockPath):
        yield


@contextlib.contextmanager
def _patch_tick_storage(mock_storage):
    """Mock TickStorage import inside check_seeder."""
    mock_storage_mod = MagicMock()
    mock_storage_mod.TickStorage.return_value = mock_storage

    old_storage = sys.modules.get("opendeviationbar.storage.parquet")
    sys.modules["opendeviationbar.storage.parquet"] = mock_storage_mod
    try:
        yield
    finally:
        if old_storage is not None:
            sys.modules["opendeviationbar.storage.parquet"] = old_storage
        else:
            sys.modules.pop("opendeviationbar.storage.parquet", None)


@contextlib.contextmanager
def _patch_registry(symbols: list[str]):
    """Mock get_registered_symbols import."""
    mock_reg_mod = MagicMock()
    mock_reg_mod.get_registered_symbols.return_value = symbols

    old_reg = sys.modules.get("opendeviationbar.symbol_registry")
    sys.modules["opendeviationbar.symbol_registry"] = mock_reg_mod
    try:
        yield
    finally:
        if old_reg is not None:
            sys.modules["opendeviationbar.symbol_registry"] = old_reg
        else:
            sys.modules.pop("opendeviationbar.symbol_registry", None)
