"""Tests for node_crash tool — virsh destroy + SSH invalidation + optional restart."""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from vmcluster_mcp.models import NodeStatus


# ---------------------------------------------------------------------------
# Fixtures / helpers
# ---------------------------------------------------------------------------


@pytest.fixture
def mock_topology():
    from vmcluster_mcp.topology.schema import NetworkSpec, NodeSpec, SshSpec, TopologySpec

    return TopologySpec(
        cluster_name="testcluster",
        base_image="/images/ubuntu.qcow2",
        overlay_dir="/overlays",
        network=NetworkSpec(name="testcluster-net", bridge="virbr100", subnet="192.168.100.0/24"),
        nodes=[
            NodeSpec(name="node1", role="worker", vcpus=2, memory_mb=2048, ip="192.168.100.10"),
            NodeSpec(name="node2", role="worker", vcpus=2, memory_mb=2048, ip="192.168.100.11"),
        ],
        ssh=SshSpec(key_path="/etc/vmcluster/ssh/id_ed25519"),
    )


# ---------------------------------------------------------------------------
# node_crash — unknown node
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_node_crash_unknown_node_returns_node_not_found(mock_topology):
    """node_crash on unknown node returns ToolResult(success=False, code=NODE_NOT_FOUND)."""
    from vmcluster_mcp.cluster import node_crash

    with patch("vmcluster_mcp.cluster.load_topology", return_value=mock_topology):
        result = await node_crash(cluster_name="testcluster", node="nonexistent")

    assert result.success is False
    assert result.error is not None
    assert result.error.code == "NODE_NOT_FOUND"
    assert result.error.recoverable is False


# ---------------------------------------------------------------------------
# node_crash — destroy semantics
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_node_crash_calls_destroy_domain(mock_topology):
    """node_crash calls _client.destroy_domain on the target domain."""
    from vmcluster_mcp.cluster import node_crash

    mock_dom = MagicMock()
    with (
        patch("vmcluster_mcp.cluster.load_topology", return_value=mock_topology),
        patch("vmcluster_mcp.cluster._client") as mock_client,
        patch("vmcluster_mcp.exec._ssh_manager") as mock_ssh_mgr,
        patch("vmcluster_mcp.cluster._wait_for_ssh", new=AsyncMock(return_value=(True, 0.5))),
    ):
        mock_client.lookup_domain = AsyncMock(return_value=mock_dom)
        mock_client.destroy_domain = AsyncMock()
        mock_client.start_domain_by_name = AsyncMock()
        mock_client.get_domain_state = AsyncMock(return_value="running")
        mock_ssh_mgr.invalidate = AsyncMock()

        result = await node_crash(cluster_name="testcluster", node="node1")

    mock_client.destroy_domain.assert_called_once_with(mock_dom)
    assert result.success is True


@pytest.mark.asyncio
async def test_node_crash_best_effort_if_domain_already_off(mock_topology):
    """node_crash still proceeds if destroy_domain raises (domain already shut off)."""
    import libvirt

    from vmcluster_mcp.cluster import node_crash

    mock_dom = MagicMock()
    with (
        patch("vmcluster_mcp.cluster.load_topology", return_value=mock_topology),
        patch("vmcluster_mcp.cluster._client") as mock_client,
        patch("vmcluster_mcp.exec._ssh_manager") as mock_ssh_mgr,
        patch("vmcluster_mcp.cluster._wait_for_ssh", new=AsyncMock(return_value=(True, 0.5))),
    ):
        mock_client.lookup_domain = AsyncMock(return_value=mock_dom)
        mock_client.destroy_domain = AsyncMock(side_effect=libvirt.libvirtError("already shut off"))
        mock_client.start_domain_by_name = AsyncMock()
        mock_client.get_domain_state = AsyncMock(return_value="running")
        mock_ssh_mgr.invalidate = AsyncMock()

        # Should not raise despite destroy failing
        result = await node_crash(cluster_name="testcluster", node="node1", restart_after=True)

    assert result.success is True
    # start_domain_by_name should still be called after the best-effort destroy
    mock_client.start_domain_by_name.assert_called_once()


# ---------------------------------------------------------------------------
# node_crash — SSH cache invalidation
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_node_crash_invalidates_ssh_cache_before_restart(mock_topology):
    """node_crash invalidates the SSH connection cache for the crashed node's IP."""
    from vmcluster_mcp.cluster import node_crash

    mock_dom = MagicMock()
    invalidate_order: list[str] = []
    start_order: list[str] = []

    async def track_invalidate(ip: str) -> None:
        invalidate_order.append(f"invalidate:{ip}")

    async def track_start(dname: str) -> None:
        start_order.append(f"start:{dname}")

    with (
        patch("vmcluster_mcp.cluster.load_topology", return_value=mock_topology),
        patch("vmcluster_mcp.cluster._client") as mock_client,
        patch("vmcluster_mcp.exec._ssh_manager") as mock_ssh_mgr,
        patch("vmcluster_mcp.cluster._wait_for_ssh", new=AsyncMock(return_value=(True, 0.5))),
    ):
        mock_client.lookup_domain = AsyncMock(return_value=mock_dom)
        mock_client.destroy_domain = AsyncMock()
        mock_client.start_domain_by_name = AsyncMock(side_effect=track_start)
        mock_client.get_domain_state = AsyncMock(return_value="running")
        mock_ssh_mgr.invalidate = AsyncMock(side_effect=track_invalidate)

        await node_crash(cluster_name="testcluster", node="node1", restart_after=True)

    assert len(invalidate_order) == 1
    assert invalidate_order[0] == "invalidate:192.168.100.10"


@pytest.mark.asyncio
async def test_node_crash_invalidates_ssh_cache_even_without_restart(mock_topology):
    """node_crash invalidates SSH cache regardless of restart_after setting."""
    from vmcluster_mcp.cluster import node_crash

    mock_dom = MagicMock()
    with (
        patch("vmcluster_mcp.cluster.load_topology", return_value=mock_topology),
        patch("vmcluster_mcp.cluster._client") as mock_client,
        patch("vmcluster_mcp.exec._ssh_manager") as mock_ssh_mgr,
    ):
        mock_client.lookup_domain = AsyncMock(return_value=mock_dom)
        mock_client.destroy_domain = AsyncMock()
        mock_ssh_mgr.invalidate = AsyncMock()

        result = await node_crash(cluster_name="testcluster", node="node1", restart_after=False)

    mock_ssh_mgr.invalidate.assert_called_once_with("192.168.100.10")
    assert result.success is True


# ---------------------------------------------------------------------------
# node_crash — restart_after=False behaviour
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_node_crash_restart_false_does_not_start_or_wait(mock_topology):
    """node_crash with restart_after=False does NOT call start_domain_by_name or _wait_for_ssh."""
    from vmcluster_mcp.cluster import node_crash

    mock_dom = MagicMock()
    with (
        patch("vmcluster_mcp.cluster.load_topology", return_value=mock_topology),
        patch("vmcluster_mcp.cluster._client") as mock_client,
        patch("vmcluster_mcp.exec._ssh_manager") as mock_ssh_mgr,
        patch("vmcluster_mcp.cluster._wait_for_ssh", new=AsyncMock()) as mock_wait,
    ):
        mock_client.lookup_domain = AsyncMock(return_value=mock_dom)
        mock_client.destroy_domain = AsyncMock()
        mock_client.start_domain_by_name = AsyncMock()
        mock_ssh_mgr.invalidate = AsyncMock()

        result = await node_crash(cluster_name="testcluster", node="node1", restart_after=False)

    mock_client.start_domain_by_name.assert_not_called()
    mock_wait.assert_not_called()
    assert result.success is True
    assert result.result is not None
    assert result.result.domain_state == "shut off"
    assert result.result.ssh_reachable is False


# ---------------------------------------------------------------------------
# node_crash — restart_after=True behaviour
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_node_crash_restart_true_calls_start_domain_by_name(mock_topology):
    """node_crash with restart_after=True calls _client.start_domain_by_name after destroy."""
    from vmcluster_mcp.cluster import node_crash

    mock_dom = MagicMock()
    with (
        patch("vmcluster_mcp.cluster.load_topology", return_value=mock_topology),
        patch("vmcluster_mcp.cluster._client") as mock_client,
        patch("vmcluster_mcp.exec._ssh_manager") as mock_ssh_mgr,
        patch("vmcluster_mcp.cluster._wait_for_ssh", new=AsyncMock(return_value=(True, 0.5))),
    ):
        mock_client.lookup_domain = AsyncMock(return_value=mock_dom)
        mock_client.destroy_domain = AsyncMock()
        mock_client.start_domain_by_name = AsyncMock()
        mock_client.get_domain_state = AsyncMock(return_value="running")
        mock_ssh_mgr.invalidate = AsyncMock()

        result = await node_crash(cluster_name="testcluster", node="node1", restart_after=True)

    mock_client.start_domain_by_name.assert_called_once_with("testcluster_node1")
    assert result.success is True


@pytest.mark.asyncio
async def test_node_crash_restart_true_wait_ssh_calls_wait_for_ssh(mock_topology):
    """node_crash with restart_after=True and wait_for_ssh=True calls _wait_for_ssh."""
    from vmcluster_mcp.cluster import node_crash

    mock_dom = MagicMock()
    wait_calls: list[tuple] = []

    async def track_wait(ip, port, user, key_path, timeout_s):
        wait_calls.append((ip, port, user, key_path, timeout_s))
        return True, 2.0

    with (
        patch("vmcluster_mcp.cluster.load_topology", return_value=mock_topology),
        patch("vmcluster_mcp.cluster._client") as mock_client,
        patch("vmcluster_mcp.exec._ssh_manager") as mock_ssh_mgr,
        patch("vmcluster_mcp.cluster._wait_for_ssh", side_effect=track_wait),
    ):
        mock_client.lookup_domain = AsyncMock(return_value=mock_dom)
        mock_client.destroy_domain = AsyncMock()
        mock_client.start_domain_by_name = AsyncMock()
        mock_client.get_domain_state = AsyncMock(return_value="running")
        mock_ssh_mgr.invalidate = AsyncMock()

        result = await node_crash(
            cluster_name="testcluster", node="node1",
            restart_after=True, wait_for_ssh=True, ssh_timeout_s=60,
        )

    assert len(wait_calls) == 1
    ip, port, user, key_path, timeout_s = wait_calls[0]
    assert ip == "192.168.100.10"
    assert port == 22
    assert timeout_s == 60
    assert result.success is True
    assert result.result.ssh_reachable is True


@pytest.mark.asyncio
async def test_node_crash_returns_running_nodestatus_after_restart_and_wait(mock_topology):
    """node_crash returns NodeStatus(domain_state='running', ssh_reachable=True) after restart+wait."""
    from vmcluster_mcp.cluster import node_crash

    mock_dom = MagicMock()
    with (
        patch("vmcluster_mcp.cluster.load_topology", return_value=mock_topology),
        patch("vmcluster_mcp.cluster._client") as mock_client,
        patch("vmcluster_mcp.exec._ssh_manager") as mock_ssh_mgr,
        patch("vmcluster_mcp.cluster._wait_for_ssh", new=AsyncMock(return_value=(True, 1.0))),
    ):
        mock_client.lookup_domain = AsyncMock(return_value=mock_dom)
        mock_client.destroy_domain = AsyncMock()
        mock_client.start_domain_by_name = AsyncMock()
        mock_client.get_domain_state = AsyncMock(return_value="running")
        mock_ssh_mgr.invalidate = AsyncMock()

        result = await node_crash(
            cluster_name="testcluster", node="node1",
            restart_after=True, wait_for_ssh=True,
        )

    assert result.success is True
    assert result.result is not None
    assert isinstance(result.result, NodeStatus)
    assert result.result.name == "node1"
    assert result.result.role == "worker"
    assert result.result.ip == "192.168.100.10"
    assert result.result.domain_state == "running"
    assert result.result.ssh_reachable is True


# ---------------------------------------------------------------------------
# node_crash — SSH timeout
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_node_crash_ssh_timeout_returns_failure(mock_topology):
    """node_crash SSH wait timeout returns ToolResult(success=False, code=SSH_TIMEOUT)."""
    from vmcluster_mcp.cluster import node_crash

    mock_dom = MagicMock()
    with (
        patch("vmcluster_mcp.cluster.load_topology", return_value=mock_topology),
        patch("vmcluster_mcp.cluster._client") as mock_client,
        patch("vmcluster_mcp.exec._ssh_manager") as mock_ssh_mgr,
        patch("vmcluster_mcp.cluster._wait_for_ssh", new=AsyncMock(return_value=(False, 120.0))),
    ):
        mock_client.lookup_domain = AsyncMock(return_value=mock_dom)
        mock_client.destroy_domain = AsyncMock()
        mock_client.start_domain_by_name = AsyncMock()
        mock_client.get_domain_state = AsyncMock(return_value="running")
        mock_ssh_mgr.invalidate = AsyncMock()

        result = await node_crash(
            cluster_name="testcluster", node="node1",
            restart_after=True, wait_for_ssh=True, ssh_timeout_s=120,
        )

    assert result.success is False
    assert result.error is not None
    assert result.error.code == "SSH_TIMEOUT"
    assert result.error.recoverable is True
