"""Integration tests for VM lifecycle and keyboard combo tools on a real VM.

These tests require a real VirtualBox VM named "DSR-QA-Windows" to be present.
They are marked with @pytest.mark.integration and skipped in CI environments.

Run locally with:
    uv run pytest packages/mcp_vm_blackbox/tests/test_integration_vm.py -v -m integration

To run without coverage (faster):
    uv run pytest packages/mcp_vm_blackbox/tests/test_integration_vm.py -v -m integration --no-cov
"""

from __future__ import annotations

import asyncio
import os
from typing import TYPE_CHECKING

import pytest

if TYPE_CHECKING:
    from mcp_vm_blackbox.models import (
        ScreenshotResult,
        VmInfoResult,
        VmKeyComboResult,
        VmListEntry,
        VmStartResult,
        VmStopResult,
    )

# Skip all tests in this module unless INTEGRATION_TEST=1
# This prevents accidental runs in CI where VirtualBox isn't available.
pytestmark = pytest.mark.skipif(
    os.environ.get("INTEGRATION_TEST") != "1", reason="Integration tests require INTEGRATION_TEST=1 and a real VM"
)

# The VM we'll test against
TEST_VM_NAME = "DSR-QA-Windows"


@pytest.fixture
async def ensure_vm_stopped():
    """Ensure the test VM is stopped before and after each test.

    This provides test isolation: each test starts with a stopped VM
    and cleans up by stopping it afterward.
    """
    from mcp_vm_blackbox.vm_inspection import vm_info
    from mcp_vm_blackbox.vm_lifecycle import vm_stop

    # Pre-test: ensure VM is stopped
    info = await vm_info(vm_name=TEST_VM_NAME, target="local")
    if info.state == "running":
        await vm_stop(vm_name=TEST_VM_NAME, mode="poweroff", target="local")
        # Wait for VM to fully stop
        for _ in range(30):
            await asyncio.sleep(1)
            info = await vm_info(vm_name=TEST_VM_NAME, target="local")
            if info.state != "running":
                break

    yield

    # Post-test: ensure VM is stopped
    info = await vm_info(vm_name=TEST_VM_NAME, target="local")
    if info.state == "running":
        await vm_stop(vm_name=TEST_VM_NAME, mode="poweroff", target="local")
        # Wait for VM to fully stop
        for _ in range(30):
            await asyncio.sleep(1)
            info = await vm_info(vm_name=TEST_VM_NAME, target="local")
            if info.state != "running":
                break


@pytest.mark.integration
@pytest.mark.asyncio
class TestVmLifecycleIntegration:
    """Integration tests for VM start, stop, and keyboard combo on real VM."""

    async def test_vm_list_shows_test_vm(self) -> None:
        """Verify the test VM exists in VirtualBox inventory.

        This is a quick smoke test to confirm the test environment is configured.
        """
        from mcp_vm_blackbox.vm_inspection import vm_list

        vms: list[VmListEntry] = await vm_list(target="local")

        vm_names = [vm.name for vm in vms]
        assert TEST_VM_NAME in vm_names, f"Test VM '{TEST_VM_NAME}' not found. Available VMs: {vm_names}"

    async def test_vm_start_stop_lifecycle(self, ensure_vm_stopped) -> None:
        """Test vm_start starts a VM and vm_stop stops it.

        Verifies:
        - vm_start returns status='ok' and VM enters 'running' state
        - vm_stop returns status='ok' and VM exits 'running' state
        """
        from mcp_vm_blackbox.vm_inspection import vm_info
        from mcp_vm_blackbox.vm_lifecycle import vm_start, vm_stop

        # Start the VM
        start_result: VmStartResult = await vm_start(vm_name=TEST_VM_NAME, mode="headless", target="local")

        assert start_result.status == "ok", f"vm_start failed: {start_result.error}"
        assert start_result.vm_name == TEST_VM_NAME
        assert start_result.mode == "headless"

        # Poll until VM is running (up to 60 seconds)
        for _ in range(60):
            info: VmInfoResult = await vm_info(vm_name=TEST_VM_NAME, target="local")
            if info.state == "running":
                break
            await asyncio.sleep(1)
        else:
            pytest.fail(f"VM did not reach 'running' state. Current state: {info.state}")

        # Verify VM info shows running state
        assert info.state == "running"

        # Stop the VM
        stop_result: VmStopResult = await vm_stop(vm_name=TEST_VM_NAME, mode="acpi", target="local")

        assert stop_result.status == "ok", f"vm_stop failed: {stop_result.error}"
        assert stop_result.vm_name == TEST_VM_NAME
        assert stop_result.mode == "acpi"

        # Poll until VM is no longer running
        for _ in range(60):
            info = await vm_info(vm_name=TEST_VM_NAME, target="local")
            if info.state != "running":
                break
            await asyncio.sleep(1)
        else:
            pytest.fail(f"VM did not stop. Current state: {info.state}")

    async def test_vm_key_combo_win_r_opens_run_dialog(self, ensure_vm_stopped) -> None:
        """Test vm_key_combo sends Win+R and opens the Run dialog.

        This is the key integration test proving that:
        1. vm_start works on a real VM
        2. vm_key_combo sends correct scancodes
        3. The VM receives and processes the keyboard input

        Verification is via screenshot showing the Run dialog.
        """
        from mcp_vm_blackbox.vm_inspection import vm_info, vm_screenshot_api
        from mcp_vm_blackbox.vm_interaction import vm_key_combo
        from mcp_vm_blackbox.vm_lifecycle import vm_start, vm_stop

        # Start the VM
        start_result: VmStartResult = await vm_start(vm_name=TEST_VM_NAME, mode="headless", target="local")
        assert start_result.status == "ok", f"vm_start failed: {start_result.error}"

        # Wait for VM to be running (Windows boot takes time)
        for _ in range(120):  # Windows boot can take up to 2 minutes
            info: VmInfoResult = await vm_info(vm_name=TEST_VM_NAME, target="local")
            if info.state == "running":
                break
            await asyncio.sleep(1)
        else:
            pytest.fail(f"VM did not reach 'running' state. Current state: {info.state}")

        # Wait additional time for Windows to be responsive (desktop visible)
        # This is critical: the VM may be "running" but not yet at the desktop
        await asyncio.sleep(30)

        # Take a baseline screenshot to verify VM is at a responsive state
        baseline_screenshot: ScreenshotResult = await vm_screenshot_api(vm_name=TEST_VM_NAME, target="local")
        assert baseline_screenshot.error is None, f"Baseline screenshot failed: {baseline_screenshot.error}"

        # Send Win+R to open the Run dialog
        combo_result: VmKeyComboResult = await vm_key_combo(vm_name=TEST_VM_NAME, combo="win+r", target="local")

        assert combo_result.status == "ok", f"vm_key_combo failed: {combo_result.error}"
        assert combo_result.combo_parsed == "win+r"
        # Win+R scancodes: press Win (e05b), press R (13), release R (93), release Win (e0db)
        assert combo_result.scancodes_sent == ["e05b", "13", "93", "e0db"], (
            f"Unexpected scancodes: {combo_result.scancodes_sent}"
        )

        # Wait briefly for the Run dialog to appear
        await asyncio.sleep(2)

        # Take screenshot to verify Run dialog appeared
        run_dialog_screenshot: ScreenshotResult = await vm_screenshot_api(vm_name=TEST_VM_NAME, target="local")
        assert run_dialog_screenshot.error is None, f"Run dialog screenshot failed: {run_dialog_screenshot.error}"

        # The screenshot is saved to the scratch directory.
        # Log the path for manual verification if needed.
        print(f"\nBaseline screenshot: {baseline_screenshot.saved_to}")
        print(f"Run dialog screenshot: {run_dialog_screenshot.saved_to}")

        # Verify the screenshot was captured successfully
        assert run_dialog_screenshot.width > 0, "Screenshot has invalid width"
        assert run_dialog_screenshot.height > 0, "Screenshot has invalid height"

        # Clean up: stop the VM
        stop_result: VmStopResult = await vm_stop(vm_name=TEST_VM_NAME, mode="poweroff", target="local")
        assert stop_result.status == "ok", f"vm_stop failed: {stop_result.error}"

    async def test_vm_key_combo_ctrl_c_sends_scancodes(self, ensure_vm_stopped) -> None:
        """Test vm_key_combo sends Ctrl+C with correct scancodes.

        This verifies the modifier+key parsing and scancode generation
        without relying on specific VM state changes.
        """
        from mcp_vm_blackbox.vm_inspection import vm_info
        from mcp_vm_blackbox.vm_interaction import vm_key_combo
        from mcp_vm_blackbox.vm_lifecycle import vm_start, vm_stop

        # Start the VM
        start_result: VmStartResult = await vm_start(vm_name=TEST_VM_NAME, mode="headless", target="local")
        assert start_result.status == "ok"

        # Wait for VM to be running
        for _ in range(120):
            info: VmInfoResult = await vm_info(vm_name=TEST_VM_NAME, target="local")
            if info.state == "running":
                break
            await asyncio.sleep(1)
        else:
            pytest.fail("VM did not reach 'running' state")

        # Send Ctrl+C
        combo_result: VmKeyComboResult = await vm_key_combo(vm_name=TEST_VM_NAME, combo="ctrl+c", target="local")

        assert combo_result.status == "ok", f"vm_key_combo failed: {combo_result.error}"
        assert combo_result.combo_parsed == "ctrl+c"
        # Ctrl+C scancodes: press Ctrl (1d), press C (2e), release C (ae), release Ctrl (9d)
        assert combo_result.scancodes_sent == ["1d", "2e", "ae", "9d"], (
            f"Unexpected scancodes for ctrl+c: {combo_result.scancodes_sent}"
        )

        # Clean up
        await vm_stop(vm_name=TEST_VM_NAME, mode="poweroff", target="local")


@pytest.mark.integration
@pytest.mark.asyncio
class TestVmKeyComboErrorCases:
    """Test error handling in vm_key_combo on a real VM."""

    async def test_invalid_combo_returns_error(self) -> None:
        """Test that an invalid combo string returns an error result."""
        from mcp_vm_blackbox.vm_interaction import vm_key_combo

        result: VmKeyComboResult = await vm_key_combo(vm_name=TEST_VM_NAME, combo="invalid+key", target="local")

        assert result.status == "error"
        assert result.error is not None
        assert "invalid" in result.error.lower() or "unknown" in result.error.lower()

    async def test_unknown_modifier_returns_error(self) -> None:
        """Test that an unknown modifier returns an error result."""
        from mcp_vm_blackbox.vm_interaction import vm_key_combo

        result: VmKeyComboResult = await vm_key_combo(vm_name=TEST_VM_NAME, combo="super+r", target="local")

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