"""Tests for vagrant_preflight MCP tool.

Tests: vagrant_preflight (all 10 checks).
How: Patch asyncio.create_subprocess_exec and filesystem so tests run offline.
Why: CI environments lack vagrant/VBoxManage/Vagrantfiles; tool must return
     correct VagrantPreflightResult models in every scenario.
"""

from __future__ import annotations

from typing import TYPE_CHECKING
from unittest.mock import AsyncMock, MagicMock, patch

from mcp_vm_blackbox.models import PreflightCheck, VagrantPreflightResult
from mcp_vm_blackbox.vagrant_preflight import (
    _check_private_network_ips,
    _check_provisioner_scripts,
    _check_synced_folders,
    _check_vagrant_validate,
    _check_vagrantfile_exists,
    _check_virtualbox_provider,
    _check_vm_names,
    _check_winrm_communicators,
    _overall,
    _run_cmd,
    _run_static_checks,
    vagrant_preflight,
)

if TYPE_CHECKING:
    from pathlib import Path


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

_MINIMAL_VAGRANTFILE = """\
Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/focal64"
  config.vm.provider "virtualbox" do |vb|
    vb.name = "my-ubuntu-vm"
  end
  config.vm.network "private_network", ip: "192.168.56.10"
  config.vm.synced_folder ".", "/vagrant"
end
"""

_WINDOWS_VAGRANTFILE = """\
Vagrant.configure("2") do |config|
  config.vm.box = "gusztavvargadr/windows-10"
  config.vm.communicator = "winrm"
  config.vm.provider "virtualbox" do |vb|
    vb.name = "win10-test"
  end
  config.vm.network "private_network", ip: "192.168.56.20"
end
"""

_WINDOWS_NO_WINRM_VAGRANTFILE = """\
Vagrant.configure("2") do |config|
  config.vm.box = "gusztavvargadr/windows-10"
  config.vm.provider "virtualbox" do |vb|
    vb.name = "win10-test"
  end
end
"""


def _make_proc(rc: int = 0, stdout: str = "", stderr: str = "") -> MagicMock:
    """Build a mock asyncio subprocess result."""
    proc = MagicMock()
    proc.returncode = rc
    proc.communicate = AsyncMock(return_value=(stdout.encode(), stderr.encode()))
    return proc


# ---------------------------------------------------------------------------
# _overall
# ---------------------------------------------------------------------------


def test_overall_all_pass_returns_pass() -> None:
    checks = [
        PreflightCheck(name="a", status="pass", message="ok"),
        PreflightCheck(name="b", status="pass", message="ok"),
    ]
    assert _overall(checks) == "pass"


def test_overall_any_warn_no_fail_returns_warn() -> None:
    checks = [
        PreflightCheck(name="a", status="pass", message="ok"),
        PreflightCheck(name="b", status="warn", message="warning"),
    ]
    assert _overall(checks) == "warn"


def test_overall_any_fail_returns_fail() -> None:
    checks = [
        PreflightCheck(name="a", status="warn", message="warning"),
        PreflightCheck(name="b", status="fail", message="failed"),
    ]
    assert _overall(checks) == "fail"


def test_overall_skip_only_returns_pass() -> None:
    checks = [PreflightCheck(name="a", status="skip", message="skipped")]
    assert _overall(checks) == "pass"


# ---------------------------------------------------------------------------
# _check_vagrantfile_exists
# ---------------------------------------------------------------------------


def test_check_vagrantfile_exists_when_file_present_returns_pass(tmp_path: Path) -> None:
    vf = tmp_path / "Vagrantfile"
    vf.write_text("Vagrant.configure(2)")
    result = _check_vagrantfile_exists(vf)
    assert result.status == "pass"
    assert "found" in result.message


def test_check_vagrantfile_exists_when_missing_returns_fail(tmp_path: Path) -> None:
    vf = tmp_path / "Vagrantfile"
    result = _check_vagrantfile_exists(vf)
    assert result.status == "fail"
    assert "not found" in result.message


# ---------------------------------------------------------------------------
# _run_cmd
# ---------------------------------------------------------------------------


async def test_run_cmd_success_returns_zero_and_stdout() -> None:
    proc = _make_proc(rc=0, stdout="Vagrant 2.3.4\n")
    with patch("asyncio.create_subprocess_exec", return_value=proc):
        rc, stdout, _stderr = await _run_cmd("vagrant", "--version")
    assert rc == 0
    assert "Vagrant 2.3.4" in stdout


async def test_run_cmd_file_not_found_returns_minus_one() -> None:
    with patch("asyncio.create_subprocess_exec", side_effect=FileNotFoundError("no such file")):
        rc, _stdout, stderr = await _run_cmd("nonexistent-tool")
    assert rc == -1
    assert "command not found" in stderr


# ---------------------------------------------------------------------------
# _check_virtualbox_provider
# ---------------------------------------------------------------------------


def test_check_virtualbox_provider_present_returns_pass() -> None:
    result = _check_virtualbox_provider('config.vm.provider "virtualbox" do |vb|')
    assert result.status == "pass"


def test_check_virtualbox_provider_absent_returns_warn() -> None:
    result = _check_virtualbox_provider('config.vm.provider "vmware_desktop" do |v|')
    assert result.status == "warn"


# ---------------------------------------------------------------------------
# _check_vm_names
# ---------------------------------------------------------------------------


def test_check_vm_names_found_returns_pass_and_names() -> None:
    text = "vb.name = \"my-vm\"\nvb.name = 'second-vm'"
    check, names = _check_vm_names(text)
    assert check.status == "pass"
    assert names == ["my-vm", "second-vm"]


def test_check_vm_names_none_found_returns_fail_and_empty_list() -> None:
    check, names = _check_vm_names("Vagrant.configure('2') do |config| end")
    assert check.status == "fail"
    assert names == []
    assert "vb.name" in check.message


# ---------------------------------------------------------------------------
# _check_winrm_communicators
# ---------------------------------------------------------------------------


def test_check_winrm_no_windows_box_returns_pass() -> None:
    result = _check_winrm_communicators('config.vm.box = "ubuntu/focal64"')
    assert result.status == "pass"
    assert "skipped" in result.message


def test_check_winrm_windows_box_with_communicator_returns_pass() -> None:
    result = _check_winrm_communicators(_WINDOWS_VAGRANTFILE)
    assert result.status == "pass"


def test_check_winrm_windows_box_without_communicator_returns_fail() -> None:
    result = _check_winrm_communicators(_WINDOWS_NO_WINRM_VAGRANTFILE)
    assert result.status == "fail"
    assert "WinRM" in result.message or "winrm" in result.message.lower()


def test_check_winrm_gusztavvargadr_box_detected_as_windows() -> None:
    text = 'config.vm.box = "gusztavvargadr/windows-server-2022-standard"'
    result = _check_winrm_communicators(text)
    assert result.status == "fail"


# ---------------------------------------------------------------------------
# _check_private_network_ips
# ---------------------------------------------------------------------------


def test_check_private_network_ips_present_returns_pass() -> None:
    text = 'config.vm.network "private_network", ip: "192.168.56.10"'
    result = _check_private_network_ips(text)
    assert result.status == "pass"


def test_check_private_network_ips_absent_returns_warn() -> None:
    result = _check_private_network_ips("Vagrant.configure('2') do |config| end")
    assert result.status == "warn"


# ---------------------------------------------------------------------------
# _check_synced_folders
# ---------------------------------------------------------------------------


def test_check_synced_folders_present_returns_pass() -> None:
    text = 'config.vm.synced_folder ".", "/vagrant"'
    result = _check_synced_folders(text)
    assert result.status == "pass"


def test_check_synced_folders_absent_returns_warn() -> None:
    result = _check_synced_folders("Vagrant.configure('2') do |config| end")
    assert result.status == "warn"


# ---------------------------------------------------------------------------
# _check_provisioner_scripts
# ---------------------------------------------------------------------------


def test_check_provisioner_scripts_all_exist_returns_pass(tmp_path: Path) -> None:
    script = tmp_path / "provision.sh"
    script.write_text("#!/bin/bash\necho hello")
    text = 'config.vm.provision "shell", path: "provision.sh"'
    result = _check_provisioner_scripts(text, tmp_path)
    assert result.status == "pass"


def test_check_provisioner_scripts_missing_file_returns_fail(tmp_path: Path) -> None:
    text = 'config.vm.provision "shell", path: "missing.sh"'
    result = _check_provisioner_scripts(text, tmp_path)
    assert result.status == "fail"
    assert "missing.sh" in result.message


def test_check_provisioner_scripts_no_paths_returns_pass(tmp_path: Path) -> None:
    text = 'config.vm.provision "shell", inline: "echo hello"'
    result = _check_provisioner_scripts(text, tmp_path)
    assert result.status == "pass"
    assert "No shell provisioner" in result.message


# ---------------------------------------------------------------------------
# _check_vagrant_validate
# ---------------------------------------------------------------------------


async def test_check_vagrant_validate_success_returns_pass(tmp_path: Path) -> None:
    proc = _make_proc(rc=0, stdout="Vagrantfile validated successfully.")
    with patch("asyncio.create_subprocess_exec", return_value=proc):
        result = await _check_vagrant_validate(tmp_path)
    assert result.status == "pass"


async def test_check_vagrant_validate_failure_returns_fail(tmp_path: Path) -> None:
    proc = _make_proc(rc=1, stderr="There was an error in your Vagrantfile")
    with patch("asyncio.create_subprocess_exec", return_value=proc):
        result = await _check_vagrant_validate(tmp_path)
    assert result.status == "fail"
    assert result.detail is not None


async def test_check_vagrant_validate_no_cli_returns_skip(tmp_path: Path) -> None:
    with patch("asyncio.create_subprocess_exec", side_effect=FileNotFoundError):
        result = await _check_vagrant_validate(tmp_path)
    assert result.status == "skip"


# ---------------------------------------------------------------------------
# _run_static_checks
# ---------------------------------------------------------------------------


def test_run_static_checks_minimal_vagrantfile_returns_six_checks(tmp_path: Path) -> None:
    checks, vm_names = _run_static_checks(_MINIMAL_VAGRANTFILE, tmp_path)
    assert len(checks) == 6
    assert vm_names == ["my-ubuntu-vm"]


def test_run_static_checks_check_names_are_distinct(tmp_path: Path) -> None:
    checks, _ = _run_static_checks(_MINIMAL_VAGRANTFILE, tmp_path)
    names = [c.name for c in checks]
    assert len(names) == len(set(names))


# ---------------------------------------------------------------------------
# vagrant_preflight (integration — full tool)
# ---------------------------------------------------------------------------


async def test_vagrant_preflight_missing_vagrantfile_returns_fail(tmp_path: Path) -> None:
    with patch("mcp_vm_blackbox.vagrant_preflight.effective_root", return_value=str(tmp_path)):
        result = await vagrant_preflight(vagrantfile_dir=str(tmp_path))

    assert isinstance(result, VagrantPreflightResult)
    assert result.overall == "fail"
    assert result.error is not None
    assert len(result.checks) == 1
    assert result.checks[0].name == "vagrantfile_exists"


async def test_vagrant_preflight_good_vagrantfile_returns_pass(tmp_path: Path) -> None:
    (tmp_path / "Vagrantfile").write_text(_MINIMAL_VAGRANTFILE)

    proc_vagrant = _make_proc(rc=0, stdout="Vagrant 2.3.7")
    proc_vbox = _make_proc(rc=0, stdout="7.0.14r161095")
    proc_validate = _make_proc(rc=0, stdout="Vagrantfile validated successfully.")

    call_count = 0

    def _fake_exec(*args: object, **kwargs: object) -> MagicMock:
        nonlocal call_count
        call_count += 1
        cmd = str(args[0]) if args else ""
        if "vboxmanage" in cmd.lower():
            return proc_vbox
        if call_count <= 2:
            return proc_vagrant
        return proc_validate

    with (
        patch("mcp_vm_blackbox.vagrant_preflight.effective_root", return_value=str(tmp_path)),
        patch("asyncio.create_subprocess_exec", side_effect=_fake_exec),
    ):
        result = await vagrant_preflight(vagrantfile_dir=str(tmp_path))

    assert isinstance(result, VagrantPreflightResult)
    assert result.vagrantfile_path is not None
    assert result.vm_names == ["my-ubuntu-vm"]
    assert result.vagrant_version is not None
    assert result.vboxmanage_version is not None
    assert len(result.checks) == 10


async def test_vagrant_preflight_no_cli_tools_returns_warn(tmp_path: Path) -> None:
    (tmp_path / "Vagrantfile").write_text(_MINIMAL_VAGRANTFILE)

    with (
        patch("mcp_vm_blackbox.vagrant_preflight.effective_root", return_value=str(tmp_path)),
        patch("asyncio.create_subprocess_exec", side_effect=FileNotFoundError("not found")),
    ):
        result = await vagrant_preflight(vagrantfile_dir=str(tmp_path))

    assert isinstance(result, VagrantPreflightResult)
    assert result.overall in ("warn", "fail")
    vagrant_cli_check = next((c for c in result.checks if c.name == "vagrant_cli"), None)
    assert vagrant_cli_check is not None
    assert vagrant_cli_check.status == "warn"


async def test_vagrant_preflight_windows_no_winrm_returns_fail(tmp_path: Path) -> None:
    (tmp_path / "Vagrantfile").write_text(_WINDOWS_NO_WINRM_VAGRANTFILE)

    with (
        patch("mcp_vm_blackbox.vagrant_preflight.effective_root", return_value=str(tmp_path)),
        patch("asyncio.create_subprocess_exec", side_effect=FileNotFoundError("not found")),
    ):
        result = await vagrant_preflight(vagrantfile_dir=str(tmp_path))

    assert result.overall == "fail"
    winrm_check = next((c for c in result.checks if c.name == "winrm_communicator"), None)
    assert winrm_check is not None
    assert winrm_check.status == "fail"


async def test_vagrant_preflight_default_dir_uses_effective_root(tmp_path: Path) -> None:
    (tmp_path / "Vagrantfile").write_text(_MINIMAL_VAGRANTFILE)

    with (
        patch("mcp_vm_blackbox.vagrant_preflight.effective_root", return_value=str(tmp_path)) as mock_root,
        patch("asyncio.create_subprocess_exec", side_effect=FileNotFoundError),
    ):
        await vagrant_preflight(vagrantfile_dir="")

    mock_root.assert_called_once_with(None)
