"""Tests for VM readiness tool (vm_wait_ready).

Tests: GuestProperty polling, readiness detection, timeout handling.
How: Mock _ssh_run and _resolve so tests run offline.
Why: vm_wait_ready polls VM state; tests must not require running VMs.
"""

from __future__ import annotations

import math
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.vm_readiness._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.vm_readiness._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.vm_readiness._ssh_run", new_callable=AsyncMock, return_value=result)


@pytest.fixture
def mock_sleep(mocker: MockerFixture) -> AsyncMock:
    """Patch asyncio.sleep to speed up tests."""
    return mocker.patch("asyncio.sleep", new_callable=AsyncMock)


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


# ---------------------------------------------------------------------------
# vm_wait_ready
# ---------------------------------------------------------------------------


class TestVmWaitReady:
    """Tests for vm_wait_ready tool."""

    async def test_ready_immediately(
        self, mock_resolve_remote: MagicMock, mock_ssh_run: AsyncMock, mock_sleep: AsyncMock
    ) -> None:
        """vm_wait_ready returns ok when properties present on first poll.

        Tests: Happy path - VM already ready.
        How: Mock guestproperty enumerate to show ServiceCurrent.
        Why: Fast startup detection for already-booted VMs.
        """
        guest_prop_output = """\
Name: /VirtualBox/GuestAdd/Vbgl/SysNt/ServiceCurrent, value: 3, timestamp: ...
Name: /VirtualBox/GuestInfo/OS/Release, value: 6.1.0, timestamp: ...
"""
        mock_ssh_run.return_value = _make_ssh_result(stdout=guest_prop_output, returncode=0)

        from mcp_vm_blackbox.vm_readiness import vm_wait_ready

        result = await vm_wait_ready(vm_name="test-vm", target="ci")

        assert result.status == "ok"
        assert result.vm_name == "test-vm"
        assert result.elapsed_s >= 0
        assert result.strategy == "guest_additions_service"
        assert "/VirtualBox/GuestAdd/Vbgl/SysNt/ServiceCurrent" in result.properties
        assert result.error is None

    async def test_ready_after_polls(
        self, mock_resolve_remote: MagicMock, mock_ssh_run: AsyncMock, mock_sleep: AsyncMock
    ) -> None:
        """vm_wait_ready polls until properties appear.

        Tests: Polling loop with eventual success.
        How: Mock first enumerate empty, second has IP property.
        Why: Real VMs take time to boot and acquire IP.
        """
        # First call: no properties (VM still booting)
        empty_output = ""
        # Second call: IP acquired
        ip_output = "Name: /VirtualBox/GuestInfo/Net/0/V4/IP, value: 10.0.2.15, timestamp: ..."

        # Need to also mock showvminfo for the "no properties" case
        vminfo_output = 'VMState="running"\n'

        side_effects = [
            _make_ssh_result(stdout=empty_output, returncode=0),  # first enumerate
            _make_ssh_result(stdout=vminfo_output, returncode=0),  # showvminfo
            _make_ssh_result(stdout=ip_output, returncode=0),  # second enumerate
        ]
        mock_ssh_run.side_effect = side_effects

        from mcp_vm_blackbox.vm_readiness import vm_wait_ready

        result = await vm_wait_ready(vm_name="test-vm", target="ci")

        assert result.status == "ok"
        assert result.strategy == "guest_info_ip"
        assert "/VirtualBox/GuestInfo/Net/0/V4/IP" in result.properties
        assert result.properties["/VirtualBox/GuestInfo/Net/0/V4/IP"] == "10.0.2.15"
        # Sleep should have been called once (between polls)
        assert mock_sleep.call_count == 1

    async def test_timeout(
        self, mock_resolve_remote: MagicMock, mock_ssh_run: AsyncMock, mock_sleep: AsyncMock
    ) -> None:
        """vm_wait_ready returns error on timeout.

        Tests: Timeout handling.
        How: Mock enumerate to always return empty, use short timeout.
        Why: Must fail cleanly when VM never becomes ready.
        """
        vminfo_output = 'VMState="running"\n'

        # Simulate time passing - each sleep advances elapsed time
        # Use mock_sleep to control the poll loop
        call_count = 0

        def mock_sleep_side_effect(_: float) -> None:
            nonlocal call_count
            call_count += 1

        mock_sleep.side_effect = mock_sleep_side_effect

        # Always return empty properties + running state
        def ssh_side_effect(*args: object, **kwargs: object) -> MagicMock:
            # Alternate between enumerate and showvminfo
            if call_count % 2 == 0:
                return _make_ssh_result(stdout="", returncode=0)  # enumerate
            return _make_ssh_result(stdout=vminfo_output, returncode=0)  # showvminfo

        mock_ssh_run.side_effect = ssh_side_effect

        from mcp_vm_blackbox.vm_readiness import vm_wait_ready

        # Use a very short timeout
        result = await vm_wait_ready(vm_name="test-vm", timeout_s=1, target="ci")

        assert result.status == "error"
        assert result.vm_name == "test-vm"
        assert "Timeout" in (result.error or "")
        assert result.elapsed_s >= 1.0

    async def test_vm_not_running(
        self, mock_resolve_remote: MagicMock, mock_ssh_run: AsyncMock, mock_sleep: AsyncMock
    ) -> None:
        """vm_wait_ready returns error when VM is not running.

        Tests: VM state detection.
        How: Mock showvminfo to show powered-off state.
        Why: Cannot wait for a stopped VM.
        """
        vminfo_output = 'VMState="poweroff"\n'

        side_effects = [
            _make_ssh_result(stdout="", returncode=0),  # enumerate empty
            _make_ssh_result(stdout=vminfo_output, returncode=0),  # showvminfo
        ]
        mock_ssh_run.side_effect = side_effects

        from mcp_vm_blackbox.vm_readiness import vm_wait_ready

        result = await vm_wait_ready(vm_name="test-vm", target="ci")

        assert result.status == "error"
        assert "not running" in (result.error or "")
        assert "poweroff" in (result.error or "")

    async def test_vm_not_found(
        self, mock_resolve_remote: MagicMock, mock_ssh_run: AsyncMock, mock_sleep: AsyncMock
    ) -> None:
        """vm_wait_ready returns error when VM does not exist.

        Tests: VM existence check.
        How: Mock showvminfo to fail.
        Why: Clear error for missing VM.
        """
        side_effects = [
            _make_ssh_result(stdout="", returncode=0),  # enumerate empty
            _make_ssh_result(
                stdout="", stderr="VBoxManage: error: Could not find a registered machine", returncode=1
            ),  # showvminfo fails
        ]
        mock_ssh_run.side_effect = side_effects

        from mcp_vm_blackbox.vm_readiness import vm_wait_ready

        result = await vm_wait_ready(vm_name="nonexistent-vm", target="ci")

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

    async def test_property_parsing(
        self, mock_resolve_remote: MagicMock, mock_ssh_run: AsyncMock, mock_sleep: AsyncMock
    ) -> None:
        """vm_wait_ready correctly parses VBoxManage guestproperty output.

        Tests: Output parsing for various property formats.
        How: Mock enumerate with multiple properties.
        Why: Must handle VBoxManage output format correctly.
        """
        guest_prop_output = """\
Name: /VirtualBox/GuestAdd/HostVersion, value: 7.0.20, timestamp: 1234567890123456, flags: TRANSIENT, RDONLYGUEST
Name: /VirtualBox/GuestInfo/Net/0/V4/IP, value: 192.168.1.100, timestamp: 1234567890123457
Name: /VirtualBox/GuestInfo/OS/Product, value: Linux, timestamp: 1234567890123458
"""
        mock_ssh_run.return_value = _make_ssh_result(stdout=guest_prop_output, returncode=0)

        from mcp_vm_blackbox.vm_readiness import vm_wait_ready

        result = await vm_wait_ready(vm_name="test-vm", target="ci")

        assert result.status == "ok"
        assert result.strategy == "guest_info_ip"
        assert result.properties["/VirtualBox/GuestAdd/HostVersion"] == "7.0.20"
        assert result.properties["/VirtualBox/GuestInfo/Net/0/V4/IP"] == "192.168.1.100"
        assert result.properties["/VirtualBox/GuestInfo/OS/Product"] == "Linux"

    async def test_custom_timeout(
        self, mock_resolve_remote: MagicMock, mock_ssh_run: AsyncMock, mock_sleep: AsyncMock
    ) -> None:
        """vm_wait_ready respects custom timeout parameter.

        Tests: Timeout configuration.
        How: Use non-default timeout, verify it's applied.
        Why: Different VMs may need different boot times.
        """
        mock_ssh_run.return_value = _make_ssh_result(
            stdout="Name: /VirtualBox/GuestAdd/Vbgl/SysNt/ServiceCurrent, value: 1, timestamp: ...", returncode=0
        )

        from mcp_vm_blackbox.vm_readiness import vm_wait_ready

        result = await vm_wait_ready(vm_name="test-vm", timeout_s=300, target="ci")

        assert result.status == "ok"
        # Should succeed immediately, not wait 300s


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


class TestEnumerateGuestProperties:
    """Tests for _enumerate_guest_properties helper."""

    async def test_parse_single_property(self, mock_resolve_remote: MagicMock, mock_ssh_run: AsyncMock) -> None:
        """_enumerate_guest_properties parses single property correctly."""
        mock_ssh_run.return_value = _make_ssh_result(
            stdout="Name: /VirtualBox/GuestInfo/Net/0/V4/IP, value: 10.0.2.15, timestamp: ...", returncode=0
        )

        from mcp_vm_blackbox.vm_readiness import _enumerate_guest_properties

        result = await _enumerate_guest_properties(mock_resolve_remote.return_value, "test-vm")

        assert result == {"/VirtualBox/GuestInfo/Net/0/V4/IP": "10.0.2.15"}

    async def test_parse_empty_value(self, mock_resolve_remote: MagicMock, mock_ssh_run: AsyncMock) -> None:
        """_enumerate_guest_properties handles empty property values."""
        mock_ssh_run.return_value = _make_ssh_result(
            stdout="Name: /VirtualBox/GuestInfo/SomeProp, value: , timestamp: ...", returncode=0
        )

        from mcp_vm_blackbox.vm_readiness import _enumerate_guest_properties

        result = await _enumerate_guest_properties(mock_resolve_remote.return_value, "test-vm")

        assert result == {"/VirtualBox/GuestInfo/SomeProp": ""}

    async def test_parse_vboxmanage_error(self, mock_resolve_remote: MagicMock, mock_ssh_run: AsyncMock) -> None:
        """_enumerate_guest_properties returns empty dict on VBoxManage error."""
        mock_ssh_run.return_value = _make_ssh_result(
            stdout="", stderr="VBoxManage: error: Machine is not running", returncode=1
        )

        from mcp_vm_blackbox.vm_readiness import _enumerate_guest_properties

        result = await _enumerate_guest_properties(mock_resolve_remote.return_value, "test-vm")

        assert result == {}


class TestCheckReadiness:
    """Tests for _check_readiness helper."""

    def test_service_current_ready(self) -> None:
        """_check_readiness detects ServiceCurrent as ready indicator."""
        from mcp_vm_blackbox.vm_readiness import _check_readiness

        properties = {"/VirtualBox/GuestAdd/Vbgl/SysNt/ServiceCurrent": "3"}
        is_ready, strategy, os_product = _check_readiness(properties)

        assert is_ready is True
        assert strategy == "guest_additions_service"
        assert os_product is None

    def test_ip_address_ready(self) -> None:
        """_check_readiness detects IP address as ready indicator."""
        from mcp_vm_blackbox.vm_readiness import _check_readiness

        properties = {"/VirtualBox/GuestInfo/Net/0/V4/IP": "10.0.2.15"}
        is_ready, strategy, os_product = _check_readiness(properties)

        assert is_ready is True
        assert strategy == "guest_info_ip"
        assert os_product is None

    def test_no_ready_properties(self) -> None:
        """_check_readiness returns not ready when indicators absent."""
        from mcp_vm_blackbox.vm_readiness import _check_readiness

        properties = {"/VirtualBox/GuestInfo/OS/Product": "Linux"}
        is_ready, strategy, os_product = _check_readiness(properties)

        assert is_ready is False
        assert strategy == ""
        assert os_product == "Linux"

    def test_empty_properties(self) -> None:
        """_check_readiness handles empty property dict."""
        from mcp_vm_blackbox.vm_readiness import _check_readiness

        is_ready, strategy, os_product = _check_readiness({})

        assert is_ready is False
        assert strategy == ""
        assert os_product is None

    def test_service_current_priority_over_ip(self) -> None:
        """_check_readiness prefers ServiceCurrent over IP when both present."""
        from mcp_vm_blackbox.vm_readiness import _check_readiness

        properties = {
            "/VirtualBox/GuestAdd/Vbgl/SysNt/ServiceCurrent": "3",
            "/VirtualBox/GuestInfo/Net/0/V4/IP": "10.0.2.15",
        }
        is_ready, strategy, _os_product = _check_readiness(properties)

        assert is_ready is True
        assert strategy == "guest_additions_service"  # ServiceCurrent checked first

    def test_os_product_extracted(self) -> None:
        """_check_readiness extracts OS product for Windows."""
        from mcp_vm_blackbox.vm_readiness import _check_readiness

        properties = {
            "/VirtualBox/GuestAdd/Vbgl/SysNt/ServiceCurrent": "3",
            "/VirtualBox/GuestInfo/OS/Product": "Windows",
        }
        is_ready, _strategy, os_product = _check_readiness(properties)

        assert is_ready is True
        assert os_product == "Windows"

    def test_os_product_linux(self) -> None:
        """_check_readiness extracts OS product for Linux."""
        from mcp_vm_blackbox.vm_readiness import _check_readiness

        properties = {"/VirtualBox/GuestInfo/Net/0/V4/IP": "10.0.2.15", "/VirtualBox/GuestInfo/OS/Product": "Linux"}
        is_ready, _strategy, os_product = _check_readiness(properties)

        assert is_ready is True
        assert os_product == "Linux"


# ---------------------------------------------------------------------------
# Blocking reason tests
# ---------------------------------------------------------------------------


class TestBlockingReason:
    """Tests for blocking_reason field in VmReadyResult."""

    async def test_timeout_blocking_reason(
        self, mock_resolve_remote: MagicMock, mock_ssh_run: AsyncMock, mock_sleep: AsyncMock
    ) -> None:
        """vm_wait_ready sets blocking_reason='timeout' on timeout."""

        def ssh_side_effect(*args: object, **kwargs: object) -> MagicMock:
            return _make_ssh_result(stdout="", returncode=0)

        mock_ssh_run.side_effect = ssh_side_effect

        # Track elapsed time via sleep calls
        sleep_times: list[float] = []

        def sleep_side_effect(delay: float) -> None:
            sleep_times.append(delay)

        mock_sleep.side_effect = sleep_side_effect

        from mcp_vm_blackbox.vm_readiness import vm_wait_ready

        result = await vm_wait_ready(vm_name="test-vm", timeout_s=1, target="ci")

        assert result.status == "error"
        assert result.blocking_reason == "timeout"
        assert "Timeout" in (result.error or "")

    async def test_vm_not_running_blocking_reason(
        self, mock_resolve_remote: MagicMock, mock_ssh_run: AsyncMock, mock_sleep: AsyncMock
    ) -> None:
        """vm_wait_ready sets blocking_reason='vm_not_running' when VM stopped."""
        vminfo_output = 'VMState="poweroff"\n'

        side_effects = [
            _make_ssh_result(stdout="", returncode=0),  # enumerate empty
            _make_ssh_result(stdout=vminfo_output, returncode=0),  # showvminfo
        ]
        mock_ssh_run.side_effect = side_effects

        from mcp_vm_blackbox.vm_readiness import vm_wait_ready

        result = await vm_wait_ready(vm_name="test-vm", target="ci")

        assert result.status == "error"
        assert result.blocking_reason == "vm_not_running"

    async def test_vm_not_found_blocking_reason(
        self, mock_resolve_remote: MagicMock, mock_ssh_run: AsyncMock, mock_sleep: AsyncMock
    ) -> None:
        """vm_wait_ready sets blocking_reason='vm_not_found' when VM missing."""
        side_effects = [
            _make_ssh_result(stdout="", returncode=0),  # enumerate empty
            _make_ssh_result(
                stdout="", stderr="VBoxManage: error: Could not find a registered machine", returncode=1
            ),  # showvminfo fails
        ]
        mock_ssh_run.side_effect = side_effects

        from mcp_vm_blackbox.vm_readiness import vm_wait_ready

        result = await vm_wait_ready(vm_name="nonexistent-vm", target="ci")

        assert result.status == "error"
        assert result.blocking_reason == "vm_not_found"


# ---------------------------------------------------------------------------
# Desktop settle tests
# ---------------------------------------------------------------------------


class TestDesktopSettle:
    """Tests for desktop_settle_s parameter."""

    async def test_windows_auto_settle(
        self, mock_resolve_remote: MagicMock, mock_ssh_run: AsyncMock, mock_sleep: AsyncMock
    ) -> None:
        """vm_wait_ready applies default settle time for Windows VMs."""
        # Include OS product to indicate Windows
        guest_prop_output = """\
Name: /VirtualBox/GuestAdd/Vbgl/SysNt/ServiceCurrent, value: 3, timestamp: ...
Name: /VirtualBox/GuestInfo/OS/Product, value: Windows, timestamp: ...
"""
        mock_ssh_run.return_value = _make_ssh_result(stdout=guest_prop_output, returncode=0)

        from mcp_vm_blackbox.vm_readiness import vm_wait_ready

        result = await vm_wait_ready(vm_name="test-vm", target="ci")

        assert result.status == "ok"
        assert result.os_product == "Windows"
        # Should have applied default Windows settle time
        assert math.isclose(result.desktop_settle_s, 10.0)
        # Sleep should have been called for the settle period
        assert mock_sleep.call_count >= 1

    async def test_linux_no_auto_settle(
        self, mock_resolve_remote: MagicMock, mock_ssh_run: AsyncMock, mock_sleep: AsyncMock
    ) -> None:
        """vm_wait_ready does not auto-settle for Linux VMs."""
        guest_prop_output = """\
Name: /VirtualBox/GuestAdd/Vbgl/SysNt/ServiceCurrent, value: 3, timestamp: ...
Name: /VirtualBox/GuestInfo/OS/Product, value: Linux, timestamp: ...
"""
        mock_ssh_run.return_value = _make_ssh_result(stdout=guest_prop_output, returncode=0)

        from mcp_vm_blackbox.vm_readiness import vm_wait_ready

        result = await vm_wait_ready(vm_name="test-vm", target="ci")

        assert result.status == "ok"
        assert result.os_product == "Linux"
        assert math.isclose(result.desktop_settle_s, 0.0)

    async def test_explicit_settle_zero(
        self, mock_resolve_remote: MagicMock, mock_ssh_run: AsyncMock, mock_sleep: AsyncMock
    ) -> None:
        """vm_wait_ready respects desktop_settle_s=0 to skip settling."""
        guest_prop_output = """\
Name: /VirtualBox/GuestAdd/Vbgl/SysNt/ServiceCurrent, value: 3, timestamp: ...
Name: /VirtualBox/GuestInfo/OS/Product, value: Windows, timestamp: ...
"""
        mock_ssh_run.return_value = _make_ssh_result(stdout=guest_prop_output, returncode=0)

        from mcp_vm_blackbox.vm_readiness import vm_wait_ready

        result = await vm_wait_ready(vm_name="test-vm", target="ci", desktop_settle_s=0.0)

        assert result.status == "ok"
        assert result.os_product == "Windows"
        assert math.isclose(result.desktop_settle_s, 0.0)

    async def test_explicit_settle_custom(
        self, mock_resolve_remote: MagicMock, mock_ssh_run: AsyncMock, mock_sleep: AsyncMock
    ) -> None:
        """vm_wait_ready respects custom desktop_settle_s value."""
        guest_prop_output = """\
Name: /VirtualBox/GuestAdd/Vbgl/SysNt/ServiceCurrent, value: 3, timestamp: ...
Name: /VirtualBox/GuestInfo/OS/Product, value: Windows, timestamp: ...
"""
        mock_ssh_run.return_value = _make_ssh_result(stdout=guest_prop_output, returncode=0)

        from mcp_vm_blackbox.vm_readiness import vm_wait_ready

        result = await vm_wait_ready(vm_name="test-vm", target="ci", desktop_settle_s=30.0)

        assert result.status == "ok"
        assert result.os_product == "Windows"
        assert math.isclose(result.desktop_settle_s, 30.0)
        # Sleep should have been called with the custom settle time
        settle_calls = [call for call in mock_sleep.call_args_list if math.isclose(call[0][0], 30.0)]
        assert len(settle_calls) == 1

    async def test_no_os_product_no_auto_settle(
        self, mock_resolve_remote: MagicMock, mock_ssh_run: AsyncMock, mock_sleep: AsyncMock
    ) -> None:
        """vm_wait_ready does not auto-settle when OS product unknown."""
        guest_prop_output = """\
Name: /VirtualBox/GuestAdd/Vbgl/SysNt/ServiceCurrent, value: 3, timestamp: ...
"""
        mock_ssh_run.return_value = _make_ssh_result(stdout=guest_prop_output, returncode=0)

        from mcp_vm_blackbox.vm_readiness import vm_wait_ready

        result = await vm_wait_ready(vm_name="test-vm", target="ci")

        assert result.status == "ok"
        assert result.os_product is None
        assert math.isclose(result.desktop_settle_s, 0.0)
