"""Unit tests for snapshot_list tool and SnapshotManager.list_snapshots.

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

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock, patch

import pytest


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


@pytest.fixture
def mock_topology():
    """A mock TopologySpec for snapshot_list 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"),
    )


def _make_snap_xml(name: str, creation_time: int, disk_path: str) -> str:
    """Build a minimal snapshot XML string for testing."""
    return (
        f"<domainsnapshot>"
        f"<name>{name}</name>"
        f"<creationTime>{creation_time}</creationTime>"
        f"<disks>"
        f"<disk name='vda'><source file='{disk_path}'/></disk>"
        f"</disks>"
        f"</domainsnapshot>"
    )


# ---------------------------------------------------------------------------
# SnapshotManager.list_snapshots tests
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_snapshot_list_returns_empty_when_no_snapshots(mock_topology):
    """list_snapshots returns empty list when no snapshots exist."""
    from vmcluster_mcp.snapshot.manager import SnapshotManager

    mock_client = MagicMock()
    mock_dom = MagicMock()
    mock_client.lookup_domain = AsyncMock(return_value=mock_dom)
    mock_client.list_all_snapshots = AsyncMock(return_value=[])

    manager = SnapshotManager(mock_client)
    result = await manager.list_snapshots(
        cluster_name="testcluster",
        nodes=mock_topology.nodes,
        overlay_dir="/overlays",
    )

    assert result.success is True
    assert result.result == []


@pytest.mark.asyncio
async def test_snapshot_list_returns_snapshot_info_for_single_snapshot(mock_topology):
    """list_snapshots returns SnapshotInfo with correct fields for one snapshot."""
    from vmcluster_mcp.snapshot.manager import SnapshotManager

    snap_xml_node1 = _make_snap_xml(
        "snap1", 1706745600, "/overlays/testcluster/snapshots/snap1/node1.qcow2"
    )
    snap_xml_node2 = _make_snap_xml(
        "snap1", 1706745600, "/overlays/testcluster/snapshots/snap1/node2.qcow2"
    )

    mock_client = MagicMock()
    mock_dom = MagicMock()
    mock_client.lookup_domain = AsyncMock(return_value=mock_dom)

    mock_snap1 = MagicMock()
    mock_snap2 = MagicMock()

    # node1 gets snap_xml_node1, node2 gets snap_xml_node2
    mock_client.list_all_snapshots = AsyncMock(side_effect=[[mock_snap1], [mock_snap2]])
    mock_client.snapshot_get_xml_desc = AsyncMock(
        side_effect=[snap_xml_node1, snap_xml_node2]
    )

    with patch("vmcluster_mcp.snapshot.manager.os.path.getsize", return_value=2048):
        manager = SnapshotManager(mock_client)
        result = await manager.list_snapshots(
            cluster_name="testcluster",
            nodes=mock_topology.nodes,
            overlay_dir="/overlays",
        )

    assert result.success is True
    assert len(result.result) == 1
    snap = result.result[0]
    assert snap.snapshot_name == "snap1"
    assert snap.cluster_name == "testcluster"
    assert "node1" in snap.per_node
    assert "node2" in snap.per_node
    assert snap.per_node["node1"].size_bytes == 2048
    assert snap.per_node["node1"].disk_path == "/overlays/testcluster/snapshots/snap1/node1.qcow2"


@pytest.mark.asyncio
async def test_snapshot_list_parses_creation_time_from_xml(mock_topology):
    """list_snapshots converts creationTime epoch to ISO timestamp string."""
    from vmcluster_mcp.snapshot.manager import SnapshotManager

    epoch = 1706745600  # 2024-02-01 00:00:00 UTC
    snap_xml = _make_snap_xml("snap1", epoch, "/overlays/testcluster/snapshots/snap1/node1.qcow2")

    mock_client = MagicMock()
    mock_dom = MagicMock()
    mock_snap = MagicMock()
    mock_client.lookup_domain = AsyncMock(return_value=mock_dom)
    mock_client.list_all_snapshots = AsyncMock(side_effect=[[mock_snap], []])
    mock_client.snapshot_get_xml_desc = AsyncMock(return_value=snap_xml)

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

    assert result.success is True
    snap = result.result[0]
    # Should be an ISO format string containing the date from epoch 1706745600
    assert "2024-02-01" in snap.created_at


@pytest.mark.asyncio
async def test_snapshot_list_reports_per_node_disk_sizes(mock_topology):
    """list_snapshots retrieves file sizes for per-node disk paths."""
    from vmcluster_mcp.snapshot.manager import SnapshotManager

    snap_xml_node1 = _make_snap_xml("snap1", 1706745600, "/overlays/testcluster/snapshots/snap1/node1.qcow2")
    snap_xml_node2 = _make_snap_xml("snap1", 1706745600, "/overlays/testcluster/snapshots/snap1/node2.qcow2")

    mock_client = MagicMock()
    mock_dom = MagicMock()
    mock_snap1 = MagicMock()
    mock_snap2 = MagicMock()
    mock_client.lookup_domain = AsyncMock(return_value=mock_dom)
    mock_client.list_all_snapshots = AsyncMock(side_effect=[[mock_snap1], [mock_snap2]])
    mock_client.snapshot_get_xml_desc = AsyncMock(side_effect=[snap_xml_node1, snap_xml_node2])

    # Different sizes for each node
    with patch("vmcluster_mcp.snapshot.manager.os.path.getsize", side_effect=[1024, 2048]):
        manager = SnapshotManager(mock_client)
        result = await manager.list_snapshots(
            cluster_name="testcluster",
            nodes=mock_topology.nodes,
            overlay_dir="/overlays",
        )

    assert result.success is True
    snap = result.result[0]
    assert snap.per_node["node1"].size_bytes == 1024
    assert snap.per_node["node2"].size_bytes == 2048


@pytest.mark.asyncio
async def test_snapshot_list_missing_file_size_returns_zero(mock_topology):
    """list_snapshots returns size_bytes=0 when disk file does not exist."""
    from vmcluster_mcp.snapshot.manager import SnapshotManager

    snap_xml = _make_snap_xml("snap1", 1706745600, "/overlays/testcluster/snapshots/snap1/node1.qcow2")

    mock_client = MagicMock()
    mock_dom = MagicMock()
    mock_snap = MagicMock()
    mock_client.lookup_domain = AsyncMock(return_value=mock_dom)
    mock_client.list_all_snapshots = AsyncMock(side_effect=[[mock_snap], []])
    mock_client.snapshot_get_xml_desc = AsyncMock(return_value=snap_xml)

    with patch("vmcluster_mcp.snapshot.manager.os.path.getsize", side_effect=FileNotFoundError):
        manager = SnapshotManager(mock_client)
        result = await manager.list_snapshots(
            cluster_name="testcluster",
            nodes=mock_topology.nodes,
            overlay_dir="/overlays",
        )

    assert result.success is True
    snap = result.result[0]
    assert snap.per_node["node1"].size_bytes == 0


@pytest.mark.asyncio
async def test_snapshot_list_groups_by_name_across_nodes(mock_topology):
    """list_snapshots groups per-node entries under a single SnapshotInfo per name."""
    from vmcluster_mcp.snapshot.manager import SnapshotManager

    snap_xml_node1 = _make_snap_xml("snap1", 1706745600, "/overlays/testcluster/snapshots/snap1/node1.qcow2")
    snap_xml_node2 = _make_snap_xml("snap1", 1706745600, "/overlays/testcluster/snapshots/snap1/node2.qcow2")

    mock_client = MagicMock()
    mock_dom = MagicMock()
    mock_snap1 = MagicMock()
    mock_snap2 = MagicMock()
    mock_client.lookup_domain = AsyncMock(return_value=mock_dom)
    # Each node has one snapshot with the same name
    mock_client.list_all_snapshots = AsyncMock(side_effect=[[mock_snap1], [mock_snap2]])
    mock_client.snapshot_get_xml_desc = AsyncMock(side_effect=[snap_xml_node1, snap_xml_node2])

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

    assert result.success is True
    # Both nodes share same snapshot name → one SnapshotInfo entry
    assert len(result.result) == 1
    snap = result.result[0]
    assert len(snap.per_node) == 2
    assert "node1" in snap.per_node
    assert "node2" in snap.per_node


@pytest.mark.asyncio
async def test_snapshot_list_multiple_distinct_snapshots(mock_topology):
    """list_snapshots returns one SnapshotInfo per distinct snapshot name."""
    from vmcluster_mcp.snapshot.manager import SnapshotManager

    snap_xml_snap1 = _make_snap_xml("snap1", 1706745600, "/overlays/testcluster/snapshots/snap1/node1.qcow2")
    snap_xml_snap2 = _make_snap_xml("snap2", 1706832000, "/overlays/testcluster/snapshots/snap2/node1.qcow2")

    mock_client = MagicMock()
    mock_dom = MagicMock()
    mock_snap_a = MagicMock()
    mock_snap_b = MagicMock()
    mock_client.lookup_domain = AsyncMock(return_value=mock_dom)
    # node1 has 2 snapshots, node2 has none
    mock_client.list_all_snapshots = AsyncMock(side_effect=[[mock_snap_a, mock_snap_b], []])
    mock_client.snapshot_get_xml_desc = AsyncMock(side_effect=[snap_xml_snap1, snap_xml_snap2])

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

    assert result.success is True
    assert len(result.result) == 2
    names = {s.snapshot_name for s in result.result}
    assert names == {"snap1", "snap2"}


# ---------------------------------------------------------------------------
# snapshot_list tool integration tests
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_snapshot_list_tool_returns_tool_result(mock_topology):
    """The snapshot_list MCP tool returns ToolResult[list[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 ToolResult

        mock_manager.list_snapshots = AsyncMock(
            return_value=ToolResult(success=True, result=[])
        )

        from vmcluster_mcp.snapshot import snapshot_list

        result = await snapshot_list(cluster_name="testcluster")

    assert result.success is True
    assert result.result == []
    mock_manager.list_snapshots.assert_called_once_with(
        cluster_name="testcluster",
        nodes=mock_topology.nodes,
        overlay_dir=mock_topology.overlay_dir,
    )
