"""TDD tests for SnapshotManager.delete_snapshot and snapshot_delete tool.

Covers:
- delete_snapshot calls LibvirtClient.delete_snapshot once per node
- Returns SnapshotDeleteResult with per-node confirmation
- Best-effort: failure on one node does not abort others (D-08)
- Active-backing guard: cannot delete snapshot whose overlay IS active disk (D-05)
- Nonexistent snapshot returns per-node error
- Removes overlay file from disk via os.remove
- LibvirtClient.delete_snapshot routes through _run executor
"""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from vmcluster_mcp.snapshot.manager import SnapshotManager


# ---------------------------------------------------------------------------
# Shared XML fixtures
# ---------------------------------------------------------------------------

_SNAP_XML = """\
<domainsnapshot>
  <name>{snap_name}</name>
  <creationTime>1711234567</creationTime>
  <disks>
    <disk name="vda"><source file="{disk_path}"/></disk>
  </disks>
</domainsnapshot>"""

_DOMAIN_XML = """\
<domain type="kvm">
  <devices>
    <disk type="file" device="disk">
      <driver name="qemu" type="qcow2"/>
      <source file="{active_disk}"/>
      <target dev="vda" bus="virtio"/>
    </disk>
  </devices>
</domain>"""


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def make_node(name: str = "node1", role: str = "worker", ip: str = "192.168.100.10"):
    node = MagicMock()
    node.name = name
    node.role = role
    node.ip = ip
    return node


def make_mock_client(
    snap_disk: str = "/overlays/tc/snapshots/s1/node1.qcow2",
    active_disk: str = "/overlays/tc/active/node1.qcow2",
):
    """Return (client, dom, snap) mocked with given disk paths."""
    from vmcluster_mcp.cluster.libvirt_client import LibvirtClient

    client = MagicMock(spec=LibvirtClient)
    dom = MagicMock()
    snap = MagicMock()

    snap_xml = _SNAP_XML.format(snap_name="s1", disk_path=snap_disk)
    domain_xml = _DOMAIN_XML.format(active_disk=active_disk)

    client.lookup_domain = AsyncMock(return_value=dom)
    client.snapshot_lookup_by_name = AsyncMock(return_value=snap)
    client.snapshot_get_xml_desc = AsyncMock(return_value=snap_xml)
    client.get_domain_xml = AsyncMock(return_value=domain_xml)
    client.delete_snapshot = AsyncMock()

    return client, dom, snap


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_delete_calls_client_delete_snapshot_once_per_node():
    """delete_snapshot invokes LibvirtClient.delete_snapshot exactly once per node."""
    client, dom, snap = make_mock_client()
    manager = SnapshotManager(client)

    with patch("os.remove"):
        result = await manager.delete_snapshot(
            cluster_name="tc",
            snapshot_name="s1",
            nodes=[make_node("node1")],
        )

    assert result.success is True
    assert client.delete_snapshot.call_count == 1


@pytest.mark.asyncio
async def test_delete_returns_per_node_confirmation():
    """Result contains SnapshotDeleteResult with per-node status for every node."""
    client, dom, snap = make_mock_client()
    manager = SnapshotManager(client)

    with patch("os.remove"):
        result = await manager.delete_snapshot(
            cluster_name="tc",
            snapshot_name="s1",
            nodes=[
                make_node("node1"),
                make_node("node2", ip="192.168.100.11"),
            ],
        )

    assert result.result is not None
    assert result.result.snapshot_name == "s1"
    assert result.result.cluster_name == "tc"
    assert "node1" in result.result.per_node
    assert "node2" in result.result.per_node
    assert result.result.per_node["node1"].deleted is True
    assert result.result.per_node["node2"].deleted is True


@pytest.mark.asyncio
async def test_delete_continues_on_individual_node_failure():
    """D-08: a single node failure is recorded but other nodes proceed."""
    from vmcluster_mcp.cluster.libvirt_client import LibvirtClient

    client = MagicMock(spec=LibvirtClient)
    dom = MagicMock()
    snap = MagicMock()

    call_count = 0

    async def lookup_snap(d, name):
        nonlocal call_count
        call_count += 1
        if call_count == 1:
            return None  # node1 has no snapshot — simulates per-node failure
        return snap

    client.lookup_domain = AsyncMock(return_value=dom)
    client.snapshot_lookup_by_name = AsyncMock(side_effect=lookup_snap)
    client.snapshot_get_xml_desc = AsyncMock(
        return_value=_SNAP_XML.format(
            snap_name="s1",
            disk_path="/overlays/tc/snapshots/s1/node2.qcow2",
        )
    )
    client.get_domain_xml = AsyncMock(
        return_value=_DOMAIN_XML.format(active_disk="/overlays/tc/active/node2.qcow2")
    )
    client.delete_snapshot = AsyncMock()

    manager = SnapshotManager(client)

    with patch("os.remove"):
        result = await manager.delete_snapshot(
            cluster_name="tc",
            snapshot_name="s1",
            nodes=[
                make_node("node1"),
                make_node("node2", ip="192.168.100.11"),
            ],
        )

    # node1 failed, node2 succeeded
    assert result.result.per_node["node1"].deleted is False
    assert result.result.per_node["node1"].error is not None
    assert result.result.per_node["node2"].deleted is True
    # Overall success=False because not all nodes deleted
    assert result.success is False


@pytest.mark.asyncio
async def test_delete_active_backing_guard_refuses_deletion():
    """D-05: Cannot delete a snapshot whose overlay IS the active domain disk."""
    same_path = "/overlays/tc/snapshots/s1/node1.qcow2"
    # snap_disk == active_disk → active backing
    client, dom, snap = make_mock_client(snap_disk=same_path, active_disk=same_path)
    manager = SnapshotManager(client)

    result = await manager.delete_snapshot(
        cluster_name="tc",
        snapshot_name="s1",
        nodes=[make_node("node1")],
    )

    assert result.success is False
    assert result.result.per_node["node1"].deleted is False
    assert "active" in result.result.per_node["node1"].error.lower()
    client.delete_snapshot.assert_not_called()


@pytest.mark.asyncio
async def test_delete_nonexistent_snapshot_returns_per_node_error():
    """snapshot_lookup_by_name returning None → per-node error, no exception raised."""
    from vmcluster_mcp.cluster.libvirt_client import LibvirtClient

    client = MagicMock(spec=LibvirtClient)
    client.lookup_domain = AsyncMock(return_value=MagicMock())
    client.snapshot_lookup_by_name = AsyncMock(return_value=None)
    client.delete_snapshot = AsyncMock()

    manager = SnapshotManager(client)

    result = await manager.delete_snapshot(
        cluster_name="tc",
        snapshot_name="nosuchsnap",
        nodes=[make_node("node1")],
    )

    assert result.success is False
    assert result.result.per_node["node1"].deleted is False
    assert result.result.per_node["node1"].error is not None
    client.delete_snapshot.assert_not_called()


@pytest.mark.asyncio
async def test_delete_removes_overlay_file():
    """os.remove is called with the snapshot's disk path after deletion."""
    snap_path = "/overlays/tc/snapshots/s1/node1.qcow2"
    client, dom, snap = make_mock_client(
        snap_disk=snap_path,
        active_disk="/overlays/tc/active/node1.qcow2",
    )
    manager = SnapshotManager(client)

    with patch("os.remove") as mock_rm:
        await manager.delete_snapshot(
            cluster_name="tc",
            snapshot_name="s1",
            nodes=[make_node("node1")],
        )

    mock_rm.assert_called_once_with(snap_path)


@pytest.mark.asyncio
async def test_delete_file_not_found_is_not_an_error():
    """FileNotFoundError when removing overlay file is silently ignored."""
    snap_path = "/overlays/tc/snapshots/s1/node1.qcow2"
    client, dom, snap = make_mock_client(
        snap_disk=snap_path,
        active_disk="/overlays/tc/active/node1.qcow2",
    )
    manager = SnapshotManager(client)

    with patch("os.remove", side_effect=FileNotFoundError):
        result = await manager.delete_snapshot(
            cluster_name="tc",
            snapshot_name="s1",
            nodes=[make_node("node1")],
        )

    assert result.success is True
    assert result.result.per_node["node1"].deleted is True


# ---------------------------------------------------------------------------
# LibvirtClient unit: delete_snapshot routes through _run
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_libvirt_delete_snapshot_uses_run():
    """delete_snapshot dispatches snap.delete through _run executor."""
    from vmcluster_mcp.cluster.libvirt_client import LibvirtClient

    client = LibvirtClient.__new__(LibvirtClient)
    mock_snap = MagicMock()
    run_calls: list[tuple] = []

    async def fake_run(func, *args):
        run_calls.append((func, args))
        return None

    client._run = fake_run
    await client.delete_snapshot(mock_snap, 0)

    assert len(run_calls) == 1
    called_fn, called_args = run_calls[0]
    assert called_fn == mock_snap.delete
    assert called_args == (0,)
