"""Tests for vm_lifecycle MCP tools.

Tests: vm_start, vm_stop.
How: Mock VBoxDispatch so tests run offline without VirtualBox.
Why: CI environments lack VirtualBox; tools must return correct Pydantic models.
"""

from __future__ import annotations

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

import pytest

from mcp_vm_blackbox.coordination_db import _reset_db_instance_for_testing
from mcp_vm_blackbox.targets import TargetConfig

if TYPE_CHECKING:
    from collections.abc import Generator

    from pytest_mock import MockerFixture


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


@pytest.fixture(autouse=True)
def reset_db_singleton() -> Generator[None, None, None]:
    """Reset the CoordinationDB singleton before and after each test to prevent xdist state leakage."""
    _reset_db_instance_for_testing()
    yield
    _reset_db_instance_for_testing()


@pytest.fixture
def mock_resolve(mocker: MockerFixture) -> MagicMock:
    """Patch _resolve to return a local TargetConfig."""
    cfg = TargetConfig(ssh_host="")
    return mocker.patch("mcp_vm_blackbox.vm_lifecycle._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.start_vm = AsyncMock(return_value=None)
    mock_dispatch.stop_vm = AsyncMock(return_value=None)
    return mocker.patch("mcp_vm_blackbox.vm_lifecycle._vbox", return_value=mock_dispatch)


# ---------------------------------------------------------------------------
# vm_start
# ---------------------------------------------------------------------------


class TestVmStart:
    """Tests for vm_start tool."""

    @pytest.fixture(autouse=True)
    def _mock_start_recording(self, mocker: MockerFixture) -> None:
        """Prevent _start_recording from hitting the real registry (unawaited coroutine guard)."""
        from mcp_vm_blackbox.models import RecordingStartResult

        mocker.patch(
            "mcp_vm_blackbox.vm_lifecycle._start_recording",
            new_callable=AsyncMock,
            return_value=RecordingStartResult(status="ok", vm_name="my-vm"),
        )

    async def test_vm_start_headless_success(self, mock_resolve: MagicMock, mock_vbox: MagicMock) -> None:
        """vm_start with default mode returns VmStartResult with status ok."""
        from mcp_vm_blackbox.vm_lifecycle import vm_start

        result = await vm_start(vm_name="my-vm", target="local")

        assert result.status == "ok"
        assert result.vm_name == "my-vm"
        assert result.mode == "headless"
        assert result.error is None
        mock_vbox.assert_called_once()
        mock_vbox.return_value.start_vm.assert_called_once_with(mode="headless")

    async def test_vm_start_gui_mode(self, mock_resolve: MagicMock, mock_vbox: MagicMock) -> None:
        """vm_start with mode='gui' passes the mode correctly."""
        from mcp_vm_blackbox.vm_lifecycle import vm_start

        result = await vm_start(vm_name="my-vm", mode="gui", target="local")

        assert result.status == "ok"
        assert result.mode == "gui"
        mock_vbox.return_value.start_vm.assert_called_once_with(mode="gui")

    async def test_vm_start_vm_not_found(self, mock_resolve: MagicMock, mock_vbox: MagicMock) -> None:
        """vm_start returns error when VM not found (RuntimeError)."""
        mock_vbox.return_value.start_vm.side_effect = RuntimeError("VBoxManage failed: VM not found")

        from mcp_vm_blackbox.vm_lifecycle import vm_start

        result = await vm_start(vm_name="missing-vm", target="local")

        assert result.status == "error"
        assert result.error is not None
        assert "not found" in result.error.lower() or "failed" in result.error.lower()


# ---------------------------------------------------------------------------
# vm_stop
# ---------------------------------------------------------------------------


class TestVmStop:
    """Tests for vm_stop tool."""

    @pytest.fixture(autouse=True)
    def _mock_stop_recording(self, mocker: MockerFixture) -> None:
        """Prevent _stop_recording from hitting the real registry (unawaited coroutine guard)."""
        from mcp_vm_blackbox.models import RecordingStopResult

        mocker.patch(
            "mcp_vm_blackbox.vm_lifecycle._stop_recording",
            new_callable=AsyncMock,
            return_value=RecordingStopResult(status="ok", vm_name="my-vm"),
        )

    async def test_vm_stop_acpi_success(self, mock_resolve: MagicMock, mock_vbox: MagicMock) -> None:
        """vm_stop with default mode returns VmStopResult with status ok."""
        from mcp_vm_blackbox.vm_lifecycle import vm_stop

        result = await vm_stop(vm_name="my-vm", target="local")

        assert result.status == "ok"
        assert result.vm_name == "my-vm"
        assert result.mode == "acpi"
        assert result.error is None
        mock_vbox.assert_called_once()
        mock_vbox.return_value.stop_vm.assert_called_once_with(mode="acpi")

    async def test_vm_stop_poweroff_mode(self, mock_resolve: MagicMock, mock_vbox: MagicMock) -> None:
        """vm_stop with mode='poweroff' passes the mode correctly."""
        from mcp_vm_blackbox.vm_lifecycle import vm_stop

        result = await vm_stop(vm_name="my-vm", mode="poweroff", target="local")

        assert result.status == "ok"
        assert result.mode == "poweroff"
        mock_vbox.return_value.stop_vm.assert_called_once_with(mode="poweroff")

    async def test_vm_stop_savestate_mode(self, mock_resolve: MagicMock, mock_vbox: MagicMock) -> None:
        """vm_stop with mode='savestate' passes the mode correctly."""
        from mcp_vm_blackbox.vm_lifecycle import vm_stop

        result = await vm_stop(vm_name="my-vm", mode="savestate", target="local")

        assert result.status == "ok"
        assert result.mode == "savestate"
        mock_vbox.return_value.stop_vm.assert_called_once_with(mode="savestate")

    async def test_vm_stop_vm_not_found(self, mock_resolve: MagicMock, mock_vbox: MagicMock) -> None:
        """vm_stop returns error when VM not found (RuntimeError)."""
        mock_vbox.return_value.stop_vm.side_effect = RuntimeError("VBoxManage failed: VM not found")

        from mcp_vm_blackbox.vm_lifecycle import vm_stop

        result = await vm_stop(vm_name="missing-vm", target="local")

        assert result.status == "error"
        assert result.error is not None
        assert "not found" in result.error.lower() or "failed" in result.error.lower()


# ---------------------------------------------------------------------------
# Recording wiring — vm_start auto-starts recording
# ---------------------------------------------------------------------------


class TestVmStartRecordingWiring:
    """Tests that vm_start auto-starts recording and handles recording failures non-fatally.

    Strategy: Mock _start_recording from vm_recording so tests run offline.
    The recording integration is tested at the unit boundary between vm_lifecycle
    and vm_recording's private helper.
    """

    async def test_vm_start_starts_recording(
        self, mock_resolve: MagicMock, mock_vbox: MagicMock, mocker: MockerFixture
    ) -> None:
        """vm_start calls _start_recording after VM starts and reflects result in VmStartResult.

        Tests: Auto-recording wiring in vm_start.
        How: Mock _start_recording to return a successful RecordingStartResult; call
             vm_start and assert recording_started=True and recording_path is set.
        Why: Always-on recording must activate automatically when the VM starts so
             the entire session is captured without manual intervention.
        """
        # Arrange
        from mcp_vm_blackbox.models import RecordingStartResult

        mock_start_recording = mocker.patch(
            "mcp_vm_blackbox.vm_lifecycle._start_recording",
            new_callable=AsyncMock,
            return_value=RecordingStartResult(
                status="ok",
                vm_name="my-vm",
                recording_path=".mcp-vm-blackbox/scratch/recordings/my-vm-20260101-000000-screen0.webm",
                started_at="2026-01-01T00:00:00+00:00",
            ),
        )

        from mcp_vm_blackbox.vm_lifecycle import vm_start

        # Act
        result = await vm_start(vm_name="my-vm", target="local")

        # Assert
        assert result.status == "ok"
        assert result.recording_started is True
        assert result.recording_path is not None
        assert "screen0.webm" in result.recording_path
        assert result.recording_warning is None
        mock_start_recording.assert_awaited_once_with("my-vm", target="local")

    async def test_vm_start_recording_failure_non_fatal(
        self, mock_resolve: MagicMock, mock_vbox: MagicMock, mocker: MockerFixture
    ) -> None:
        """vm_start succeeds even when _start_recording returns an error result.

        Tests: Non-fatal recording failure on vm_start.
        How: Mock _start_recording to return RecordingStartResult(status='error').
             Assert vm_start still returns status='ok' with recording_started=False
             and a recording_warning message.
        Why: The VM must be usable even if the always-on recording fails to start.
             An unrecordable session is better than a failed boot.
        """
        # Arrange
        from mcp_vm_blackbox.models import RecordingStartResult

        mocker.patch(
            "mcp_vm_blackbox.vm_lifecycle._start_recording",
            new_callable=AsyncMock,
            return_value=RecordingStartResult(
                status="error", vm_name="my-vm", error="VBoxManage: error: VM not running"
            ),
        )

        from mcp_vm_blackbox.vm_lifecycle import vm_start

        # Act
        result = await vm_start(vm_name="my-vm", target="local")

        # Assert
        assert result.status == "ok"
        assert result.recording_started is False
        assert result.recording_warning is not None
        assert len(result.recording_warning) > 0

    async def test_vm_start_recording_exception_non_fatal(
        self, mock_resolve: MagicMock, mock_vbox: MagicMock, mocker: MockerFixture
    ) -> None:
        """vm_start succeeds when _start_recording raises an unexpected exception.

        Tests: Exception-safe recording start in vm_start.
        How: Mock _start_recording to raise RuntimeError. Assert vm_start still
             returns status='ok' and recording_warning is set.
        Why: Unexpected exceptions from the recording subsystem must not bubble up
             and prevent the VM from being used.
        """
        # Arrange
        mocker.patch(
            "mcp_vm_blackbox.vm_lifecycle._start_recording",
            new_callable=AsyncMock,
            side_effect=RuntimeError("VBoxManage not found on PATH"),
        )

        from mcp_vm_blackbox.vm_lifecycle import vm_start

        # Act
        result = await vm_start(vm_name="my-vm", target="local")

        # Assert
        assert result.status == "ok"
        assert result.recording_started is False
        assert result.recording_warning is not None


# ---------------------------------------------------------------------------
# Recording wiring — vm_stop auto-stops recording
# ---------------------------------------------------------------------------


class TestVmStopRecordingWiring:
    """Tests that vm_stop auto-stops recording before shutdown and handles failures non-fatally.

    Strategy: Mock _stop_recording from vm_recording so tests run offline.
    """

    async def test_vm_stop_stops_recording(
        self, mock_resolve: MagicMock, mock_vbox: MagicMock, mocker: MockerFixture
    ) -> None:
        """vm_stop calls _stop_recording before stopping the VM and reflects result.

        Tests: Auto-recording stop wiring in vm_stop.
        How: Mock _stop_recording to return a successful RecordingStopResult; call
             vm_stop and assert recording_stopped=True and recording_path is set.
        Why: The recording must be finalized before the VM powers off so the WebM
             file is not truncated mid-write.
        """
        # Arrange
        from mcp_vm_blackbox.models import RecordingStopResult

        mock_stop_recording = mocker.patch(
            "mcp_vm_blackbox.vm_lifecycle._stop_recording",
            new_callable=AsyncMock,
            return_value=RecordingStopResult(
                status="ok",
                vm_name="my-vm",
                recording_path=".mcp-vm-blackbox/scratch/recordings/my-vm-20260101-000000-screen0.webm",
                duration_s=120.5,
                file_size_bytes=1_048_576,
            ),
        )

        from mcp_vm_blackbox.vm_lifecycle import vm_stop

        # Act
        result = await vm_stop(vm_name="my-vm", target="local")

        # Assert
        assert result.status == "ok"
        assert result.recording_stopped is True
        assert result.recording_path is not None
        assert "screen0.webm" in result.recording_path
        mock_stop_recording.assert_awaited_once_with("my-vm", target="local")

    async def test_vm_stop_recording_failure_non_fatal(
        self, mock_resolve: MagicMock, mock_vbox: MagicMock, mocker: MockerFixture
    ) -> None:
        """vm_stop proceeds with shutdown even when _stop_recording returns an error result.

        Tests: Non-fatal recording failure on vm_stop.
        How: Mock _stop_recording to return RecordingStopResult(status='error').
             Assert vm_stop still returns status='ok' with recording_stopped=False.
        Why: A recording that fails to stop cleanly must not prevent VM shutdown.
             VirtualBox handles truncated WebM files gracefully.
        """
        # Arrange
        from mcp_vm_blackbox.models import RecordingStopResult

        mocker.patch(
            "mcp_vm_blackbox.vm_lifecycle._stop_recording",
            new_callable=AsyncMock,
            return_value=RecordingStopResult(
                status="error", vm_name="my-vm", error="VBoxManage: error: recording not active"
            ),
        )

        from mcp_vm_blackbox.vm_lifecycle import vm_stop

        # Act
        result = await vm_stop(vm_name="my-vm", target="local")

        # Assert
        assert result.status == "ok"
        assert result.recording_stopped is False

    async def test_vm_stop_recording_exception_non_fatal(
        self, mock_resolve: MagicMock, mock_vbox: MagicMock, mocker: MockerFixture
    ) -> None:
        """vm_stop proceeds with shutdown when _stop_recording raises an unexpected exception.

        Tests: Exception-safe recording stop in vm_stop.
        How: Mock _stop_recording to raise RuntimeError. Assert vm_stop still
             returns status='ok'.
        Why: Unexpected exceptions from the recording subsystem during shutdown
             must not leave the VM running.
        """
        # Arrange
        mocker.patch(
            "mcp_vm_blackbox.vm_lifecycle._stop_recording",
            new_callable=AsyncMock,
            side_effect=RuntimeError("asyncssh connection refused"),
        )

        from mcp_vm_blackbox.vm_lifecycle import vm_stop

        # Act
        result = await vm_stop(vm_name="my-vm", target="local")

        # Assert
        assert result.status == "ok"
        assert result.recording_stopped is False
