"""Unit tests for ScriptRunner (validation and path logic only)."""

from __future__ import annotations

from pathlib import Path

import pytest

from tui.services.script_runner import ScriptRunner


# ── _get_packer_file path mapping ──────────────────────────────────


class TestGetPackerFile:
    """Tests for _get_packer_file distro→path mapping."""

    @pytest.fixture(autouse=True)
    def _runner(self, tmp_packer_dir: Path) -> None:
        self.runner = ScriptRunner.__new__(ScriptRunner)
        self.runner.packer_dir = tmp_packer_dir
        self.runner.scripts_dir = tmp_packer_dir.parent / "scripts"
        self.runner.project_root = tmp_packer_dir.parent

    # Ubuntu
    def test_ubuntu_2204(self) -> None:
        p = self.runner._get_packer_file("ubuntu", "22.04")
        assert p == self.runner.packer_dir / "ubuntu" / "ubuntu-22.04.pkr.hcl"

    def test_ubuntu_2404(self) -> None:
        p = self.runner._get_packer_file("ubuntu", "24.04")
        assert p == self.runner.packer_dir / "ubuntu" / "ubuntu-24.04.pkr.hcl"

    def test_ubuntu_2404_desktop(self) -> None:
        p = self.runner._get_packer_file("ubuntu", "24.04-desktop")
        assert p == self.runner.packer_dir / "ubuntu" / "ubuntu-24.04-desktop.pkr.hcl"

    # Debian
    def test_debian_12(self) -> None:
        p = self.runner._get_packer_file("debian", "12")
        assert p == self.runner.packer_dir / "debian" / "debian-12.pkr.hcl"

    # RHEL family
    def test_rocky_9(self) -> None:
        p = self.runner._get_packer_file("rocky", "9")
        assert p == self.runner.packer_dir / "rhel-family" / "rocky-9.pkr.hcl"

    def test_almalinux_9(self) -> None:
        p = self.runner._get_packer_file("almalinux", "9")
        assert p == self.runner.packer_dir / "rhel-family" / "alma-9.pkr.hcl"

    def test_fedora_42(self) -> None:
        p = self.runner._get_packer_file("fedora", "42")
        assert p == self.runner.packer_dir / "rhel-family" / "fedora-42.pkr.hcl"

    def test_fedora_42_desktop(self) -> None:
        p = self.runner._get_packer_file("fedora", "42-desktop")
        assert p == self.runner.packer_dir / "rhel-family" / "fedora-42-desktop.pkr.hcl"

    # openSUSE
    def test_opensuse_156(self) -> None:
        p = self.runner._get_packer_file("opensuse", "15.6")
        assert p == self.runner.packer_dir / "suse" / "opensuse-leap-15.6.pkr.hcl"

    def test_opensuse_156_desktop(self) -> None:
        p = self.runner._get_packer_file("opensuse", "15.6-desktop")
        assert p == self.runner.packer_dir / "suse" / "opensuse-leap-15.6-desktop.pkr.hcl"

    # Unknown distro falls back to flat file
    def test_unknown_distro_fallback(self) -> None:
        p = self.runner._get_packer_file("gentoo", "2024")
        assert p == self.runner.packer_dir / "gentoo-2024.pkr.hcl"


# ── input validation via run_deploy_vm ─────────────────────────────


class TestVMNameValidation:
    """Tests that run_deploy_vm rejects invalid VM names."""

    @pytest.fixture(autouse=True)
    def _runner(self, tmp_path: Path) -> None:
        self.runner = ScriptRunner(project_root=tmp_path)
        scripts = tmp_path / "scripts"
        scripts.mkdir()
        (scripts / "deploy-vm.sh").write_text("#!/bin/bash\nexit 0\n")

    @pytest.mark.asyncio
    async def test_valid_vm_name(self) -> None:
        """Valid names should not fail validation (may fail on script execution)."""
        import re
        for name in ["my-vm", "vm_01", "test.server", "WebApp-v2"]:
            assert re.match(r'^[a-zA-Z0-9._-]+$', name), f"{name} should be valid"

    @pytest.mark.asyncio
    async def test_rejects_spaces(self) -> None:
        errors = []
        def capture(line: str) -> None:
            errors.append(line)

        rc = await self.runner.run_deploy_vm(
            template_id=9200, vm_id=100, vm_name="bad name",
            cores=2, memory=2048, disk_size="20G",
            network_mode="dhcp", clone_type="linked", playbooks=[],
            on_output=capture,
        )
        assert rc == 1
        assert any("Invalid VM name" in e for e in errors)

    @pytest.mark.asyncio
    async def test_rejects_semicolon(self) -> None:
        errors = []
        rc = await self.runner.run_deploy_vm(
            template_id=9200, vm_id=100, vm_name="vm;rm -rf /",
            cores=2, memory=2048, disk_size="20G",
            network_mode="dhcp", clone_type="linked", playbooks=[],
            on_output=lambda l: errors.append(l),
        )
        assert rc == 1

    @pytest.mark.asyncio
    async def test_rejects_path_traversal(self) -> None:
        errors = []
        rc = await self.runner.run_deploy_vm(
            template_id=9200, vm_id=100, vm_name="../../../etc/passwd",
            cores=2, memory=2048, disk_size="20G",
            network_mode="dhcp", clone_type="linked", playbooks=[],
            on_output=lambda l: errors.append(l),
        )
        assert rc == 1


# ── packer build input validation ──────────────────────────────────


class TestPackerBuildValidation:
    """Tests that run_packer_build rejects invalid distro/version names."""

    @pytest.fixture(autouse=True)
    def _runner(self, tmp_packer_dir: Path) -> None:
        self.runner = ScriptRunner.__new__(ScriptRunner)
        self.runner.packer_dir = tmp_packer_dir
        self.runner.scripts_dir = tmp_packer_dir.parent / "scripts"
        self.runner.project_root = tmp_packer_dir.parent

    @pytest.mark.asyncio
    async def test_rejects_invalid_distro(self) -> None:
        errors = []
        rc = await self.runner.run_packer_build(
            distro="ubuntu; rm -rf /", version="22.04",
            template_id=9200, template_name="test",
            cores=2, memory=2048, disk_size="10G",
            package_profile="base",
            on_output=lambda l: errors.append(l),
        )
        assert rc == 1
        assert any("Invalid distribution" in e for e in errors)

    @pytest.mark.asyncio
    async def test_rejects_invalid_version(self) -> None:
        errors = []
        rc = await self.runner.run_packer_build(
            distro="ubuntu", version="22.04; cat /etc/shadow",
            template_id=9200, template_name="test",
            cores=2, memory=2048, disk_size="10G",
            package_profile="base",
            on_output=lambda l: errors.append(l),
        )
        assert rc == 1
        assert any("Invalid version" in e for e in errors)

    @pytest.mark.asyncio
    async def test_rejects_packer_file_outside_dir(self) -> None:
        """Path traversal in distro/version should be caught."""
        errors = []
        rc = await self.runner.run_packer_build(
            distro="..--..--etc", version="passwd",
            template_id=9200, template_name="test",
            cores=2, memory=2048, disk_size="10G",
            package_profile="base",
            on_output=lambda l: errors.append(l),
        )
        # Should fail (file not found or invalid distro)
        assert rc == 1


# ── get_available_playbooks ────────────────────────────────────────


class TestGetAvailablePlaybooks:
    """Tests for the static playbook list."""

    def test_returns_list_of_tuples(self, tmp_path: Path) -> None:
        runner = ScriptRunner(project_root=tmp_path)
        playbooks = runner.get_available_playbooks()
        assert isinstance(playbooks, list)
        assert all(len(t) == 3 for t in playbooks)

    def test_contains_expected_playbooks(self, tmp_path: Path) -> None:
        runner = ScriptRunner(project_root=tmp_path)
        names = [name for name, _, _ in runner.get_available_playbooks()]
        assert "base-config" in names
        assert "docker-compose" in names
        assert "k3s" in names
        assert "k3s-cluster" in names
        assert "mssql" in names

    def test_base_config_is_default_selected(self, tmp_path: Path) -> None:
        runner = ScriptRunner(project_root=tmp_path)
        for name, _, default in runner.get_available_playbooks():
            if name == "base-config":
                assert default is True
                return
        pytest.fail("base-config not found")

    def test_no_duplicate_names(self, tmp_path: Path) -> None:
        runner = ScriptRunner(project_root=tmp_path)
        names = [name for name, _, _ in runner.get_available_playbooks()]
        assert len(names) == len(set(names))
