"""Unit tests for ``mgf.common.health`` — C-4 federation primitive (v0.37.0)."""

from __future__ import annotations

import pytest

from mgf.common.health import (
    ComponentCheck,
    ComponentResult,
    HealthRegistry,
    LivenessCheck,
    ReadinessCheck,
)

# Fixture helpers ------------------------------------------------------------


def _make_check(*, name: str, status, message: str = "", as_class=None):
    """Build a ComponentCheck dynamically.

    `as_class` lets us mark it as a LivenessCheck / ReadinessCheck
    subclass so the registry routes it correctly.
    """
    if as_class is None:

        class _C:
            def __init__(self):
                self.name = name

            def check(self):
                return ComponentResult(name=name, status=status, message=message)

        return _C()
    if as_class is LivenessCheck:

        class _LC(LivenessCheck):
            @property
            def name(self):
                return name

            def check(self):
                return ComponentResult(name=name, status=status, message=message)

        return _LC()
    if as_class is ReadinessCheck:

        class _RC(ReadinessCheck):
            @property
            def name(self):
                return name

            def check(self):
                return ComponentResult(name=name, status=status, message=message)

        return _RC()
    raise AssertionError("unreachable")


# ---------------------------------------------------------------------------
# ComponentResult shape
# ---------------------------------------------------------------------------


class TestComponentResult:
    def test_frozen(self) -> None:
        from dataclasses import FrozenInstanceError

        r = ComponentResult(name="x", status="ok")
        with pytest.raises(FrozenInstanceError):
            r.status = "unhealthy"  # type: ignore[misc]

    def test_defaults(self) -> None:
        r = ComponentResult(name="x", status="ok")
        assert r.message == ""
        assert r.latency_ms is None


# ---------------------------------------------------------------------------
# Protocol conformance
# ---------------------------------------------------------------------------


class TestComponentCheckProtocol:
    def test_duck_typed_passes(self) -> None:
        check = _make_check(name="x", status="ok")
        assert isinstance(check, ComponentCheck)


# ---------------------------------------------------------------------------
# HealthRegistry — registration + aggregation
# ---------------------------------------------------------------------------


class TestRegistryAggregation:
    def test_empty_registry_returns_ok(self) -> None:
        registry = HealthRegistry()
        report = registry.evaluate_all()
        assert report.status == "ok"
        assert report.components == ()

    def test_single_ok(self) -> None:
        registry = HealthRegistry()
        registry.add(_make_check(name="db", status="ok"))
        report = registry.evaluate_all()
        assert report.status == "ok"
        assert len(report.components) == 1
        assert report.components[0].name == "db"

    def test_worst_case_aggregation(self) -> None:
        registry = HealthRegistry()
        registry.add(_make_check(name="a", status="ok"))
        registry.add(_make_check(name="b", status="degraded"))
        registry.add(_make_check(name="c", status="ok"))
        report = registry.evaluate_all()
        assert report.status == "degraded"

    def test_unhealthy_dominates(self) -> None:
        registry = HealthRegistry()
        registry.add(_make_check(name="a", status="degraded"))
        registry.add(_make_check(name="b", status="unhealthy"))
        report = registry.evaluate_all()
        assert report.status == "unhealthy"

    def test_check_exception_reported_as_unhealthy(self) -> None:
        class _Buggy:
            @property
            def name(self) -> str:
                return "bad"

            def check(self) -> ComponentResult:
                raise RuntimeError("kaboom")

        registry = HealthRegistry()
        registry.add(_Buggy())
        report = registry.evaluate_all()
        assert report.status == "unhealthy"
        assert "kaboom" in report.components[0].message
        assert "RuntimeError" in report.components[0].message

    def test_latency_auto_populated(self) -> None:
        registry = HealthRegistry()
        registry.add(_make_check(name="x", status="ok"))
        report = registry.evaluate_all()
        # Latency filled in by registry when the check didn't set it.
        assert report.components[0].latency_ms is not None
        assert report.components[0].latency_ms >= 0

    def test_add_replaces_by_name(self) -> None:
        registry = HealthRegistry()
        registry.add(_make_check(name="db", status="ok"))
        registry.add(_make_check(name="db", status="unhealthy"))
        report = registry.evaluate_all()
        assert len(report.components) == 1
        assert report.components[0].status == "unhealthy"

    def test_remove(self) -> None:
        registry = HealthRegistry()
        registry.add(_make_check(name="x", status="ok"))
        registry.remove("x")
        registry.remove("never-existed")  # no-op
        assert len(registry.evaluate_all().components) == 0


# ---------------------------------------------------------------------------
# Liveness vs readiness routing
# ---------------------------------------------------------------------------


class TestLivenessReadinessRouting:
    def test_liveness_only(self) -> None:
        registry = HealthRegistry()
        registry.add(_make_check(name="proc", status="ok", as_class=LivenessCheck))
        registry.add(_make_check(name="db", status="ok", as_class=ReadinessCheck))
        report = registry.evaluate_liveness()
        names = {c.name for c in report.components}
        assert names == {"proc"}

    def test_readiness_only(self) -> None:
        registry = HealthRegistry()
        registry.add(_make_check(name="proc", status="ok", as_class=LivenessCheck))
        registry.add(_make_check(name="db", status="ok", as_class=ReadinessCheck))
        report = registry.evaluate_readiness()
        names = {c.name for c in report.components}
        assert names == {"db"}

    def test_unmarked_excluded_from_both(self) -> None:
        # A check that's neither LivenessCheck nor ReadinessCheck
        # appears only in `evaluate_all`.
        registry = HealthRegistry()
        registry.add(_make_check(name="unmarked", status="ok"))
        assert registry.evaluate_liveness().components == ()
        assert registry.evaluate_readiness().components == ()
        assert len(registry.evaluate_all().components) == 1


# ---------------------------------------------------------------------------
# HealthReport JSON shape
# ---------------------------------------------------------------------------


class TestReportRender:
    def test_render_json_shape(self) -> None:
        registry = HealthRegistry()
        registry.add(_make_check(name="db", status="ok", message="ping ok"))
        report = registry.evaluate_all()
        payload = report.render_json()
        assert payload["status"] == "ok"
        assert len(payload["components"]) == 1
        comp = payload["components"][0]
        assert comp["name"] == "db"
        assert comp["status"] == "ok"
        assert comp["message"] == "ping ok"
        assert "latency_ms" in comp
