"""Unit tests for ArtifactInstaller: four install modes, fan-out, best-effort."""

from __future__ import annotations

from unittest.mock import AsyncMock

import pytest

from vmcluster_mcp.artifact.installer import ArtifactInstaller
from vmcluster_mcp.exec.ssh import SSHConnectionManager
from vmcluster_mcp.models import (
    BuildArtifact,
    ExecResult,
    InstallNodeResult,
    InstallStatus,
)


# -- Helpers --


def _exec_result(exit_code: int = 0, stdout: str = "", stderr: str = "") -> ExecResult:
    return ExecResult(
        exit_code=exit_code,
        stdout=stdout,
        stderr=stderr,
        duration_s=0.1,
        timed_out=False,
    )


def _make_artifact(kernel_version: str = "6.1.0", artifact_id: str = "abc123") -> BuildArtifact:
    return BuildArtifact(
        artifact_id=artifact_id,
        source_path="/build",
        build_type="raw_tree",
        kernel_version=kernel_version,
        modules=["lib/modules/mymod.ko"],
        userspace_bins=[],
        build_timestamp="2026-01-01T00:00:00",
    )


class _FakeNode:
    def __init__(self, name: str, ip: str) -> None:
        self.name = name
        self.ip = ip


class _FakeSsh:
    user = "root"
    key_path = "/key"


# -- modprobe_path tests --


async def test_modprobe_path_creates_correct_ssh_command():
    """modprobe_path mode builds the right SSH command including depmod -a."""
    ssh_manager = AsyncMock(spec=SSHConnectionManager)
    ssh_manager.execute = AsyncMock(
        return_value=_exec_result(
            exit_code=0,
            stdout="/lib/modules/6.1.0/extra/vmcluster/mymod.ko\n",
        )
    )
    installer = ArtifactInstaller(ssh_manager)
    result = await installer.install_node(
        ip="10.0.0.1",
        port=22,
        user="root",
        key_path="/key",
        artifact=_make_artifact(),
        dest_base="/opt/vmcluster/artifacts",
        install_mode="modprobe_path",
    )
    assert result.exit_code == 0
    cmd = ssh_manager.execute.call_args[0][4]  # 5th positional arg: command
    assert "depmod -a" in cmd
    assert "vmcluster" in cmd
    assert "6.1.0" in cmd


async def test_modprobe_path_returns_installed_module_paths():
    """modprobe_path parses find output as installed_module_paths."""
    ssh_manager = AsyncMock(spec=SSHConnectionManager)
    ssh_manager.execute = AsyncMock(
        return_value=_exec_result(
            exit_code=0,
            stdout=(
                "/lib/modules/6.1.0/extra/vmcluster/mymod.ko\n"
                "/lib/modules/6.1.0/extra/vmcluster/other.ko\n"
            ),
        )
    )
    installer = ArtifactInstaller(ssh_manager)
    result = await installer.install_node(
        ip="10.0.0.1",
        port=22,
        user="root",
        key_path="/key",
        artifact=_make_artifact(),
        dest_base="/opt/vmcluster/artifacts",
        install_mode="modprobe_path",
    )
    assert len(result.installed_module_paths) == 2
    assert "/lib/modules/6.1.0/extra/vmcluster/mymod.ko" in result.installed_module_paths


async def test_modprobe_path_depmod_output_from_stderr():
    """modprobe_path returns stderr as depmod_output."""
    ssh_manager = AsyncMock(spec=SSHConnectionManager)
    ssh_manager.execute = AsyncMock(
        return_value=_exec_result(
            exit_code=0,
            stdout="/lib/modules/6.1.0/extra/vmcluster/mymod.ko\n",
            stderr="depmod: WARNING: ...",
        )
    )
    installer = ArtifactInstaller(ssh_manager)
    result = await installer.install_node(
        ip="10.0.0.1",
        port=22,
        user="root",
        key_path="/key",
        artifact=_make_artifact(),
        dest_base="/opt/vmcluster/artifacts",
        install_mode="modprobe_path",
    )
    assert result.depmod_output == "depmod: WARNING: ..."


# -- make_install tests --


async def test_make_install_runs_correct_command():
    """make_install mode runs 'cd {dest_path} && make install' via SSH."""
    ssh_manager = AsyncMock(spec=SSHConnectionManager)
    ssh_manager.execute = AsyncMock(
        return_value=_exec_result(exit_code=0, stdout="make install output")
    )
    installer = ArtifactInstaller(ssh_manager)
    result = await installer.install_node(
        ip="10.0.0.1",
        port=22,
        user="root",
        key_path="/key",
        artifact=_make_artifact(),
        dest_base="/opt/vmcluster/artifacts",
        install_mode="make_install",
    )
    assert result.exit_code == 0
    cmd = ssh_manager.execute.call_args[0][4]
    assert "make install" in cmd
    assert "abc123" in cmd  # artifact_id in path


async def test_make_install_stdout_in_depmod_output():
    """make_install mode returns stdout as depmod_output, empty module paths."""
    ssh_manager = AsyncMock(spec=SSHConnectionManager)
    ssh_manager.execute = AsyncMock(
        return_value=_exec_result(exit_code=0, stdout="install log here")
    )
    installer = ArtifactInstaller(ssh_manager)
    result = await installer.install_node(
        ip="10.0.0.1",
        port=22,
        user="root",
        key_path="/key",
        artifact=_make_artifact(),
        dest_base="/opt/vmcluster/artifacts",
        install_mode="make_install",
    )
    assert result.depmod_output == "install log here"
    assert result.installed_module_paths == []


# -- rpm tests --


async def test_rpm_mode_runs_correct_command():
    """rpm mode runs 'rpm -ivh' on .rpm packages found in dest_path."""
    ssh_manager = AsyncMock(spec=SSHConnectionManager)
    ssh_manager.execute = AsyncMock(
        return_value=_exec_result(exit_code=0, stdout="installed")
    )
    installer = ArtifactInstaller(ssh_manager)
    result = await installer.install_node(
        ip="10.0.0.1",
        port=22,
        user="root",
        key_path="/key",
        artifact=_make_artifact(),
        dest_base="/opt/vmcluster/artifacts",
        install_mode="rpm",
    )
    assert result.exit_code == 0
    cmd = ssh_manager.execute.call_args[0][4]
    assert "rpm -ivh" in cmd
    assert ".rpm" in cmd


async def test_rpm_mode_non_zero_exit_when_no_packages():
    """rpm mode exits non-zero when no .rpm files are found."""
    ssh_manager = AsyncMock(spec=SSHConnectionManager)
    ssh_manager.execute = AsyncMock(
        return_value=_exec_result(exit_code=1, stdout="No .rpm files found")
    )
    installer = ArtifactInstaller(ssh_manager)
    result = await installer.install_node(
        ip="10.0.0.1",
        port=22,
        user="root",
        key_path="/key",
        artifact=_make_artifact(),
        dest_base="/opt/vmcluster/artifacts",
        install_mode="rpm",
    )
    assert result.exit_code == 1


# -- deb tests --


async def test_deb_mode_runs_correct_command():
    """deb mode runs 'dpkg -i' on .deb packages found in dest_path."""
    ssh_manager = AsyncMock(spec=SSHConnectionManager)
    ssh_manager.execute = AsyncMock(
        return_value=_exec_result(exit_code=0, stdout="installed")
    )
    installer = ArtifactInstaller(ssh_manager)
    result = await installer.install_node(
        ip="10.0.0.1",
        port=22,
        user="root",
        key_path="/key",
        artifact=_make_artifact(),
        dest_base="/opt/vmcluster/artifacts",
        install_mode="deb",
    )
    assert result.exit_code == 0
    cmd = ssh_manager.execute.call_args[0][4]
    assert "dpkg -i" in cmd
    assert ".deb" in cmd


async def test_deb_mode_non_zero_exit_when_no_packages():
    """deb mode exits non-zero when no .deb files are found."""
    ssh_manager = AsyncMock(spec=SSHConnectionManager)
    ssh_manager.execute = AsyncMock(
        return_value=_exec_result(exit_code=1, stdout="No .deb files found")
    )
    installer = ArtifactInstaller(ssh_manager)
    result = await installer.install_node(
        ip="10.0.0.1",
        port=22,
        user="root",
        key_path="/key",
        artifact=_make_artifact(),
        dest_base="/opt/vmcluster/artifacts",
        install_mode="deb",
    )
    assert result.exit_code == 1


# -- install_all / asyncio.gather fan-out tests --


async def test_install_all_fans_out_to_all_nodes():
    """install_all fans out to all target nodes via asyncio.gather."""
    ssh_manager = AsyncMock(spec=SSHConnectionManager)
    ssh_manager.execute = AsyncMock(
        return_value=_exec_result(exit_code=0, stdout="")
    )
    installer = ArtifactInstaller(ssh_manager)
    nodes = [_FakeNode("node1", "10.0.0.1"), _FakeNode("node2", "10.0.0.2")]
    status = await installer.install_all(
        nodes=nodes,
        ssh_spec=_FakeSsh(),
        artifact=_make_artifact(),
        dest_base="/opt/vmcluster/artifacts",
        install_mode="make_install",
    )
    assert isinstance(status, InstallStatus)
    assert "node1" in status.per_node
    assert "node2" in status.per_node
    assert status.per_node["node1"].exit_code == 0
    assert status.per_node["node2"].exit_code == 0


async def test_install_all_per_node_failure_does_not_abort_others():
    """Per-node failure does not abort other nodes (best-effort D-09)."""
    ssh_manager = AsyncMock(spec=SSHConnectionManager)
    call_count = 0

    async def _selective(*args, **kwargs):
        nonlocal call_count
        call_count += 1
        if call_count == 1:
            raise RuntimeError("SSH failure")
        return _exec_result(exit_code=0, stdout="ok")

    ssh_manager.execute = AsyncMock(side_effect=_selective)
    installer = ArtifactInstaller(ssh_manager)
    nodes = [_FakeNode("fail_node", "10.0.0.1"), _FakeNode("ok_node", "10.0.0.2")]
    status = await installer.install_all(
        nodes=nodes,
        ssh_spec=_FakeSsh(),
        artifact=_make_artifact(),
        dest_base="/opt/vmcluster/artifacts",
        install_mode="make_install",
    )
    assert "fail_node" in status.per_node
    assert "ok_node" in status.per_node
    assert status.per_node["fail_node"].exit_code == -1
    assert status.per_node["ok_node"].exit_code == 0


async def test_install_node_non_zero_exit_code():
    """Non-zero exit code from SSH is reflected in InstallNodeResult.exit_code."""
    ssh_manager = AsyncMock(spec=SSHConnectionManager)
    ssh_manager.execute = AsyncMock(
        return_value=_exec_result(exit_code=2, stderr="make: error")
    )
    installer = ArtifactInstaller(ssh_manager)
    result = await installer.install_node(
        ip="10.0.0.1",
        port=22,
        user="root",
        key_path="/key",
        artifact=_make_artifact(),
        dest_base="/opt/vmcluster/artifacts",
        install_mode="make_install",
    )
    assert result.exit_code == 2


async def test_install_all_result_node_names_match_node_names():
    """install_all per_node keys use node.name, not IP address."""
    ssh_manager = AsyncMock(spec=SSHConnectionManager)
    ssh_manager.execute = AsyncMock(
        return_value=_exec_result(exit_code=0, stdout="")
    )
    installer = ArtifactInstaller(ssh_manager)
    nodes = [_FakeNode("worker-1", "192.168.1.10")]
    status = await installer.install_all(
        nodes=nodes,
        ssh_spec=_FakeSsh(),
        artifact=_make_artifact(),
        dest_base="/opt/vmcluster/artifacts",
        install_mode="make_install",
    )
    assert "worker-1" in status.per_node
    assert "192.168.1.10" not in status.per_node
