"""Tests for the RancherDesktopProvider.

Mocks Rdctl and Nerdctl at the module level so no subprocess calls are made.
Validates orchestration logic, state mapping, and delegation.
"""

from __future__ import annotations

import dataclasses
import subprocess
from unittest.mock import MagicMock, patch

import pytest

from citadel_operator.k3s.nerdctl import ImageInfo
from citadel_operator.k3s.provider import (
    ClusterInfo,
    ClusterState,
    RancherDesktopProvider,
    ResetResult,
)
from citadel_operator.k3s.rdctl import RdctlSettings


def _completed(
    returncode: int = 0,
    stdout: str = "",
    stderr: str = "",
) -> subprocess.CompletedProcess[str]:
    return subprocess.CompletedProcess(
        args=[], returncode=returncode, stdout=stdout, stderr=stderr,
    )


def _make_settings(
    version: str = "1.28.3",
    flannel: bool = False,
    traefik: bool = False,
    engine: str = "containerd",
) -> RdctlSettings:
    return RdctlSettings(
        kubernetes_version=version,
        flannel_enabled=flannel,
        traefik_enabled=traefik,
        container_engine=engine,
    )


@pytest.fixture()
def provider():
    """Build a provider with mocked Rdctl and Nerdctl instances."""
    with (
        patch("citadel_operator.k3s.provider.Rdctl") as mock_rdctl_cls,
        patch("citadel_operator.k3s.provider.Nerdctl") as mock_nerdctl_cls,
    ):
        mock_rdctl = MagicMock()
        mock_nerdctl = MagicMock()
        mock_rdctl_cls.return_value = mock_rdctl
        mock_nerdctl_cls.return_value = mock_nerdctl

        p = RancherDesktopProvider(
            rdctl_bin="/usr/bin/rdctl",
            nerdctl_bin="/usr/bin/nerdctl",
            kube_context="test-context",
        )
        # Expose mocks for assertions
        p._mock_rdctl = mock_rdctl  # type: ignore[attr-defined]
        p._mock_nerdctl = mock_nerdctl  # type: ignore[attr-defined]
        yield p


class TestGetStatus:
    """Tests for RancherDesktopProvider.get_status()."""

    def test_running_cluster(self, provider):
        provider._mock_rdctl.list_settings.return_value = _make_settings()

        with patch.object(provider, "is_ready", return_value=True), \
             patch.object(provider, "_kubectl") as mock_kubectl:
            mock_kubectl.return_value = _completed(stdout="lima-rancher-desktop")
            info = provider.get_status()

        assert isinstance(info, ClusterInfo)
        assert info.state == ClusterState.RUNNING
        assert info.kubernetes_version == "1.28.3"
        assert info.flannel_enabled is False
        assert info.traefik_enabled is False
        assert info.container_engine == "containerd"
        assert info.node_name == "lima-rancher-desktop"
        assert info.node_ready is True

    def test_stopped_cluster(self, provider):
        provider._mock_rdctl.list_settings.return_value = _make_settings()

        with patch.object(provider, "is_ready", return_value=False), \
             patch.object(provider, "_kubectl") as mock_kubectl:
            mock_kubectl.return_value = _completed(returncode=1, stderr="refused")
            info = provider.get_status()

        assert info.state == ClusterState.STOPPED
        assert info.node_ready is False

    def test_rdctl_failure_returns_unknown(self, provider):
        provider._mock_rdctl.list_settings.side_effect = RuntimeError("boom")
        info = provider.get_status()

        assert info.state == ClusterState.UNKNOWN


class TestIsReady:
    """Tests for RancherDesktopProvider.is_ready()."""

    def test_ready_when_kubectl_succeeds(self, provider):
        with patch.object(provider, "_kubectl", return_value=_completed()):
            assert provider.is_ready() is True

    def test_not_ready_when_kubectl_fails(self, provider):
        with patch.object(
            provider, "_kubectl",
            return_value=_completed(returncode=1),
        ):
            assert provider.is_ready() is False

    def test_not_ready_on_exception(self, provider):
        with patch.object(
            provider, "_kubectl",
            side_effect=FileNotFoundError("kubectl not found"),
        ):
            assert provider.is_ready() is False


class TestConfigureForCilium:
    """Tests for RancherDesktopProvider.configure_for_cilium()."""

    def test_calls_set_and_shell(self, provider):
        provider._mock_rdctl.set.return_value = _completed()
        provider._mock_rdctl.shell.return_value = _completed()

        result = provider.configure_for_cilium()

        assert result is True
        provider._mock_rdctl.set.assert_called_once_with(
            kubernetes_options_flannel=False,
            kubernetes_options_traefik=False,
        )
        provider._mock_rdctl.shell.assert_called_once_with(
            "sudo", "mount", "--make-rshared", "/",
        )

    def test_returns_false_on_set_failure(self, provider):
        provider._mock_rdctl.set.return_value = _completed(
            returncode=1, stderr="set failed",
        )

        assert provider.configure_for_cilium() is False
        # shell should not be called when set fails
        provider._mock_rdctl.shell.assert_not_called()

    def test_returns_true_even_if_mount_fails(self, provider):
        """Mount failure is logged as a warning but does not fail the operation."""
        provider._mock_rdctl.set.return_value = _completed()
        provider._mock_rdctl.shell.return_value = _completed(
            returncode=1, stderr="mount failed",
        )

        assert provider.configure_for_cilium() is True


class TestResetK3s:
    """Tests for RancherDesktopProvider.reset_k3s()."""

    def test_successful_reset(self, provider):
        provider._mock_rdctl.list_settings.return_value = _make_settings()
        provider._mock_rdctl.factory_reset.return_value = _completed()

        with patch.object(provider, "is_ready", return_value=True), \
             patch.object(provider, "_kubectl", return_value=_completed(stdout="node1")):
            result = provider.reset_k3s()

        assert isinstance(result, ResetResult)
        assert result.success is True
        assert result.previous_state == ClusterState.RUNNING
        assert result.new_state == ClusterState.STOPPED
        assert result.error is None

    def test_failed_reset(self, provider):
        provider._mock_rdctl.list_settings.return_value = _make_settings()
        provider._mock_rdctl.factory_reset.return_value = _completed(
            returncode=1, stderr="reset error",
        )

        with patch.object(provider, "is_ready", return_value=True), \
             patch.object(provider, "_kubectl", return_value=_completed(stdout="node1")):
            result = provider.reset_k3s()

        assert result.success is False
        assert result.new_state == ClusterState.ERROR
        assert result.error == "reset error"


class TestBuildImage:
    """Tests for RancherDesktopProvider.build_image() delegation."""

    def test_delegates_to_nerdctl(self, provider):
        provider._mock_nerdctl.build.return_value = True
        result = provider.build_image("/app", "myimage:v1")

        assert result is True
        provider._mock_nerdctl.build.assert_called_once_with(
            "/app", "myimage:v1", dockerfile=None,
        )

    def test_passes_dockerfile_kwarg(self, provider):
        provider._mock_nerdctl.build.return_value = True
        provider.build_image("/app", "myimage:v1", dockerfile="Dockerfile.prod")

        provider._mock_nerdctl.build.assert_called_once_with(
            "/app", "myimage:v1", dockerfile="Dockerfile.prod",
        )

    def test_returns_false_on_failure(self, provider):
        provider._mock_nerdctl.build.return_value = False
        assert provider.build_image("/app", "bad:tag") is False


class TestListImages:
    """Tests for RancherDesktopProvider.list_images() delegation."""

    def test_delegates_to_nerdctl(self, provider):
        fake_images = [
            ImageInfo(repository="a", tag="v1", image_id="id1", size="10MB"),
        ]
        provider._mock_nerdctl.images.return_value = fake_images
        result = provider.list_images(filter_name="a")

        assert result == fake_images
        provider._mock_nerdctl.images.assert_called_once_with(filter_name="a")


class TestLoadImage:
    """Tests for RancherDesktopProvider.load_image() delegation."""

    def test_delegates_to_nerdctl(self, provider):
        provider._mock_nerdctl.load.return_value = True
        assert provider.load_image("/tmp/img.tar") is True
        provider._mock_nerdctl.load.assert_called_once_with("/tmp/img.tar")


class TestFrozenDataclasses:
    """All provider dataclasses must be immutable."""

    def test_cluster_info_is_frozen(self):
        info = ClusterInfo(state=ClusterState.RUNNING)
        with pytest.raises(dataclasses.FrozenInstanceError):
            info.state = ClusterState.STOPPED  # type: ignore[misc]

    def test_cluster_info_is_dataclass(self):
        assert dataclasses.is_dataclass(ClusterInfo)

    def test_reset_result_is_frozen(self):
        result = ResetResult(
            success=True,
            previous_state=ClusterState.RUNNING,
            new_state=ClusterState.STOPPED,
        )
        with pytest.raises(dataclasses.FrozenInstanceError):
            result.success = False  # type: ignore[misc]

    def test_reset_result_is_dataclass(self):
        assert dataclasses.is_dataclass(ResetResult)

    def test_cluster_state_values(self):
        """Verify all expected enum members exist."""
        assert ClusterState.RUNNING.value == "running"
        assert ClusterState.STOPPED.value == "stopped"
        assert ClusterState.STARTING.value == "starting"
        assert ClusterState.ERROR.value == "error"
        assert ClusterState.UNKNOWN.value == "unknown"
