"""Tests for wizard/cluster.py — local cluster lifecycle management."""

from __future__ import annotations

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

import pytest

from ilum.config.models import ClusterRecord
from ilum.errors import PrerequisiteError
from ilum.wizard.cluster import (
    PRESETS,
    ClusterManager,
    ClusterPreset,
    ClusterProvider,
    DiscoveredCluster,
    _discover_k3d,
    _discover_kind,
    _discover_minikube,
)


@pytest.fixture()
def cluster_mgr() -> ClusterManager:
    return ClusterManager()


@pytest.fixture()
def mock_console() -> MagicMock:
    from ilum.cli.output import IlumConsole

    return MagicMock(spec=IlumConsole)


class TestClusterManager:
    def test_create_k3d(
        self,
        cluster_mgr: ClusterManager,
        mock_console: MagicMock,
    ) -> None:
        """k3d cluster create should be called with the correct arguments."""
        preset = PRESETS["dev"]
        with (
            patch("ilum.wizard.cluster.shutil.which", return_value="/usr/bin/k3d"),
            patch("ilum.wizard.cluster.subprocess.run") as mock_run,
        ):
            record = cluster_mgr.create(ClusterProvider.K3D, preset, mock_console)

        mock_run.assert_called_once()
        args = mock_run.call_args[0][0]
        assert args[:3] == ["k3d", "cluster", "create"]
        assert preset.name in args
        assert record.kubecontext == f"k3d-{preset.name}"
        assert record.provider == "k3d"
        assert record.name == preset.name

    def test_create_minikube(
        self,
        cluster_mgr: ClusterManager,
        mock_console: MagicMock,
    ) -> None:
        """minikube start should be called with profile, cpus, and memory."""
        preset = PRESETS["dev"]
        with (
            patch("ilum.wizard.cluster.shutil.which", return_value="/usr/bin/minikube"),
            patch("ilum.wizard.cluster.subprocess.run") as mock_run,
        ):
            record = cluster_mgr.create(ClusterProvider.MINIKUBE, preset, mock_console)

        mock_run.assert_called_once()
        args = mock_run.call_args[0][0]
        assert args[0] == "minikube"
        assert "--profile" in args
        assert str(preset.cpus) in args
        assert preset.memory in args
        assert record.kubecontext == preset.name
        assert record.provider == "minikube"

    def test_create_kind(
        self,
        cluster_mgr: ClusterManager,
        mock_console: MagicMock,
    ) -> None:
        """kind create cluster should be called with the correct name."""
        preset = PRESETS["dev"]
        with (
            patch("ilum.wizard.cluster.shutil.which", return_value="/usr/bin/kind"),
            patch("ilum.wizard.cluster.subprocess.run") as mock_run,
        ):
            record = cluster_mgr.create(ClusterProvider.KIND, preset, mock_console)

        mock_run.assert_called_once()
        args = mock_run.call_args[0][0]
        assert args[:3] == ["kind", "create", "cluster"]
        assert record.kubecontext == f"kind-{preset.name}"

    def test_create_custom_name(
        self,
        cluster_mgr: ClusterManager,
        mock_console: MagicMock,
    ) -> None:
        """Custom name overrides the preset default."""
        preset = PRESETS["dev"]
        with (
            patch("ilum.wizard.cluster.shutil.which", return_value="/usr/bin/k3d"),
            patch("ilum.wizard.cluster.subprocess.run"),
        ):
            record = cluster_mgr.create(
                ClusterProvider.K3D, preset, mock_console, name="my-cluster"
            )

        assert record.name == "my-cluster"
        assert record.kubecontext == "k3d-my-cluster"

    def test_create_tool_not_found(
        self,
        cluster_mgr: ClusterManager,
        mock_console: MagicMock,
    ) -> None:
        """Error raised when the provider binary is not on PATH."""
        from ilum.doctor.checks import CheckResult, CheckStatus

        preset = PRESETS["dev"]
        fail = CheckResult(name="k3d", status=CheckStatus.FAIL, message="k3d not found")
        with (
            patch("ilum.wizard.deps.check_binary", return_value=fail),
            pytest.raises(PrerequisiteError, match="required but not available"),
        ):
            cluster_mgr.create(ClusterProvider.K3D, preset, mock_console)

    def test_delete_k3d(
        self,
        cluster_mgr: ClusterManager,
        mock_console: MagicMock,
    ) -> None:
        """k3d cluster delete should be called."""
        with (
            patch("ilum.wizard.cluster.shutil.which", return_value="/usr/bin/k3d"),
            patch("ilum.wizard.cluster.subprocess.run") as mock_run,
        ):
            cluster_mgr.delete(ClusterProvider.K3D, "test-cluster", mock_console)

        args = mock_run.call_args[0][0]
        assert args == ["k3d", "cluster", "delete", "test-cluster"]

    def test_delete_minikube(
        self,
        cluster_mgr: ClusterManager,
        mock_console: MagicMock,
    ) -> None:
        with (
            patch("ilum.wizard.cluster.shutil.which", return_value="/usr/bin/minikube"),
            patch("ilum.wizard.cluster.subprocess.run") as mock_run,
        ):
            cluster_mgr.delete(ClusterProvider.MINIKUBE, "test-cluster", mock_console)

        args = mock_run.call_args[0][0]
        assert args == ["minikube", "delete", "--profile", "test-cluster"]

    def test_delete_kind(
        self,
        cluster_mgr: ClusterManager,
        mock_console: MagicMock,
    ) -> None:
        with (
            patch("ilum.wizard.cluster.shutil.which", return_value="/usr/bin/kind"),
            patch("ilum.wizard.cluster.subprocess.run") as mock_run,
        ):
            cluster_mgr.delete(ClusterProvider.KIND, "test-cluster", mock_console)

        args = mock_run.call_args[0][0]
        assert args == ["kind", "delete", "cluster", "--name", "test-cluster"]

    @patch("ilum.wizard.cluster.discover_clusters", return_value=[])
    def test_list_local(self, _mock_discover: MagicMock) -> None:
        """list_local returns managed records when no clusters discovered."""
        records = [
            ClusterRecord(name="a", provider="k3d", kubecontext="k3d-a"),
            ClusterRecord(name="b", provider="kind", kubecontext="kind-b"),
        ]
        result = ClusterManager.list_local(records)
        assert len(result) == 2
        assert result[0].name == "a"
        assert result[0].source == "managed"


class TestClusterDiscovery:
    """Tests for individual provider discovery functions."""

    # -- minikube --

    @patch("ilum.wizard.cluster.shutil.which", return_value=None)
    def test_minikube_no_binary(self, _w: MagicMock) -> None:
        assert _discover_minikube() == []

    @patch("ilum.wizard.cluster.shutil.which", return_value="/usr/bin/minikube")
    @patch("ilum.wizard.cluster.subprocess.run")
    def test_minikube_success(self, mock_run: MagicMock, _w: MagicMock) -> None:
        mock_run.return_value = subprocess.CompletedProcess(
            args=[],
            returncode=0,
            stdout=json.dumps(
                {
                    "valid": [
                        {"Name": "ilum-dev", "Status": "OK"},
                        {"Name": "old-cluster", "Status": "Stopped"},
                    ]
                }
            ),
            stderr="",
        )
        result = _discover_minikube()
        assert len(result) == 2
        assert result[0] == DiscoveredCluster(
            name="ilum-dev", provider="minikube", kubecontext="ilum-dev", status="Running"
        )
        assert result[1].status == "Stopped"

    @patch("ilum.wizard.cluster.shutil.which", return_value="/usr/bin/minikube")
    @patch("ilum.wizard.cluster.subprocess.run", side_effect=subprocess.TimeoutExpired("cmd", 10))
    def test_minikube_timeout(self, _r: MagicMock, _w: MagicMock) -> None:
        assert _discover_minikube() == []

    @patch("ilum.wizard.cluster.shutil.which", return_value="/usr/bin/minikube")
    @patch("ilum.wizard.cluster.subprocess.run")
    def test_minikube_bad_json(self, mock_run: MagicMock, _w: MagicMock) -> None:
        mock_run.return_value = subprocess.CompletedProcess(
            args=[], returncode=0, stdout="not json", stderr=""
        )
        assert _discover_minikube() == []

    @patch("ilum.wizard.cluster.shutil.which", return_value="/usr/bin/minikube")
    @patch("ilum.wizard.cluster.subprocess.run")
    def test_minikube_empty_stdout(self, mock_run: MagicMock, _w: MagicMock) -> None:
        mock_run.return_value = subprocess.CompletedProcess(
            args=[], returncode=1, stdout="", stderr=""
        )
        assert _discover_minikube() == []

    # -- k3d --

    @patch("ilum.wizard.cluster.shutil.which", return_value=None)
    def test_k3d_no_binary(self, _w: MagicMock) -> None:
        assert _discover_k3d() == []

    @patch("ilum.wizard.cluster.shutil.which", return_value="/usr/bin/k3d")
    @patch("ilum.wizard.cluster.subprocess.run")
    def test_k3d_success(self, mock_run: MagicMock, _w: MagicMock) -> None:
        mock_run.return_value = subprocess.CompletedProcess(
            args=[],
            returncode=0,
            stdout=json.dumps(
                [
                    {"name": "my-k3d", "serversRunning": 1},
                    {"name": "stopped-k3d", "serversRunning": 0},
                ]
            ),
            stderr="",
        )
        result = _discover_k3d()
        assert len(result) == 2
        assert result[0] == DiscoveredCluster(
            name="my-k3d", provider="k3d", kubecontext="k3d-my-k3d", status="Running"
        )
        assert result[1].status == "Stopped"

    @patch("ilum.wizard.cluster.shutil.which", return_value="/usr/bin/k3d")
    @patch(
        "ilum.wizard.cluster.subprocess.run",
        side_effect=subprocess.CalledProcessError(1, "k3d"),
    )
    def test_k3d_failure(self, _r: MagicMock, _w: MagicMock) -> None:
        assert _discover_k3d() == []

    # -- kind --

    @patch("ilum.wizard.cluster.shutil.which", return_value=None)
    def test_kind_no_binary(self, _w: MagicMock) -> None:
        assert _discover_kind() == []

    @patch("ilum.wizard.cluster.shutil.which", return_value="/usr/bin/kind")
    @patch("ilum.wizard.cluster.subprocess.run")
    def test_kind_success(self, mock_run: MagicMock, _w: MagicMock) -> None:
        mock_run.return_value = subprocess.CompletedProcess(
            args=[], returncode=0, stdout="my-kind\nother\n", stderr=""
        )
        result = _discover_kind()
        assert len(result) == 2
        assert result[0] == DiscoveredCluster(
            name="my-kind", provider="kind", kubecontext="kind-my-kind", status="Running"
        )

    @patch("ilum.wizard.cluster.shutil.which", return_value="/usr/bin/kind")
    @patch(
        "ilum.wizard.cluster.subprocess.run",
        side_effect=subprocess.CalledProcessError(1, "kind"),
    )
    def test_kind_failure(self, _r: MagicMock, _w: MagicMock) -> None:
        assert _discover_kind() == []


class TestClusterMerge:
    """Tests for list_local merge logic between managed and discovered clusters."""

    @patch("ilum.wizard.cluster.discover_clusters", return_value=[])
    def test_no_overlap_empty_discovery(self, _d: MagicMock) -> None:
        """Managed records returned as-is when nothing is discovered."""
        records = [ClusterRecord(name="a", provider="k3d", kubecontext="k3d-a")]
        result = ClusterManager.list_local(records)
        assert len(result) == 1
        assert result[0].source == "managed"

    @patch("ilum.wizard.cluster.discover_clusters")
    def test_no_overlap_detected_added(self, mock_discover: MagicMock) -> None:
        """Discovered clusters appear as detected when not in config."""
        mock_discover.return_value = [
            DiscoveredCluster(name="ext", provider="minikube", kubecontext="ext", status="Running"),
        ]
        result = ClusterManager.list_local([])
        assert len(result) == 1
        assert result[0].name == "ext"
        assert result[0].source == "detected"
        assert result[0].status == "Running"

    @patch("ilum.wizard.cluster.discover_clusters")
    def test_overlap_enriches_managed(self, mock_discover: MagicMock) -> None:
        """When a managed cluster is also discovered, its status is enriched."""
        mock_discover.return_value = [
            DiscoveredCluster(name="a", provider="k3d", kubecontext="k3d-a", status="Running"),
        ]
        records = [ClusterRecord(name="a", provider="k3d", kubecontext="k3d-a")]
        result = ClusterManager.list_local(records)
        assert len(result) == 1
        assert result[0].source == "managed"
        assert result[0].status == "Running"

    @patch("ilum.wizard.cluster.discover_clusters")
    def test_same_name_different_provider_both_kept(self, mock_discover: MagicMock) -> None:
        """Same name but different provider → both kept."""
        mock_discover.return_value = [
            DiscoveredCluster(name="a", provider="minikube", kubecontext="a", status="Running"),
        ]
        records = [ClusterRecord(name="a", provider="k3d", kubecontext="k3d-a")]
        result = ClusterManager.list_local(records)
        assert len(result) == 2
        providers = {r.provider for r in result}
        assert providers == {"k3d", "minikube"}

    @patch("ilum.wizard.cluster.discover_clusters", return_value=[])
    def test_empty_both(self, _d: MagicMock) -> None:
        """No managed records and no discovery → empty list."""
        result = ClusterManager.list_local([])
        assert result == []


class TestPresets:
    def test_dev_preset_exists(self) -> None:
        assert "dev" in PRESETS
        assert PRESETS["dev"].cpus == 6
        assert PRESETS["dev"].memory == "12g"

    def test_full_preset_exists(self) -> None:
        assert "full" in PRESETS
        assert PRESETS["full"].cpus == 8


class TestClusterPreset:
    def test_frozen(self) -> None:
        preset = ClusterPreset(cpus=2, memory="4g", name="test")
        with pytest.raises(AttributeError):
            preset.cpus = 8  # type: ignore[misc]
