"""TDD tests for SnapshotManager.revert_snapshot and snapshot_revert tool.

Covers:
- Destroy-all-before-revert ordering discipline (D-07)
- Per-node revert call verification
- restart_after=True start behaviour
- wait_for_ssh blocking behaviour
- SSH timeout failure propagation (D-07 all-or-nothing)
- Per-node status in result (ClusterStatus)
- restart_after=False leaves domains shut off
- Nonexistent snapshot returns error
- LibvirtClient.revert_to_snapshot routes through _run
"""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock

import pytest

from vmcluster_mcp.snapshot.manager import SnapshotManager


# ---------------------------------------------------------------------------
# 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_topo(
    ssh_user: str = "root",
    ssh_key_path: str = "/key",
    network_name: str = "testnet",
):
    topo = MagicMock()
    topo.ssh.user = ssh_user
    topo.ssh.key_path = ssh_key_path
    topo.network.name = network_name
    return topo


def make_mock_client():
    """Return (client, dom, snap, net) with sensible async defaults."""
    from vmcluster_mcp.cluster.libvirt_client import LibvirtClient

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

    client.lookup_domain = AsyncMock(return_value=dom)
    client.is_domain_active = AsyncMock(return_value=True)
    client.destroy_domain = AsyncMock()
    client.snapshot_lookup_by_name = AsyncMock(return_value=snap)
    client.revert_to_snapshot = AsyncMock()
    client.start_domain_by_name = AsyncMock()
    client.get_domain_state = AsyncMock(return_value="running")
    client.lookup_network = AsyncMock(return_value=net)
    client.is_network_active = AsyncMock(return_value=True)

    return client, dom, snap, net


# ---------------------------------------------------------------------------
# Phase 1: Destroy-all-before-revert ordering
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_revert_destroys_all_nodes_before_any_revert():
    """Destroy-all discipline: every destroy() call must precede the first revert().

    This is the core D-07 constraint: no partial reverts allowed.
    """
    client, dom, snap, net = make_mock_client()
    call_order: list[str] = []

    async def track_destroy(_d):
        call_order.append("destroy")

    async def track_revert(_d, _s, _f):
        call_order.append("revert")

    client.destroy_domain = AsyncMock(side_effect=track_destroy)
    client.revert_to_snapshot = AsyncMock(side_effect=track_revert)
    nodes = [make_node("n1"), make_node("n2")]
    manager = SnapshotManager(client)

    result = await manager.revert_snapshot(
        cluster_name="testcluster",
        snapshot_name="snap1",
        nodes=nodes,
        overlay_dir="/overlays",
        topo=make_topo(),
        restart_after=False,
        wait_for_ssh=False,
    )

    assert result.success is True
    destroy_idxs = [i for i, x in enumerate(call_order) if x == "destroy"]
    revert_idxs = [i for i, x in enumerate(call_order) if x == "revert"]
    assert len(destroy_idxs) == 2, "Expected 2 destroy calls"
    assert len(revert_idxs) == 2, "Expected 2 revert calls"
    assert max(destroy_idxs) < min(revert_idxs), (
        "All destroys must complete before first revert"
    )


@pytest.mark.asyncio
async def test_revert_calls_revert_to_snapshot_for_each_node():
    """revert_to_snapshot is invoked exactly once per node in Phase 2."""
    client, dom, snap, net = make_mock_client()
    nodes = [make_node("n1"), make_node("n2"), make_node("n3")]
    manager = SnapshotManager(client)

    result = await manager.revert_snapshot(
        cluster_name="testcluster",
        snapshot_name="snap1",
        nodes=nodes,
        overlay_dir="/overlays",
        topo=make_topo(),
        restart_after=False,
        wait_for_ssh=False,
    )

    assert result.success is True
    assert client.revert_to_snapshot.call_count == 3


# ---------------------------------------------------------------------------
# Phase 3: Restart
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_revert_restart_after_true_starts_all_domains():
    """restart_after=True: start_domain_by_name called once per node."""
    client, dom, snap, net = make_mock_client()
    nodes = [make_node("n1"), make_node("n2")]
    manager = SnapshotManager(client)

    result = await manager.revert_snapshot(
        cluster_name="testcluster",
        snapshot_name="snap1",
        nodes=nodes,
        overlay_dir="/overlays",
        topo=make_topo(),
        restart_after=True,
        wait_for_ssh=False,
    )

    assert result.success is True
    assert client.start_domain_by_name.call_count == 2


@pytest.mark.asyncio
async def test_revert_restart_after_false_does_not_start_domains():
    """restart_after=False: start_domain_by_name is never called."""
    client, dom, snap, net = make_mock_client()
    manager = SnapshotManager(client)

    result = await manager.revert_snapshot(
        cluster_name="testcluster",
        snapshot_name="snap1",
        nodes=[make_node("n1")],
        overlay_dir="/overlays",
        topo=make_topo(),
        restart_after=False,
        wait_for_ssh=False,
    )

    assert result.success is True
    client.start_domain_by_name.assert_not_called()


# ---------------------------------------------------------------------------
# Phase 4: SSH wait
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_revert_wait_for_ssh_calls_wait_per_node():
    """wait_for_ssh=True: _wait_for_ssh called once per node with correct arguments."""
    from unittest.mock import patch

    client, dom, snap, net = make_mock_client()
    nodes = [make_node("n1", ip="10.0.0.1"), make_node("n2", ip="10.0.0.2")]
    manager = SnapshotManager(client)

    mock_wait = AsyncMock(return_value=(True, 1.0))
    with patch("vmcluster_mcp.cluster._wait_for_ssh", mock_wait):
        result = await manager.revert_snapshot(
            cluster_name="testcluster",
            snapshot_name="snap1",
            nodes=nodes,
            overlay_dir="/overlays",
            topo=make_topo(ssh_user="root", ssh_key_path="/key"),
            restart_after=True,
            wait_for_ssh=True,
            ssh_timeout_s=60,
        )

    assert result.success is True
    assert mock_wait.call_count == 2
    ips_called = {c.args[0] for c in mock_wait.call_args_list}
    assert ips_called == {"10.0.0.1", "10.0.0.2"}


@pytest.mark.asyncio
async def test_revert_ssh_timeout_returns_failure():
    """SSH timeout on any node → success=False with SSH_TIMEOUT code (D-07)."""
    from unittest.mock import patch

    client, dom, snap, net = make_mock_client()
    nodes = [make_node("n1", ip="10.0.0.1"), make_node("n2", ip="10.0.0.2")]
    manager = SnapshotManager(client)

    async def ssh_timeout(ip, port, user, key, timeout):
        if ip == "10.0.0.2":
            return False, float(timeout)  # n2 times out
        return True, 1.0

    with patch(
        "vmcluster_mcp.cluster._wait_for_ssh",
        AsyncMock(side_effect=ssh_timeout),
    ):
        result = await manager.revert_snapshot(
            cluster_name="testcluster",
            snapshot_name="snap1",
            nodes=nodes,
            overlay_dir="/overlays",
            topo=make_topo(),
            restart_after=True,
            wait_for_ssh=True,
            ssh_timeout_s=10,
        )

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


@pytest.mark.asyncio
async def test_revert_restart_false_never_checks_ssh():
    """restart_after=False: _wait_for_ssh never called even with wait_for_ssh=True."""
    from unittest.mock import patch

    client, dom, snap, net = make_mock_client()
    manager = SnapshotManager(client)

    mock_wait = AsyncMock(return_value=(True, 1.0))
    with patch("vmcluster_mcp.cluster._wait_for_ssh", mock_wait):
        await manager.revert_snapshot(
            cluster_name="testcluster",
            snapshot_name="snap1",
            nodes=[make_node("n1")],
            overlay_dir="/overlays",
            topo=make_topo(),
            restart_after=False,
            wait_for_ssh=True,  # must be ignored when restart_after=False
        )

    mock_wait.assert_not_called()


# ---------------------------------------------------------------------------
# Result shape
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_revert_returns_cluster_status_with_per_node_entries():
    """Successful revert returns ClusterStatus with one NodeStatus per node."""
    from unittest.mock import patch

    client, dom, snap, net = make_mock_client()
    nodes = [make_node("n1"), make_node("n2")]
    manager = SnapshotManager(client)

    with patch(
        "vmcluster_mcp.cluster._wait_for_ssh",
        AsyncMock(return_value=(True, 1.0)),
    ):
        result = await manager.revert_snapshot(
            cluster_name="testcluster",
            snapshot_name="snap1",
            nodes=nodes,
            overlay_dir="/overlays",
            topo=make_topo(),
            restart_after=True,
            wait_for_ssh=True,
        )

    assert result.success is True
    assert result.result is not None
    assert result.result.cluster_name == "testcluster"
    assert len(result.result.nodes) == 2


# ---------------------------------------------------------------------------
# Error scenarios
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_revert_nonexistent_snapshot_returns_error():
    """snapshot_lookup_by_name returning None → success=False with error detail."""
    client, dom, snap, net = make_mock_client()
    client.snapshot_lookup_by_name = AsyncMock(return_value=None)
    manager = SnapshotManager(client)

    result = await manager.revert_snapshot(
        cluster_name="testcluster",
        snapshot_name="nosuchsnap",
        nodes=[make_node("n1")],
        overlay_dir="/overlays",
        topo=make_topo(),
        restart_after=False,
        wait_for_ssh=False,
    )

    assert result.success is False
    assert result.error is not None


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


@pytest.mark.asyncio
async def test_libvirt_revert_to_snapshot_uses_run():
    """revert_to_snapshot dispatches dom.revertToSnapshot through _run executor."""
    from vmcluster_mcp.cluster.libvirt_client import LibvirtClient

    client = LibvirtClient.__new__(LibvirtClient)
    mock_dom = MagicMock()
    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.revert_to_snapshot(mock_dom, mock_snap, 2)

    assert len(run_calls) == 1
    called_fn, called_args = run_calls[0]
    assert called_fn == mock_dom.revertToSnapshot
    assert called_args == (mock_snap, 2)
