"""Tests for the PreflightChecker.

Validates:
- Disk space check (blocker and warning thresholds)
- VRAM sufficiency check
- Network connectivity check (mocked)
- Port availability check (mocked)
- Customer-friendly messages (no internal names)
- Combined check_all flow
"""

from __future__ import annotations

from unittest.mock import AsyncMock, patch

import pytest

from llmhosts.discovery.models import GPUInfo, HardwareProfile
from llmhosts.preflight import PreflightChecker, PreflightResult

# Internal names that must NEVER appear in customer-facing messages
_FORBIDDEN_STRINGS = ["ollama", "Ollama", "winget", "qwen", "Qwen"]


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


def _hw(
    vram_mb: int = 0,
    ram_gb: float = 16.0,
    disk_free_gb: float = 50.0,
    disk_total_gb: float = 500.0,
) -> HardwareProfile:
    """Build a HardwareProfile with specified resources."""
    gpus = [GPUInfo(name="Test GPU", vram_total_mb=vram_mb)] if vram_mb > 0 else []
    return HardwareProfile(
        ram_total_gb=ram_gb,
        gpus=gpus,
        disk_free_gb=disk_free_gb,
        disk_total_gb=disk_total_gb,
    )


# ---------------------------------------------------------------------------
# PreflightResult
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestPreflightResult:
    def test_can_proceed_no_blockers(self) -> None:
        result = PreflightResult(warnings=["minor issue"])
        assert result.can_proceed is True

    def test_cannot_proceed_with_blockers(self) -> None:
        result = PreflightResult(blockers=["critical issue"])
        assert result.can_proceed is False

    def test_empty_result(self) -> None:
        result = PreflightResult()
        assert result.can_proceed is True
        assert result.blockers == []
        assert result.warnings == []


# ---------------------------------------------------------------------------
# Disk space checks
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestDiskSpaceCheck:
    async def test_sufficient_disk_space(self) -> None:
        """Plenty of disk space should produce no issues."""
        checker = PreflightChecker(port=19999)
        hw = _hw(disk_free_gb=100.0)
        result = await checker.check_all(hw, model_size_gb=5.0)
        disk_blockers = [b for b in result.blockers if "free" in b.lower() or "disk" in b.lower()]
        assert len(disk_blockers) == 0

    async def test_insufficient_disk_space_is_blocker(self) -> None:
        """Not enough disk space should be a blocker."""
        checker = PreflightChecker(port=19999)
        hw = _hw(disk_free_gb=1.0)
        result = await checker.check_all(hw, model_size_gb=5.0)
        assert not result.can_proceed
        assert any("free" in b.lower() for b in result.blockers)

    async def test_tight_disk_space_is_warning(self) -> None:
        """Disk space that's tight (enough but not 2x) should warn."""
        checker = PreflightChecker(port=19999)
        # Need 5.0 * 1.2 = 6.0 GB. Tight = between 6.0 and 12.0
        hw = _hw(disk_free_gb=8.0)
        result = await checker.check_all(hw, model_size_gb=5.0)
        disk_warnings = [w for w in result.warnings if "disk" in w.lower() or "tight" in w.lower()]
        assert len(disk_warnings) > 0


# ---------------------------------------------------------------------------
# VRAM checks
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestVRAMCheck:
    async def test_gpu_present(self) -> None:
        """With a GPU, should not produce VRAM blockers."""
        checker = PreflightChecker(port=19999)
        hw = _hw(vram_mb=8192, disk_free_gb=100.0)
        result = await checker.check_all(hw, model_size_gb=5.0)
        vram_blockers = [b for b in result.blockers if "vram" in b.lower() or "ram" in b.lower()]
        assert len(vram_blockers) == 0

    async def test_no_gpu_sufficient_ram(self) -> None:
        """Without GPU but enough RAM should produce a warning, not a blocker."""
        checker = PreflightChecker(port=19999)
        hw = _hw(vram_mb=0, ram_gb=16.0, disk_free_gb=100.0)
        result = await checker.check_all(hw, model_size_gb=5.0)
        assert any("no gpu" in w.lower() or "cpu" in w.lower() for w in result.warnings)

    async def test_no_gpu_insufficient_ram_is_blocker(self) -> None:
        """Without GPU and very little RAM should be a blocker."""
        checker = PreflightChecker(port=19999)
        hw = _hw(vram_mb=0, ram_gb=2.0, disk_free_gb=100.0)
        result = await checker.check_all(hw, model_size_gb=1.4)
        assert any("ram" in b.lower() for b in result.blockers)


# ---------------------------------------------------------------------------
# Network check
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestNetworkCheck:
    async def test_network_ok(self) -> None:
        """Successful network check should not add warnings."""
        checker = PreflightChecker(port=19999)
        hw = _hw(vram_mb=8192, disk_free_gb=100.0)

        mock_response = AsyncMock()
        mock_response.status_code = 200

        with patch("httpx.AsyncClient") as mock_client_cls:
            mock_client = AsyncMock()
            mock_client.__aenter__ = AsyncMock(return_value=mock_client)
            mock_client.__aexit__ = AsyncMock(return_value=False)
            mock_client.head = AsyncMock(return_value=mock_response)
            mock_client_cls.return_value = mock_client

            result = await checker.check_all(hw, model_size_gb=5.0)

        network_warnings = [w for w in result.warnings if "registry" in w.lower() or "network" in w.lower()]
        assert len(network_warnings) == 0

    async def test_network_failure_is_warning(self) -> None:
        """Network failure should produce a warning (not a blocker)."""
        checker = PreflightChecker(port=19999)
        hw = _hw(vram_mb=8192, disk_free_gb=100.0)

        with patch("httpx.AsyncClient") as mock_client_cls:
            mock_client = AsyncMock()
            mock_client.__aenter__ = AsyncMock(return_value=mock_client)
            mock_client.__aexit__ = AsyncMock(return_value=False)
            mock_client.head = AsyncMock(side_effect=Exception("Connection refused"))
            mock_client_cls.return_value = mock_client

            result = await checker.check_all(hw, model_size_gb=5.0)

        assert any("registry" in w.lower() or "internet" in w.lower() for w in result.warnings)
        assert result.can_proceed  # network issue is a warning, not a blocker


# ---------------------------------------------------------------------------
# Port check
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestPortCheck:
    async def test_port_available(self) -> None:
        """Available port should produce no blockers."""
        # Use a high port unlikely to be in use
        checker = PreflightChecker(port=19999)
        hw = _hw(vram_mb=8192, disk_free_gb=100.0)

        with patch("httpx.AsyncClient") as mock_client_cls:
            mock_response = AsyncMock()
            mock_response.status_code = 200
            mock_client = AsyncMock()
            mock_client.__aenter__ = AsyncMock(return_value=mock_client)
            mock_client.__aexit__ = AsyncMock(return_value=False)
            mock_client.head = AsyncMock(return_value=mock_response)
            mock_client_cls.return_value = mock_client

            result = await checker.check_all(hw, model_size_gb=5.0)

        port_blockers = [b for b in result.blockers if "port" in b.lower()]
        assert len(port_blockers) == 0

    async def test_port_in_use_is_blocker(self) -> None:
        """Port in use should be a blocker."""
        import socket

        # Bind a port so it's in use
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        sock.bind(("0.0.0.0", 19998))
        sock.listen(1)

        try:
            checker = PreflightChecker(port=19998)
            hw = _hw(vram_mb=8192, disk_free_gb=100.0)

            with patch("httpx.AsyncClient") as mock_client_cls:
                mock_response = AsyncMock()
                mock_response.status_code = 200
                mock_client = AsyncMock()
                mock_client.__aenter__ = AsyncMock(return_value=mock_client)
                mock_client.__aexit__ = AsyncMock(return_value=False)
                mock_client.head = AsyncMock(return_value=mock_response)
                mock_client_cls.return_value = mock_client

                result = await checker.check_all(hw, model_size_gb=5.0)

            assert any("port" in b.lower() for b in result.blockers)
        finally:
            sock.close()


# ---------------------------------------------------------------------------
# Customer-friendly messages
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestPreflightMessagesNoLeaks:
    """All preflight messages should be free of internal implementation names."""

    async def test_all_blockers_clean(self) -> None:
        """Blockers should not contain forbidden strings."""
        checker = PreflightChecker(port=19999)
        hw = _hw(vram_mb=0, ram_gb=2.0, disk_free_gb=0.5)
        result = await checker.check_all(hw, model_size_gb=5.0)

        for msg in result.blockers + result.warnings:
            for forbidden in _FORBIDDEN_STRINGS:
                assert forbidden not in msg, f"Message contains '{forbidden}': {msg}"

    async def test_all_warnings_clean(self) -> None:
        """Warnings should not contain forbidden strings."""
        checker = PreflightChecker(port=19999)
        hw = _hw(vram_mb=0, ram_gb=16.0, disk_free_gb=100.0)
        result = await checker.check_all(hw, model_size_gb=5.0)

        for msg in result.warnings:
            for forbidden in _FORBIDDEN_STRINGS:
                assert forbidden not in msg, f"Warning contains '{forbidden}': {msg}"
