"""Comprehensive tests for llmhosts.server -- LLMHostsServer orchestrator.

Targets uncovered lines: 84, 125-130, 142, 149-167, 177-179, 182, 185-187,
200-215, 229-231, 238, 240, 242, 251, 253, 255, 297-303, 313-319.
"""

from __future__ import annotations

import socket
from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from llmhosts.server import LLMHostsServer, StartupReport, _is_importable, is_port_available

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


def _make_config(**overrides: Any) -> MagicMock:
    """Build a minimal LLMHostsConfig mock."""
    cfg = MagicMock()
    cfg.ollama.auto_discover = overrides.get("auto_discover", True)
    cfg.ollama.host = "http://127.0.0.1:11434"
    cfg.ollama.timeout = 5.0
    cfg.server.host = "0.0.0.0"
    cfg.server.port = 4000
    cfg.server.workers = 1
    cfg.server.log_level = "info"
    cfg.router.enable_qwen = False
    cfg.router.enable_modernbert = False
    cfg.router.enable_knn = False
    cfg.cache.enabled = True
    cfg.cache.enable_vcache = False
    cfg.cache.enable_namespace = False
    cfg.dashboard.enabled = True
    cfg.dashboard.web = True
    cfg.dashboard.tui = False
    cfg.observability = MagicMock()
    return cfg


def _make_gpu(name: str = "RTX 4090", vram_mb: int = 24576) -> SimpleNamespace:
    """Build a GPU-like object with the fields used in _detect_hardware."""
    return SimpleNamespace(name=name, vram_total_mb=vram_mb)


def _make_hardware_profile(
    gpus: list[SimpleNamespace] | None = None,
    ram_total_gb: float = 64.0,
) -> SimpleNamespace:
    return SimpleNamespace(gpus=gpus or [], ram_total_gb=ram_total_gb)


# ---------------------------------------------------------------------------
# TestStartupReport
# ---------------------------------------------------------------------------


class TestStartupReport:
    """StartupReport Pydantic model."""

    def test_defaults(self) -> None:
        report = StartupReport()
        assert report.ollama_available is False
        assert report.ollama_models == []
        assert report.cloud_providers == []
        assert report.hardware_summary == ""
        assert report.router_tier == "Rule-based (Tier 0)"
        assert report.cache_tier == "Exact hash"
        assert report.lan_nodes == 0
        assert report.lan_discovery is False
        assert report.endpoints == []
        assert report.warnings == []

    def test_custom_values(self) -> None:
        report = StartupReport(
            ollama_available=True,
            ollama_models=["llama3:8b"],
            cloud_providers=["openai"],
            hardware_summary="RTX 4090 (24.0GB), 64GB RAM",
            router_tier="Full hybrid (Tier 3)",
            cache_tier="vCache + Namespace + Exact hash",
            lan_nodes=3,
            lan_discovery=True,
            endpoints=["/health"],
            warnings=["test warning"],
        )
        assert report.ollama_available is True
        assert report.ollama_models == ["llama3:8b"]
        assert report.lan_nodes == 3

    def test_serialization_roundtrip(self) -> None:
        report = StartupReport(ollama_available=True, hardware_summary="test")
        data = report.model_dump()
        restored = StartupReport(**data)
        assert restored == report


# ---------------------------------------------------------------------------
# TestServerInit
# ---------------------------------------------------------------------------


class TestServerInit:
    """Constructor."""

    def test_initial_state(self) -> None:
        cfg = _make_config()
        srv = LLMHostsServer(cfg)
        assert srv.config is cfg
        assert srv._app is None
        assert srv._report is None
        assert srv._mdns is None


# ---------------------------------------------------------------------------
# TestStartup
# ---------------------------------------------------------------------------


class TestStartup:
    """Full startup() flow with mocked internals."""

    async def test_full_startup_auto_discover_enabled(self) -> None:
        cfg = _make_config(auto_discover=True)
        srv = LLMHostsServer(cfg)

        with (
            patch.object(srv, "_discover_ollama", new_callable=AsyncMock) as mock_ollama,
            patch.object(srv, "_detect_hardware", new_callable=AsyncMock) as mock_hw,
            patch.object(srv, "_start_lan_discovery", new_callable=AsyncMock) as mock_lan,
            patch.object(srv, "_enumerate_cloud_keys") as mock_keys,
            patch.object(srv, "_determine_router_tier") as mock_router,
            patch.object(srv, "_determine_cache_tier") as mock_cache,
            patch.object(srv, "_auto_provision", new_callable=AsyncMock) as mock_provision,
            patch.object(srv, "_list_endpoints") as mock_ep,
            patch.object(srv, "_create_app") as mock_app,
            patch("llmhosts.observability.init_observability") as mock_obs,
            patch("llmhosts.config.llmhosts_dir") as mock_dir,
        ):
            mock_dir.return_value = "/tmp/fake"
            # Each internal helper returns the report it receives
            mock_ollama.side_effect = lambda r, w: r
            mock_hw.side_effect = lambda r: r
            mock_lan.side_effect = lambda r, w: r
            mock_keys.side_effect = lambda r, w: r
            mock_router.side_effect = lambda r: r
            mock_cache.side_effect = lambda r: r
            mock_provision.side_effect = lambda r, w: r
            mock_ep.return_value = ["/health"]
            mock_app.return_value = MagicMock()

            report = await srv.startup()

            mock_ollama.assert_awaited_once()
            mock_hw.assert_awaited_once()
            mock_provision.assert_awaited_once()  # auto-provision triggered (no backends)
            mock_obs.assert_called_once_with(cfg.observability)
            mock_app.assert_called_once()
            assert srv._app is not None
            assert isinstance(report, StartupReport)

    async def test_startup_auto_discover_disabled(self) -> None:
        """Lines 84-86: ollama auto-discover disabled path."""
        cfg = _make_config(auto_discover=False)
        srv = LLMHostsServer(cfg)

        with (
            patch.object(srv, "_discover_ollama", new_callable=AsyncMock) as mock_ollama,
            patch.object(srv, "_detect_hardware", new_callable=AsyncMock, side_effect=lambda r: r),
            patch.object(srv, "_start_lan_discovery", new_callable=AsyncMock, side_effect=lambda r, w: r),
            patch.object(srv, "_enumerate_cloud_keys", side_effect=lambda r, w: r),
            patch.object(srv, "_determine_router_tier", side_effect=lambda r: r),
            patch.object(srv, "_determine_cache_tier", side_effect=lambda r: r),
            patch.object(srv, "_list_endpoints", return_value=[]),
            patch.object(srv, "_create_app", return_value=MagicMock()),
            patch("llmhosts.observability.init_observability"),
            patch("llmhosts.config.llmhosts_dir", return_value="/tmp/fake"),
        ):
            report = await srv.startup()

            mock_ollama.assert_not_awaited()
            assert "Ollama auto-discovery disabled in config" in report.warnings

    async def test_startup_no_backends_warning(self) -> None:
        """Warning when neither ollama nor cloud providers are available."""
        cfg = _make_config(auto_discover=False)
        srv = LLMHostsServer(cfg)

        with (
            patch.object(srv, "_detect_hardware", new_callable=AsyncMock, side_effect=lambda r: r),
            patch.object(srv, "_start_lan_discovery", new_callable=AsyncMock, side_effect=lambda r, w: r),
            patch.object(srv, "_enumerate_cloud_keys", side_effect=lambda r, w: r),
            patch.object(srv, "_determine_router_tier", side_effect=lambda r: r),
            patch.object(srv, "_determine_cache_tier", side_effect=lambda r: r),
            patch.object(srv, "_list_endpoints", return_value=[]),
            patch.object(srv, "_create_app", return_value=MagicMock()),
            patch("llmhosts.observability.init_observability"),
            patch("llmhosts.config.llmhosts_dir", return_value="/tmp/fake"),
        ):
            report = await srv.startup()
            assert any("No backends available" in w for w in report.warnings)


# ---------------------------------------------------------------------------
# TestRun
# ---------------------------------------------------------------------------


class TestRun:
    """run() — RuntimeError check and uvicorn call (lines 125-130)."""

    def test_run_raises_when_app_is_none(self) -> None:
        """Line 125-126: RuntimeError when app is None."""
        cfg = _make_config()
        srv = LLMHostsServer(cfg)
        assert srv._app is None

        with pytest.raises(RuntimeError, match="Call startup\\(\\) before run\\(\\)"):
            srv.run()

    def test_run_calls_uvicorn(self) -> None:
        """Lines 128-137: uvicorn.run is called with correct params."""
        cfg = _make_config()
        srv = LLMHostsServer(cfg)
        fake_app = MagicMock()
        srv._app = fake_app

        with patch("llmhosts.server.uvicorn") as mock_uvicorn:
            srv.run()

            call_kwargs = mock_uvicorn.run.call_args[1]
            assert call_kwargs["host"] == "0.0.0.0"
            assert call_kwargs["port"] == 4000
            assert call_kwargs["workers"] == 1
            assert call_kwargs["log_level"] == "info"
            assert call_kwargs["access_log"] is False
            assert call_kwargs["http"] in ("auto", "h11")  # platform-dependent

    def test_run_access_log_on_debug(self) -> None:
        """access_log=True when log_level is debug."""
        cfg = _make_config()
        cfg.server.log_level = "DEBUG"
        srv = LLMHostsServer(cfg)
        srv._app = MagicMock()

        with patch("llmhosts.server.uvicorn") as mock_uvicorn:
            srv.run()
            call_kwargs = mock_uvicorn.run.call_args
            assert call_kwargs[1]["access_log"] is True
            assert call_kwargs[1]["log_level"] == "debug"


# ---------------------------------------------------------------------------
# TestShutdown
# ---------------------------------------------------------------------------


class TestShutdown:
    """shutdown() — with/without mdns (line 142)."""

    async def test_shutdown_with_mdns(self) -> None:
        """Line 142: mdns.stop() called when mdns is set."""
        cfg = _make_config()
        srv = LLMHostsServer(cfg)
        mock_mdns = AsyncMock()
        srv._mdns = mock_mdns

        await srv.shutdown()
        mock_mdns.stop.assert_awaited_once()

    async def test_shutdown_without_mdns(self) -> None:
        """mdns is None — no exception."""
        cfg = _make_config()
        srv = LLMHostsServer(cfg)
        assert srv._mdns is None

        await srv.shutdown()  # should not raise


# ---------------------------------------------------------------------------
# TestDiscoverOllama
# ---------------------------------------------------------------------------


class TestDiscoverOllama:
    """_discover_ollama — lines 149-167."""

    async def test_available_with_models(self) -> None:
        """Lines 158-159: ollama available, models present."""
        cfg = _make_config()
        srv = LLMHostsServer(cfg)

        model1 = SimpleNamespace(name="llama3:8b")
        model2 = SimpleNamespace(name="mistral:7b")

        mock_ollama_instance = AsyncMock()
        mock_ollama_instance.is_available = AsyncMock(return_value=True)
        mock_ollama_instance.list_models = AsyncMock(return_value=[model1, model2])
        mock_ollama_instance.__aenter__ = AsyncMock(return_value=mock_ollama_instance)
        mock_ollama_instance.__aexit__ = AsyncMock(return_value=False)

        with patch("llmhosts.discovery.ollama.OllamaDiscovery", return_value=mock_ollama_instance):
            report = StartupReport()
            warnings: list[str] = []
            result = await srv._discover_ollama(report, warnings)

            assert result.ollama_available is True
            assert result.ollama_models == ["llama3:8b", "mistral:7b"]
            assert len(warnings) == 0

    async def test_available_no_models(self) -> None:
        """Lines 160-161: ollama available but no models installed."""
        cfg = _make_config()
        srv = LLMHostsServer(cfg)

        mock_ollama_instance = AsyncMock()
        mock_ollama_instance.is_available = AsyncMock(return_value=True)
        mock_ollama_instance.list_models = AsyncMock(return_value=[])
        mock_ollama_instance.__aenter__ = AsyncMock(return_value=mock_ollama_instance)
        mock_ollama_instance.__aexit__ = AsyncMock(return_value=False)

        with patch("llmhosts.discovery.ollama.OllamaDiscovery", return_value=mock_ollama_instance):
            report = StartupReport()
            warnings: list[str] = []
            result = await srv._discover_ollama(report, warnings)

            assert result.ollama_available is True
            assert result.ollama_models == []
            assert any("no models installed" in w for w in warnings)

    async def test_not_available(self) -> None:
        """Lines 162-166: ollama not reachable."""
        cfg = _make_config()
        srv = LLMHostsServer(cfg)

        mock_ollama_instance = AsyncMock()
        mock_ollama_instance.is_available = AsyncMock(return_value=False)
        mock_ollama_instance.__aenter__ = AsyncMock(return_value=mock_ollama_instance)
        mock_ollama_instance.__aexit__ = AsyncMock(return_value=False)

        with patch("llmhosts.discovery.ollama.OllamaDiscovery", return_value=mock_ollama_instance):
            report = StartupReport()
            warnings: list[str] = []
            result = await srv._discover_ollama(report, warnings)

            assert result.ollama_available is False
            assert any("Ollama not reachable" in w for w in warnings)


# ---------------------------------------------------------------------------
# TestDetectHardware
# ---------------------------------------------------------------------------


class TestDetectHardware:
    """_detect_hardware — lines 177-179, 182, 185-187."""

    async def test_with_gpu(self) -> None:
        """Lines 177-179, 182: GPU detected."""
        cfg = _make_config()
        srv = LLMHostsServer(cfg)

        gpu = _make_gpu("RTX 4090", 24576)
        hw = _make_hardware_profile(gpus=[gpu], ram_total_gb=64.0)

        with patch("llmhosts.discovery.hardware.HardwareDetector") as mock_cls:
            mock_cls.detect = AsyncMock(return_value=hw)

            report = StartupReport()
            result = await srv._detect_hardware(report)

            assert "RTX 4090" in result.hardware_summary
            assert "24.0GB" in result.hardware_summary
            assert "64GB RAM" in result.hardware_summary

    async def test_with_multiple_gpus(self) -> None:
        """Multiple GPUs joined with commas."""
        cfg = _make_config()
        srv = LLMHostsServer(cfg)

        gpu1 = _make_gpu("RTX 4090", 24576)
        gpu2 = _make_gpu("RTX 3090", 24576)
        hw = _make_hardware_profile(gpus=[gpu1, gpu2], ram_total_gb=128.0)

        with patch("llmhosts.discovery.hardware.HardwareDetector") as mock_cls:
            mock_cls.detect = AsyncMock(return_value=hw)

            report = StartupReport()
            result = await srv._detect_hardware(report)

            assert "RTX 4090" in result.hardware_summary
            assert "RTX 3090" in result.hardware_summary
            assert "128GB RAM" in result.hardware_summary

    async def test_cpu_only(self) -> None:
        """Line 184: no GPUs — CPU-only path."""
        cfg = _make_config()
        srv = LLMHostsServer(cfg)

        hw = _make_hardware_profile(gpus=[], ram_total_gb=32.0)

        with patch("llmhosts.discovery.hardware.HardwareDetector") as mock_cls:
            mock_cls.detect = AsyncMock(return_value=hw)

            report = StartupReport()
            result = await srv._detect_hardware(report)

            assert result.hardware_summary == "CPU only, 32GB RAM"

    async def test_exception_path(self) -> None:
        """Lines 185-187: hardware detection fails."""
        cfg = _make_config()
        srv = LLMHostsServer(cfg)

        with patch("llmhosts.discovery.hardware.HardwareDetector") as mock_cls:
            mock_cls.detect = AsyncMock(side_effect=RuntimeError("nvidia-smi not found"))

            report = StartupReport()
            result = await srv._detect_hardware(report)

            assert result.hardware_summary == "detection failed"


# ---------------------------------------------------------------------------
# TestStartLanDiscovery
# ---------------------------------------------------------------------------


class TestStartLanDiscovery:
    """_start_lan_discovery — lines 200-215."""

    async def test_not_available(self) -> None:
        """Line 196-198: zeroconf not installed."""
        cfg = _make_config()
        srv = LLMHostsServer(cfg)

        mock_mdns = MagicMock()
        mock_mdns.is_available = False

        with patch("llmhosts.discovery.mdns.MDNSDiscovery", return_value=mock_mdns):
            report = StartupReport()
            warnings: list[str] = []
            result = await srv._start_lan_discovery(report, warnings)

            assert result.lan_discovery is False
            assert srv._mdns is None

    async def test_available_and_started(self) -> None:
        """Lines 200-215: mdns available, started successfully, nodes found."""
        cfg = _make_config()
        srv = LLMHostsServer(cfg)

        mock_mdns = MagicMock()
        mock_mdns.is_available = True
        mock_mdns.start = AsyncMock(return_value=True)
        mock_mdns.get_nodes.return_value = [MagicMock(), MagicMock()]

        with (
            patch("llmhosts.discovery.mdns.MDNSDiscovery", return_value=mock_mdns),
            patch("asyncio.sleep", new_callable=AsyncMock) as mock_sleep,
        ):
            report = StartupReport(
                hardware_summary="RTX 4090",
                ollama_models=["llama3:8b"],
            )
            warnings: list[str] = []
            result = await srv._start_lan_discovery(report, warnings)

            mock_mdns.start.assert_awaited_once_with(
                port=4000,
                gpu_summary="RTX 4090",
                models=["llama3:8b"],
            )
            mock_sleep.assert_awaited_once_with(1.0)
            assert result.lan_discovery is True
            assert result.lan_nodes == 2
            assert srv._mdns is mock_mdns

    async def test_available_but_start_fails(self) -> None:
        """mdns.start() returns False — not started."""
        cfg = _make_config()
        srv = LLMHostsServer(cfg)

        mock_mdns = MagicMock()
        mock_mdns.is_available = True
        mock_mdns.start = AsyncMock(return_value=False)

        with patch("llmhosts.discovery.mdns.MDNSDiscovery", return_value=mock_mdns):
            report = StartupReport()
            warnings: list[str] = []
            result = await srv._start_lan_discovery(report, warnings)

            assert result.lan_discovery is False
            assert srv._mdns is None

    async def test_exception_path(self) -> None:
        """Lines 216-218: exception during LAN discovery."""
        cfg = _make_config()
        srv = LLMHostsServer(cfg)

        with patch("llmhosts.discovery.mdns.MDNSDiscovery", side_effect=OSError("network down")):
            report = StartupReport()
            warnings: list[str] = []
            result = await srv._start_lan_discovery(report, warnings)

            assert any("LAN discovery failed" in w for w in warnings)
            assert result.lan_discovery is False


# ---------------------------------------------------------------------------
# TestEnumerateCloudKeys
# ---------------------------------------------------------------------------


class TestEnumerateCloudKeys:
    """_enumerate_cloud_keys — lines 229-231."""

    def test_success(self) -> None:
        """Lines 227-228: keys enumerated successfully."""
        cfg = _make_config()
        srv = LLMHostsServer(cfg)

        provider1 = SimpleNamespace(provider="openai")
        provider2 = SimpleNamespace(provider="anthropic")

        mock_mgr = MagicMock()
        mock_mgr.list_providers.return_value = [provider1, provider2]

        with patch("llmhosts.keys.manager.KeyManager", return_value=mock_mgr):
            report = StartupReport()
            warnings: list[str] = []
            result = srv._enumerate_cloud_keys(report, warnings)

            assert result.cloud_providers == ["openai", "anthropic"]
            assert len(warnings) == 0

    def test_exception(self) -> None:
        """Lines 229-231: key enumeration fails."""
        cfg = _make_config()
        srv = LLMHostsServer(cfg)

        with patch("llmhosts.keys.manager.KeyManager", side_effect=PermissionError("access denied")):
            report = StartupReport()
            warnings: list[str] = []
            result = srv._enumerate_cloud_keys(report, warnings)

            assert result.cloud_providers == []
            assert any("Could not read BYOK keys" in w for w in warnings)

    def test_no_providers(self) -> None:
        """Empty provider list."""
        cfg = _make_config()
        srv = LLMHostsServer(cfg)

        mock_mgr = MagicMock()
        mock_mgr.list_providers.return_value = []

        with patch("llmhosts.keys.manager.KeyManager", return_value=mock_mgr):
            report = StartupReport()
            warnings: list[str] = []
            result = srv._enumerate_cloud_keys(report, warnings)

            assert result.cloud_providers == []


# ---------------------------------------------------------------------------
# TestDetermineRouterTier
# ---------------------------------------------------------------------------


class TestDetermineRouterTier:
    """_determine_router_tier — lines 238, 240, 242, 244."""

    def test_tier3_qwen(self) -> None:
        """Line 238: Qwen enabled + torch importable."""
        cfg = _make_config()
        cfg.router.enable_qwen = True
        cfg.router.enable_modernbert = True
        cfg.router.enable_knn = True
        srv = LLMHostsServer(cfg)

        with patch("llmhosts.server._is_importable", return_value=True):
            report = StartupReport()
            result = srv._determine_router_tier(report)
            assert "Tier 3" in result.router_tier

    def test_tier2_modernbert(self) -> None:
        """Line 240: ModernBERT enabled + onnxruntime importable."""
        cfg = _make_config()
        cfg.router.enable_qwen = False
        cfg.router.enable_modernbert = True
        cfg.router.enable_knn = True
        srv = LLMHostsServer(cfg)

        with patch("llmhosts.server._is_importable", side_effect=lambda m: m == "onnxruntime"):
            report = StartupReport()
            result = srv._determine_router_tier(report)
            assert "Tier 2" in result.router_tier

    def test_tier1_knn(self) -> None:
        """Line 242: kNN enabled + faiss importable."""
        cfg = _make_config()
        cfg.router.enable_qwen = False
        cfg.router.enable_modernbert = False
        cfg.router.enable_knn = True
        srv = LLMHostsServer(cfg)

        with patch("llmhosts.server._is_importable", side_effect=lambda m: m == "faiss"):
            report = StartupReport()
            result = srv._determine_router_tier(report)
            assert "Tier 1" in result.router_tier

    def test_tier0_rule_based(self) -> None:
        """Line 244: default tier 0."""
        cfg = _make_config()
        srv = LLMHostsServer(cfg)

        with patch("llmhosts.server._is_importable", return_value=False):
            report = StartupReport()
            result = srv._determine_router_tier(report)
            assert result.router_tier == "Rule-based (Tier 0)"

    def test_tier3_qwen_no_torch(self) -> None:
        """Qwen enabled but torch not importable — falls through."""
        cfg = _make_config()
        cfg.router.enable_qwen = True
        cfg.router.enable_modernbert = False
        cfg.router.enable_knn = False
        srv = LLMHostsServer(cfg)

        with patch("llmhosts.server._is_importable", return_value=False):
            report = StartupReport()
            result = srv._determine_router_tier(report)
            assert result.router_tier == "Rule-based (Tier 0)"


# ---------------------------------------------------------------------------
# TestDetermineCacheTier
# ---------------------------------------------------------------------------


class TestDetermineCacheTier:
    """_determine_cache_tier — lines 251, 253, 255."""

    def test_cache_disabled(self) -> None:
        """Line 251: cache disabled."""
        cfg = _make_config()
        cfg.cache.enabled = False
        srv = LLMHostsServer(cfg)

        report = StartupReport()
        result = srv._determine_cache_tier(report)
        assert result.cache_tier == "Disabled"

    def test_vcache(self) -> None:
        """Line 253: vcache enabled."""
        cfg = _make_config()
        cfg.cache.enabled = True
        cfg.cache.enable_vcache = True
        srv = LLMHostsServer(cfg)

        report = StartupReport()
        result = srv._determine_cache_tier(report)
        assert result.cache_tier == "vCache + Namespace + Exact hash"

    def test_namespace(self) -> None:
        """Line 255: namespace enabled (no vcache)."""
        cfg = _make_config()
        cfg.cache.enabled = True
        cfg.cache.enable_vcache = False
        cfg.cache.enable_namespace = True
        srv = LLMHostsServer(cfg)

        report = StartupReport()
        result = srv._determine_cache_tier(report)
        assert result.cache_tier == "Namespace + Exact hash"

    def test_exact_hash_only(self) -> None:
        """Line 257: default exact hash."""
        cfg = _make_config()
        cfg.cache.enabled = True
        cfg.cache.enable_vcache = False
        cfg.cache.enable_namespace = False
        srv = LLMHostsServer(cfg)

        report = StartupReport()
        result = srv._determine_cache_tier(report)
        assert result.cache_tier == "Exact hash"


# ---------------------------------------------------------------------------
# TestListEndpoints
# ---------------------------------------------------------------------------


class TestListEndpoints:
    """_list_endpoints."""

    def test_with_dashboard(self) -> None:
        cfg = _make_config()
        cfg.dashboard.enabled = True
        cfg.dashboard.web = True
        srv = LLMHostsServer(cfg)

        endpoints = srv._list_endpoints()
        assert any("/dashboard" in e for e in endpoints)
        assert any("/health" in e for e in endpoints)
        assert any("/v1/chat/completions" in e for e in endpoints)

    def test_without_dashboard(self) -> None:
        cfg = _make_config()
        cfg.dashboard.enabled = False
        srv = LLMHostsServer(cfg)

        endpoints = srv._list_endpoints()
        assert not any("/dashboard" in e for e in endpoints)
        assert any("/health" in e for e in endpoints)

    def test_dashboard_enabled_web_disabled(self) -> None:
        cfg = _make_config()
        cfg.dashboard.enabled = True
        cfg.dashboard.web = False
        srv = LLMHostsServer(cfg)

        endpoints = srv._list_endpoints()
        assert not any("/dashboard" in e for e in endpoints)


# ---------------------------------------------------------------------------
# TestCreateApp
# ---------------------------------------------------------------------------


class TestCreateApp:
    """_create_app wires config and report onto app.state."""

    def test_create_app_sets_state(self) -> None:
        cfg = _make_config()
        srv = LLMHostsServer(cfg)
        srv._report = StartupReport(ollama_available=True)

        fake_app = MagicMock()
        with patch("llmhosts.proxy.app.create_app", return_value=fake_app):
            result = srv._create_app()

            assert result is fake_app
            assert fake_app.state.llmhosts_config is cfg
            assert fake_app.state.startup_report is srv._report


# ---------------------------------------------------------------------------
# TestIsPortAvailable
# ---------------------------------------------------------------------------


class TestIsPortAvailable:
    """is_port_available — lines 297-303."""

    def test_free_port(self) -> None:
        """Line 301: port is free."""
        # Bind to port 0 to get an OS-assigned free port, then release it
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.bind(("127.0.0.1", 0))
            _, free_port = s.getsockname()
        # Port was released by closing the context manager
        assert is_port_available("127.0.0.1", free_port) is True

    def test_occupied_port(self) -> None:
        """Port with a listening socket should be detected as unavailable."""
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            s.bind(("127.0.0.1", 0))
            s.listen(1)  # Must listen for connect-based detection
            _, occupied_port = s.getsockname()
            # While listening, port should be unavailable
            assert is_port_available("127.0.0.1", occupied_port) is False

    def test_with_mock_connection_success(self) -> None:
        """When connect succeeds (something listening), port is unavailable."""
        with patch("llmhosts.ports.socket.socket") as mock_sock:
            mock_instance = MagicMock()
            mock_instance.__enter__ = MagicMock(return_value=mock_instance)
            mock_instance.__exit__ = MagicMock(return_value=False)
            # connect() succeeding means something is listening
            mock_instance.connect.return_value = None
            mock_sock.return_value = mock_instance

            assert is_port_available("0.0.0.0", 4000) is False


# ---------------------------------------------------------------------------
# TestIsImportable
# ---------------------------------------------------------------------------


class TestIsImportable:
    """_is_importable — lines 313-319."""

    def test_importable_module(self) -> None:
        """Line 317: module exists."""
        assert _is_importable("json") is True
        assert _is_importable("os") is True

    def test_not_importable_module(self) -> None:
        """Lines 318-319: module does not exist."""
        assert _is_importable("nonexistent_module_xyz_123") is False

    def test_importable_submodule(self) -> None:
        """Submodule import works."""
        assert _is_importable("os.path") is True

    def test_not_importable_submodule(self) -> None:
        """Non-existent submodule fails."""
        assert _is_importable("os.nonexistent_sub") is False
