"""Tests for RBAC permission checking and fix YAML generation."""

from __future__ import annotations

import yaml
from click.testing import CliRunner

from kuberoku.cli.main import cli
from kuberoku.factory import KuberokuFactory
from kuberoku.models import RbacFixYaml, RbacPermission, RbacReport
from kuberoku.sdk.rbac import ALL_RULES, check_rbac, generate_rbac_yaml
from tests.backends.fake import FakeK8sClient

# ── SDK: check_rbac ────────────────────────────────────────────


class TestCheckRbac:
    """SDK-level tests for RBAC permission checking."""

    def test_all_allowed_on_fresh_fake(self) -> None:
        k8s = FakeK8sClient()
        report = check_rbac(k8s, "test-ns")
        assert report.all_required_allowed is True
        assert report.all_elevated_allowed is True

    def test_returns_rbac_report(self) -> None:
        k8s = FakeK8sClient()
        report = check_rbac(k8s, "test-ns")
        assert isinstance(report, RbacReport)
        assert len(report.permissions) > 0

    def test_permissions_are_typed(self) -> None:
        k8s = FakeK8sClient()
        report = check_rbac(k8s, "test-ns")
        for p in report.permissions:
            assert isinstance(p, RbacPermission)

    def test_has_required_and_elevated(self) -> None:
        k8s = FakeK8sClient()
        report = check_rbac(k8s, "test-ns")
        required = [p for p in report.permissions if p.required]
        elevated = [p for p in report.permissions if not p.required]
        assert len(required) > 0
        assert len(elevated) > 0

    def test_denied_required_fails_report(self) -> None:
        k8s = FakeK8sClient()
        k8s.inject_denied_permission("get", "configmaps", "", "")
        report = check_rbac(k8s, "test-ns")
        assert report.all_required_allowed is False

    def test_denied_elevated_does_not_fail_required(self) -> None:
        k8s = FakeK8sClient()
        k8s.inject_denied_permission("get", "secrets", "", "")
        report = check_rbac(k8s, "test-ns")
        assert report.all_required_allowed is True
        assert report.all_elevated_allowed is False

    def test_denied_pods_log_subresource(self) -> None:
        k8s = FakeK8sClient()
        k8s.inject_denied_permission("get", "pods", "", "log")
        report = check_rbac(k8s, "test-ns")
        assert report.all_required_allowed is False
        denied = [p for p in report.permissions if not p.allowed]
        assert len(denied) == 1
        assert denied[0].resource == "pods"
        assert denied[0].subresource == "log"

    def test_denied_pods_exec_subresource(self) -> None:
        k8s = FakeK8sClient()
        k8s.inject_denied_permission("create", "pods", "", "exec")
        report = check_rbac(k8s, "test-ns")
        assert report.all_required_allowed is False
        denied = [p for p in report.permissions if not p.allowed]
        assert len(denied) == 1
        assert denied[0].subresource == "exec"

    def test_denied_deployments_api_group(self) -> None:
        k8s = FakeK8sClient()
        k8s.inject_denied_permission("create", "deployments", "apps", "")
        report = check_rbac(k8s, "test-ns")
        assert report.all_required_allowed is False

    def test_denied_ingresses_elevated(self) -> None:
        k8s = FakeK8sClient()
        k8s.inject_denied_permission("get", "ingresses", "networking.k8s.io", "")
        report = check_rbac(k8s, "test-ns")
        assert report.all_required_allowed is True
        assert report.all_elevated_allowed is False

    def test_denied_networkpolicies_elevated(self) -> None:
        k8s = FakeK8sClient()
        k8s.inject_denied_permission("get", "networkpolicies", "networking.k8s.io", "")
        report = check_rbac(k8s, "test-ns")
        assert report.all_required_allowed is True
        assert report.all_elevated_allowed is False

    def test_multiple_denials(self) -> None:
        k8s = FakeK8sClient()
        k8s.inject_denied_permission("get", "configmaps", "", "")
        k8s.inject_denied_permission("create", "deployments", "apps", "")
        k8s.inject_denied_permission("get", "secrets", "", "")
        report = check_rbac(k8s, "test-ns")
        assert report.all_required_allowed is False
        assert report.all_elevated_allowed is False
        denied = [p for p in report.permissions if not p.allowed]
        assert len(denied) == 3

    def test_permissions_have_used_by(self) -> None:
        k8s = FakeK8sClient()
        report = check_rbac(k8s, "test-ns")
        for p in report.permissions:
            assert p.used_by != ""

    def test_rules_table_has_required_resources(self) -> None:
        required_resources = {r.resource for r in ALL_RULES if r.required}
        assert "configmaps" in required_resources
        assert "deployments" in required_resources
        assert "pods" in required_resources
        assert "services" in required_resources
        assert "jobs" in required_resources
        assert "namespaces" in required_resources

    def test_rules_table_has_elevated_resources(self) -> None:
        elevated_resources = {r.resource for r in ALL_RULES if not r.required}
        assert "secrets" in elevated_resources
        assert "ingresses" in elevated_resources
        assert "statefulsets" in elevated_resources
        assert "persistentvolumeclaims" in elevated_resources
        assert "networkpolicies" in elevated_resources


# ── SDK: generate_rbac_yaml ──────────────────────────────────


class TestGenerateRbacYaml:
    """Tests for RBAC fix YAML generation."""

    def test_no_missing_returns_empty(self) -> None:
        k8s = FakeK8sClient()
        report = check_rbac(k8s, "test-ns")
        fix = generate_rbac_yaml(report, "test-ns")
        assert isinstance(fix, RbacFixYaml)
        assert fix.missing_count == 0
        assert fix.role_yaml == ""
        assert fix.rolebinding_yaml == ""

    def test_missing_generates_role(self) -> None:
        k8s = FakeK8sClient()
        k8s.inject_denied_permission("get", "configmaps", "", "")
        report = check_rbac(k8s, "test-ns")
        fix = generate_rbac_yaml(report, "test-ns")
        assert fix.missing_count > 0
        assert "Role" in fix.role_yaml
        assert "configmaps" in fix.role_yaml

    def test_missing_generates_rolebinding(self) -> None:
        k8s = FakeK8sClient()
        k8s.inject_denied_permission("get", "configmaps", "", "")
        report = check_rbac(k8s, "test-ns")
        fix = generate_rbac_yaml(report, "test-ns")
        assert "RoleBinding" in fix.rolebinding_yaml

    def test_role_yaml_is_valid_yaml(self) -> None:
        k8s = FakeK8sClient()
        k8s.inject_denied_permission("create", "deployments", "apps", "")
        report = check_rbac(k8s, "test-ns")
        fix = generate_rbac_yaml(report, "test-ns")
        role = yaml.safe_load(fix.role_yaml)
        assert role["kind"] == "Role"
        assert role["metadata"]["namespace"] == "test-ns"
        assert len(role["rules"]) >= 1

    def test_rolebinding_yaml_is_valid_yaml(self) -> None:
        k8s = FakeK8sClient()
        k8s.inject_denied_permission("create", "deployments", "apps", "")
        report = check_rbac(k8s, "test-ns")
        fix = generate_rbac_yaml(report, "test-ns")
        binding = yaml.safe_load(fix.rolebinding_yaml)
        assert binding["kind"] == "RoleBinding"
        assert binding["metadata"]["namespace"] == "test-ns"

    def test_role_includes_correct_api_group(self) -> None:
        k8s = FakeK8sClient()
        k8s.inject_denied_permission("create", "deployments", "apps", "")
        report = check_rbac(k8s, "test-ns")
        fix = generate_rbac_yaml(report, "test-ns")
        role = yaml.safe_load(fix.role_yaml)
        api_groups = [r["apiGroups"][0] for r in role["rules"]]
        assert "apps" in api_groups

    def test_subresource_in_role(self) -> None:
        k8s = FakeK8sClient()
        k8s.inject_denied_permission("get", "pods", "", "log")
        report = check_rbac(k8s, "test-ns")
        fix = generate_rbac_yaml(report, "test-ns")
        role = yaml.safe_load(fix.role_yaml)
        resources = [r["resources"][0] for r in role["rules"]]
        assert "pods/log" in resources

    def test_custom_role_name(self) -> None:
        k8s = FakeK8sClient()
        k8s.inject_denied_permission("get", "configmaps", "", "")
        report = check_rbac(k8s, "test-ns")
        fix = generate_rbac_yaml(report, "test-ns", role_name="my-role")
        role = yaml.safe_load(fix.role_yaml)
        assert role["metadata"]["name"] == "my-role"
        binding = yaml.safe_load(fix.rolebinding_yaml)
        assert binding["roleRef"]["name"] == "my-role"

    def test_multiple_missing_grouped(self) -> None:
        k8s = FakeK8sClient()
        k8s.inject_denied_permission("get", "configmaps", "", "")
        k8s.inject_denied_permission("create", "configmaps", "", "")
        report = check_rbac(k8s, "test-ns")
        fix = generate_rbac_yaml(report, "test-ns")
        role = yaml.safe_load(fix.role_yaml)
        # Both verbs should be in one rule for configmaps
        cm_rules = [r for r in role["rules"] if "configmaps" in r["resources"]]
        assert len(cm_rules) == 1
        assert "get" in cm_rules[0]["verbs"]
        assert "create" in cm_rules[0]["verbs"]


# ── Doctor integration ───────────────────────────────────────


class TestDoctorRbacIntegration:
    """Tests for RBAC check in doctor registry."""

    def test_rbac_check_in_registry(self) -> None:
        k8s = FakeK8sClient()
        factory = KuberokuFactory(k8s_client=k8s, namespace="test-ns")
        results = factory.doctor.run_checks()
        names = [r.name for r in results]
        assert "rbac_permissions" in names

    def test_rbac_check_passes_on_fake(self) -> None:
        k8s = FakeK8sClient()
        factory = KuberokuFactory(k8s_client=k8s, namespace="test-ns")
        results = factory.doctor.run_checks()
        rbac = next(r for r in results if r.name == "rbac_permissions")
        assert rbac.passed is True
        assert "All RBAC permissions granted" in rbac.message

    def test_rbac_check_fails_on_denied_required(self) -> None:
        k8s = FakeK8sClient()
        k8s.inject_denied_permission("get", "configmaps", "", "")
        factory = KuberokuFactory(k8s_client=k8s, namespace="test-ns")
        results = factory.doctor.run_checks()
        rbac = next(r for r in results if r.name == "rbac_permissions")
        assert rbac.passed is False
        assert "Missing required" in rbac.message

    def test_rbac_check_passes_with_elevated_warning(self) -> None:
        k8s = FakeK8sClient()
        k8s.inject_denied_permission("get", "secrets", "", "")
        factory = KuberokuFactory(k8s_client=k8s, namespace="test-ns")
        results = factory.doctor.run_checks()
        rbac = next(r for r in results if r.name == "rbac_permissions")
        assert rbac.passed is True  # elevated missing is still a pass
        assert "elevated" in rbac.message

    def test_doctor_check_rbac_permissions_method(self) -> None:
        k8s = FakeK8sClient()
        factory = KuberokuFactory(k8s_client=k8s, namespace="test-ns")
        report = factory.doctor.check_rbac_permissions()
        assert isinstance(report, RbacReport)
        assert report.all_required_allowed is True

    def test_doctor_generate_rbac_fix(self) -> None:
        k8s = FakeK8sClient()
        k8s.inject_denied_permission("get", "configmaps", "", "")
        factory = KuberokuFactory(k8s_client=k8s, namespace="test-ns")
        fix = factory.doctor.generate_rbac_fix()
        assert isinstance(fix, RbacFixYaml)
        assert fix.missing_count > 0

    def test_doctor_generate_rbac_fix_empty_when_all_allowed(self) -> None:
        k8s = FakeK8sClient()
        factory = KuberokuFactory(k8s_client=k8s, namespace="test-ns")
        fix = factory.doctor.generate_rbac_fix()
        assert fix.missing_count == 0


# ── CLI: clusters:doctor --fix ─────────────────────────────


class TestDoctorFixCLI:
    """CLI tests for clusters:doctor --fix."""

    def test_fix_no_missing(self) -> None:
        k8s = FakeK8sClient()
        factory = KuberokuFactory(k8s_client=k8s, namespace="test-ns")
        runner = CliRunner()
        result = runner.invoke(cli, ["clusters:doctor", "--fix"], obj={"factory": factory})
        assert result.exit_code == 0
        assert "already granted" in result.output

    def test_fix_generates_yaml(self) -> None:
        k8s = FakeK8sClient()
        k8s.inject_denied_permission("get", "configmaps", "", "")
        factory = KuberokuFactory(k8s_client=k8s, namespace="test-ns")
        runner = CliRunner()
        result = runner.invoke(cli, ["clusters:doctor", "--fix"], obj={"factory": factory})
        assert result.exit_code == 0
        assert "---" in result.output
        assert "Role" in result.output
        assert "RoleBinding" in result.output
        assert "configmaps" in result.output

    def test_fix_shows_missing_count(self) -> None:
        k8s = FakeK8sClient()
        k8s.inject_denied_permission("get", "configmaps", "", "")
        factory = KuberokuFactory(k8s_client=k8s, namespace="test-ns")
        runner = CliRunner()
        result = runner.invoke(cli, ["clusters:doctor", "--fix"], obj={"factory": factory})
        assert "Missing" in result.output
        assert "RBAC rule" in result.output

    def test_fix_shows_kubectl_hint(self) -> None:
        k8s = FakeK8sClient()
        k8s.inject_denied_permission("get", "configmaps", "", "")
        factory = KuberokuFactory(k8s_client=k8s, namespace="test-ns")
        runner = CliRunner()
        result = runner.invoke(cli, ["clusters:doctor", "--fix"], obj={"factory": factory})
        assert "kubectl apply" in result.output

    def test_doctor_without_fix_shows_checks(self) -> None:
        k8s = FakeK8sClient()
        factory = KuberokuFactory(k8s_client=k8s, namespace="test-ns")
        runner = CliRunner()
        result = runner.invoke(cli, ["clusters:doctor"], obj={"factory": factory})
        assert "[+]" in result.output
        assert "checks passed" in result.output
