"""Tests for vm_interaction MCP tools.

Tests: vm_powershell, vm_type, vm_key, vm_mouse_click.
How: Mock _vbox, _load_vboxapi, SshTunnel, WinRMClient so tests run offline.
Why: CI environments lack VirtualBox/WinRM; tools must return correct models.
"""

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(mocker: MockerFixture) -> MagicMock:
    """Patch _resolve to return a local TargetConfig (ssh_host='')."""
    cfg = TargetConfig(ssh_host="", winrm_port=5985)
    return mocker.patch("mcp_vm_blackbox.vm_interaction._resolve", return_value=cfg)


@pytest.fixture
def mock_vbox(mocker: MockerFixture) -> MagicMock:
    """Patch _vbox to return a mock VBoxDispatch with async methods."""
    mock_dispatch = MagicMock()
    mock_dispatch.type_text = AsyncMock(return_value=None)
    mock_dispatch.key = AsyncMock(return_value=None)
    mock_dispatch.key_combo = AsyncMock(return_value=("ctrl+c", ["1d", "2e", "ae", "9d"]))
    return mocker.patch("mcp_vm_blackbox.vm_interaction._vbox", return_value=mock_dispatch)


# ---------------------------------------------------------------------------
# vm_type
# ---------------------------------------------------------------------------


class TestVmType:
    """Tests for vm_type tool."""

    @pytest.mark.asyncio
    async def test_vm_type_success(self, mock_resolve: MagicMock, mock_vbox: MagicMock) -> None:
        """vm_type returns VmTypeResult with status ok on success."""
        from mcp_vm_blackbox.vm_interaction import vm_type

        result = await vm_type(vm_name="my-vm", text="Hello", target="local")

        assert result.status == "ok"
        assert result.chars_sent == 5
        assert result.error is None

    @pytest.mark.asyncio
    async def test_vm_type_vm_not_found(self, mock_resolve: MagicMock, mock_vbox: MagicMock) -> None:
        """vm_type returns error when VM not found (RuntimeError)."""
        mock_vbox.return_value.type_text.side_effect = RuntimeError("VBoxManage failed: VM not found")

        from mcp_vm_blackbox.vm_interaction import vm_type

        result = await vm_type(vm_name="missing-vm", text="Hello", target="local")

        assert result.error is not None
        assert "not found" in result.error


# ---------------------------------------------------------------------------
# vm_key
# ---------------------------------------------------------------------------


class TestVmKey:
    """Tests for vm_key tool."""

    @pytest.mark.asyncio
    async def test_vm_key_success(self, mock_resolve: MagicMock, mock_vbox: MagicMock) -> None:
        """vm_key returns VmKeyResult with status ok for valid key."""
        from mcp_vm_blackbox.vm_interaction import vm_key

        result = await vm_key(vm_name="my-vm", key="enter", target="local")

        assert result.status == "ok"
        assert result.key_sent == "enter"
        assert result.error is None

    @pytest.mark.asyncio
    async def test_vm_key_expanded_key_set(self, mock_resolve: MagicMock, mock_vbox: MagicMock) -> None:
        """vm_key accepts expanded key set (letters, numbers, function keys, etc.)."""
        from mcp_vm_blackbox.vm_interaction import vm_key

        # Test a letter
        result = await vm_key(vm_name="my-vm", key="a", target="local")
        assert result.status == "ok"
        assert result.key_sent == "a"

        # Test a function key
        result = await vm_key(vm_name="my-vm", key="f1", target="local")
        assert result.status == "ok"

        # Test an arrow key
        result = await vm_key(vm_name="my-vm", key="up", target="local")
        assert result.status == "ok"

    @pytest.mark.asyncio
    async def test_vm_key_unknown_key_returns_error(self, mock_resolve: MagicMock, mock_vbox: MagicMock) -> None:
        """vm_key returns error for unknown key name."""
        from mcp_vm_blackbox.vm_interaction import vm_key

        result = await vm_key(vm_name="my-vm", key="invalid_key", target="local")

        assert result.error is not None
        assert "Unknown key" in result.error
        mock_vbox.assert_not_called()

    @pytest.mark.asyncio
    async def test_vm_key_vm_not_found(self, mock_resolve: MagicMock, mock_vbox: MagicMock) -> None:
        """vm_key returns error when VM not found."""
        mock_vbox.return_value.key.side_effect = RuntimeError("VBoxManage failed: VM not found")

        from mcp_vm_blackbox.vm_interaction import vm_key

        result = await vm_key(vm_name="missing-vm", key="enter", target="local")

        assert result.error is not None
        assert "not found" in result.error


class TestVmKeyCombo:
    """Tests for vm_key_combo tool."""

    @pytest.mark.asyncio
    async def test_vm_key_combo_ctrl_c(self, mock_resolve: MagicMock, mock_vbox: MagicMock) -> None:
        """vm_key_combo sends ctrl+c correctly."""
        mock_vbox.return_value.key_combo = AsyncMock(return_value=("ctrl+c", ["1d", "2e", "ae", "9d"]))

        from mcp_vm_blackbox.vm_interaction import vm_key_combo

        result = await vm_key_combo(vm_name="my-vm", combo="ctrl+c", target="local")

        assert result.status == "ok"
        assert result.combo_parsed == "ctrl+c"
        assert result.scancodes_sent == ["1d", "2e", "ae", "9d"]
        assert result.error is None

    @pytest.mark.asyncio
    async def test_vm_key_combo_win_r(self, mock_resolve: MagicMock, mock_vbox: MagicMock) -> None:
        """vm_key_combo sends win+r correctly."""
        mock_vbox.return_value.key_combo = AsyncMock(return_value=("win+r", ["e05b", "13", "93", "e0db"]))

        from mcp_vm_blackbox.vm_interaction import vm_key_combo

        result = await vm_key_combo(vm_name="my-vm", combo="win+r", target="local")

        assert result.status == "ok"
        assert result.combo_parsed == "win+r"
        assert result.scancodes_sent == ["e05b", "13", "93", "e0db"]

    @pytest.mark.asyncio
    async def test_vm_key_combo_alt_f4(self, mock_resolve: MagicMock, mock_vbox: MagicMock) -> None:
        """vm_key_combo sends alt+f4 correctly."""
        mock_vbox.return_value.key_combo = AsyncMock(return_value=("alt+f4", ["38", "3e", "be", "b8"]))

        from mcp_vm_blackbox.vm_interaction import vm_key_combo

        result = await vm_key_combo(vm_name="my-vm", combo="alt+f4", target="local")

        assert result.status == "ok"
        assert result.combo_parsed == "alt+f4"

    @pytest.mark.asyncio
    async def test_vm_key_combo_shift_tab(self, mock_resolve: MagicMock, mock_vbox: MagicMock) -> None:
        """vm_key_combo sends shift+tab correctly."""
        mock_vbox.return_value.key_combo = AsyncMock(return_value=("shift+tab", ["2a", "0f", "8f", "aa"]))

        from mcp_vm_blackbox.vm_interaction import vm_key_combo

        result = await vm_key_combo(vm_name="my-vm", combo="shift+tab", target="local")

        assert result.status == "ok"
        assert result.combo_parsed == "shift+tab"

    @pytest.mark.asyncio
    async def test_vm_key_combo_invalid_combo_returns_error(
        self, mock_resolve: MagicMock, mock_vbox: MagicMock
    ) -> None:
        """vm_key_combo returns error for invalid combo."""
        mock_vbox.return_value.key_combo = AsyncMock(side_effect=ValueError("Unknown modifier: 'super'"))

        from mcp_vm_blackbox.vm_interaction import vm_key_combo

        result = await vm_key_combo(vm_name="my-vm", combo="super+c", target="local")

        assert result.status == "error"
        assert result.error is not None
        assert "Invalid combo" in result.error

    @pytest.mark.asyncio
    async def test_vm_key_combo_vm_not_found(self, mock_resolve: MagicMock, mock_vbox: MagicMock) -> None:
        """vm_key_combo returns error when VM not found."""
        mock_vbox.return_value.key_combo = AsyncMock(side_effect=RuntimeError("VBoxManage failed: VM not found"))

        from mcp_vm_blackbox.vm_interaction import vm_key_combo

        result = await vm_key_combo(vm_name="missing-vm", combo="ctrl+c", target="local")

        assert result.status == "error"
        assert result.error is not None
        assert "not found" in result.error


# ---------------------------------------------------------------------------
# vm_mouse_click
# ---------------------------------------------------------------------------


class TestVmMouseClick:
    """Tests for vm_mouse_click tool."""

    def test_vm_mouse_click_non_local_raises_tool_error(self, mock_resolve: MagicMock) -> None:
        """vm_mouse_click raises ToolError for target != 'local'."""
        from fastmcp.exceptions import ToolError

        from mcp_vm_blackbox.vm_interaction import vm_mouse_click

        with pytest.raises(ToolError, match="only supports target='local'"):
            vm_mouse_click(vm_name="my-vm", x=100, y=200, target="ci")

    def test_vm_mouse_click_unknown_button_raises_tool_error(self, mock_resolve: MagicMock) -> None:
        """vm_mouse_click raises ToolError for unknown button."""
        from fastmcp.exceptions import ToolError

        from mcp_vm_blackbox.vm_interaction import vm_mouse_click

        with pytest.raises(ToolError, match="Unknown button"):
            vm_mouse_click(vm_name="my-vm", x=100, y=200, button="invalid", target="local")


# ---------------------------------------------------------------------------
# vm_powershell (needs WinRM mock)
# ---------------------------------------------------------------------------


class TestVmPowershell:
    """Tests for vm_powershell tool."""

    @pytest.mark.asyncio
    async def test_vm_powershell_success(self, mock_resolve: MagicMock, mocker: MockerFixture) -> None:
        """vm_powershell returns CommandResult with stdout on success."""
        mock_tunnel = mocker.patch("mcp_vm_blackbox.vm_interaction.SshTunnel")
        mock_tunnel.return_value.__aenter__ = AsyncMock(return_value=None)
        mock_tunnel.return_value.__aexit__ = AsyncMock(return_value=None)

        mock_client = MagicMock()
        mock_client.run_ps.return_value = "Hello from PowerShell"
        mocker.patch("mcp_vm_blackbox.vm_interaction.WinRMClient", return_value=mock_client)

        from mcp_vm_blackbox.vm_interaction import vm_powershell

        result = await vm_powershell(vm_name="my-vm", script="Write-Output 'Hello from PowerShell'", target="local")

        assert result.stdout == "Hello from PowerShell"
        assert result.exit_code == 0
        assert result.error is None

    @pytest.mark.asyncio
    async def test_vm_powershell_runtime_error(self, mock_resolve: MagicMock, mocker: MockerFixture) -> None:
        """vm_powershell returns CommandResult with exit_code 1 on RuntimeError."""
        mock_tunnel = mocker.patch("mcp_vm_blackbox.vm_interaction.SshTunnel")
        mock_tunnel.return_value.__aenter__ = AsyncMock(return_value=None)
        mock_tunnel.return_value.__aexit__ = AsyncMock(return_value=None)

        mock_client = MagicMock()
        mock_client.run_ps.side_effect = RuntimeError("WinRM failed")
        mocker.patch("mcp_vm_blackbox.vm_interaction.WinRMClient", return_value=mock_client)

        from mcp_vm_blackbox.vm_interaction import vm_powershell

        result = await vm_powershell(vm_name="my-vm", script="fail", target="local")

        assert result.stdout == ""
        assert result.stderr == "WinRM failed"
        assert result.exit_code == 1
