"""Tests for the rdctl CLI wrapper.

Tests the argument building, JSON parsing, and error handling with
mocked subprocess calls. No rdctl binary required.
"""

from __future__ import annotations

import dataclasses
import subprocess
from unittest.mock import patch

import pytest

from citadel_operator.k3s.rdctl import Rdctl, RdctlSettings


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


_SETTINGS_JSON = """{
    "kubernetes": {
        "version": "1.28.3",
        "options": {
            "flannel": false,
            "traefik": false
        }
    },
    "containerEngine": {
        "name": "containerd"
    }
}"""


class TestRdctlSet:
    """Tests for Rdctl.set() argument construction."""

    @patch.object(Rdctl, "_run", return_value=_completed())
    def test_bool_false_kwarg(self, mock_run):
        rdctl = Rdctl("/usr/bin/rdctl")
        rdctl.set(kubernetes_options_flannel=False)

        mock_run.assert_called_once_with(
            ["/usr/bin/rdctl", "set", "--kubernetes.options.flannel=false"],
        )

    @patch.object(Rdctl, "_run", return_value=_completed())
    def test_bool_true_kwarg(self, mock_run):
        rdctl = Rdctl("/usr/bin/rdctl")
        rdctl.set(kubernetes_options_traefik=True)

        mock_run.assert_called_once_with(
            ["/usr/bin/rdctl", "set", "--kubernetes.options.traefik=true"],
        )

    @patch.object(Rdctl, "_run", return_value=_completed())
    def test_string_kwarg(self, mock_run):
        rdctl = Rdctl("/usr/bin/rdctl")
        rdctl.set(kubernetes_version="1.28.3")

        mock_run.assert_called_once_with(
            ["/usr/bin/rdctl", "set", "--kubernetes.version=1.28.3"],
        )

    @patch.object(Rdctl, "_run", return_value=_completed())
    def test_multiple_kwargs(self, mock_run):
        rdctl = Rdctl("/usr/bin/rdctl")
        rdctl.set(
            kubernetes_options_flannel=False,
            kubernetes_options_traefik=False,
        )

        call_args = mock_run.call_args[0][0]
        assert call_args[0:2] == ["/usr/bin/rdctl", "set"]
        assert "--kubernetes.options.flannel=false" in call_args
        assert "--kubernetes.options.traefik=false" in call_args


class TestRdctlShell:
    """Tests for Rdctl.shell() command passthrough."""

    @patch.object(Rdctl, "_run", return_value=_completed())
    def test_passes_command(self, mock_run):
        rdctl = Rdctl("/usr/bin/rdctl")
        rdctl.shell("ls", "-la", "/tmp")

        mock_run.assert_called_once_with(
            ["/usr/bin/rdctl", "shell", "--", "ls", "-la", "/tmp"],
        )

    @patch.object(Rdctl, "_run", return_value=_completed())
    def test_single_command(self, mock_run):
        rdctl = Rdctl("/usr/bin/rdctl")
        rdctl.shell("whoami")

        mock_run.assert_called_once_with(
            ["/usr/bin/rdctl", "shell", "--", "whoami"],
        )


class TestRdctlListSettings:
    """Tests for Rdctl.list_settings() JSON parsing."""

    @patch.object(Rdctl, "_run", return_value=_completed(stdout=_SETTINGS_JSON))
    def test_parses_settings(self, mock_run):
        rdctl = Rdctl("/usr/bin/rdctl")
        settings = rdctl.list_settings()

        assert isinstance(settings, RdctlSettings)
        assert settings.kubernetes_version == "1.28.3"
        assert settings.flannel_enabled is False
        assert settings.traefik_enabled is False
        assert settings.container_engine == "containerd"

    @patch.object(Rdctl, "_run", return_value=_completed(stdout=_SETTINGS_JSON))
    def test_calls_correct_command(self, mock_run):
        rdctl = Rdctl("/usr/bin/rdctl")
        rdctl.list_settings()

        mock_run.assert_called_once_with(["/usr/bin/rdctl", "list-settings"])

    @patch.object(
        Rdctl, "_run",
        return_value=_completed(returncode=1, stderr="connection refused"),
    )
    def test_nonzero_exit_raises(self, mock_run):
        rdctl = Rdctl("/usr/bin/rdctl")

        with pytest.raises(RuntimeError, match="rdctl list-settings failed"):
            rdctl.list_settings()

    @patch.object(Rdctl, "_run")
    def test_defaults_for_missing_keys(self, mock_run):
        """Missing keys fall back to defaults (empty string, True, containerd)."""
        mock_run.return_value = _completed(stdout="{}")
        rdctl = Rdctl("/usr/bin/rdctl")
        settings = rdctl.list_settings()

        assert settings.kubernetes_version == ""
        assert settings.flannel_enabled is True
        assert settings.traefik_enabled is True
        assert settings.container_engine == "containerd"


class TestRdctlVersion:
    """Tests for Rdctl.version()."""

    @patch.object(
        Rdctl, "_run",
        return_value=_completed(stdout="  1.12.1\n"),
    )
    def test_returns_stripped_string(self, mock_run):
        rdctl = Rdctl("/usr/bin/rdctl")
        assert rdctl.version() == "1.12.1"

    @patch.object(
        Rdctl, "_run",
        return_value=_completed(returncode=1, stderr="not found"),
    )
    def test_nonzero_exit_raises(self, mock_run):
        rdctl = Rdctl("/usr/bin/rdctl")

        with pytest.raises(RuntimeError, match="rdctl version failed"):
            rdctl.version()


class TestRdctlBinaryNotFound:
    """Tests for handling a missing rdctl binary."""

    def test_run_raises_file_not_found(self):
        """subprocess.run raises FileNotFoundError when binary is missing."""
        with patch(
            "citadel_operator.k3s.rdctl.subprocess.run",
            side_effect=FileNotFoundError("/no/such/rdctl"),
        ):
            rdctl = Rdctl("/no/such/rdctl")
            with pytest.raises(FileNotFoundError):
                rdctl.version()


class TestRdctlSettingsFrozen:
    """RdctlSettings must be a frozen dataclass."""

    def test_is_frozen(self):
        settings = RdctlSettings(
            kubernetes_version="1.28.3",
            flannel_enabled=False,
            traefik_enabled=False,
            container_engine="containerd",
        )
        with pytest.raises(dataclasses.FrozenInstanceError):
            settings.kubernetes_version = "1.29.0"  # type: ignore[misc]

    def test_is_dataclass(self):
        assert dataclasses.is_dataclass(RdctlSettings)
