"""Tests for the doctor service — SDK + CLI."""

from __future__ import annotations

from typing import Any
from unittest.mock import patch

from click.testing import CliRunner

from kuberoku.cli.main import cli
from kuberoku.factory import KuberokuFactory
from kuberoku.models import CheckResult
from tests.backends.fake import FakeK8sClient


class TestDoctorServiceSDK:
    """SDK-level tests for DoctorService."""

    def test_all_checks_run(self, factory: KuberokuFactory) -> None:
        results = factory.doctor.run_checks()
        assert len(results) == 14

    def test_most_checks_pass_on_fake(self, factory: KuberokuFactory) -> None:
        results = factory.doctor.run_checks()
        # All pass except cni_enforcement (no DaemonSets in fake)
        passed_names = {r.name for r in results if r.passed}
        assert "api_reachable" in passed_names
        assert "namespace_access" in passed_names
        assert "configmap_crud" in passed_names
        assert "secret_crud" in passed_names
        assert "deployment_crud" in passed_names
        assert "service_crud" in passed_names
        assert "network_policy_crud" in passed_names
        assert "storage_class" in passed_names

    def test_check_names_are_unique(self, factory: KuberokuFactory) -> None:
        results = factory.doctor.run_checks()
        names = [c.name for c in results]
        assert len(names) == len(set(names))

    def test_returns_frozen_dataclasses(self, factory: KuberokuFactory) -> None:
        results = factory.doctor.run_checks()
        for check in results:
            assert isinstance(check, CheckResult)

    def test_on_step_callback(self, factory: KuberokuFactory) -> None:
        steps: list[str] = []
        factory.doctor.run_checks(on_step=steps.append)
        assert len(steps) == 14
        assert all("Checking" in s for s in steps)

    def test_api_reachable_passes(self, factory: KuberokuFactory) -> None:
        results = factory.doctor.run_checks()
        api_check = next(c for c in results if c.name == "api_reachable")
        assert api_check.passed is True

    def test_namespace_access_passes(self, factory: KuberokuFactory) -> None:
        results = factory.doctor.run_checks()
        ns_check = next(c for c in results if c.name == "namespace_access")
        assert ns_check.passed is True

    def test_configmap_crud_passes(self, factory: KuberokuFactory) -> None:
        results = factory.doctor.run_checks()
        cm_check = next(c for c in results if c.name == "configmap_crud")
        assert cm_check.passed is True

    def test_secret_crud_passes(self, factory: KuberokuFactory) -> None:
        results = factory.doctor.run_checks()
        secret_check = next(c for c in results if c.name == "secret_crud")
        assert secret_check.passed is True

    def test_deployment_crud_passes(self, factory: KuberokuFactory) -> None:
        results = factory.doctor.run_checks()
        dep_check = next(c for c in results if c.name == "deployment_crud")
        assert dep_check.passed is True

    def test_service_crud_passes(self, factory: KuberokuFactory) -> None:
        results = factory.doctor.run_checks()
        svc_check = next(c for c in results if c.name == "service_crud")
        assert svc_check.passed is True

    def test_network_policy_crud_passes(self, factory: KuberokuFactory) -> None:
        results = factory.doctor.run_checks()
        np_check = next(c for c in results if c.name == "network_policy_crud")
        assert np_check.passed is True

    def test_storage_class_passes(self, factory: KuberokuFactory) -> None:
        results = factory.doctor.run_checks()
        sc_check = next(c for c in results if c.name == "storage_class")
        assert sc_check.passed is True
        assert "Default StorageClass exists" in sc_check.message

    def test_probe_resources_cleaned_up(self, factory: KuberokuFactory) -> None:
        """Doctor probes don't leave resources behind."""
        factory.doctor.run_checks()
        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 == []

    def test_failed_check_has_detail_or_fix_hint(self) -> None:
        """When a check fails, it should have a detail message or fix hint."""
        k8s = _FailingK8sClient()
        factory = KuberokuFactory(k8s_client=k8s, namespace="test-ns")
        results = factory.doctor.run_checks()
        failed = [c for c in results if not c.passed]
        assert len(failed) > 0
        for check in failed:
            assert check.detail is not None or check.fix_hint is not None

    def test_partial_failure(self) -> None:
        """Some checks fail, others pass."""
        k8s = _PartialFailK8sClient()
        factory = KuberokuFactory(k8s_client=k8s, namespace="test-ns")
        results = factory.doctor.run_checks()
        passed = [c for c in results if c.passed]
        failed = [c for c in results if not c.passed]
        assert len(passed) > 0
        assert len(failed) > 0

    def test_backward_compat_run_alias(self, factory: KuberokuFactory) -> None:
        """run() is an alias for run_checks()."""
        results = factory.doctor.run()
        assert len(results) == 14

    def test_fix_all_no_fixes_needed(self) -> None:
        """When all checks pass (including CNI + cert-manager), fix_all returns no fixes."""
        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="new-ns")
        fixes = factory.doctor.fix_all()
        assert len(fixes) == 0
        assert k8s.namespace_exists("new-ns")

    @patch("kuberoku.sdk.checks.subprocess.run")
    def test_fix_all_with_on_step(self, mock_run: Any) -> None:
        """fix_all invokes on_step callback."""
        k8s = FakeK8sClient()
        factory = KuberokuFactory(k8s_client=k8s, namespace="new-ns")
        steps: list[str] = []
        factory.doctor.fix_all(on_step=steps.append)
        assert any("fix" in s.lower() for s in steps)

    def test_check_rbac_with_on_step(self) -> None:
        """check_rbac_permissions invokes on_step callback."""
        k8s = FakeK8sClient()
        factory = KuberokuFactory(k8s_client=k8s, namespace="test-ns")
        steps: list[str] = []
        factory.doctor.check_rbac_permissions(on_step=steps.append)
        assert any("rbac" in s.lower() for s in steps)


class TestDoctorCLI:
    """CLI-level tests for clusters:doctor command."""

    def test_doctor_cli_shows_checks(
        self, cli_runner: CliRunner, factory: KuberokuFactory
    ) -> None:
        result = cli_runner.invoke(cli, ["clusters:doctor"], obj={"factory": factory})
        assert "[+]" in result.output
        assert "checks passed" in result.output

    def test_doctor_cli_all_pass_with_cni(self, cli_runner: CliRunner) -> None:
        """All 13 checks pass when CNI, ingress, cert-manager, and LB are present."""
        k8s = FakeK8sClient()
        k8s.create_namespace("kube-system")
        k8s.create_daemonset(
            "kube-system",
            {
                "apiVersion": "apps/v1",
                "kind": "DaemonSet",
                "metadata": {"name": "calico-node"},
                "spec": {},
            },
        )
        k8s.create_ingress_class(
            {
                "apiVersion": "networking.k8s.io/v1",
                "kind": "IngressClass",
                "metadata": {"name": "nginx"},
                "spec": {"controller": "k8s.io/ingress-nginx"},
            }
        )
        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")
        result = cli_runner.invoke(cli, ["clusters:doctor"], obj={"factory": factory})
        assert result.exit_code == 0
        assert "All" in result.output and "checks passed" in result.output

    def test_doctor_cli_shows_numbered_checks(
        self, cli_runner: CliRunner, factory: KuberokuFactory
    ) -> None:
        result = cli_runner.invoke(cli, ["clusters:doctor"], obj={"factory": factory})
        assert "1." in result.output
        assert "2." in result.output

    def test_doctor_cli_failure_suggests_setup(self, cli_runner: CliRunner) -> None:
        k8s = _FailingK8sClient()
        factory = KuberokuFactory(k8s_client=k8s, namespace="test-ns")
        result = cli_runner.invoke(cli, ["clusters:doctor"], obj={"factory": factory})
        assert result.exit_code != 0
        assert "clusters:setup" in result.output

    def test_doctor_cli_help(self, cli_runner: CliRunner) -> None:
        result = cli_runner.invoke(cli, ["clusters", "doctor", "--help"])
        assert result.exit_code == 0
        assert "cluster" in result.output.lower()


class TestSetupCLI:
    """CLI-level tests for clusters:setup command."""

    @patch("kuberoku.sdk.checks.subprocess.run")
    def test_setup_runs_checks(
        self, mock_run: Any, cli_runner: CliRunner, factory: KuberokuFactory
    ) -> None:
        result = cli_runner.invoke(cli, ["clusters:setup"], obj={"factory": factory})
        assert "[+]" in result.output
        assert "checks passed" in result.output

    def test_setup_all_pass_with_cni(self, cli_runner: CliRunner) -> None:
        k8s = FakeK8sClient()
        k8s.create_namespace("kube-system")
        k8s.create_daemonset(
            "kube-system",
            {
                "apiVersion": "apps/v1",
                "kind": "DaemonSet",
                "metadata": {"name": "calico-node"},
                "spec": {},
            },
        )
        k8s.create_ingress_class(
            {
                "apiVersion": "networking.k8s.io/v1",
                "kind": "IngressClass",
                "metadata": {"name": "nginx"},
                "spec": {"controller": "k8s.io/ingress-nginx"},
            }
        )
        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")
        result = cli_runner.invoke(cli, ["clusters:setup"], obj={"factory": factory})
        assert result.exit_code == 0
        assert "Cluster is ready" in result.output

    @patch("kuberoku.sdk.checks.subprocess.run")
    def test_setup_shows_manual_fixes(self, mock_run: Any, cli_runner: CliRunner) -> None:
        """Setup shows fix instructions for non-fixable failures."""
        k8s = _PartialFailK8sClient()
        factory = KuberokuFactory(k8s_client=k8s, namespace="test-ns")
        result = cli_runner.invoke(cli, ["clusters:setup"], obj={"factory": factory})
        assert "Manual steps needed" in result.output

    @patch("kuberoku.sdk.checks.subprocess.run")
    def test_setup_creates_namespace(self, mock_run: Any, cli_runner: CliRunner) -> None:
        k8s = FakeK8sClient()
        factory = KuberokuFactory(k8s_client=k8s, namespace="new-ns")
        cli_runner.invoke(cli, ["clusters:setup"], obj={"factory": factory})
        # Namespace is created during the check phase
        assert k8s.namespace_exists("new-ns")


# ── Test helpers: broken K8s clients ─────────────────────────────


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")


class _PartialFailK8sClient(FakeK8sClient):
    """K8s client where NetworkPolicy, Deployment, and Service fail, but CMs/Secrets work."""

    def create_deployment(self, namespace, body):  # type: ignore[override]
        raise PermissionError("Deployments forbidden")

    def create_service(self, namespace, body):  # type: ignore[override]
        raise PermissionError("Services forbidden")

    def create_network_policy(self, namespace, body):  # type: ignore[override]
        raise PermissionError("NetworkPolicies forbidden")
