"""Unit tests for node_exec_all MCP tool."""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from vmcluster_mcp.models import ExecResult, ExecAllResult, ToolResult


# --- Fixtures ---

def _make_node(name: str, ip: str) -> MagicMock:
    node = MagicMock()
    node.name = name
    node.ip = ip
    return node


def _make_topology(node_specs=None):
    topo = MagicMock()
    topo.cluster_name = "test-cluster"
    topo.ssh.user = "root"
    topo.ssh.key_path = "/etc/vmcluster/ssh/id_ed25519"
    if node_specs is None:
        topo.nodes = [
            _make_node("node1", "10.0.0.1"),
            _make_node("node2", "10.0.0.2"),
            _make_node("node3", "10.0.0.3"),
        ]
    else:
        topo.nodes = node_specs
    return topo


def _ok_result(node_name: str = "node") -> ExecResult:
    return ExecResult(exit_code=0, stdout=f"ok from {node_name}", stderr="", duration_s=0.1, timed_out=False)


def _fail_result(node_name: str = "node") -> ExecResult:
    return ExecResult(exit_code=1, stdout="", stderr=f"error on {node_name}", duration_s=0.1, timed_out=False)


def _timeout_result() -> ExecResult:
    return ExecResult(exit_code=-1, stdout="", stderr="Command timed out", duration_s=30.0, timed_out=True)


@pytest.mark.asyncio
async def test_node_exec_all_success_all_nodes():
    """Test 1: All nodes succeed — ToolResult.success=True, all_succeeded=True, failed_nodes=[]."""
    with (
        patch("vmcluster_mcp.exec.load_topology", return_value=_make_topology()),
        patch("vmcluster_mcp.exec._ssh_manager") as mock_mgr,
    ):
        mock_mgr.execute = AsyncMock(side_effect=[
            _ok_result("node1"), _ok_result("node2"), _ok_result("node3"),
        ])
        from vmcluster_mcp.exec import node_exec_all
        result: ToolResult = await node_exec_all.__wrapped__("test-cluster", "echo ok")

    assert result.success is True
    assert result.result.all_succeeded is True
    assert result.result.failed_nodes == []
    assert len(result.result.results) == 3


@pytest.mark.asyncio
async def test_node_exec_all_partial_failure_require_all_true():
    """Test 2: One node fails, require_all=True → success=False, PARTIAL_FAILURE."""
    with (
        patch("vmcluster_mcp.exec.load_topology", return_value=_make_topology()),
        patch("vmcluster_mcp.exec._ssh_manager") as mock_mgr,
    ):
        mock_mgr.execute = AsyncMock(side_effect=[
            _ok_result("node1"), _fail_result("node2"), _ok_result("node3"),
        ])
        from vmcluster_mcp.exec import node_exec_all
        result: ToolResult = await node_exec_all.__wrapped__(
            "test-cluster", "faulty-cmd", None, True
        )

    assert result.success is False
    assert result.result.all_succeeded is False
    assert "node2" in result.result.failed_nodes
    assert result.error is not None
    assert result.error.code == "PARTIAL_FAILURE"


@pytest.mark.asyncio
async def test_node_exec_all_partial_failure_require_all_false():
    """Test 3: One node fails, require_all=False → success=True, failed_nodes populated."""
    with (
        patch("vmcluster_mcp.exec.load_topology", return_value=_make_topology()),
        patch("vmcluster_mcp.exec._ssh_manager") as mock_mgr,
    ):
        mock_mgr.execute = AsyncMock(side_effect=[
            _ok_result("node1"), _fail_result("node2"), _ok_result("node3"),
        ])
        from vmcluster_mcp.exec import node_exec_all
        result: ToolResult = await node_exec_all.__wrapped__(
            "test-cluster", "cmd", None, False
        )

    assert result.success is True
    assert result.result.all_succeeded is False
    assert "node2" in result.result.failed_nodes


@pytest.mark.asyncio
async def test_node_exec_all_subset_of_nodes():
    """Test 4: nodes=["node1", "node2"] executes only those 2 nodes."""
    with (
        patch("vmcluster_mcp.exec.load_topology", return_value=_make_topology()),
        patch("vmcluster_mcp.exec._ssh_manager") as mock_mgr,
    ):
        mock_mgr.execute = AsyncMock(side_effect=[
            _ok_result("node1"), _ok_result("node2"),
        ])
        from vmcluster_mcp.exec import node_exec_all
        result: ToolResult = await node_exec_all.__wrapped__(
            "test-cluster", "echo hi", ["node1", "node2"]
        )

    assert result.success is True
    # Only 2 nodes executed
    assert mock_mgr.execute.call_count == 2
    assert len(result.result.results) == 2
    assert "node1" in result.result.results
    assert "node2" in result.result.results
    assert "node3" not in result.result.results


@pytest.mark.asyncio
async def test_node_exec_all_empty_nodes_targets_all():
    """Test 5: nodes=None or nodes=[] executes ALL nodes in topology."""
    with (
        patch("vmcluster_mcp.exec.load_topology", return_value=_make_topology()),
        patch("vmcluster_mcp.exec._ssh_manager") as mock_mgr,
    ):
        mock_mgr.execute = AsyncMock(side_effect=[
            _ok_result("node1"), _ok_result("node2"), _ok_result("node3"),
        ])
        from vmcluster_mcp.exec import node_exec_all
        result: ToolResult = await node_exec_all.__wrapped__("test-cluster", "echo hi", None)

    assert mock_mgr.execute.call_count == 3
    assert len(result.result.results) == 3


@pytest.mark.asyncio
async def test_node_exec_all_invalid_node_in_subset():
    """Test 6: Invalid node name in subset → success=False, NODE_NOT_FOUND."""
    with (
        patch("vmcluster_mcp.exec.load_topology", return_value=_make_topology()),
        patch("vmcluster_mcp.exec._ssh_manager"),
    ):
        from vmcluster_mcp.exec import node_exec_all
        result: ToolResult = await node_exec_all.__wrapped__(
            "test-cluster", "echo hi", ["node1", "ghost-node"]
        )

    assert result.success is False
    assert result.error.code == "NODE_NOT_FOUND"
    assert "ghost-node" in result.error.message


@pytest.mark.asyncio
async def test_node_exec_all_timed_out_node_in_failed_nodes():
    """Test 7: Node timing out → appears in failed_nodes with timed_out=True in its ExecResult."""
    with (
        patch("vmcluster_mcp.exec.load_topology", return_value=_make_topology()),
        patch("vmcluster_mcp.exec._ssh_manager") as mock_mgr,
    ):
        mock_mgr.execute = AsyncMock(side_effect=[
            _ok_result("node1"), _timeout_result(), _ok_result("node3"),
        ])
        from vmcluster_mcp.exec import node_exec_all
        result: ToolResult = await node_exec_all.__wrapped__(
            "test-cluster", "slow cmd", None, True
        )

    assert result.success is False
    assert "node2" in result.result.failed_nodes
    assert result.result.results["node2"].timed_out is True


@pytest.mark.asyncio
async def test_node_exec_all_uses_asyncio_gather():
    """Test 8: asyncio.gather is used for concurrent execution."""
    gather_calls = []

    original_gather = __import__("asyncio").gather

    async def mock_gather(*coros, **kwargs):
        gather_calls.append(len(coros))
        return await original_gather(*coros, **kwargs)

    with (
        patch("vmcluster_mcp.exec.load_topology", return_value=_make_topology()),
        patch("vmcluster_mcp.exec._ssh_manager") as mock_mgr,
        patch("vmcluster_mcp.exec.asyncio.gather", side_effect=mock_gather),
    ):
        mock_mgr.execute = AsyncMock(side_effect=[
            _ok_result("node1"), _ok_result("node2"), _ok_result("node3"),
        ])
        from vmcluster_mcp.exec import node_exec_all
        await node_exec_all.__wrapped__("test-cluster", "cmd")

    # asyncio.gather was called with all 3 tasks
    assert len(gather_calls) == 1
    assert gather_calls[0] == 3
