"""Tests for FaultRegistry: atomic persistence, CRUD, reconciliation."""

from __future__ import annotations

import json
from unittest.mock import AsyncMock, patch

import pytest

from vmcluster_mcp.config import Config, load_config
from vmcluster_mcp.models import NetFaultHandle, NetFaultInfo
from vmcluster_mcp.net.fault_registry import FaultRegistry


# --- Config tests ---


def test_config_has_fault_registry_default():
    cfg = Config()
    assert cfg.fault_registry == "/var/lib/vmcluster/faults/registry.json"


def test_config_env_var_overrides_fault_registry(monkeypatch, tmp_path):
    custom_path = str(tmp_path / "custom_faults.json")
    monkeypatch.setenv("VMCLUSTER_FAULT_REGISTRY", custom_path)
    monkeypatch.setenv("VMCLUSTER_CONFIG", "/nonexistent/config.yaml")
    cfg = load_config()
    assert cfg.fault_registry == custom_path


# --- FaultRegistry CRUD tests ---


def test_load_empty_when_file_missing(tmp_path):
    reg = FaultRegistry(str(tmp_path / "registry.json"))
    reg.load()
    assert reg.all_handles() == []


def test_save_uses_atomic_write(tmp_path):
    path = tmp_path / "registry.json"
    reg = FaultRegistry(str(path))
    reg.load()
    reg.save()
    assert path.exists()
    assert not (tmp_path / "registry.json.tmp").exists()


def test_save_creates_parent_dirs(tmp_path):
    path = tmp_path / "nested" / "dir" / "registry.json"
    reg = FaultRegistry(str(path))
    reg.load()
    reg.save()
    assert path.exists()


def test_register_stores_entry(tmp_path):
    reg = FaultRegistry(str(tmp_path / "registry.json"))
    reg.load()
    handle = reg.register(
        handle_id="h1",
        cluster_name="cluster1",
        fault_type="partition",
        affected_nodes=["node1", "node2"],
        parameters={"direction": "both"},
        rule_identifiers={"comment_tag": "vmcluster:cluster1:h1"},
    )
    assert isinstance(handle, NetFaultHandle)
    assert handle.handle_id == "h1"
    assert handle.fault_type == "partition"
    assert handle.affected_nodes == ["node1", "node2"]


def test_register_saves_immediately(tmp_path):
    path = tmp_path / "registry.json"
    reg = FaultRegistry(str(path))
    reg.load()
    reg.register(
        handle_id="h1",
        cluster_name="cluster1",
        fault_type="partition",
        affected_nodes=["node1", "node2"],
        parameters={},
        rule_identifiers={},
    )
    assert path.exists()
    with open(path) as f:
        data = json.load(f)
    assert "h1" in data


def test_get_retrieves_stored_entry(tmp_path):
    reg = FaultRegistry(str(tmp_path / "registry.json"))
    reg.load()
    reg.register(
        handle_id="h1",
        cluster_name="cluster1",
        fault_type="partition",
        affected_nodes=["node1"],
        parameters={},
        rule_identifiers={},
    )
    entry = reg.get("h1")
    assert entry is not None
    assert entry["cluster_name"] == "cluster1"


def test_get_returns_none_for_unknown(tmp_path):
    reg = FaultRegistry(str(tmp_path / "registry.json"))
    reg.load()
    assert reg.get("nonexistent") is None


def test_remove_deletes_entry(tmp_path):
    reg = FaultRegistry(str(tmp_path / "registry.json"))
    reg.load()
    reg.register(
        handle_id="h1",
        cluster_name="cluster1",
        fault_type="partition",
        affected_nodes=["node1"],
        parameters={},
        rule_identifiers={},
    )
    result = reg.remove("h1")
    assert result is True
    assert reg.get("h1") is None


def test_remove_returns_false_for_missing(tmp_path):
    reg = FaultRegistry(str(tmp_path / "registry.json"))
    reg.load()
    assert reg.remove("nonexistent") is False


def test_remove_saves_immediately(tmp_path):
    path = tmp_path / "registry.json"
    reg = FaultRegistry(str(path))
    reg.load()
    reg.register(
        handle_id="h1",
        cluster_name="cluster1",
        fault_type="partition",
        affected_nodes=["node1"],
        parameters={},
        rule_identifiers={},
    )
    reg.remove("h1")
    with open(path) as f:
        data = json.load(f)
    assert "h1" not in data


def test_list_faults_returns_matching_cluster(tmp_path):
    reg = FaultRegistry(str(tmp_path / "registry.json"))
    reg.load()
    reg.register(
        handle_id="h1",
        cluster_name="cluster1",
        fault_type="partition",
        affected_nodes=["node1"],
        parameters={"direction": "both"},
        rule_identifiers={},
    )
    reg.register(
        handle_id="h2",
        cluster_name="cluster2",
        fault_type="impairment",
        affected_nodes=["node2"],
        parameters={"latency_ms": 100},
        rule_identifiers={},
    )
    faults = reg.list_faults("cluster1")
    assert len(faults) == 1
    assert faults[0].handle_id == "h1"
    assert isinstance(faults[0], NetFaultInfo)


def test_list_faults_returns_empty_for_unknown_cluster(tmp_path):
    reg = FaultRegistry(str(tmp_path / "registry.json"))
    reg.load()
    faults = reg.list_faults("unknown_cluster")
    assert faults == []


def test_round_trip_persistence(tmp_path):
    path = tmp_path / "registry.json"
    reg1 = FaultRegistry(str(path))
    reg1.load()
    reg1.register(
        handle_id="h1",
        cluster_name="cluster1",
        fault_type="partition",
        affected_nodes=["node1", "node2"],
        parameters={"direction": "both"},
        rule_identifiers={"comment_tag": "vmcluster:cluster1:h1"},
    )
    # New instance — simulates MCP server restart
    reg2 = FaultRegistry(str(path))
    reg2.load()
    entry = reg2.get("h1")
    assert entry is not None
    assert entry["cluster_name"] == "cluster1"
    faults = reg2.list_faults("cluster1")
    assert len(faults) == 1
    assert faults[0].handle_id == "h1"


# --- Reconciliation tests ---


async def test_reconcile_retains_partition_with_matching_tag(tmp_path):
    reg = FaultRegistry(str(tmp_path / "registry.json"))
    reg.load()
    reg.register(
        handle_id="h1",
        cluster_name="cluster1",
        fault_type="partition",
        affected_nodes=["node1", "node2"],
        parameters={},
        rule_identifiers={"comment_tag": "vmcluster:cluster1:h1"},
    )
    iptables_output = "Chain FORWARD (policy ACCEPT)\n ... /* vmcluster:cluster1:h1 */\n"
    with patch("vmcluster_mcp.net.fault_registry._run_host_cmd", new_callable=AsyncMock) as mock_cmd:
        mock_cmd.return_value = (0, iptables_output)
        pruned = await reg.reconcile()
    assert pruned == 0
    assert reg.get("h1") is not None


async def test_reconcile_prunes_partition_with_missing_tag(tmp_path):
    reg = FaultRegistry(str(tmp_path / "registry.json"))
    reg.load()
    reg.register(
        handle_id="h1",
        cluster_name="cluster1",
        fault_type="partition",
        affected_nodes=["node1", "node2"],
        parameters={},
        rule_identifiers={"comment_tag": "vmcluster:cluster1:h1"},
    )
    iptables_output = "Chain FORWARD (policy ACCEPT)\n(no rules)\n"
    with patch("vmcluster_mcp.net.fault_registry._run_host_cmd", new_callable=AsyncMock) as mock_cmd:
        mock_cmd.return_value = (0, iptables_output)
        pruned = await reg.reconcile()
    assert pruned == 1
    assert reg.get("h1") is None


async def test_reconcile_retains_impairment_with_existing_qdisc(tmp_path):
    reg = FaultRegistry(str(tmp_path / "registry.json"))
    reg.load()
    reg.register(
        handle_id="h2",
        cluster_name="cluster1",
        fault_type="impairment",
        affected_nodes=["node1"],
        parameters={"latency_ms": 50},
        rule_identifiers={"interface": "vnet0"},
    )
    tc_output = "qdisc netem 8001: dev vnet0 root refcnt 2 limit 1000 delay 50ms\n"
    with patch("vmcluster_mcp.net.fault_registry._run_host_cmd", new_callable=AsyncMock) as mock_cmd:
        mock_cmd.return_value = (0, tc_output)
        pruned = await reg.reconcile()
    assert pruned == 0
    assert reg.get("h2") is not None


async def test_reconcile_prunes_impairment_with_missing_qdisc(tmp_path):
    reg = FaultRegistry(str(tmp_path / "registry.json"))
    reg.load()
    reg.register(
        handle_id="h2",
        cluster_name="cluster1",
        fault_type="impairment",
        affected_nodes=["node1"],
        parameters={"latency_ms": 50},
        rule_identifiers={"interface": "vnet0"},
    )
    tc_output = "qdisc noqueue 0: dev vnet0 root refcnt 2\n"
    with patch("vmcluster_mcp.net.fault_registry._run_host_cmd", new_callable=AsyncMock) as mock_cmd:
        mock_cmd.return_value = (0, tc_output)
        pruned = await reg.reconcile()
    assert pruned == 1
    assert reg.get("h2") is None


async def test_reconcile_saves_after_pruning(tmp_path):
    path = tmp_path / "registry.json"
    reg = FaultRegistry(str(path))
    reg.load()
    reg.register(
        handle_id="h1",
        cluster_name="cluster1",
        fault_type="partition",
        affected_nodes=["node1"],
        parameters={},
        rule_identifiers={"comment_tag": "vmcluster:cluster1:h1"},
    )
    with patch("vmcluster_mcp.net.fault_registry._run_host_cmd", new_callable=AsyncMock) as mock_cmd:
        mock_cmd.return_value = (0, "no matching rules")
        pruned = await reg.reconcile()
    assert pruned == 1
    with open(path) as f:
        data = json.load(f)
    assert "h1" not in data


async def test_reconcile_no_save_when_no_stale_entries(tmp_path):
    reg = FaultRegistry(str(tmp_path / "registry.json"))
    reg.load()
    reg.register(
        handle_id="h1",
        cluster_name="cluster1",
        fault_type="partition",
        affected_nodes=["node1"],
        parameters={},
        rule_identifiers={"comment_tag": "vmcluster:cluster1:h1"},
    )
    iptables_output = "... vmcluster:cluster1:h1 ..."
    with patch("vmcluster_mcp.net.fault_registry._run_host_cmd", new_callable=AsyncMock) as mock_cmd:
        mock_cmd.return_value = (0, iptables_output)
        with patch.object(reg, "save") as mock_save:
            await reg.reconcile()
            mock_save.assert_not_called()


async def test_reconcile_empty_registry_noop(tmp_path):
    reg = FaultRegistry(str(tmp_path / "registry.json"))
    reg.load()
    with patch("vmcluster_mcp.net.fault_registry._run_host_cmd", new_callable=AsyncMock) as mock_cmd:
        pruned = await reg.reconcile()
        assert pruned == 0
        mock_cmd.assert_not_called()
