"""Tests for wizard/deps.py — tool detection, auto-install, and ensure_tool API."""

from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest

from ilum.doctor.checks import CheckResult, CheckStatus
from ilum.errors import PrerequisiteError
from ilum.wizard.deps import (
    TOOL_REGISTRY,
    ToolInstaller,
    _ask_or_abort,
    _detect_platform,
    _install_docker,
    _install_k3d,
    _install_kind,
    _install_minikube,
    ensure_tool,
    ensure_tools,
)


@pytest.fixture()
def installer() -> ToolInstaller:
    return ToolInstaller()


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

    return MagicMock(spec=IlumConsole)


@pytest.fixture(autouse=True)
def _mock_ensure_tools():
    """Override conftest's _mock_ensure_tools — we test the real functions here."""
    yield


@pytest.fixture(autouse=True)
def _clear_verified_cache():
    """Clear the verified-tools cache before/after each test."""
    from ilum.wizard import deps

    deps._verified_tools.clear()
    yield
    deps._verified_tools.clear()


# ---- TestToolRegistry ---------------------------------------------------


class TestToolRegistry:
    def test_all_six_tools_registered(self) -> None:
        expected = {"helm", "kubectl", "docker", "minikube", "k3d", "kind"}
        assert set(TOOL_REGISTRY.keys()) == expected

    def test_core_tools(self) -> None:
        for name in ("helm", "kubectl", "docker"):
            assert TOOL_REGISTRY[name].category == "core"

    def test_provider_tools(self) -> None:
        for name in ("minikube", "k3d", "kind"):
            assert TOOL_REGISTRY[name].category == "provider"

    def test_providers_have_no_min_version(self) -> None:
        for name in ("minikube", "k3d", "kind"):
            assert TOOL_REGISTRY[name].min_version is None

    def test_docker_is_installable(self) -> None:
        """Docker should be in the registry — it's now auto-installable."""
        assert "docker" in TOOL_REGISTRY
        assert TOOL_REGISTRY["docker"].manual_url == "https://docs.docker.com/get-docker/"


# ---- TestEnsureTool -----------------------------------------------------


class TestEnsureTool:
    def test_already_installed(self, mock_console: MagicMock) -> None:
        """When the binary passes check_binary, no prompt and no raise."""
        ok = CheckResult(name="helm", status=CheckStatus.OK, message="helm 3.14")
        with patch("ilum.wizard.deps.check_binary", return_value=ok):
            ensure_tool("helm", mock_console, interactive=False)
        # Should not prompt or raise

    def test_missing_non_interactive_raises(self, mock_console: MagicMock) -> None:
        fail = CheckResult(name="helm", status=CheckStatus.FAIL, message="helm not found")
        with patch("ilum.wizard.deps.check_binary", return_value=fail):
            with pytest.raises(PrerequisiteError, match="required but not available"):
                ensure_tool("helm", mock_console, interactive=False)

    def test_missing_interactive_user_declines(self, mock_console: MagicMock) -> None:
        fail = CheckResult(name="helm", status=CheckStatus.FAIL, message="helm not found")
        with (
            patch("ilum.wizard.deps.check_binary", return_value=fail),
            patch("ilum.wizard.deps.questionary.confirm") as mock_confirm,
        ):
            mock_confirm.return_value.ask.return_value = False
            with pytest.raises(PrerequisiteError, match="required but not available"):
                ensure_tool("helm", mock_console, interactive=True)

    def test_missing_interactive_user_accepts(self, mock_console: MagicMock) -> None:
        fail = CheckResult(name="helm", status=CheckStatus.FAIL, message="helm not found")
        ok = CheckResult(name="helm", status=CheckStatus.OK, message="helm 3.14")

        with (
            patch("ilum.wizard.deps.check_binary", side_effect=[fail, ok]),
            patch("ilum.wizard.deps.questionary.confirm") as mock_confirm,
            patch("ilum.wizard.deps._install_tool") as mock_install,
        ):
            mock_confirm.return_value.ask.return_value = True
            ensure_tool("helm", mock_console, interactive=True)
            mock_install.assert_called_once()
        mock_console.success.assert_called()

    def test_cache_prevents_repeated_checks(self, mock_console: MagicMock) -> None:
        ok = CheckResult(name="helm", status=CheckStatus.OK, message="helm 3.14")
        with patch("ilum.wizard.deps.check_binary", return_value=ok) as mock_check:
            ensure_tool("helm", mock_console, interactive=False)
            ensure_tool("helm", mock_console, interactive=False)
            # check_binary should only be called once
            assert mock_check.call_count == 1

    def test_unknown_tool_raises(self, mock_console: MagicMock) -> None:
        with pytest.raises(PrerequisiteError, match="Unknown tool"):
            ensure_tool("nonexistent-tool", mock_console, interactive=False)

    def test_install_failure_raises(self, mock_console: MagicMock) -> None:
        fail = CheckResult(name="helm", status=CheckStatus.FAIL, message="not found")
        with (
            patch("ilum.wizard.deps.check_binary", return_value=fail),
            patch("ilum.wizard.deps.questionary.confirm") as mock_confirm,
            patch(
                "ilum.wizard.deps._install_tool",
                side_effect=OSError("brew not found"),
            ),
        ):
            mock_confirm.return_value.ask.return_value = True
            with pytest.raises(PrerequisiteError, match="Automatic installation.*failed"):
                ensure_tool("helm", mock_console, interactive=True)


# ---- TestEnsureTools ----------------------------------------------------


class TestEnsureTools:
    def test_checks_all_tools(self, mock_console: MagicMock) -> None:
        ok = CheckResult(name="ok", status=CheckStatus.OK, message="found")
        with patch("ilum.wizard.deps.check_binary", return_value=ok):
            ensure_tools(["helm", "docker"], mock_console, interactive=False)
        # Should not raise

    def test_stops_on_first_failure(self, mock_console: MagicMock) -> None:
        fail = CheckResult(name="helm", status=CheckStatus.FAIL, message="not found")
        with patch("ilum.wizard.deps.check_binary", return_value=fail):
            with pytest.raises(PrerequisiteError):
                ensure_tools(["helm", "docker"], mock_console, interactive=False)


# ---- TestToolInstaller (backward compat) --------------------------------


class TestToolInstaller:
    def test_all_tools_present(
        self,
        installer: ToolInstaller,
        mock_console: MagicMock,
    ) -> None:
        """When all tools pass, no install prompts are triggered."""
        ok = CheckResult(name="test", status=CheckStatus.OK, message="found")

        with patch("ilum.wizard.deps.check_binary", return_value=ok):
            results = installer.check_and_install(mock_console, non_interactive=True)

        assert len(results) == 3
        assert all(r.status == CheckStatus.OK for r in results)
        mock_console.success.assert_called()

    def test_missing_helm_non_interactive(
        self,
        installer: ToolInstaller,
        mock_console: MagicMock,
    ) -> None:
        """In non-interactive mode, missing tools are reported but not installed."""
        fail = CheckResult(
            name="helm",
            status=CheckStatus.FAIL,
            message="helm not found on PATH",
        )
        ok = CheckResult(name="ok", status=CheckStatus.OK, message="found")

        side_effects = [fail, ok, ok]
        with patch("ilum.wizard.deps.check_binary", side_effect=side_effects):
            results = installer.check_and_install(mock_console, non_interactive=True)

        assert results[0].status == CheckStatus.FAIL
        mock_console.warning.assert_called()

    def test_missing_docker_non_interactive(
        self,
        installer: ToolInstaller,
        mock_console: MagicMock,
    ) -> None:
        """Docker is now installable — in non-interactive mode it's just reported as missing."""
        ok = CheckResult(name="ok", status=CheckStatus.OK, message="found")
        fail = CheckResult(
            name="docker",
            status=CheckStatus.FAIL,
            message="docker not found",
        )

        side_effects = [ok, ok, fail]
        with patch("ilum.wizard.deps.check_binary", side_effect=side_effects):
            results = installer.check_and_install(mock_console, non_interactive=True)

        assert results[2].status == CheckStatus.FAIL
        mock_console.warning.assert_called()

    def test_platform_detection_darwin(self, installer: ToolInstaller) -> None:
        with patch("ilum.wizard.deps.platform.system", return_value="Darwin"):
            assert _detect_platform() == "darwin"

    def test_platform_detection_linux(self, installer: ToolInstaller) -> None:
        with patch("ilum.wizard.deps.platform.system", return_value="Linux"):
            assert _detect_platform() == "linux"

    def test_platform_detection_windows(self, installer: ToolInstaller) -> None:
        with patch("ilum.wizard.deps.platform.system", return_value="Windows"):
            assert _detect_platform() == "windows"

    def test_install_helm_darwin(
        self,
        installer: ToolInstaller,
        mock_console: MagicMock,
    ) -> None:
        with patch("ilum.wizard.deps.subprocess.run") as mock_run:
            installer._install_helm("darwin", mock_console)
            mock_run.assert_called_once()
            args = mock_run.call_args[0][0]
            assert args == ["brew", "install", "helm"]

    def test_install_helm_linux(
        self,
        installer: ToolInstaller,
        mock_console: MagicMock,
    ) -> None:
        with patch("ilum.wizard.deps.subprocess.run") as mock_run:
            installer._install_helm("linux", mock_console)
            mock_run.assert_called_once()
            args = mock_run.call_args[0][0]
            assert args[0] == "bash"

    def test_install_kubectl_darwin(
        self,
        installer: ToolInstaller,
        mock_console: MagicMock,
    ) -> None:
        with patch("ilum.wizard.deps.subprocess.run") as mock_run:
            installer._install_kubectl("darwin", mock_console)
            mock_run.assert_called_once()
            args = mock_run.call_args[0][0]
            assert args == ["brew", "install", "kubectl"]

    def test_install_helm_windows(
        self,
        installer: ToolInstaller,
        mock_console: MagicMock,
    ) -> None:
        with patch("ilum.wizard.deps.subprocess.run") as mock_run:
            installer._install_helm("windows", mock_console)
            mock_run.assert_called_once()
            args = mock_run.call_args[0][0]
            assert args[0] == "winget"
            assert "Helm.Helm" in args

    def test_install_kubectl_windows(
        self,
        installer: ToolInstaller,
        mock_console: MagicMock,
    ) -> None:
        with patch("ilum.wizard.deps.subprocess.run") as mock_run:
            installer._install_kubectl("windows", mock_console)
            mock_run.assert_called_once()
            args = mock_run.call_args[0][0]
            assert args[0] == "winget"
            assert "Kubernetes.kubectl" in args


# ---- TestDockerInstall --------------------------------------------------


class TestDockerInstall:
    def test_darwin_uses_brew_cask(self, mock_console: MagicMock) -> None:
        ok = MagicMock(returncode=0)
        with patch("ilum.wizard.deps.subprocess.run", return_value=ok) as mock_run:
            _install_docker("darwin", mock_console)
            # First call: brew install --cask docker
            first_call_args = mock_run.call_args_list[0][0][0]
            assert first_call_args == ["brew", "install", "--cask", "docker"]
            # Second call: open -a Docker (start Docker Desktop)
            second_call_args = mock_run.call_args_list[1][0][0]
            assert second_call_args == ["open", "-a", "Docker"]

    def test_linux_uses_get_docker(self, mock_console: MagicMock) -> None:
        with patch("ilum.wizard.deps.subprocess.run") as mock_run:
            _install_docker("linux", mock_console)
            # First call is the install script; subsequent calls are
            # post-install steps (usermod, systemctl).
            first_call_args = mock_run.call_args_list[0][0][0]
            assert first_call_args[0] == "bash"
            assert "get.docker.com" in first_call_args[-1]

    def test_linux_post_install_adds_docker_group(self, mock_console: MagicMock) -> None:
        sudo_ok = MagicMock(returncode=0)
        with (
            patch("ilum.wizard.deps.subprocess.run", return_value=sudo_ok) as mock_run,
            patch.dict("os.environ", {"USER": "testuser"}),
        ):
            _install_docker("linux", mock_console)
            # Should call usermod to add user to docker group
            usermod_calls = [
                c for c in mock_run.call_args_list if c[0][0][:2] == ["sudo", "usermod"]
            ]
            assert len(usermod_calls) == 1
            assert "docker" in usermod_calls[0][0][0]
            assert "testuser" in usermod_calls[0][0][0]
            # Should warn about log out / newgrp
            mock_console.warning.assert_called()

    def test_linux_post_install_no_sudo_warns(self, mock_console: MagicMock) -> None:
        """When passwordless sudo is unavailable, show manual instructions."""
        sudo_fail = MagicMock(returncode=1)
        call_count = 0

        def side_effect(cmd, **kwargs):
            nonlocal call_count
            call_count += 1
            # First call: get.docker.com install script
            if call_count == 1:
                return MagicMock(returncode=0)
            # Second call: sudo -n true check
            return sudo_fail

        with patch("ilum.wizard.deps.subprocess.run", side_effect=side_effect):
            _install_docker("linux", mock_console)
        # Should warn with manual instructions
        warning_msg = mock_console.warning.call_args[0][0]
        assert "sudo usermod" in warning_msg
        assert "systemctl" in warning_msg

    def test_windows_uses_winget(self, mock_console: MagicMock) -> None:
        with patch("ilum.wizard.deps.subprocess.run") as mock_run:
            _install_docker("windows", mock_console)
            first_call_args = mock_run.call_args_list[0][0][0]
            assert first_call_args[0] == "winget"
            assert "Docker.DockerDesktop" in first_call_args
            # Should warn about restart / launch
            mock_console.warning.assert_called()


# ---- TestProviderInstall ------------------------------------------------


class TestProviderInstall:
    # -- minikube --

    def test_minikube_darwin(self, mock_console: MagicMock) -> None:
        with patch("ilum.wizard.deps.subprocess.run") as mock_run:
            _install_minikube("darwin", mock_console)
            args = mock_run.call_args[0][0]
            assert args == ["brew", "install", "minikube"]

    def test_minikube_linux(self, mock_console: MagicMock) -> None:
        with patch("ilum.wizard.deps.subprocess.run") as mock_run:
            _install_minikube("linux", mock_console)
            args = mock_run.call_args[0][0]
            assert args[0] == "bash"
            assert "minikube" in args[-1]

    def test_minikube_windows(self, mock_console: MagicMock) -> None:
        with patch("ilum.wizard.deps.subprocess.run") as mock_run:
            _install_minikube("windows", mock_console)
            args = mock_run.call_args[0][0]
            assert args[0] == "winget"
            assert "Kubernetes.minikube" in args

    # -- k3d --

    def test_k3d_darwin(self, mock_console: MagicMock) -> None:
        with patch("ilum.wizard.deps.subprocess.run") as mock_run:
            _install_k3d("darwin", mock_console)
            args = mock_run.call_args[0][0]
            assert args == ["brew", "install", "k3d"]

    def test_k3d_linux(self, mock_console: MagicMock) -> None:
        with patch("ilum.wizard.deps.subprocess.run") as mock_run:
            _install_k3d("linux", mock_console)
            args = mock_run.call_args[0][0]
            assert args[0] == "bash"
            assert "k3d" in args[-1]

    def test_k3d_windows(self, mock_console: MagicMock) -> None:
        with patch("ilum.wizard.deps.subprocess.run") as mock_run:
            _install_k3d("windows", mock_console)
            args = mock_run.call_args[0][0]
            assert args[0] == "winget"
            assert "k3d-io.k3d" in args

    # -- kind --

    def test_kind_darwin(self, mock_console: MagicMock) -> None:
        with patch("ilum.wizard.deps.subprocess.run") as mock_run:
            _install_kind("darwin", mock_console)
            args = mock_run.call_args[0][0]
            assert args == ["brew", "install", "kind"]

    def test_kind_linux(self, mock_console: MagicMock) -> None:
        with patch("ilum.wizard.deps.subprocess.run") as mock_run:
            _install_kind("linux", mock_console)
            args = mock_run.call_args[0][0]
            assert args[0] == "bash"
            assert "kind" in args[-1]

    def test_kind_windows(self, mock_console: MagicMock) -> None:
        with patch("ilum.wizard.deps.subprocess.run") as mock_run:
            _install_kind("windows", mock_console)
            args = mock_run.call_args[0][0]
            assert args[0] == "winget"
            assert "Kubernetes.kind" in args


class TestAskOrAbort:
    """Ctrl+C handling: _ask_or_abort raises KeyboardInterrupt when .ask() returns None."""

    def test_returns_value_when_not_none(self) -> None:
        question = MagicMock()
        question.ask.return_value = True
        assert _ask_or_abort(question) is True

    def test_raises_keyboard_interrupt_on_none(self) -> None:
        question = MagicMock()
        question.ask.return_value = None
        with pytest.raises(KeyboardInterrupt):
            _ask_or_abort(question)

    def test_passes_through_false(self) -> None:
        """False means user answered 'no' — should not raise."""
        question = MagicMock()
        question.ask.return_value = False
        assert _ask_or_abort(question) is False
