"""Tests for the check registry (sdk/checks.py)."""

from __future__ import annotations

from typing import Any
from unittest.mock import patch

import pytest

from kuberoku.factory import KuberokuFactory
from kuberoku.models import CheckResult, FixResult
from kuberoku.sdk.checks import (
    CLUSTER_REQUIREMENTS,
    ClusterRequirement,
    fix_all,
    run_all_checks,
)
from tests.backends.fake import FakeK8sClient


class TestClusterRequirements:
    """Tests for the CLUSTER_REQUIREMENTS registry."""

    def test_all_have_unique_names(self) -> None:
        names = [r.name for r in CLUSTER_REQUIREMENTS]
        assert len(names) == len(set(names))

    def test_all_are_cluster_requirement_type(self) -> None:
        for req in CLUSTER_REQUIREMENTS:
            assert isinstance(req, ClusterRequirement)

    def test_count(self) -> None:
        assert len(CLUSTER_REQUIREMENTS) == 14

    def test_new_phase9_checks_exist(self) -> None:
        names = [r.name for r in CLUSTER_REQUIREMENTS]
        assert "ingress_controller" in names
        assert "ingress_crud" in names
        assert "cert_manager" in names
        assert "lb_support" in names
        assert "rbac_permissions" in names

    def test_categories_are_valid(self) -> None:
        valid_categories = {"connectivity", "rbac", "storage", "infra"}
        for req in CLUSTER_REQUIREMENTS:
            assert req.category in valid_categories

    def test_namespace_access_is_fixable(self) -> None:
        ns_req = next(r for r in CLUSTER_REQUIREMENTS if r.name == "namespace_access")
        assert ns_req.fix_fn is not None

    def test_cni_enforcement_is_fixable(self) -> None:
        cni_req = next(r for r in CLUSTER_REQUIREMENTS if r.name == "cni_enforcement")
        assert cni_req.fix_fn is not None

    def test_cert_manager_is_fixable(self) -> None:
        cm_req = next(r for r in CLUSTER_REQUIREMENTS if r.name == "cert_manager")
        assert cm_req.fix_fn is not None

    def test_fixable_checks_count(self) -> None:
        fixable = [r for r in CLUSTER_REQUIREMENTS if r.fix_fn is not None]
        assert len(fixable) == 3  # namespace_access, cni_enforcement, cert_manager


class TestRunAllChecks:
    """Tests for run_all_checks on FakeK8sClient."""

    def test_all_pass_on_fake(self, factory: KuberokuFactory) -> None:
        results = run_all_checks(factory)
        assert len(results) == 14
        for result in results:
            assert isinstance(result, CheckResult)
        # All should pass except cni_enforcement, ingress_controller,
        # cert_manager, lb_support (fake has no DaemonSets/IngressClasses/etc.)
        names_passed = {r.name for r in results if r.passed}
        assert "api_reachable" in names_passed
        assert "namespace_access" in names_passed
        assert "configmap_crud" in names_passed
        assert "secret_crud" in names_passed
        assert "deployment_crud" in names_passed
        assert "service_crud" in names_passed
        assert "network_policy_crud" in names_passed
        assert "storage_class" in names_passed
        assert "rbac_permissions" in names_passed

    def test_cni_check_fails_on_fake(
        self, factory: KuberokuFactory, request: pytest.FixtureRequest
    ) -> None:
        """FakeK8sClient has no DaemonSets, so CNI check fails."""
        if request.config.getoption("--backend") == "real":
            pytest.skip("Real K8s has a CNI installed — this test is fake-only")
        results = run_all_checks(factory)
        cni = next(r for r in results if r.name == "cni_enforcement")
        assert cni.passed is False
        assert "No known NetworkPolicy CNI detected" in cni.message

    def test_cni_check_passes_with_calico(self) -> None:
        """If calico-node DaemonSet exists in kube-system, CNI check passes."""
        k8s = FakeK8sClient()
        k8s.create_namespace("kube-system")
        k8s.create_daemonset(
            "kube-system",
            {
                "apiVersion": "apps/v1",
                "kind": "DaemonSet",
                "metadata": {"name": "calico-node"},
                "spec": {},
            },
        )
        factory = KuberokuFactory(k8s_client=k8s, namespace="test-ns")
        results = run_all_checks(factory)
        cni = next(r for r in results if r.name == "cni_enforcement")
        assert cni.passed is True
        assert "calico-node" in cni.message

    def test_check_results_have_categories(self, factory: KuberokuFactory) -> None:
        results = run_all_checks(factory)
        for r in results:
            assert r.category in {"connectivity", "rbac", "storage", "infra"}

    def test_failed_checks_have_detail_or_fix_hint(self) -> None:
        k8s = _FailingK8sClient()
        factory = KuberokuFactory(k8s_client=k8s, namespace="test-ns")
        results = run_all_checks(factory)
        failed = [r for r in results if not r.passed]
        assert len(failed) > 0
        for r in failed:
            # Failed checks should have either detail or fix_hint
            assert r.detail is not None or r.fix_hint is not None

    def test_probe_resources_cleaned_up(self, factory: KuberokuFactory) -> None:
        run_all_checks(factory)
        k8s = factory.k8s
        ns = factory.namespace
        prefix = factory.prefix
        probe_cms = [
            cm
            for cm in k8s.list_configmaps(ns)
            if cm.get("metadata", {}).get("name", "").startswith(f"{prefix}-doctor-")
        ]
        assert probe_cms == []


class TestFixAll:
    """Tests for fix_all."""

    def test_no_fixes_when_all_pass(self) -> None:
        k8s = FakeK8sClient()
        k8s.create_namespace("kube-system")
        k8s.create_daemonset(
            "kube-system",
            {
                "apiVersion": "apps/v1",
                "kind": "DaemonSet",
                "metadata": {"name": "calico-node"},
                "spec": {},
            },
        )
        # Install cert-manager so that check passes and fix isn't attempted
        k8s.create_deployment(
            "cert-manager",
            {
                "apiVersion": "apps/v1",
                "kind": "Deployment",
                "metadata": {"name": "cert-manager"},
                "spec": {
                    "replicas": 1,
                    "selector": {"matchLabels": {"app": "cert-manager"}},
                    "template": {
                        "metadata": {"labels": {"app": "cert-manager"}},
                        "spec": {"containers": [{"name": "cm", "image": "cert-manager:v1.14"}]},
                    },
                },
            },
        )
        factory = KuberokuFactory(k8s_client=k8s, namespace="test-ns")
        fixes = fix_all(factory)
        assert len(fixes) == 0

    def test_no_fixes_when_namespace_auto_created(self) -> None:
        """On FakeK8sClient with CNI + cert-manager, no fix needed."""
        k8s = FakeK8sClient()
        k8s.create_namespace("kube-system")
        k8s.create_daemonset(
            "kube-system",
            {
                "apiVersion": "apps/v1",
                "kind": "DaemonSet",
                "metadata": {"name": "calico-node"},
                "spec": {},
            },
        )
        k8s.create_deployment(
            "cert-manager",
            {
                "apiVersion": "apps/v1",
                "kind": "Deployment",
                "metadata": {"name": "cert-manager"},
                "spec": {
                    "replicas": 1,
                    "selector": {"matchLabels": {"app": "cert-manager"}},
                    "template": {
                        "metadata": {"labels": {"app": "cert-manager"}},
                        "spec": {"containers": [{"name": "cm", "image": "cert-manager:v1.14"}]},
                    },
                },
            },
        )
        factory = KuberokuFactory(k8s_client=k8s, namespace="new-ns")
        run_all_checks(factory)
        fixes = fix_all(factory)
        assert len(fixes) == 0
        assert k8s.namespace_exists("new-ns")

    @patch("kuberoku.sdk.checks.subprocess.run")
    def test_fixes_namespace_when_check_cannot_create(self, mock_run: Any) -> None:
        """When check can't create ns (permission denied), fix creates it."""
        k8s = _CheckFailCreateClient()
        factory = KuberokuFactory(k8s_client=k8s, namespace="new-ns")
        # fix_all should find namespace_access failed and fix it
        # (also finds cni_enforcement failed but that calls subprocess)
        fixes = fix_all(factory)
        ns_fixes = [f for f in fixes if f.check_name == "namespace_access"]
        assert len(ns_fixes) == 1
        assert ns_fixes[0].fixed is True
        assert k8s.namespace_exists("new-ns")

    @patch("kuberoku.sdk.checks.subprocess.run")
    def test_fix_results_are_typed(self, mock_run: Any) -> None:
        k8s = _CheckFailCreateClient()
        factory = KuberokuFactory(k8s_client=k8s, namespace="new-ns")
        fixes = fix_all(factory)
        for f in fixes:
            assert isinstance(f, FixResult)


# ── Test helpers ────────────────────────────────────────────────


class _CheckFailCreateClient(FakeK8sClient):
    """K8s client where create_namespace fails the first time.

    Simulates permission denied during check.

    The fix function calls create_namespace again — by that time we allow it.
    """

    def __init__(self) -> None:
        super().__init__()
        self._create_fail_count = 1

    def create_namespace(self, name: str) -> dict[str, Any]:  # type: ignore[override]
        if self._create_fail_count > 0:
            self._create_fail_count -= 1
            raise PermissionError("Namespace creation denied")
        return super().create_namespace(name)


class _FailingK8sClient(FakeK8sClient):
    """K8s client where everything fails."""

    def list_configmaps(self, namespace, labels=None):  # type: ignore[override]
        raise ConnectionError("API unreachable")

    def namespace_exists(self, name):  # type: ignore[override]
        raise ConnectionError("API unreachable")

    def create_namespace(self, name):  # type: ignore[override]
        raise ConnectionError("API unreachable")

    def create_configmap(self, namespace, body):  # type: ignore[override]
        raise ConnectionError("API unreachable")

    def create_secret(self, namespace, body):  # type: ignore[override]
        raise ConnectionError("API unreachable")

    def create_deployment(self, namespace, body):  # type: ignore[override]
        raise ConnectionError("API unreachable")

    def create_service(self, namespace, body):  # type: ignore[override]
        raise ConnectionError("API unreachable")

    def create_network_policy(self, namespace, body):  # type: ignore[override]
        raise ConnectionError("API unreachable")

    def list_storage_classes(self):  # type: ignore[override]
        raise ConnectionError("API unreachable")

    def list_daemonsets(self, namespace, labels=None):  # type: ignore[override]
        raise ConnectionError("API unreachable")
