"""Integration tests for citadel install / uninstall / status with mocked K8s APIs."""

from __future__ import annotations

from types import SimpleNamespace
from unittest.mock import MagicMock, patch

import pytest

from kubernetes import config as kube_config
from kubernetes.client.rest import ApiException


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _api_exception(status: int, reason: str = "") -> ApiException:
    exc = ApiException(status=status, reason=reason)
    exc.status = status
    return exc


def _conflict() -> ApiException:
    return _api_exception(409, "Conflict")


def _not_found() -> ApiException:
    return _api_exception(404, "Not Found")


def _mock_deployment_status(available: int = 1):
    """Return a mock that looks like a Deployment status response."""
    return SimpleNamespace(
        status=SimpleNamespace(available_replicas=available),
    )


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture(autouse=True)
def _mock_kube_config():
    """Prevent real kubeconfig loading in all tests."""
    with (
        patch("citadel_operator.cli._install.config.load_incluster_config", side_effect=kube_config.ConfigException),
        patch("citadel_operator.cli._install.config.load_kube_config"),
        patch("citadel_operator.cli._uninstall.config.load_incluster_config", side_effect=kube_config.ConfigException),
        patch("citadel_operator.cli._uninstall.config.load_kube_config"),
        patch("citadel_operator.cli._status.config.load_incluster_config", side_effect=kube_config.ConfigException),
        patch("citadel_operator.cli._status.config.load_kube_config"),
    ):
        yield


@pytest.fixture
def mock_clients():
    """Return mocked K8s API clients used by install."""
    with (
        patch("citadel_operator.cli._install.client.CoreV1Api") as core_cls,
        patch("citadel_operator.cli._install.client.AppsV1Api") as apps_cls,
        patch("citadel_operator.cli._install.client.RbacAuthorizationV1Api") as rbac_cls,
        patch("citadel_operator.cli._install.client.ApiextensionsV1Api") as ext_cls,
        patch("citadel_operator.cli._install.client.CustomObjectsApi") as custom_cls,
    ):
        core = core_cls.return_value
        apps = apps_cls.return_value
        rbac = rbac_cls.return_value
        ext = ext_cls.return_value
        custom = custom_cls.return_value

        # Default: deployment becomes ready immediately
        apps.read_namespaced_deployment_status.return_value = _mock_deployment_status(1)

        yield SimpleNamespace(
            core=core, apps=apps, rbac=rbac, ext=ext, custom=custom,
        )


@pytest.fixture
def mock_uninstall_clients():
    """Return mocked K8s API clients used by uninstall."""
    with (
        patch("citadel_operator.cli._uninstall.client.CoreV1Api") as core_cls,
        patch("citadel_operator.cli._uninstall.client.AppsV1Api") as apps_cls,
        patch("citadel_operator.cli._uninstall.client.RbacAuthorizationV1Api") as rbac_cls,
        patch("citadel_operator.cli._uninstall.client.ApiextensionsV1Api") as ext_cls,
        patch("citadel_operator.cli._uninstall.client.CustomObjectsApi") as custom_cls,
    ):
        core = core_cls.return_value
        apps = apps_cls.return_value
        rbac = rbac_cls.return_value
        ext = ext_cls.return_value
        custom = custom_cls.return_value

        # Default: namespace is empty (so it gets deleted)
        core.list_namespaced_pod.return_value = SimpleNamespace(items=[])
        custom.list_cluster_custom_object.return_value = {"items": []}

        yield SimpleNamespace(
            core=core, apps=apps, rbac=rbac, ext=ext, custom=custom,
        )


@pytest.fixture
def mock_status_clients():
    """Return mocked K8s API clients used by status."""
    with (
        patch("citadel_operator.cli._status.client.CoreV1Api") as core_cls,
        patch("citadel_operator.cli._status.client.AppsV1Api") as apps_cls,
        patch("citadel_operator.cli._status.client.ApiextensionsV1Api") as ext_cls,
    ):
        core = core_cls.return_value
        apps = apps_cls.return_value
        ext = ext_cls.return_value

        yield SimpleNamespace(core=core, apps=apps, ext=ext)


# ---------------------------------------------------------------------------
# Install tests
# ---------------------------------------------------------------------------


class TestInstall:
    def test_creates_all_resources(self, mock_clients):
        from citadel_operator.cli._install import install

        ok = install(with_default_policies=False, timeout=5)

        assert ok is True
        mock_clients.core.create_namespace.assert_called_once()
        mock_clients.ext.create_custom_resource_definition.assert_called()
        mock_clients.core.create_namespaced_service_account.assert_called_once()
        mock_clients.rbac.create_cluster_role.assert_called_once()
        mock_clients.rbac.create_cluster_role_binding.assert_called_once()
        mock_clients.apps.create_namespaced_deployment.assert_called_once()

    def test_idempotent_on_conflict(self, mock_clients):
        from citadel_operator.cli._install import install

        # All creates return 409 → should patch instead
        mock_clients.core.create_namespace.side_effect = _conflict()
        mock_clients.ext.create_custom_resource_definition.side_effect = _conflict()
        mock_clients.core.create_namespaced_service_account.side_effect = _conflict()
        mock_clients.rbac.create_cluster_role.side_effect = _conflict()
        mock_clients.rbac.create_cluster_role_binding.side_effect = _conflict()
        mock_clients.apps.create_namespaced_deployment.side_effect = _conflict()

        ok = install(with_default_policies=False, timeout=5)

        assert ok is True
        mock_clients.ext.patch_custom_resource_definition.assert_called()
        mock_clients.core.patch_namespaced_service_account.assert_called_once()
        mock_clients.rbac.patch_cluster_role.assert_called_once()
        mock_clients.rbac.patch_cluster_role_binding.assert_called_once()
        mock_clients.apps.patch_namespaced_deployment.assert_called_once()

    def test_waits_for_deployment(self, mock_clients):
        from citadel_operator.cli._install import install

        # First call: 0 replicas; second call: 1 replica
        mock_clients.apps.read_namespaced_deployment_status.side_effect = [
            _mock_deployment_status(0),
            _mock_deployment_status(1),
        ]

        ok = install(with_default_policies=False, timeout=30)
        assert ok is True
        assert mock_clients.apps.read_namespaced_deployment_status.call_count == 2

    def test_timeout_returns_false(self, mock_clients):
        from citadel_operator.cli._install import install

        mock_clients.apps.read_namespaced_deployment_status.return_value = (
            _mock_deployment_status(0)
        )

        ok = install(with_default_policies=False, timeout=1)
        assert ok is False

    def test_with_default_policies(self, mock_clients):
        from citadel_operator.cli._install import install

        ok = install(with_default_policies=True, timeout=5)

        assert ok is True
        assert mock_clients.custom.create_cluster_custom_object.call_count >= 6

    def test_without_default_policies(self, mock_clients):
        from citadel_operator.cli._install import install

        ok = install(with_default_policies=False, timeout=5)

        assert ok is True
        mock_clients.custom.create_cluster_custom_object.assert_not_called()


# ---------------------------------------------------------------------------
# Uninstall tests
# ---------------------------------------------------------------------------


class TestUninstall:
    def test_removes_all_resources(self, mock_uninstall_clients):
        from citadel_operator.cli._uninstall import uninstall

        ok = uninstall()

        assert ok is True
        mock_uninstall_clients.apps.delete_namespaced_deployment.assert_called_once()
        mock_uninstall_clients.rbac.delete_cluster_role_binding.assert_called_once()
        mock_uninstall_clients.rbac.delete_cluster_role.assert_called_once()
        mock_uninstall_clients.core.delete_namespaced_service_account.assert_called_once()
        mock_uninstall_clients.core.delete_namespace.assert_called_once()

    def test_delete_crds_flag(self, mock_uninstall_clients):
        from citadel_operator.cli._uninstall import uninstall

        ok = uninstall(delete_crds=True)

        assert ok is True
        assert mock_uninstall_clients.ext.delete_custom_resource_definition.call_count == 2

    def test_skips_crds_by_default(self, mock_uninstall_clients):
        from citadel_operator.cli._uninstall import uninstall

        ok = uninstall(delete_crds=False)

        assert ok is True
        mock_uninstall_clients.ext.delete_custom_resource_definition.assert_not_called()

    def test_ignores_404(self, mock_uninstall_clients):
        from citadel_operator.cli._uninstall import uninstall

        mock_uninstall_clients.apps.delete_namespaced_deployment.side_effect = _not_found()
        mock_uninstall_clients.rbac.delete_cluster_role_binding.side_effect = _not_found()
        mock_uninstall_clients.rbac.delete_cluster_role.side_effect = _not_found()
        mock_uninstall_clients.core.delete_namespaced_service_account.side_effect = _not_found()
        mock_uninstall_clients.core.list_namespaced_pod.side_effect = _not_found()

        ok = uninstall()
        assert ok is True


# ---------------------------------------------------------------------------
# Status tests
# ---------------------------------------------------------------------------


class TestStatus:
    def test_healthy(self, mock_status_clients):
        from citadel_operator.cli._status import status

        # Namespace exists
        mock_status_clients.core.read_namespace.return_value = True
        # CRDs exist
        mock_status_clients.ext.read_custom_resource_definition.return_value = True
        # Deployment ready
        mock_status_clients.apps.read_namespaced_deployment.return_value = SimpleNamespace(
            status=SimpleNamespace(available_replicas=1),
        )
        # Pod running and ready
        mock_status_clients.core.list_namespaced_pod.return_value = SimpleNamespace(
            items=[
                SimpleNamespace(
                    metadata=SimpleNamespace(name="citadel-operator-abc123"),
                    status=SimpleNamespace(
                        phase="Running",
                        conditions=[
                            SimpleNamespace(type="Ready", status="True"),
                        ],
                    ),
                ),
            ],
        )

        ok = status()
        assert ok is True

    def test_unhealthy_no_deployment(self, mock_status_clients):
        from citadel_operator.cli._status import status

        mock_status_clients.core.read_namespace.return_value = True
        mock_status_clients.ext.read_custom_resource_definition.return_value = True
        mock_status_clients.apps.read_namespaced_deployment.side_effect = _not_found()
        mock_status_clients.core.list_namespaced_pod.return_value = SimpleNamespace(items=[])

        ok = status()
        assert ok is False
