"""Tests for net_heal (handle-specific fault removal) and net_heal_all (cluster-wide cleanup)."""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock, patch

import pytest


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


def _make_partition_entry(handle_id: str = "h-part-01", cluster: str = "cluster1") -> dict:
    return {
        "cluster_name": cluster,
        "fault_type": "partition",
        "affected_nodes": ["node1", "node2"],
        "created_at": "2026-01-01T00:00:00+00:00",
        "parameters": {"partition_a": ["node1"], "partition_b": ["node2"]},
        "rule_identifiers": {"comment_tag": f"vmcluster:{cluster}:{handle_id}"},
    }


def _make_impairment_entry(handle_id: str = "h-imp-01", cluster: str = "cluster1") -> dict:
    return {
        "cluster_name": cluster,
        "fault_type": "impairment",
        "affected_nodes": ["node1"],
        "created_at": "2026-01-01T00:00:00+00:00",
        "parameters": {"source_node": "node1", "latency_ms": 100},
        "rule_identifiers": {"interface": "vnet3"},
    }


# ---------------------------------------------------------------------------
# net_heal — unknown handle
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_net_heal_returns_error_for_unknown_handle():
    """net_heal returns ToolResult(success=False, error.code=HANDLE_NOT_FOUND) for unknown handle."""
    from vmcluster_mcp.net import net_heal

    with patch("vmcluster_mcp.net._registry.get", return_value=None):
        result = await net_heal(cluster_name="cluster1", fault_handle="nonexistent")

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


# ---------------------------------------------------------------------------
# net_heal — partition handle
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_net_heal_partition_calls_remove_rules_by_comment():
    """net_heal with a partition handle calls remove_rules_by_comment with the comment tag."""
    from vmcluster_mcp.net import net_heal

    entry = _make_partition_entry("h1")
    with (
        patch("vmcluster_mcp.net._registry.get", return_value=entry),
        patch("vmcluster_mcp.net.remove_rules_by_comment", new_callable=AsyncMock, return_value=2) as mock_rm,
        patch("vmcluster_mcp.net._registry.remove", return_value=True),
    ):
        result = await net_heal(cluster_name="cluster1", fault_handle="h1")

    mock_rm.assert_called_once_with("vmcluster:cluster1:h1")
    assert result.success is True


@pytest.mark.asyncio
async def test_net_heal_partition_removes_registry_entry_after_cleanup():
    """net_heal removes the registry entry after host-side cleanup succeeds."""
    from vmcluster_mcp.net import net_heal

    entry = _make_partition_entry("h1")
    remove_called_with = []

    def capture_remove(handle_id: str) -> bool:
        remove_called_with.append(handle_id)
        return True

    with (
        patch("vmcluster_mcp.net._registry.get", return_value=entry),
        patch("vmcluster_mcp.net.remove_rules_by_comment", new_callable=AsyncMock, return_value=2),
        patch("vmcluster_mcp.net._registry.remove", side_effect=capture_remove),
    ):
        await net_heal(cluster_name="cluster1", fault_handle="h1")

    assert remove_called_with == ["h1"]


@pytest.mark.asyncio
async def test_net_heal_partition_succeeds_even_if_rules_already_absent():
    """net_heal succeeds (D-10) even when remove_rules_by_comment returns 0 (no rules found)."""
    from vmcluster_mcp.net import net_heal

    entry = _make_partition_entry("h1")
    with (
        patch("vmcluster_mcp.net._registry.get", return_value=entry),
        patch("vmcluster_mcp.net.remove_rules_by_comment", new_callable=AsyncMock, return_value=0),
        patch("vmcluster_mcp.net._registry.remove", return_value=True),
    ):
        result = await net_heal(cluster_name="cluster1", fault_handle="h1")

    assert result.success is True
    assert result.result is True


# ---------------------------------------------------------------------------
# net_heal — impairment handle
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_net_heal_impairment_calls_remove_impairment():
    """net_heal with an impairment handle calls remove_impairment with the tap device."""
    from vmcluster_mcp.net import net_heal

    entry = _make_impairment_entry("h2")
    with (
        patch("vmcluster_mcp.net._registry.get", return_value=entry),
        patch("vmcluster_mcp.net.remove_impairment", new_callable=AsyncMock, return_value=True) as mock_rm,
        patch("vmcluster_mcp.net._registry.remove", return_value=True),
    ):
        result = await net_heal(cluster_name="cluster1", fault_handle="h2")

    mock_rm.assert_called_once_with("vnet3")
    assert result.success is True


@pytest.mark.asyncio
async def test_net_heal_impairment_succeeds_when_tap_already_absent():
    """net_heal succeeds (D-10) when remove_impairment returns False (qdisc already absent)."""
    from vmcluster_mcp.net import net_heal

    entry = _make_impairment_entry("h2")
    with (
        patch("vmcluster_mcp.net._registry.get", return_value=entry),
        patch("vmcluster_mcp.net.remove_impairment", new_callable=AsyncMock, return_value=False),
        patch("vmcluster_mcp.net._registry.remove", return_value=True),
    ):
        result = await net_heal(cluster_name="cluster1", fault_handle="h2")

    assert result.success is True


@pytest.mark.asyncio
async def test_net_heal_impairment_removes_registry_entry_after_cleanup():
    """net_heal removes the registry entry after impairment cleanup."""
    from vmcluster_mcp.net import net_heal

    entry = _make_impairment_entry("h2")
    removed = []

    def capture_remove(handle_id: str) -> bool:
        removed.append(handle_id)
        return True

    with (
        patch("vmcluster_mcp.net._registry.get", return_value=entry),
        patch("vmcluster_mcp.net.remove_impairment", new_callable=AsyncMock, return_value=True),
        patch("vmcluster_mcp.net._registry.remove", side_effect=capture_remove),
    ):
        await net_heal(cluster_name="cluster1", fault_handle="h2")

    assert removed == ["h2"]


# ---------------------------------------------------------------------------
# net_heal_all
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_net_heal_all_returns_zero_for_empty_cluster():
    """net_heal_all returns count=0 when there are no active faults."""
    from vmcluster_mcp.net import net_heal_all

    with patch("vmcluster_mcp.net._registry.list_faults", return_value=[]):
        result = await net_heal_all(cluster_name="cluster1")

    assert result.success is True
    assert result.result == 0


@pytest.mark.asyncio
async def test_net_heal_all_removes_all_cluster_faults():
    """net_heal_all removes all active faults for a cluster and returns the count."""
    from vmcluster_mcp.models import NetFaultInfo
    from vmcluster_mcp.net import net_heal_all

    faults = [
        NetFaultInfo(
            handle_id="h1", fault_type="partition", affected_nodes=["node1", "node2"],
            created_at="2026-01-01T00:00:00+00:00", parameters={},
        ),
        NetFaultInfo(
            handle_id="h2", fault_type="impairment", affected_nodes=["node1"],
            created_at="2026-01-01T00:00:00+00:00", parameters={},
        ),
    ]

    with (
        patch("vmcluster_mcp.net._registry.list_faults", return_value=faults),
        patch("vmcluster_mcp.net._registry.get", side_effect=[
            _make_partition_entry("h1"),
            _make_impairment_entry("h2"),
        ]),
        patch("vmcluster_mcp.net.remove_rules_by_comment", new_callable=AsyncMock, return_value=2),
        patch("vmcluster_mcp.net.remove_impairment", new_callable=AsyncMock, return_value=True),
        patch("vmcluster_mcp.net._registry.remove", return_value=True),
    ):
        result = await net_heal_all(cluster_name="cluster1")

    assert result.success is True
    assert result.result == 2


@pytest.mark.asyncio
async def test_net_heal_all_continues_after_per_handle_failure():
    """net_heal_all is best-effort (D-11): one handle failure does not abort the rest."""
    from vmcluster_mcp.models import NetFaultInfo
    from vmcluster_mcp.net import net_heal_all

    faults = [
        NetFaultInfo(
            handle_id="h1", fault_type="partition", affected_nodes=["node1", "node2"],
            created_at="2026-01-01T00:00:00+00:00", parameters={},
        ),
        NetFaultInfo(
            handle_id="h2", fault_type="impairment", affected_nodes=["node1"],
            created_at="2026-01-01T00:00:00+00:00", parameters={},
        ),
    ]

    def get_side_effect(handle_id: str):
        if handle_id == "h1":
            return _make_partition_entry("h1")
        return _make_impairment_entry("h2")

    with (
        patch("vmcluster_mcp.net._registry.list_faults", return_value=faults),
        patch("vmcluster_mcp.net._registry.get", side_effect=get_side_effect),
        # h1 cleanup fails with an exception
        patch("vmcluster_mcp.net.remove_rules_by_comment", new_callable=AsyncMock,
              side_effect=RuntimeError("iptables error")),
        patch("vmcluster_mcp.net.remove_impairment", new_callable=AsyncMock, return_value=True),
        patch("vmcluster_mcp.net._registry.remove", return_value=True),
    ):
        result = await net_heal_all(cluster_name="cluster1")

    # h2 should still have been processed despite h1 failing
    assert result.success is True
    assert result.result == 1  # only h2 succeeded


@pytest.mark.asyncio
async def test_net_heal_all_net_fault_list_empty_after_success():
    """After net_heal_all, list_faults returns empty (via registry.remove calls)."""
    from vmcluster_mcp.models import NetFaultInfo
    from vmcluster_mcp.net import net_heal_all

    faults = [
        NetFaultInfo(
            handle_id="h1", fault_type="partition", affected_nodes=["node1"],
            created_at="2026-01-01T00:00:00+00:00", parameters={},
        ),
    ]
    removed_handles: list[str] = []

    with (
        patch("vmcluster_mcp.net._registry.list_faults", return_value=faults),
        patch("vmcluster_mcp.net._registry.get", return_value=_make_partition_entry("h1")),
        patch("vmcluster_mcp.net.remove_rules_by_comment", new_callable=AsyncMock, return_value=1),
        patch("vmcluster_mcp.net._registry.remove", side_effect=lambda h: removed_handles.append(h) or True),
    ):
        await net_heal_all(cluster_name="cluster1")

    assert "h1" in removed_handles
