"""Tests for cross-platform bulletproofing.

Validates:
- Multi-GPU VRAM calculation (max single GPU, not sum)
- Apple Silicon unified memory offset (4 GB OS overhead)
- GPUInfo.effective_vram_mb computed property
- HardwareProfile.largest_gpu_vram_mb computed property
- NVIDIA driver staleness detection (numeric comparison)
- ROCm version parsing and staleness detection
- _select_model_for_hardware() uses largest single GPU
"""

from __future__ import annotations

from unittest.mock import patch

import pytest

from llmhosts.discovery.models import GPUInfo, HardwareProfile
from llmhosts.discovery.provisioner import (
    EngineProvisioner,
    _check_nvidia_driver,
    _check_rocm_version,
    _parse_nvidia_driver_major,
    _parse_rocm_version,
    _select_model_for_hardware,
)
from llmhosts.progress import NullProgressReporter

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


def _hw_multi_gpu(gpus: list[GPUInfo], ram_gb: float = 32.0) -> HardwareProfile:
    """Build a HardwareProfile with multiple GPUs."""
    return HardwareProfile(ram_total_gb=ram_gb, gpus=gpus)


def _nvidia_gpu(name: str, vram_mb: int, driver: str = "560.35.03") -> GPUInfo:
    return GPUInfo(name=name, vram_total_mb=vram_mb, gpu_type="nvidia", driver_version=driver)


def _amd_gpu(name: str, vram_mb: int) -> GPUInfo:
    return GPUInfo(name=name, vram_total_mb=vram_mb, gpu_type="amd", driver_version="")


def _apple_gpu(name: str, vram_mb: int) -> GPUInfo:
    return GPUInfo(name=name, vram_total_mb=vram_mb, gpu_type="apple_silicon", driver_version="")


# ===========================================================================
# 2a. Multi-GPU VRAM calculation
# ===========================================================================


@pytest.mark.unit
class TestMultiGPUVRAM:
    """Verify that model selection uses the largest single GPU, not total VRAM."""

    def test_single_gpu_uses_its_vram(self) -> None:
        """With one GPU, largest_gpu_vram_mb equals that GPU's VRAM."""
        hw = _hw_multi_gpu([_nvidia_gpu("RTX 4090", 24576)])
        assert hw.largest_gpu_vram_mb == 24576
        assert hw.total_vram_mb == 24576

    def test_two_identical_gpus(self) -> None:
        """Two identical GPUs: largest = single GPU VRAM, total = 2x."""
        hw = _hw_multi_gpu(
            [
                _nvidia_gpu("RTX 4090", 24576),
                _nvidia_gpu("RTX 4090", 24576),
            ]
        )
        assert hw.largest_gpu_vram_mb == 24576
        assert hw.total_vram_mb == 49152  # sum = 2x
        # Model should be selected based on single GPU (24GB -> qwen3:32b)
        model, _ = _select_model_for_hardware(hw)
        assert model == "qwen3:32b"

    def test_two_different_gpus_uses_largest(self) -> None:
        """Two different GPUs: should use the larger one."""
        hw = _hw_multi_gpu(
            [
                _nvidia_gpu("RTX 4090", 24576),
                _nvidia_gpu("RTX 3060", 12288),
            ]
        )
        assert hw.largest_gpu_vram_mb == 24576
        model, _ = _select_model_for_hardware(hw)
        assert model == "qwen3:32b"

    def test_four_gpus_model_based_on_largest_single(self) -> None:
        """Four GPUs with varying sizes: model based on largest single GPU."""
        hw = _hw_multi_gpu(
            [
                _nvidia_gpu("A100", 81920),  # 80 GB
                _nvidia_gpu("A100", 81920),  # 80 GB
                _nvidia_gpu("RTX 4090", 24576),
                _nvidia_gpu("RTX 3060", 12288),
            ]
        )
        # Total = 200704 MB (~196 GB), but largest single = 80 GB
        assert hw.largest_gpu_vram_mb == 81920
        assert hw.total_vram_mb == 200704
        model, _ = _select_model_for_hardware(hw)
        assert model == "llama3.3:70b"  # 80 GB > 40 GB threshold

    def test_multi_gpu_sum_would_overprovision(self) -> None:
        """Demonstrate the bug fix: sum would suggest a model too large for any single GPU."""
        hw = _hw_multi_gpu(
            [
                _nvidia_gpu("RTX 3060", 12288),  # 12 GB each
                _nvidia_gpu("RTX 3060", 12288),
                _nvidia_gpu("RTX 3060", 12288),
                _nvidia_gpu("RTX 3060", 12288),
            ]
        )
        # Sum = 48 GB -> would pick llama3.3:70b (needs 40+ GB single GPU)
        # Max = 12 GB -> correctly picks qwen3:14b (10-16 GB tier)
        assert hw.total_vram_mb == 49152
        assert hw.largest_gpu_vram_mb == 12288
        model, _ = _select_model_for_hardware(hw)
        assert model == "qwen3:14b"  # NOT llama3.3:70b

    def test_no_gpu_fallback_to_ram(self) -> None:
        """Without GPU, should use RAM/2."""
        hw = _hw_multi_gpu([], ram_gb=32.0)
        assert hw.largest_gpu_vram_mb == 0
        model, _ = _select_model_for_hardware(hw)
        assert model == "gpt-oss:20b"  # 32/2 = 16 GB effective


# ===========================================================================
# 2b. Apple Silicon unified memory offset
# ===========================================================================


@pytest.mark.unit
class TestAppleSiliconOffset:
    """Apple Silicon: effective VRAM = total - 4096 MB (OS overhead)."""

    def test_effective_vram_subtracts_4gb(self) -> None:
        """Apple Silicon GPU should report 4 GB less effective VRAM."""
        gpu = _apple_gpu("Apple M2 Pro", 32768)  # 32 GB unified memory
        assert gpu.effective_vram_mb == 32768 - 4096  # 28672 MB
        assert gpu.vram_total_mb == 32768  # raw is unchanged

    def test_effective_vram_nvidia_no_offset(self) -> None:
        """NVIDIA GPUs should NOT have the 4 GB offset."""
        gpu = _nvidia_gpu("RTX 4090", 24576)
        assert gpu.effective_vram_mb == 24576

    def test_effective_vram_amd_no_offset(self) -> None:
        """AMD GPUs should NOT have the 4 GB offset."""
        gpu = _amd_gpu("RX 7900 XTX", 24576)
        assert gpu.effective_vram_mb == 24576

    def test_apple_silicon_small_memory_floor_zero(self) -> None:
        """Apple Silicon with <= 4 GB should floor at 0."""
        gpu = _apple_gpu("Apple M1", 4096)  # 4 GB unified
        assert gpu.effective_vram_mb == 0

    def test_apple_silicon_model_selection(self) -> None:
        """Apple Silicon 16 GB: effective = 12 GB -> should pick qwen3:14b."""
        hw = _hw_multi_gpu([_apple_gpu("Apple M2", 16384)])  # 16 GB
        # effective = 16384 - 4096 = 12288 MB = 12 GB
        assert hw.largest_gpu_vram_mb == 12288
        model, _ = _select_model_for_hardware(hw)
        assert model == "qwen3:14b"  # 10-16 GB tier

    def test_apple_silicon_32gb_model_selection(self) -> None:
        """Apple Silicon 32 GB: effective = 28 GB -> should pick qwen3:32b."""
        hw = _hw_multi_gpu([_apple_gpu("Apple M2 Max", 32768)])  # 32 GB
        # effective = 32768 - 4096 = 28672 MB = 28 GB
        assert hw.largest_gpu_vram_mb == 28672
        model, _ = _select_model_for_hardware(hw)
        assert model == "qwen3:32b"  # 22-40 GB tier

    def test_apple_silicon_8gb_model_selection(self) -> None:
        """Apple Silicon 8 GB: effective = 4 GB -> should pick qwen3:4b."""
        hw = _hw_multi_gpu([_apple_gpu("Apple M1", 8192)])  # 8 GB
        # effective = 8192 - 4096 = 4096 MB = 4 GB
        assert hw.largest_gpu_vram_mb == 4096
        model, _ = _select_model_for_hardware(hw)
        assert model == "qwen3:4b"  # 4-7 GB tier


# ===========================================================================
# 2e. Driver staleness detection
# ===========================================================================


@pytest.mark.unit
class TestNVIDIADriverVersionParsing:
    """Parse NVIDIA driver version strings into major version numbers."""

    @pytest.mark.parametrize(
        "version_str,expected",
        [
            ("560.35.03", 560),
            ("560.94", 560),
            ("560", 560),
            ("535.183.01", 535),
            ("470.239.06", 470),
            ("  560.35.03  ", 560),  # whitespace
        ],
    )
    def test_parse_valid_versions(self, version_str: str, expected: int) -> None:
        assert _parse_nvidia_driver_major(version_str) == expected

    @pytest.mark.parametrize(
        "version_str",
        [
            "",
            "unknown",
            "abc.def",
            "N/A",
        ],
    )
    def test_parse_invalid_versions(self, version_str: str) -> None:
        assert _parse_nvidia_driver_major(version_str) is None


@pytest.mark.unit
class TestNVIDIADriverStaleness:
    """Verify NVIDIA driver staleness recommendations."""

    def test_current_driver_no_warning(self) -> None:
        """Driver >= 560 should produce no recommendations."""
        recs = _check_nvidia_driver("560.35.03", "linux")
        assert len(recs) == 0

    def test_new_driver_no_warning(self) -> None:
        """Driver newer than minimum should produce no recommendations."""
        recs = _check_nvidia_driver("570.10.01", "linux")
        assert len(recs) == 0

    def test_old_driver_linux_warning(self) -> None:
        """Old driver on Linux should produce apt install recommendation."""
        recs = _check_nvidia_driver("535.183.01", "linux")
        assert len(recs) == 1
        assert "535.183.01" in recs[0]
        assert "560" in recs[0]
        assert "apt install" in recs[0].lower() or "nvidia-driver" in recs[0].lower()

    def test_old_driver_windows_warning(self) -> None:
        """Old driver on Windows should link to nvidia.com/download."""
        recs = _check_nvidia_driver("535.183.01", "windows")
        assert len(recs) == 1
        assert "nvidia.com/download" in recs[0].lower()

    def test_very_old_driver(self) -> None:
        """Very old driver should still produce a warning."""
        recs = _check_nvidia_driver("470.239.06", "linux")
        assert len(recs) == 1
        assert "470.239.06" in recs[0]

    def test_unparseable_driver_no_crash(self) -> None:
        """Unparseable driver version should return empty, not crash."""
        recs = _check_nvidia_driver("unknown", "linux")
        assert len(recs) == 0


@pytest.mark.unit
class TestGPUDriverRecommendationsIntegrated:
    """Test the full gpu_driver_recommendations() static method."""

    def test_nvidia_stale_driver_detected(self) -> None:
        """With an old NVIDIA driver, should get a recommendation."""
        hw = _hw_multi_gpu([_nvidia_gpu("RTX 3060", 12288, driver="535.183.01")])
        with patch("platform.system", return_value="Linux"):
            recs = EngineProvisioner.gpu_driver_recommendations(hw)
        assert len(recs) >= 1
        assert "535.183.01" in recs[0]

    def test_nvidia_current_driver_no_rec(self) -> None:
        """With a current NVIDIA driver, should get no recommendations."""
        hw = _hw_multi_gpu([_nvidia_gpu("RTX 4090", 24576, driver="560.35.03")])
        with patch("platform.system", return_value="Linux"):
            recs = EngineProvisioner.gpu_driver_recommendations(hw)
        assert len(recs) == 0

    def test_amd_gpu_checks_rocm(self) -> None:
        """AMD GPU should trigger ROCm version check."""
        hw = _hw_multi_gpu([_amd_gpu("RX 7900 XTX", 24576)])
        with (
            patch("platform.system", return_value="Linux"),
            patch(
                "llmhosts.discovery.provisioner._read_rocm_version_file",
                return_value="5.7.0",
            ),
        ):
            recs = EngineProvisioner.gpu_driver_recommendations(hw)
        assert len(recs) >= 1
        assert "5.7.0" in recs[0]
        assert "6.4" in recs[0]

    def test_amd_gpu_rocm_current(self) -> None:
        """AMD GPU with current ROCm should get no recommendations."""
        hw = _hw_multi_gpu([_amd_gpu("RX 7900 XTX", 24576)])
        with (
            patch("platform.system", return_value="Linux"),
            patch(
                "llmhosts.discovery.provisioner._read_rocm_version_file",
                return_value="6.4.0",
            ),
        ):
            recs = EngineProvisioner.gpu_driver_recommendations(hw)
        assert len(recs) == 0

    def test_amd_gpu_no_rocm_installed(self) -> None:
        """AMD GPU without ROCm should suggest install."""
        hw = _hw_multi_gpu([_amd_gpu("RX 7900 XTX", 24576)])
        with (
            patch("platform.system", return_value="Linux"),
            patch("llmhosts.discovery.provisioner._read_rocm_version_file", return_value=None),
            patch("shutil.which", return_value=None),
        ):
            recs = EngineProvisioner.gpu_driver_recommendations(hw)
        assert len(recs) >= 1
        assert "rocm" in recs[0].lower()

    def test_apple_silicon_no_driver_rec(self) -> None:
        """Apple Silicon should get no driver recommendations."""
        hw = _hw_multi_gpu([_apple_gpu("Apple M2 Pro", 32768)])
        with patch("platform.system", return_value="Darwin"):
            recs = EngineProvisioner.gpu_driver_recommendations(hw)
        assert len(recs) == 0

    def test_no_gpu_linux(self) -> None:
        """No GPU on Linux should suggest NVIDIA and AMD drivers."""
        hw = _hw_multi_gpu([])
        with patch("platform.system", return_value="Linux"):
            recs = EngineProvisioner.gpu_driver_recommendations(hw)
        assert len(recs) >= 2
        combined = " ".join(recs).lower()
        assert "nvidia" in combined
        assert "rocm" in combined or "amd" in combined


# ===========================================================================
# ROCm version parsing
# ===========================================================================


@pytest.mark.unit
class TestROCmVersionParsing:
    """Parse ROCm version strings."""

    @pytest.mark.parametrize(
        "version_str,expected",
        [
            ("6.4.0", (6, 4)),
            ("6.4", (6, 4)),
            ("5.7.0", (5, 7)),
            ("6.0.2", (6, 0)),
            ("  6.4.0  ", (6, 4)),
        ],
    )
    def test_parse_valid(self, version_str: str, expected: tuple[int, int]) -> None:
        assert _parse_rocm_version(version_str) == expected

    @pytest.mark.parametrize(
        "version_str",
        [
            "",
            "unknown",
            "abc",
        ],
    )
    def test_parse_invalid(self, version_str: str) -> None:
        assert _parse_rocm_version(version_str) is None


@pytest.mark.unit
class TestROCmStaleness:
    """Verify ROCm version staleness detection."""

    def test_old_rocm_warns(self) -> None:
        """ROCm < 6.4 should produce a warning."""
        with patch("llmhosts.discovery.provisioner._read_rocm_version_file", return_value="5.7.0"):
            recs = _check_rocm_version("linux")
        assert len(recs) == 1
        assert "5.7.0" in recs[0]
        assert "6.4" in recs[0]

    def test_current_rocm_no_warning(self) -> None:
        """ROCm >= 6.4 should not produce a warning."""
        with patch("llmhosts.discovery.provisioner._read_rocm_version_file", return_value="6.4.0"):
            recs = _check_rocm_version("linux")
        assert len(recs) == 0

    def test_newer_rocm_no_warning(self) -> None:
        """ROCm > 6.4 should not produce a warning."""
        with patch("llmhosts.discovery.provisioner._read_rocm_version_file", return_value="7.0.1"):
            recs = _check_rocm_version("linux")
        assert len(recs) == 0

    def test_no_rocm_file_no_rocmsmi(self) -> None:
        """No ROCm version file and no rocm-smi: suggest install on Linux."""
        with (
            patch("llmhosts.discovery.provisioner._read_rocm_version_file", return_value=None),
            patch("shutil.which", return_value=None),
        ):
            recs = _check_rocm_version("linux")
        assert len(recs) == 1
        assert "install" in recs[0].lower()

    def test_no_rocm_file_but_rocmsmi_exists(self) -> None:
        """rocm-smi exists but no version file: can't determine version, no rec."""
        with (
            patch("llmhosts.discovery.provisioner._read_rocm_version_file", return_value=None),
            patch("shutil.which", return_value="/usr/bin/rocm-smi"),
        ):
            recs = _check_rocm_version("linux")
        assert len(recs) == 0


# ===========================================================================
# No "Ollama" in customer-facing messages
# ===========================================================================


_FORBIDDEN_STRINGS = ["ollama", "Ollama", "winget", "qwen", "Qwen"]


@pytest.mark.unit
class TestNoInternalNamesInMessages:
    """Ensure no internal implementation names leak to customers."""

    async def test_provision_with_multi_gpu_messages_clean(self) -> None:
        """All progress messages during multi-GPU provision should be clean."""
        reporter = NullProgressReporter()
        provisioner = EngineProvisioner(progress=reporter)
        hw = _hw_multi_gpu(
            [
                _nvidia_gpu("RTX 4090", 24576),
                _nvidia_gpu("RTX 3060", 12288),
            ]
        )

        with patch.object(provisioner, "_download_model", return_value=True):
            result = await provisioner.provision(hw)

        assert result.success is True
        for event in reporter.events:
            for forbidden in _FORBIDDEN_STRINGS:
                assert forbidden not in event.message, f"Event contains '{forbidden}': {event.message}"
