"""Unit tests for snapshot_create tool and related LibvirtClient snapshot methods.

All libvirt calls are mocked — no real hypervisor required.
"""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock, call, patch

import pytest


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


@pytest.fixture
def mock_topology():
    """A mock TopologySpec for snapshot testing."""
    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", extra_disks=[]),
            NodeSpec(name="node2", role="worker", vcpus=2, memory_mb=2048, ip="192.168.100.11", extra_disks=[]),
        ],
        ssh=SshSpec(key_path="/etc/vmcluster/ssh/id_ed25519"),
    )


# ---------------------------------------------------------------------------
# LibvirtClient snapshot method tests
# ---------------------------------------------------------------------------


@pytest.fixture
def mock_libvirt():
    """Patch libvirt module inside libvirt_client."""
    with patch("vmcluster_mcp.cluster.libvirt_client.libvirt") as mock_lv:
        mock_conn = MagicMock()
        mock_lv.open.return_value = mock_conn
        mock_lv.VIR_DOMAIN_SNAPSHOT_CREATE_DISK_ONLY = 16
        yield mock_lv, mock_conn


@pytest.mark.asyncio
async def test_snapshot_create_xml_calls_dom_method(mock_libvirt):
    """snapshot_create_xml dispatches to dom.snapshotCreateXML via _run executor."""
    from vmcluster_mcp.cluster.libvirt_client import LibvirtClient

    mock_lv, mock_conn = mock_libvirt
    mock_dom = MagicMock()
    mock_snap = MagicMock()
    mock_dom.snapshotCreateXML.return_value = mock_snap

    client = LibvirtClient("qemu:///system")
    xml = "<domainsnapshot><name>snap1</name></domainsnapshot>"
    flags = mock_lv.VIR_DOMAIN_SNAPSHOT_CREATE_DISK_ONLY
    result = await client.snapshot_create_xml(mock_dom, xml, flags)

    mock_dom.snapshotCreateXML.assert_called_once_with(xml, flags)
    assert result is mock_snap


@pytest.mark.asyncio
async def test_list_all_snapshots_calls_dom_method(mock_libvirt):
    """list_all_snapshots dispatches to dom.listAllSnapshots via _run executor."""
    from vmcluster_mcp.cluster.libvirt_client import LibvirtClient

    mock_lv, mock_conn = mock_libvirt
    mock_dom = MagicMock()
    mock_snaps = [MagicMock(), MagicMock()]
    mock_dom.listAllSnapshots.return_value = mock_snaps

    client = LibvirtClient("qemu:///system")
    result = await client.list_all_snapshots(mock_dom)

    mock_dom.listAllSnapshots.assert_called_once_with(0)
    assert result is mock_snaps


@pytest.mark.asyncio
async def test_snapshot_lookup_by_name_found(mock_libvirt):
    """snapshot_lookup_by_name returns the snapshot when it exists."""
    from vmcluster_mcp.cluster.libvirt_client import LibvirtClient

    mock_lv, mock_conn = mock_libvirt
    mock_dom = MagicMock()
    mock_snap = MagicMock()
    mock_dom.snapshotLookupByName.return_value = mock_snap

    client = LibvirtClient("qemu:///system")
    result = await client.snapshot_lookup_by_name(mock_dom, "snap1")

    mock_dom.snapshotLookupByName.assert_called_once_with("snap1", 0)
    assert result is mock_snap


@pytest.mark.asyncio
async def test_snapshot_lookup_by_name_not_found_returns_none(mock_libvirt):
    """snapshot_lookup_by_name returns None when snapshot does not exist."""
    from vmcluster_mcp.cluster.libvirt_client import LibvirtClient

    mock_lv, mock_conn = mock_libvirt
    mock_lv.libvirtError = Exception
    mock_dom = MagicMock()
    mock_dom.snapshotLookupByName.side_effect = Exception("snapshot not found")

    client = LibvirtClient("qemu:///system")
    result = await client.snapshot_lookup_by_name(mock_dom, "nonexistent")

    assert result is None


@pytest.mark.asyncio
async def test_snapshot_get_xml_desc(mock_libvirt):
    """snapshot_get_xml_desc dispatches to snap.getXMLDesc via _run executor."""
    from vmcluster_mcp.cluster.libvirt_client import LibvirtClient

    mock_lv, mock_conn = mock_libvirt
    mock_snap = MagicMock()
    mock_snap.getXMLDesc.return_value = "<domainsnapshot/>"

    client = LibvirtClient("qemu:///system")
    result = await client.snapshot_get_xml_desc(mock_snap)

    mock_snap.getXMLDesc.assert_called_once_with(0)
    assert result == "<domainsnapshot/>"


@pytest.mark.asyncio
async def test_get_domain_xml(mock_libvirt):
    """get_domain_xml dispatches to dom.XMLDesc via _run executor."""
    from vmcluster_mcp.cluster.libvirt_client import LibvirtClient

    mock_lv, mock_conn = mock_libvirt
    mock_dom = MagicMock()
    mock_dom.XMLDesc.return_value = "<domain/>"

    client = LibvirtClient("qemu:///system")
    result = await client.get_domain_xml(mock_dom)

    mock_dom.XMLDesc.assert_called_once_with(0)
    assert result == "<domain/>"


# ---------------------------------------------------------------------------
# SnapshotManager.create_snapshot tests
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_snapshot_create_returns_snapshot_info(mock_topology):
    """create_snapshot returns SnapshotInfo with correct fields and per-node data."""
    from vmcluster_mcp.snapshot.manager import SnapshotManager

    mock_client = MagicMock()
    mock_dom = MagicMock()
    mock_client.lookup_domain = AsyncMock(return_value=mock_dom)
    mock_client.snapshot_lookup_by_name = AsyncMock(return_value=None)
    mock_client.snapshot_create_xml = AsyncMock(return_value=MagicMock())

    with (
        patch("vmcluster_mcp.snapshot.manager.os.makedirs"),
        patch("vmcluster_mcp.snapshot.manager.os.path.getsize", return_value=1024),
    ):
        manager = SnapshotManager(mock_client)
        result = await manager.create_snapshot(
            cluster_name="testcluster",
            snapshot_name="snap1",
            nodes=mock_topology.nodes,
            overlay_dir="/overlays",
        )

    assert result.success is True
    assert result.result is not None
    assert result.result.snapshot_name == "snap1"
    assert result.result.cluster_name == "testcluster"
    assert "node1" in result.result.per_node
    assert "node2" in result.result.per_node
    assert result.result.per_node["node1"].size_bytes == 1024
    assert result.result.per_node["node1"].disk_path == "/overlays/testcluster/snapshots/snap1/node1.qcow2"


@pytest.mark.asyncio
async def test_snapshot_create_creates_directory(mock_topology):
    """create_snapshot creates the snapshot directory before creating snapshots."""
    from vmcluster_mcp.snapshot.manager import SnapshotManager

    mock_client = MagicMock()
    mock_dom = MagicMock()
    mock_client.lookup_domain = AsyncMock(return_value=mock_dom)
    mock_client.snapshot_lookup_by_name = AsyncMock(return_value=None)
    mock_client.snapshot_create_xml = AsyncMock(return_value=MagicMock())

    with (
        patch("vmcluster_mcp.snapshot.manager.os.makedirs") as mock_makedirs,
        patch("vmcluster_mcp.snapshot.manager.os.path.getsize", return_value=512),
    ):
        manager = SnapshotManager(mock_client)
        await manager.create_snapshot(
            cluster_name="testcluster",
            snapshot_name="snap1",
            nodes=mock_topology.nodes,
            overlay_dir="/overlays",
        )

    mock_makedirs.assert_called_once_with(
        "/overlays/testcluster/snapshots/snap1", exist_ok=True
    )


@pytest.mark.asyncio
async def test_snapshot_create_uses_libvirt_disk_only_flag(mock_topology):
    """create_snapshot calls snapshot_create_xml with VIR_DOMAIN_SNAPSHOT_CREATE_DISK_ONLY."""
    import libvirt

    from vmcluster_mcp.snapshot.manager import SnapshotManager

    mock_client = MagicMock()
    mock_dom = MagicMock()
    mock_client.lookup_domain = AsyncMock(return_value=mock_dom)
    mock_client.snapshot_lookup_by_name = AsyncMock(return_value=None)
    mock_client.snapshot_create_xml = AsyncMock(return_value=MagicMock())

    with (
        patch("vmcluster_mcp.snapshot.manager.os.makedirs"),
        patch("vmcluster_mcp.snapshot.manager.os.path.getsize", return_value=512),
    ):
        manager = SnapshotManager(mock_client)
        await manager.create_snapshot(
            cluster_name="testcluster",
            snapshot_name="snap1",
            nodes=mock_topology.nodes,
            overlay_dir="/overlays",
        )

    # All snapshot_create_xml calls should use DISK_ONLY flag
    for c in mock_client.snapshot_create_xml.call_args_list:
        _, flags = c.args[1], c.args[2]
        assert flags == libvirt.VIR_DOMAIN_SNAPSHOT_CREATE_DISK_ONLY


@pytest.mark.asyncio
async def test_snapshot_create_concurrent_for_all_nodes(mock_topology):
    """create_snapshot calls snapshot_create_xml for every node in the topology."""
    from vmcluster_mcp.snapshot.manager import SnapshotManager

    mock_client = MagicMock()
    mock_dom = MagicMock()
    mock_client.lookup_domain = AsyncMock(return_value=mock_dom)
    mock_client.snapshot_lookup_by_name = AsyncMock(return_value=None)
    mock_client.snapshot_create_xml = AsyncMock(return_value=MagicMock())

    with (
        patch("vmcluster_mcp.snapshot.manager.os.makedirs"),
        patch("vmcluster_mcp.snapshot.manager.os.path.getsize", return_value=512),
    ):
        manager = SnapshotManager(mock_client)
        result = await manager.create_snapshot(
            cluster_name="testcluster",
            snapshot_name="snap1",
            nodes=mock_topology.nodes,
            overlay_dir="/overlays",
        )

    # One snapshot_create_xml call per node
    assert mock_client.snapshot_create_xml.call_count == len(mock_topology.nodes)
    assert result.success is True


@pytest.mark.asyncio
async def test_snapshot_create_returns_failure_when_node_fails(mock_topology):
    """create_snapshot returns success=False (D-06) when any node snapshot fails."""
    from vmcluster_mcp.snapshot.manager import SnapshotManager

    mock_client = MagicMock()
    mock_dom = MagicMock()
    mock_client.lookup_domain = AsyncMock(return_value=mock_dom)
    mock_client.snapshot_lookup_by_name = AsyncMock(return_value=None)
    # First node succeeds, second fails
    mock_client.snapshot_create_xml = AsyncMock(
        side_effect=[MagicMock(), RuntimeError("libvirt error on node2")]
    )

    with (
        patch("vmcluster_mcp.snapshot.manager.os.makedirs"),
        patch("vmcluster_mcp.snapshot.manager.os.path.getsize", return_value=512),
    ):
        manager = SnapshotManager(mock_client)
        result = await manager.create_snapshot(
            cluster_name="testcluster",
            snapshot_name="snap1",
            nodes=mock_topology.nodes,
            overlay_dir="/overlays",
        )

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


@pytest.mark.asyncio
async def test_snapshot_create_idempotency_guard(mock_topology):
    """create_snapshot returns error if snapshot name already exists in libvirt."""
    from vmcluster_mcp.snapshot.manager import SnapshotManager

    mock_client = MagicMock()
    mock_dom = MagicMock()
    existing_snap = MagicMock()  # Non-None means snapshot already exists
    mock_client.lookup_domain = AsyncMock(return_value=mock_dom)
    mock_client.snapshot_lookup_by_name = AsyncMock(return_value=existing_snap)
    mock_client.snapshot_create_xml = AsyncMock()

    with patch("vmcluster_mcp.snapshot.manager.os.makedirs"):
        manager = SnapshotManager(mock_client)
        result = await manager.create_snapshot(
            cluster_name="testcluster",
            snapshot_name="snap1",
            nodes=mock_topology.nodes,
            overlay_dir="/overlays",
        )

    assert result.success is False
    assert result.error is not None
    assert result.error.code == "SNAPSHOT_EXISTS"
    # Should NOT have called snapshot_create_xml
    mock_client.snapshot_create_xml.assert_not_called()


@pytest.mark.asyncio
async def test_snapshot_create_routes_through_libvirt_client(mock_topology):
    """create_snapshot uses LibvirtClient methods (not direct libvirt calls)."""
    from vmcluster_mcp.snapshot.manager import SnapshotManager

    mock_client = MagicMock()
    mock_dom = MagicMock()
    mock_client.lookup_domain = AsyncMock(return_value=mock_dom)
    mock_client.snapshot_lookup_by_name = AsyncMock(return_value=None)
    mock_client.snapshot_create_xml = AsyncMock(return_value=MagicMock())

    with (
        patch("vmcluster_mcp.snapshot.manager.os.makedirs"),
        patch("vmcluster_mcp.snapshot.manager.os.path.getsize", return_value=256),
    ):
        manager = SnapshotManager(mock_client)
        await manager.create_snapshot(
            cluster_name="testcluster",
            snapshot_name="snap1",
            nodes=mock_topology.nodes,
            overlay_dir="/overlays",
        )

    # Verify all libvirt-touching calls go through the client's methods
    assert mock_client.lookup_domain.called
    assert mock_client.snapshot_create_xml.called


# ---------------------------------------------------------------------------
# snapshot_create tool integration tests
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_snapshot_create_tool_returns_snapshot_info(mock_topology):
    """The snapshot_create MCP tool returns ToolResult[SnapshotInfo]."""
    with (
        patch("vmcluster_mcp.snapshot.load_topology", return_value=mock_topology),
        patch("vmcluster_mcp.snapshot._manager") as mock_manager,
    ):
        from vmcluster_mcp.models import SnapshotInfo, SnapshotNodeInfo, ToolResult

        expected = ToolResult(
            success=True,
            result=SnapshotInfo(
                snapshot_name="snap1",
                cluster_name="testcluster",
                created_at="2026-01-01T00:00:00+00:00",
                per_node={
                    "node1": SnapshotNodeInfo(node_name="node1", disk_path="/overlays/testcluster/snapshots/snap1/node1.qcow2", size_bytes=1024),
                },
            ),
        )
        mock_manager.create_snapshot = AsyncMock(return_value=expected)

        from vmcluster_mcp.snapshot import snapshot_create

        result = await snapshot_create(cluster_name="testcluster", snapshot_name="snap1")

    assert result.success is True
    assert result.result.snapshot_name == "snap1"
    mock_manager.create_snapshot.assert_called_once_with(
        cluster_name="testcluster",
        snapshot_name="snap1",
        nodes=mock_topology.nodes,
        overlay_dir=mock_topology.overlay_dir,
    )


@pytest.mark.asyncio
async def test_snapshot_create_tool_include_memory_proceeds_disk_only(mock_topology):
    """snapshot_create with include_memory=True still creates disk-only snapshot."""
    with (
        patch("vmcluster_mcp.snapshot.load_topology", return_value=mock_topology),
        patch("vmcluster_mcp.snapshot._manager") as mock_manager,
    ):
        from vmcluster_mcp.models import SnapshotInfo, ToolResult

        mock_manager.create_snapshot = AsyncMock(
            return_value=ToolResult(
                success=True,
                result=SnapshotInfo(
                    snapshot_name="snap1",
                    cluster_name="testcluster",
                    created_at="2026-01-01T00:00:00+00:00",
                    per_node={},
                ),
            )
        )

        from vmcluster_mcp.snapshot import snapshot_create

        result = await snapshot_create(
            cluster_name="testcluster",
            snapshot_name="snap1",
            include_memory=True,
        )

    # Should still succeed (disk-only)
    assert result.success is True
    # create_snapshot should still be called
    mock_manager.create_snapshot.assert_called_once()
