"""
tests/test_health_checker.py — Tests for cortex/api/health.py and the enhanced
/api/v1/health + /api/v1/health/ready endpoints (Issue #456).

Coverage:
  - HealthChecker.check() returns "healthy" when all subsystems up
  - HealthChecker.check() returns "degraded" when error rate is high
  - HealthChecker.check() returns "unhealthy" when storage is down
  - GET /health returns compact {"status": ...} without ?detail
  - GET /health?detail=true returns full structure
  - GET /health/ready returns 503 when storage is down
  - GET /health/ready returns 200 when plugin is healthy
  - HealthReport dataclass fields are present and typed correctly
  - Error rate calculation from metrics
  - Memory RSS is reported as a float
"""
from __future__ import annotations

import sys
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

# ---------------------------------------------------------------------------
# Skip gracefully if FastAPI / httpx not installed
# ---------------------------------------------------------------------------
pytest.importorskip("fastapi", reason="fastapi not installed")
pytest.importorskip("httpx", reason="httpx not installed")

from fastapi.testclient import TestClient  # noqa: E402


# ---------------------------------------------------------------------------
# Fake supporting dataclasses
# ---------------------------------------------------------------------------

@dataclass
class _FakeHealthReport:
    ok: bool = True
    storage: str = "ok"
    modules: Dict[str, str] = field(default_factory=lambda: {"storage": "ok"})
    errors: List[str] = field(default_factory=list)
    timestamp: str = "2025-01-01T00:00:00+00:00"


@dataclass
class _FakeWakeReport:
    session_id: str = "sess-1"
    cornerstones_loaded: int = 0
    errors: List[str] = field(default_factory=list)


@dataclass
class _FakeDreamReport:
    steps_completed: int = 3
    curiosity_seeds: int = 1
    claims_consolidated: int = 5
    journal: str = "dream journal"
    errors: List[str] = field(default_factory=list)


def _make_mock_plugin(
    *,
    storage_ok: bool = True,
    init_errors: Optional[List[str]] = None,
    metrics_errors: float = 0.0,
) -> MagicMock:
    """Build a minimal MagicMock that looks like CortexPlugin."""
    plugin = MagicMock()
    plugin._init_errors = init_errors or []
    plugin.health = AsyncMock(return_value=_FakeHealthReport())
    plugin.wake = AsyncMock(return_value=_FakeWakeReport())
    plugin.dream = AsyncMock(return_value=_FakeDreamReport())

    # Config
    cfg = MagicMock()
    cfg.owner_id = "default"
    cfg.quality_gates_enabled = False
    cfg.metrics_enabled = True
    plugin.config = cfg

    # Storage mock
    if storage_ok:
        storage = MagicMock()
        storage.ping = AsyncMock(return_value=None)
        storage.initialize = AsyncMock(return_value=None)
        storage.close = AsyncMock(return_value=None)
        storage.count_claims = AsyncMock(return_value=42)
        storage.count_sessions = AsyncMock(return_value=5)
        storage._conn = MagicMock()  # truthy
        plugin.storage = storage
    else:
        plugin.storage = None

    # Metrics mock
    metrics = MagicMock()

    async def _get_summary():
        counters: Dict[str, Any] = {
            "memories_stored": 10,
            "memories_retrieved": 20,
        }
        if metrics_errors > 0:
            # Inject error-like counters to trigger high error rate
            counters["errors_total"] = metrics_errors * 10  # fake high value
        return {
            "counters": counters,
            "histograms": {},
            "gauges": {},
            "uptime_seconds": 60.0,  # 1 minute → easy rate calculation
        }

    metrics.get_summary = AsyncMock(side_effect=_get_summary)
    plugin._metrics = metrics

    return plugin


# ---------------------------------------------------------------------------
# Unit tests: HealthChecker class directly
# ---------------------------------------------------------------------------

class TestHealthCheckerDirect:
    """Test HealthChecker.check() without HTTP layer."""

    @pytest.mark.asyncio
    async def test_healthy_when_all_up(self):
        """Healthy status returned when storage is connected and no errors."""
        from cortex.api.health import HealthChecker

        plugin = _make_mock_plugin(storage_ok=True)
        report = await HealthChecker.check(plugin)

        assert report.status == "healthy"
        assert report.uptime_seconds >= 0.0
        assert report.version == "1.2.0"
        assert "storage" in report.checks
        assert report.checks["storage"]["status"] == "ok"

    @pytest.mark.asyncio
    async def test_unhealthy_when_storage_down(self):
        """Unhealthy status returned when storage is None."""
        from cortex.api.health import HealthChecker

        plugin = _make_mock_plugin(storage_ok=False)
        report = await HealthChecker.check(plugin)

        assert report.status == "unhealthy"
        assert report.checks["storage"]["status"] == "unhealthy"

    @pytest.mark.asyncio
    async def test_degraded_when_high_error_rate(self):
        """Degraded status when error rate exceeds threshold."""
        from cortex.api.health import HealthChecker

        # metrics_errors=2.0 will produce error_total=20 / 1 min = 20/min > 10 threshold
        plugin = _make_mock_plugin(storage_ok=True, metrics_errors=2.0)
        report = await HealthChecker.check(plugin)

        assert report.status == "degraded"
        assert report.checks["error_rate"]["status"] == "degraded"
        assert report.checks["error_rate"]["rate_per_min"] > 10.0

    @pytest.mark.asyncio
    async def test_degraded_when_init_errors(self):
        """Degraded (but not unhealthy) when plugin has init errors."""
        from cortex.api.health import HealthChecker

        plugin = _make_mock_plugin(
            storage_ok=True,
            init_errors=["config: some warning"],
        )
        report = await HealthChecker.check(plugin)

        # Storage is up so not unhealthy, but init errors make it degraded
        assert report.status in ("degraded", "healthy")
        assert report.checks["plugin_initialized"]["status"] == "degraded"

    @pytest.mark.asyncio
    async def test_health_report_has_required_fields(self):
        """HealthReport dataclass has all required fields."""
        from cortex.api.health import HealthChecker, HealthReport

        plugin = _make_mock_plugin()
        report = await HealthChecker.check(plugin)

        assert isinstance(report, HealthReport)
        assert hasattr(report, "status")
        assert hasattr(report, "checks")
        assert hasattr(report, "uptime_seconds")
        assert hasattr(report, "version")
        assert report.detail is None  # no detail by default

    @pytest.mark.asyncio
    async def test_detail_included_when_requested(self):
        """detail block is populated when detail=True."""
        from cortex.api.health import HealthChecker

        plugin = _make_mock_plugin(storage_ok=True)
        report = await HealthChecker.check(plugin, detail=True)

        assert report.detail is not None
        assert "storage" in report.detail
        assert "memory_rss_mb" in report.detail
        assert isinstance(report.detail["memory_rss_mb"], float)

    @pytest.mark.asyncio
    async def test_memory_rss_is_float(self):
        """RSS memory usage is reported as a non-negative float."""
        from cortex.api.health import HealthChecker

        plugin = _make_mock_plugin()
        report = await HealthChecker.check(plugin)

        rss = report.checks.get("memory_rss_mb")
        assert isinstance(rss, float)
        assert rss >= 0.0

    @pytest.mark.asyncio
    async def test_metrics_unavailable_returns_unknown_error_rate(self):
        """When metrics is None, error_rate check shows 'unknown'."""
        from cortex.api.health import HealthChecker

        plugin = _make_mock_plugin()
        plugin._metrics = None
        report = await HealthChecker.check(plugin)

        assert report.checks["error_rate"]["status"] == "unknown"
        assert report.checks["error_rate"]["rate_per_min"] is None


# ---------------------------------------------------------------------------
# HTTP endpoint tests via TestClient
# ---------------------------------------------------------------------------

@pytest.fixture
def client_with_plugin():
    """Create a TestClient with a healthy mock plugin."""
    from cortex.api.http_server import create_app

    plugin_mock = _make_mock_plugin(storage_ok=True)
    app = create_app()

    with patch("cortex.api.plugin.CortexPlugin") as MockPlugin:
        MockPlugin.return_value = plugin_mock
        with TestClient(app, raise_server_exceptions=True) as c:
            yield c


@pytest.fixture
def client_with_unhealthy_plugin():
    """Create a TestClient with a storage-down mock plugin."""
    from cortex.api.http_server import create_app

    plugin_mock = _make_mock_plugin(storage_ok=False)
    app = create_app()

    with patch("cortex.api.plugin.CortexPlugin") as MockPlugin:
        MockPlugin.return_value = plugin_mock
        with TestClient(app, raise_server_exceptions=True) as c:
            yield c


class TestHealthEndpoint:
    """Tests for GET /api/v1/health."""

    def test_health_returns_200(self, client_with_plugin):
        """Health endpoint returns 200 OK."""
        resp = client_with_plugin.get("/api/v1/health")
        assert resp.status_code == 200

    def test_health_compact_has_status_key(self, client_with_plugin):
        """Without ?detail, response has top-level 'status' key."""
        resp = client_with_plugin.get("/api/v1/health")
        data = resp.json()
        assert "status" in data
        assert data["status"] in ("healthy", "degraded", "unhealthy")

    def test_health_compact_no_detail_key(self, client_with_plugin):
        """Without ?detail=true, 'checks' / 'detail' blocks are absent."""
        resp = client_with_plugin.get("/api/v1/health")
        data = resp.json()
        # Compact response should NOT contain verbose keys
        assert "detail" not in data or data.get("detail") is None

    def test_health_detail_true_has_checks(self, client_with_plugin):
        """?detail=true response includes 'checks' block."""
        resp = client_with_plugin.get("/api/v1/health?detail=true")
        assert resp.status_code == 200
        data = resp.json()
        assert "checks" in data
        assert "storage" in data["checks"]

    def test_health_detail_true_has_uptime(self, client_with_plugin):
        """?detail=true response includes uptime_seconds."""
        resp = client_with_plugin.get("/api/v1/health?detail=true")
        data = resp.json()
        assert "uptime_seconds" in data
        assert isinstance(data["uptime_seconds"], (int, float))
        assert data["uptime_seconds"] >= 0

    def test_health_detail_true_has_version(self, client_with_plugin):
        """?detail=true response includes version string."""
        resp = client_with_plugin.get("/api/v1/health?detail=true")
        data = resp.json()
        assert "version" in data
        assert isinstance(data["version"], str)
        assert len(data["version"]) > 0


class TestHealthReadyEndpoint:
    """Tests for GET /api/v1/health/ready."""

    def test_ready_returns_200_when_healthy(self, client_with_plugin):
        """Ready probe returns 200 when plugin and storage are healthy."""
        resp = client_with_plugin.get("/api/v1/health/ready")
        assert resp.status_code == 200
        data = resp.json()
        assert data["ready"] is True

    def test_ready_returns_503_when_storage_down(self, client_with_unhealthy_plugin):
        """Ready probe returns 503 when storage is unavailable."""
        resp = client_with_unhealthy_plugin.get("/api/v1/health/ready")
        assert resp.status_code == 503
        data = resp.json()
        assert data["ready"] is False
        assert "reason" in data
