"""Tests for KVM module management tools (kvm_unload, kvm_reload).

Tests: KVM module detection, CPU vendor detection, active user blocking, dry_run mode.
How: Mock _ssh_run and _resolve so tests run offline.
Why: KVM tools modify host state; tests must not require actual KVM modules.
"""

from __future__ import annotations

from typing import TYPE_CHECKING
from unittest.mock import AsyncMock, MagicMock

import pytest

from mcp_vm_blackbox.targets import TargetConfig

if TYPE_CHECKING:
    from pytest_mock import MockerFixture


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


@pytest.fixture
def mock_resolve_local(mocker: MockerFixture) -> MagicMock:
    """Patch _resolve to return a local TargetConfig (ssh_host='')."""
    cfg = TargetConfig(ssh_host="")
    return mocker.patch("mcp_vm_blackbox.kvm_tools._resolve", return_value=cfg)


@pytest.fixture
def mock_resolve_remote(mocker: MockerFixture) -> MagicMock:
    """Patch _resolve to return a remote TargetConfig."""
    cfg = TargetConfig(ssh_host="ci@runner.example.com")
    return mocker.patch("mcp_vm_blackbox.kvm_tools._resolve", return_value=cfg)


@pytest.fixture
def mock_ssh_run(mocker: MockerFixture) -> AsyncMock:
    """Patch _ssh_run for controlled output."""
    result = MagicMock()
    result.stdout = ""
    result.stderr = ""
    result.returncode = 0
    return mocker.patch("mcp_vm_blackbox.kvm_tools._ssh_run", new_callable=AsyncMock, return_value=result)


def _make_ssh_result(stdout: str = "", stderr: str = "", returncode: int = 0) -> MagicMock:
    """Build a mock _SshRunResult for tests."""
    r = MagicMock()
    r.stdout = stdout
    r.stderr = stderr
    r.returncode = returncode
    return r


# ---------------------------------------------------------------------------
# kvm_unload
# ---------------------------------------------------------------------------


class TestKvmUnload:
    """Tests for kvm_unload tool."""

    async def test_unload_success(self, mock_resolve_remote: MagicMock, mock_ssh_run: AsyncMock) -> None:
        """kvm_unload unloads modules when present and no active users.

        Tests: Happy path with kvm_intel and kvm loaded.
        How: Mock lsmod to show kvm_intel/kvm, lscpu to show Intel, fuser empty.
        Why: Normal case - agent needs to unload KVM for VirtualBox.
        """
        # Sequence of calls: _detect_cpu_vendor (lscpu), _lsmod_kvm, _check_kvm_users,
        # _lsmod_kvm (verify after unload)
        side_effects = [
            _make_ssh_result(stdout="Vendor ID: GenuineIntel", returncode=0),  # lscpu
            _make_ssh_result(stdout="kvm_intel\nkvm", returncode=0),  # lsmod before
            _make_ssh_result(stdout="", returncode=0),  # fuser (no PIDs)
            _make_ssh_result(stdout="", returncode=0),  # modprobe -r kvm_intel
            _make_ssh_result(stdout="", returncode=0),  # modprobe -r kvm
            _make_ssh_result(stdout="", returncode=0),  # lsmod after (empty)
        ]
        mock_ssh_run.side_effect = side_effects

        from mcp_vm_blackbox.kvm_tools import kvm_unload

        result = await kvm_unload(target="ci")

        assert result.status == "ok"
        assert "kvm_intel" in result.modules_before
        assert "kvm" in result.modules_before
        assert result.modules_after == []
        assert result.cpu_vendor == "intel"
        assert result.dry_run is False
        assert result.error is None

    async def test_unload_already_unloaded(self, mock_resolve_remote: MagicMock, mock_ssh_run: AsyncMock) -> None:
        """kvm_unload returns ok when no KVM modules are loaded.

        Tests: Early-exit when lsmod shows no KVM.
        How: Mock lsmod to return empty.
        Why: Idempotent - already unloaded is success.
        """
        side_effects = [
            _make_ssh_result(stdout="Vendor ID: GenuineIntel", returncode=0),  # lscpu
            _make_ssh_result(stdout="", returncode=0),  # lsmod (empty)
        ]
        mock_ssh_run.side_effect = side_effects

        from mcp_vm_blackbox.kvm_tools import kvm_unload

        result = await kvm_unload(target="ci")

        assert result.status == "ok"
        assert result.modules_before == []
        assert result.modules_after == []
        assert result.cpu_vendor == "intel"

    async def test_unload_active_users_blocked(self, mock_resolve_remote: MagicMock, mock_ssh_run: AsyncMock) -> None:
        """kvm_unload returns blocked when fuser shows active PIDs.

        Tests: Active user detection blocks unload.
        How: Mock fuser to return PIDs.
        Why: Cannot unload while VMs/QEMU are using KVM.
        """
        side_effects = [
            _make_ssh_result(stdout="Vendor ID: GenuineIntel", returncode=0),  # lscpu
            _make_ssh_result(stdout="kvm_intel\nkvm", returncode=0),  # lsmod
            _make_ssh_result(stdout="1234 5678", returncode=0),  # fuser with PIDs
        ]
        mock_ssh_run.side_effect = side_effects

        from mcp_vm_blackbox.kvm_tools import kvm_unload

        result = await kvm_unload(target="ci")

        assert result.status == "blocked"
        assert result.modules_before == ["kvm_intel", "kvm"]
        assert result.modules_after == ["kvm_intel", "kvm"]
        assert "1234" in (result.error or "") or "PIDs" in (result.error or "")

    async def test_unload_dry_run(self, mock_resolve_remote: MagicMock, mock_ssh_run: AsyncMock) -> None:
        """kvm_unload with dry_run=True returns planned actions without executing.

        Tests: Dry-run mode inspection.
        How: Mock lsmod, verify no modprobe calls made.
        Why: Agent can inspect state without side effects.
        """
        side_effects = [
            _make_ssh_result(stdout="Vendor ID: GenuineIntel", returncode=0),  # lscpu
            _make_ssh_result(stdout="kvm_intel\nkvm", returncode=0),  # lsmod
            _make_ssh_result(stdout="", returncode=0),  # fuser (no PIDs)
        ]
        mock_ssh_run.side_effect = side_effects

        from mcp_vm_blackbox.kvm_tools import kvm_unload

        result = await kvm_unload(target="ci", dry_run=True)

        assert result.status == "ok"
        assert result.dry_run is True
        assert "kvm_intel" in result.modules_before
        assert result.modules_after == []  # Planned, not actual
        assert result.error is None
        # Verify modprobe was NOT called (only 3 calls: lscpu, lsmod, fuser)
        assert mock_ssh_run.call_count == 3

    async def test_unload_amd_vendor(self, mock_resolve_remote: MagicMock, mock_ssh_run: AsyncMock) -> None:
        """kvm_unload correctly handles AMD CPU and kvm_amd module.

        Tests: AMD vendor detection and module unloading.
        How: Mock lscpu to show AMD.
        Why: Must handle both Intel and AMD hosts.
        """
        side_effects = [
            _make_ssh_result(stdout="Vendor ID: AuthenticAMD", returncode=0),  # lscpu
            _make_ssh_result(stdout="kvm_amd\nkvm", returncode=0),  # lsmod
            _make_ssh_result(stdout="", returncode=0),  # fuser
            _make_ssh_result(stdout="", returncode=0),  # modprobe -r kvm_amd
            _make_ssh_result(stdout="", returncode=0),  # modprobe -r kvm
            _make_ssh_result(stdout="", returncode=0),  # lsmod after
        ]
        mock_ssh_run.side_effect = side_effects

        from mcp_vm_blackbox.kvm_tools import kvm_unload

        result = await kvm_unload(target="ci")

        assert result.status == "ok"
        assert result.cpu_vendor == "amd"
        assert "kvm_amd" in result.modules_before


class TestKvmReload:
    """Tests for kvm_reload tool."""

    async def test_reload_success(self, mock_resolve_remote: MagicMock, mock_ssh_run: AsyncMock) -> None:
        """kvm_reload loads kvm and vendor module when absent.

        Tests: Happy path loading modules.
        How: Mock lsmod empty, lscpu shows Intel.
        Why: Agent needs to restore KVM after VirtualBox use.
        """
        side_effects = [
            _make_ssh_result(stdout="Vendor ID: GenuineIntel", returncode=0),  # lscpu
            _make_ssh_result(stdout="", returncode=0),  # lsmod before (empty)
            _make_ssh_result(stdout="", returncode=0),  # modprobe kvm
            _make_ssh_result(stdout="", returncode=0),  # modprobe kvm_intel
            _make_ssh_result(stdout="kvm_intel\nkvm", returncode=0),  # lsmod after
        ]
        mock_ssh_run.side_effect = side_effects

        from mcp_vm_blackbox.kvm_tools import kvm_reload

        result = await kvm_reload(target="ci")

        assert result.status == "ok"
        assert result.modules_before == []
        assert "kvm" in result.modules_after
        assert "kvm_intel" in result.modules_after
        assert result.cpu_vendor == "intel"

    async def test_reload_already_loaded(self, mock_resolve_remote: MagicMock, mock_ssh_run: AsyncMock) -> None:
        """kvm_reload returns ok when modules already loaded.

        Tests: Idempotent reload.
        How: Mock lsmod to show kvm_intel and kvm present.
        Why: No error if already in desired state.
        """
        side_effects = [
            _make_ssh_result(stdout="Vendor ID: GenuineIntel", returncode=0),  # lscpu
            _make_ssh_result(stdout="kvm_intel\nkvm", returncode=0),  # lsmod
        ]
        mock_ssh_run.side_effect = side_effects

        from mcp_vm_blackbox.kvm_tools import kvm_reload

        result = await kvm_reload(target="ci")

        assert result.status == "ok"
        assert "kvm" in result.modules_before
        assert "kvm_intel" in result.modules_before
        # Should not call modprobe when already loaded
        assert mock_ssh_run.call_count == 2

    async def test_reload_unknown_vendor(self, mock_resolve_remote: MagicMock, mock_ssh_run: AsyncMock) -> None:
        """kvm_reload returns error when CPU vendor is unknown.

        Tests: Unknown vendor error handling.
        How: Mock lscpu to show neither Intel nor AMD.
        Why: Cannot determine correct vendor module.
        """
        side_effects = [
            _make_ssh_result(stdout="Vendor ID: Unknown", returncode=0),  # lscpu
            _make_ssh_result(stdout="Vendor ID: Unknown", returncode=0),  # cpuinfo fallback
            _make_ssh_result(stdout="", returncode=0),  # lsmod
        ]
        mock_ssh_run.side_effect = side_effects

        from mcp_vm_blackbox.kvm_tools import kvm_reload

        result = await kvm_reload(target="ci")

        assert result.status == "error"
        assert result.cpu_vendor == "unknown"
        assert "unknown" in (result.error or "").lower()


# ---------------------------------------------------------------------------
# Helper tests
# ---------------------------------------------------------------------------


class TestDetectCpuVendor:
    """Tests for _detect_cpu_vendor helper."""

    async def test_detect_intel_vendor(self, mock_resolve_remote: MagicMock, mock_ssh_run: AsyncMock) -> None:
        """_detect_cpu_vendor returns 'intel' for GenuineIntel.

        Tests: Intel vendor detection via lscpu.
        How: Mock lscpu output.
        Why: Correct vendor module must be used.
        """
        mock_ssh_run.return_value = _make_ssh_result(stdout="Vendor ID: GenuineIntel", returncode=0)

        from mcp_vm_blackbox.kvm_tools import _detect_cpu_vendor

        result = await _detect_cpu_vendor(mock_resolve_remote.return_value)

        assert result == "intel"

    async def test_detect_amd_vendor(self, mock_resolve_remote: MagicMock, mock_ssh_run: AsyncMock) -> None:
        """_detect_cpu_vendor returns 'amd' for AuthenticAMD.

        Tests: AMD vendor detection via lscpu.
        How: Mock lscpu output.
        Why: Correct vendor module must be used.
        """
        mock_ssh_run.return_value = _make_ssh_result(stdout="Vendor ID: AuthenticAMD", returncode=0)

        from mcp_vm_blackbox.kvm_tools import _detect_cpu_vendor

        result = await _detect_cpu_vendor(mock_resolve_remote.return_value)

        assert result == "amd"

    async def test_detect_vendor_fallback_cpuinfo(
        self, mock_resolve_remote: MagicMock, mock_ssh_run: AsyncMock
    ) -> None:
        """_detect_cpu_vendor falls back to /proc/cpuinfo when lscpu fails.

        Tests: Fallback detection path.
        How: Mock lscpu to fail, /proc/cpuinfo to show Intel.
        Why: Some minimal hosts may not have lscpu.
        """
        # First call (lscpu) fails, second call (cpuinfo) succeeds
        side_effects = [
            _make_ssh_result(stdout="", stderr="not found", returncode=1),  # lscpu
            _make_ssh_result(stdout="model name\t: Intel(R) Core(TM)", returncode=0),  # cpuinfo
        ]
        mock_ssh_run.side_effect = side_effects

        from mcp_vm_blackbox.kvm_tools import _detect_cpu_vendor

        result = await _detect_cpu_vendor(mock_resolve_remote.return_value)

        assert result == "intel"
