"""Unit tests for dashboard/routes/health.py.

Covers helper functions, caching, endpoint responses, and graceful
degradation when subsystems (Ollama, SQLite) are unavailable.

Fixes #945
"""

from __future__ import annotations

import time
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from dashboard.routes.health import (
    DependencyStatus,
    HealthStatus,
    SovereigntyReport,
    _calculate_overall_score,
    _check_lightning,
    _check_ollama_sync,
    _check_sqlite,
    _generate_recommendations,
)

# ---------------------------------------------------------------------------
# Pydantic models
# ---------------------------------------------------------------------------


class TestDependencyStatusModel:
    """Validate DependencyStatus model."""

    def test_fields(self):
        dep = DependencyStatus(
            name="Test", status="healthy", sovereignty_score=8, details={"key": "val"}
        )
        assert dep.name == "Test"
        assert dep.status == "healthy"
        assert dep.sovereignty_score == 8
        assert dep.details == {"key": "val"}

    def test_empty_details(self):
        dep = DependencyStatus(name="X", status="unavailable", sovereignty_score=0, details={})
        assert dep.details == {}


class TestSovereigntyReportModel:
    """Validate SovereigntyReport model."""

    def test_fields(self):
        report = SovereigntyReport(
            overall_score=9.3,
            dependencies=[],
            timestamp="2026-01-01T00:00:00+00:00",
            recommendations=["All good"],
        )
        assert report.overall_score == 9.3
        assert report.dependencies == []
        assert report.recommendations == ["All good"]


class TestHealthStatusModel:
    """Validate HealthStatus model."""

    def test_fields(self):
        hs = HealthStatus(
            status="ok",
            timestamp="2026-01-01T00:00:00+00:00",
            version="2.0.0",
            uptime_seconds=42.5,
        )
        assert hs.status == "ok"
        assert hs.uptime_seconds == 42.5


# ---------------------------------------------------------------------------
# Helper functions
# ---------------------------------------------------------------------------


class TestCalculateOverallScore:
    """Test _calculate_overall_score."""

    def test_empty_deps(self):
        assert _calculate_overall_score([]) == 0.0

    def test_single_dep(self):
        deps = [DependencyStatus(name="A", status="healthy", sovereignty_score=7, details={})]
        assert _calculate_overall_score(deps) == 7.0

    def test_averages_multiple(self):
        deps = [
            DependencyStatus(name="A", status="healthy", sovereignty_score=10, details={}),
            DependencyStatus(name="B", status="healthy", sovereignty_score=8, details={}),
            DependencyStatus(name="C", status="unavailable", sovereignty_score=6, details={}),
        ]
        assert _calculate_overall_score(deps) == 8.0

    def test_rounding(self):
        deps = [
            DependencyStatus(name="A", status="healthy", sovereignty_score=10, details={}),
            DependencyStatus(name="B", status="healthy", sovereignty_score=9, details={}),
            DependencyStatus(name="C", status="healthy", sovereignty_score=10, details={}),
        ]
        assert _calculate_overall_score(deps) == 9.7


class TestGenerateRecommendations:
    """Test _generate_recommendations."""

    def test_all_healthy(self):
        deps = [DependencyStatus(name="X", status="healthy", sovereignty_score=10, details={})]
        recs = _generate_recommendations(deps)
        assert recs == ["System operating optimally - all dependencies healthy"]

    def test_unavailable_service(self):
        deps = [
            DependencyStatus(
                name="Ollama AI", status="unavailable", sovereignty_score=10, details={}
            )
        ]
        recs = _generate_recommendations(deps)
        assert any("Ollama AI is unavailable" in r for r in recs)

    def test_degraded_lightning_mock(self):
        deps = [
            DependencyStatus(
                name="Lightning Payments",
                status="degraded",
                sovereignty_score=8,
                details={"backend": "mock"},
            )
        ]
        recs = _generate_recommendations(deps)
        assert any("Switch to real Lightning" in r for r in recs)

    def test_degraded_non_lightning(self):
        """Degraded non-Lightning dep produces no specific recommendation."""
        deps = [DependencyStatus(name="Redis", status="degraded", sovereignty_score=5, details={})]
        recs = _generate_recommendations(deps)
        assert recs == ["System operating optimally - all dependencies healthy"]

    def test_multiple_unavailable(self):
        deps = [
            DependencyStatus(name="A", status="unavailable", sovereignty_score=5, details={}),
            DependencyStatus(name="B", status="unavailable", sovereignty_score=5, details={}),
        ]
        recs = _generate_recommendations(deps)
        assert len(recs) == 2
        assert "A is unavailable" in recs[0]
        assert "B is unavailable" in recs[1]


# ---------------------------------------------------------------------------
# _check_lightning (static)
# ---------------------------------------------------------------------------


class TestCheckLightning:
    """Test _check_lightning — always returns unavailable for now."""

    def test_returns_unavailable(self):
        dep = _check_lightning()
        assert dep.name == "Lightning Payments"
        assert dep.status == "unavailable"
        assert dep.sovereignty_score == 8
        assert "removed" in dep.details.get("note", "").lower()


# ---------------------------------------------------------------------------
# _check_ollama_sync
# ---------------------------------------------------------------------------


class TestCheckOllamaSync:
    """Test synchronous Ollama health probe."""

    def test_healthy_when_reachable(self):
        mock_resp = MagicMock()
        mock_resp.status = 200
        mock_resp.__enter__ = MagicMock(return_value=mock_resp)
        mock_resp.__exit__ = MagicMock(return_value=False)

        with patch("urllib.request.urlopen", return_value=mock_resp):
            dep = _check_ollama_sync()

        assert dep.status == "healthy"
        assert dep.name == "Ollama AI"
        assert dep.sovereignty_score == 10

    def test_unavailable_on_connection_error(self):
        with patch(
            "urllib.request.urlopen",
            side_effect=ConnectionError("refused"),
        ):
            dep = _check_ollama_sync()

        assert dep.status == "unavailable"
        assert "Cannot connect" in dep.details.get("error", "")

    def test_unavailable_on_timeout(self):
        from urllib.error import URLError

        with patch(
            "urllib.request.urlopen",
            side_effect=URLError("timeout"),
        ):
            dep = _check_ollama_sync()

        assert dep.status == "unavailable"


# ---------------------------------------------------------------------------
# _check_sqlite
# ---------------------------------------------------------------------------


class TestCheckSQLite:
    """Test SQLite health probe."""

    def test_healthy_when_db_reachable(self, tmp_path):
        import sqlite3

        db_path = tmp_path / "data" / "timmy.db"
        db_path.parent.mkdir(parents=True)
        sqlite3.connect(str(db_path)).close()

        with patch("dashboard.routes.health.settings") as mock_settings:
            mock_settings.repo_root = str(tmp_path)
            dep = _check_sqlite()

        assert dep.status == "healthy"
        assert dep.name == "SQLite Database"

    def test_unavailable_on_missing_db(self, tmp_path):
        with patch("dashboard.routes.health.settings") as mock_settings:
            mock_settings.repo_root = str(tmp_path / "nonexistent")
            dep = _check_sqlite()

        assert dep.status == "unavailable"
        assert "error" in dep.details


# ---------------------------------------------------------------------------
# _check_ollama (async, with caching)
# ---------------------------------------------------------------------------


class TestCheckOllamaAsync:
    """Test async Ollama check with TTL cache."""

    @pytest.fixture(autouse=True)
    def _reset_cache(self):
        """Clear the module-level Ollama cache before each test."""
        import dashboard.routes.health as mod

        mod._ollama_cache = None
        mod._ollama_cache_ts = 0.0
        yield
        mod._ollama_cache = None
        mod._ollama_cache_ts = 0.0

    @pytest.mark.asyncio
    async def test_returns_dependency_status(self):
        healthy = DependencyStatus(
            name="Ollama AI", status="healthy", sovereignty_score=10, details={}
        )
        with patch(
            "dashboard.routes.health._check_ollama_sync",
            return_value=healthy,
        ):
            from dashboard.routes.health import _check_ollama

            result = await _check_ollama()

        assert result.status == "healthy"

    @pytest.mark.asyncio
    async def test_caches_result(self):
        healthy = DependencyStatus(
            name="Ollama AI", status="healthy", sovereignty_score=10, details={}
        )
        with patch(
            "dashboard.routes.health._check_ollama_sync",
            return_value=healthy,
        ) as mock_sync:
            from dashboard.routes.health import _check_ollama

            await _check_ollama()
            await _check_ollama()

        # Should only call the sync function once due to cache
        assert mock_sync.call_count == 1

    @pytest.mark.asyncio
    async def test_cache_expires(self):
        healthy = DependencyStatus(
            name="Ollama AI", status="healthy", sovereignty_score=10, details={}
        )
        import dashboard.routes.health as mod

        with patch(
            "dashboard.routes.health._check_ollama_sync",
            return_value=healthy,
        ) as mock_sync:
            from dashboard.routes.health import _check_ollama

            await _check_ollama()
            # Expire the cache
            mod._ollama_cache_ts = time.monotonic() - 60
            await _check_ollama()

        assert mock_sync.call_count == 2

    @pytest.mark.asyncio
    async def test_fallback_on_thread_exception(self):
        """If to_thread raises, return unavailable status."""
        import asyncio

        with patch.object(
            asyncio,
            "to_thread",
            side_effect=RuntimeError("thread pool exhausted"),
        ):
            from dashboard.routes.health import _check_ollama

            result = await _check_ollama()

        assert result.status == "unavailable"


class TestCheckOllamaBool:
    """Test the legacy bool wrapper."""

    @pytest.fixture(autouse=True)
    def _reset_cache(self):
        import dashboard.routes.health as mod

        mod._ollama_cache = None
        mod._ollama_cache_ts = 0.0
        yield
        mod._ollama_cache = None
        mod._ollama_cache_ts = 0.0

    @pytest.mark.asyncio
    async def test_true_when_healthy(self):
        healthy = DependencyStatus(
            name="Ollama AI", status="healthy", sovereignty_score=10, details={}
        )
        with patch("dashboard.routes.health._check_ollama_sync", return_value=healthy):
            from dashboard.routes.health import check_ollama

            assert await check_ollama() is True

    @pytest.mark.asyncio
    async def test_false_when_unavailable(self):
        down = DependencyStatus(
            name="Ollama AI", status="unavailable", sovereignty_score=10, details={}
        )
        with patch("dashboard.routes.health._check_ollama_sync", return_value=down):
            from dashboard.routes.health import check_ollama

            assert await check_ollama() is False


# ---------------------------------------------------------------------------
# Endpoint tests via FastAPI TestClient
# ---------------------------------------------------------------------------


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

    def test_returns_200(self, client):
        response = client.get("/health")
        assert response.status_code == 200

    def test_ok_when_ollama_up(self, client):
        with patch(
            "dashboard.routes.health.check_ollama", new_callable=AsyncMock, return_value=True
        ):
            data = client.get("/health").json()

        assert data["status"] == "ok"
        assert data["services"]["ollama"] == "up"
        assert data["agents"]["agent"]["status"] == "idle"

    def test_degraded_when_ollama_down(self, client):
        with patch(
            "dashboard.routes.health.check_ollama", new_callable=AsyncMock, return_value=False
        ):
            data = client.get("/health").json()

        assert data["status"] == "degraded"
        assert data["services"]["ollama"] == "down"
        assert data["agents"]["agent"]["status"] == "offline"

    def test_extended_fields(self, client):
        data = client.get("/health").json()
        assert "timestamp" in data
        assert "version" in data
        assert "uptime_seconds" in data
        assert isinstance(data["uptime_seconds"], (int, float))
        assert "llm_backend" in data
        assert "llm_model" in data


class TestHealthStatusPanel:
    """Tests for GET /health/status (HTML response)."""

    def test_returns_html(self, client):
        response = client.get("/health/status")
        assert response.status_code == 200
        assert "text/html" in response.headers["content-type"]

    def test_shows_up_when_ollama_healthy(self, client):
        with patch(
            "dashboard.routes.health.check_ollama", new_callable=AsyncMock, return_value=True
        ):
            text = client.get("/health/status").text

        assert "UP" in text

    def test_shows_down_when_ollama_unhealthy(self, client):
        with patch(
            "dashboard.routes.health.check_ollama", new_callable=AsyncMock, return_value=False
        ):
            text = client.get("/health/status").text

        assert "DOWN" in text

    def test_includes_model_name(self, client):
        text = client.get("/health/status").text
        assert "Model:" in text


class TestSovereigntyEndpoint:
    """Tests for GET /health/sovereignty."""

    def test_aggregates_three_subsystems(self, client):
        data = client.get("/health/sovereignty").json()
        names = [d["name"] for d in data["dependencies"]]
        assert "Ollama AI" in names
        assert "Lightning Payments" in names
        assert "SQLite Database" in names

    def test_score_range(self, client):
        data = client.get("/health/sovereignty").json()
        assert 0 <= data["overall_score"] <= 10


class TestComponentsEndpoint:
    """Tests for GET /health/components."""

    def test_returns_timestamp(self, client):
        data = client.get("/health/components").json()
        assert "timestamp" in data

    def test_config_keys(self, client):
        data = client.get("/health/components").json()
        cfg = data["config"]
        assert "debug" in cfg
        assert "model_backend" in cfg
        assert "ollama_model" in cfg


class TestSnapshotEndpoint:
    """Tests for GET /health/snapshot."""

    def test_returns_200(self, client):
        response = client.get("/health/snapshot")
        assert response.status_code == 200

    def test_overall_status_valid(self, client):
        data = client.get("/health/snapshot").json()
        assert data["overall_status"] in ["green", "yellow", "red", "unknown"]

    def test_graceful_fallback_on_import_error(self, client):
        """Snapshot degrades gracefully when automation module fails."""
        with patch(
            "dashboard.routes.health.asyncio.to_thread",
            side_effect=ImportError("no module"),
        ):
            data = client.get("/health/snapshot").json()

        assert data["overall_status"] == "unknown"
        assert "error" in data
        assert data["ci"]["status"] == "unknown"

    def test_graceful_fallback_on_runtime_error(self, client):
        with patch(
            "dashboard.routes.health.asyncio.to_thread",
            side_effect=RuntimeError("boom"),
        ):
            data = client.get("/health/snapshot").json()

        assert data["overall_status"] == "unknown"
