"""Tests for main.py — core endpoints and helper functions."""

import time
from unittest.mock import AsyncMock, MagicMock

import pytest

from main import get_allowed_origins, _build_api_status, _fallback_services, _serialize_services, TTLCache


# --- get_allowed_origins ---


class TestGetAllowedOrigins:

    def test_returns_env_origins_when_set(self, monkeypatch):
        monkeypatch.setenv("DASHBOARD_ALLOWED_ORIGINS", "http://foo:3000,http://bar:3001")
        origins = get_allowed_origins()
        assert origins == ["http://foo:3000", "http://bar:3001"]

    def test_returns_defaults_when_env_not_set(self, monkeypatch):
        monkeypatch.delenv("DASHBOARD_ALLOWED_ORIGINS", raising=False)
        origins = get_allowed_origins()
        assert "http://localhost:3001" in origins
        assert "http://127.0.0.1:3001" in origins

    def test_includes_lan_ips(self, monkeypatch):
        monkeypatch.delenv("DASHBOARD_ALLOWED_ORIGINS", raising=False)
        monkeypatch.setattr("main.socket.gethostname", lambda: "test-host")
        monkeypatch.setattr(
            "main.socket.gethostbyname_ex",
            lambda h: ("test-host", [], ["192.168.1.100"]),
        )
        origins = get_allowed_origins()
        assert "http://192.168.1.100:3001" in origins
        assert "http://192.168.1.100:3000" in origins

    def test_handles_socket_error(self, monkeypatch):
        import socket
        monkeypatch.delenv("DASHBOARD_ALLOWED_ORIGINS", raising=False)
        monkeypatch.setattr("main.socket.gethostname", lambda: "test-host")
        monkeypatch.setattr(
            "main.socket.gethostbyname_ex",
            MagicMock(side_effect=socket.gaierror("lookup failed")),
        )
        # Should not raise; just returns defaults without LAN IPs
        origins = get_allowed_origins()
        assert "http://localhost:3001" in origins


# --- /api/preflight/docker ---


class TestPreflightDocker:

    def test_docker_available(self, test_client, monkeypatch):
        import asyncio
        import os.path as _ospath
        monkeypatch.setattr(_ospath, "exists", lambda p: False)

        mock_proc = AsyncMock()
        mock_proc.returncode = 0
        mock_proc.communicate = AsyncMock(return_value=(b"Docker version 24.0.7, build afdd53b", b""))

        monkeypatch.setattr(asyncio, "create_subprocess_exec", AsyncMock(return_value=mock_proc))

        resp = test_client.get("/api/preflight/docker", headers=test_client.auth_headers)
        assert resp.status_code == 200
        data = resp.json()
        assert data["available"] is True
        assert "24.0.7" in data["version"]

    def test_docker_not_installed(self, test_client, monkeypatch):
        import asyncio
        import os.path as _ospath
        monkeypatch.setattr(_ospath, "exists", lambda p: False)
        monkeypatch.setattr(
            asyncio, "create_subprocess_exec",
            AsyncMock(side_effect=FileNotFoundError("docker not found")),
        )

        resp = test_client.get("/api/preflight/docker", headers=test_client.auth_headers)
        assert resp.status_code == 200
        data = resp.json()
        assert data["available"] is False
        assert "not installed" in data["error"]

    def test_docker_timeout(self, test_client, monkeypatch):
        import asyncio
        import os.path as _ospath
        monkeypatch.setattr(_ospath, "exists", lambda p: False)

        mock_proc = AsyncMock()
        mock_proc.communicate = AsyncMock(side_effect=asyncio.TimeoutError())

        monkeypatch.setattr(asyncio, "create_subprocess_exec", AsyncMock(return_value=mock_proc))
        monkeypatch.setattr(asyncio, "wait_for", AsyncMock(side_effect=asyncio.TimeoutError()))

        resp = test_client.get("/api/preflight/docker", headers=test_client.auth_headers)
        assert resp.status_code == 200
        data = resp.json()
        assert data["available"] is False
        assert "timed out" in data["error"]


# --- /api/preflight/gpu ---


class TestPreflightGpu:

    def test_gpu_available(self, test_client, monkeypatch):
        from models import GPUInfo
        gpu = GPUInfo(
            name="RTX 4090", memory_used_mb=2048, memory_total_mb=24576,
            memory_percent=8.3, utilization_percent=35, temperature_c=62,
            gpu_backend="nvidia",
        )
        monkeypatch.setattr("main.get_gpu_info", lambda: gpu)

        resp = test_client.get("/api/preflight/gpu", headers=test_client.auth_headers)
        assert resp.status_code == 200
        data = resp.json()
        assert data["available"] is True
        assert data["name"] == "RTX 4090"
        assert data["backend"] == "nvidia"

    def test_gpu_unavailable_amd(self, test_client, monkeypatch):
        monkeypatch.setattr("main.get_gpu_info", lambda: None)
        monkeypatch.setenv("GPU_BACKEND", "amd")

        resp = test_client.get("/api/preflight/gpu", headers=test_client.auth_headers)
        assert resp.status_code == 200
        data = resp.json()
        assert data["available"] is False
        assert "AMD" in data["error"]

    def test_unified_memory_label(self, test_client, monkeypatch):
        from models import GPUInfo
        gpu = GPUInfo(
            name="AMD Strix Halo", memory_used_mb=10240, memory_total_mb=98304,
            memory_percent=10.4, utilization_percent=15, temperature_c=55,
            memory_type="unified", gpu_backend="amd",
        )
        monkeypatch.setattr("main.get_gpu_info", lambda: gpu)

        resp = test_client.get("/api/preflight/gpu", headers=test_client.auth_headers)
        assert resp.status_code == 200
        data = resp.json()
        assert data["available"] is True
        assert data["memory_type"] == "unified"
        assert "Unified" in data["memory_label"]


# --- /api/preflight/disk ---


class TestPreflightDisk:

    def test_returns_disk_info(self, test_client, monkeypatch):
        from collections import namedtuple
        DiskUsageTuple = namedtuple('usage', ['total', 'used', 'free'])
        monkeypatch.setattr("main.os.path.exists", lambda p: True)
        monkeypatch.setattr("main.shutil.disk_usage", lambda p: DiskUsageTuple(500 * 1024**3, 200 * 1024**3, 300 * 1024**3))

        resp = test_client.get("/api/preflight/disk", headers=test_client.auth_headers)
        assert resp.status_code == 200
        data = resp.json()
        assert data["total"] == 500 * 1024**3
        assert data["used"] == 200 * 1024**3
        assert data["free"] == 300 * 1024**3

    def test_handles_exception(self, test_client, monkeypatch):
        monkeypatch.setattr("main.os.path.exists", lambda p: True)
        monkeypatch.setattr("main.shutil.disk_usage", MagicMock(side_effect=OSError("disk error")))

        resp = test_client.get("/api/preflight/disk", headers=test_client.auth_headers)
        assert resp.status_code == 200
        data = resp.json()
        assert "error" in data


# --- _build_api_status ---


class TestBuildApiStatus:

    @pytest.mark.asyncio
    async def test_returns_full_structure(self, monkeypatch):
        from models import GPUInfo, BootstrapStatus, ModelInfo

        gpu = GPUInfo(
            name="RTX 4090", memory_used_mb=2048, memory_total_mb=24576,
            memory_percent=8.3, utilization_percent=35, temperature_c=62,
            gpu_backend="nvidia",
        )
        monkeypatch.setattr("main.get_gpu_info", lambda: gpu)
        monkeypatch.setattr("main.get_all_services", AsyncMock(return_value=[]))
        monkeypatch.setattr("main.get_model_info", lambda: ModelInfo(name="Test-32B", size_gb=16.0, context_length=32768))
        monkeypatch.setattr("main.get_bootstrap_status", lambda: BootstrapStatus(active=False))
        monkeypatch.setattr("main.get_loaded_model", AsyncMock(return_value="Test-32B"))
        monkeypatch.setattr("main.get_llama_metrics", AsyncMock(return_value={"tokens_per_second": 25.5, "lifetime_tokens": 10000}))
        monkeypatch.setattr("main.get_llama_context_size", AsyncMock(return_value=32768))
        monkeypatch.setattr("main.get_uptime", lambda: 3600)
        monkeypatch.setattr("main.get_cpu_metrics", lambda: {"percent": 15.0, "temp_c": 55})
        monkeypatch.setattr("main.get_ram_metrics", lambda: {"used_gb": 16.0, "total_gb": 64.0, "percent": 25.0})

        result = await _build_api_status()
        assert result["gpu"] is not None
        assert result["gpu"]["name"] == "RTX 4090"
        assert result["tier"] == "Prosumer"
        assert result["uptime"] == 3600
        assert result["inference"]["tokensPerSecond"] == 25.5
        assert result["inference"]["loadedModel"] == "Test-32B"

    @pytest.mark.asyncio
    async def test_tier_professional(self, monkeypatch):
        from models import GPUInfo, BootstrapStatus

        gpu = GPUInfo(
            name="H100", memory_used_mb=4096, memory_total_mb=81920,
            memory_percent=5.0, utilization_percent=10, temperature_c=45,
            gpu_backend="nvidia",
        )
        monkeypatch.setattr("main.get_gpu_info", lambda: gpu)
        monkeypatch.setattr("main.get_all_services", AsyncMock(return_value=[]))
        monkeypatch.setattr("main.get_model_info", lambda: None)
        monkeypatch.setattr("main.get_bootstrap_status", lambda: BootstrapStatus(active=False))
        monkeypatch.setattr("main.get_loaded_model", AsyncMock(return_value=None))
        monkeypatch.setattr("main.get_llama_metrics", AsyncMock(return_value={"tokens_per_second": 0, "lifetime_tokens": 0}))
        monkeypatch.setattr("main.get_llama_context_size", AsyncMock(return_value=None))
        monkeypatch.setattr("main.get_uptime", lambda: 0)
        monkeypatch.setattr("main.get_cpu_metrics", lambda: {"percent": 0, "temp_c": None})
        monkeypatch.setattr("main.get_ram_metrics", lambda: {"used_gb": 0, "total_gb": 0, "percent": 0})

        result = await _build_api_status()
        assert result["tier"] == "Professional"

    @pytest.mark.asyncio
    async def test_tier_strix_halo(self, monkeypatch):
        from models import GPUInfo, BootstrapStatus

        gpu = GPUInfo(
            name="Strix Halo", memory_used_mb=10240, memory_total_mb=98304,
            memory_percent=10.4, utilization_percent=15, temperature_c=55,
            memory_type="unified", gpu_backend="amd",
        )
        monkeypatch.setattr("main.get_gpu_info", lambda: gpu)
        monkeypatch.setattr("main.get_all_services", AsyncMock(return_value=[]))
        monkeypatch.setattr("main.get_model_info", lambda: None)
        monkeypatch.setattr("main.get_bootstrap_status", lambda: BootstrapStatus(active=False))
        monkeypatch.setattr("main.get_loaded_model", AsyncMock(return_value=None))
        monkeypatch.setattr("main.get_llama_metrics", AsyncMock(return_value={"tokens_per_second": 0, "lifetime_tokens": 0}))
        monkeypatch.setattr("main.get_llama_context_size", AsyncMock(return_value=None))
        monkeypatch.setattr("main.get_uptime", lambda: 0)
        monkeypatch.setattr("main.get_cpu_metrics", lambda: {"percent": 0, "temp_c": None})
        monkeypatch.setattr("main.get_ram_metrics", lambda: {"used_gb": 0, "total_gb": 0, "percent": 0})

        result = await _build_api_status()
        assert result["tier"] == "Strix Halo 90+"


# --- /api/service-tokens ---


class TestServiceTokens:

    def test_returns_token_from_env(self, test_client, monkeypatch):
        monkeypatch.setenv("OPENCLAW_TOKEN", "my-secret-token")

        resp = test_client.get("/api/service-tokens", headers=test_client.auth_headers)
        assert resp.status_code == 200
        data = resp.json()
        assert data.get("openclaw") == "my-secret-token"

    def test_returns_empty_when_no_token(self, test_client, monkeypatch):
        monkeypatch.delenv("OPENCLAW_TOKEN", raising=False)
        # The file-based fallback paths (/data/openclaw/..., /dream-server/.env)
        # won't exist in test environment, so all fallbacks fail gracefully.

        resp = test_client.get("/api/service-tokens", headers=test_client.auth_headers)
        assert resp.status_code == 200
        data = resp.json()
        # Either empty dict or no openclaw key
        assert "openclaw" not in data


# --- /api/external-links ---


class TestExternalLinks:

    def test_returns_links_for_services(self, test_client, monkeypatch):
        import config
        monkeypatch.setattr(config, "SERVICES", {
            "open-webui": {"name": "Open WebUI", "port": 3000, "external_port": 3000, "health": "/health", "host": "localhost"},
            "n8n": {"name": "n8n", "port": 5678, "external_port": 5678, "health": "/healthz", "host": "localhost"},
            "dashboard-api": {"name": "Dashboard API", "port": 3002, "external_port": 3002, "health": "/health", "host": "localhost"},
        })
        # Also patch the SERVICES imported in main module
        monkeypatch.setattr("main.SERVICES", config.SERVICES)

        resp = test_client.get("/api/external-links", headers=test_client.auth_headers)
        assert resp.status_code == 200
        data = resp.json()
        link_ids = [link["id"] for link in data]
        assert "open-webui" in link_ids
        assert "n8n" in link_ids

    def test_excludes_dashboard_api(self, test_client, monkeypatch):
        import config
        monkeypatch.setattr(config, "SERVICES", {
            "dashboard-api": {"name": "Dashboard API", "port": 3002, "external_port": 3002, "health": "/health", "host": "localhost"},
        })
        monkeypatch.setattr("main.SERVICES", config.SERVICES)

        resp = test_client.get("/api/external-links", headers=test_client.auth_headers)
        assert resp.status_code == 200
        data = resp.json()
        assert len(data) == 0


# --- /api/storage ---


class TestApiStorage:

    def test_returns_storage_breakdown(self, test_client, monkeypatch):
        from models import DiskUsage
        monkeypatch.setattr("main.get_disk_usage", lambda: DiskUsage(
            path="/tmp", used_gb=100.0, total_gb=500.0, percent=20.0,
        ))
        monkeypatch.setattr("main.DATA_DIR", "/tmp/dream-test-nonexistent-data")

        resp = test_client.get("/api/storage", headers=test_client.auth_headers)
        assert resp.status_code == 200
        data = resp.json()
        assert "models" in data
        assert "vector_db" in data
        assert "total_data" in data
        assert "disk" in data
        assert data["disk"]["total_gb"] == 500.0


# --- TTLCache ---


class TestTTLCache:

    def test_set_and_get(self):
        cache = TTLCache()
        cache.set("k", "v", ttl=10)
        assert cache.get("k") == "v"

    def test_expired_key_returns_none(self):
        cache = TTLCache()
        cache.set("k", "v", ttl=0.01)
        time.sleep(0.02)
        assert cache.get("k") is None

    def test_missing_key_returns_none(self):
        cache = TTLCache()
        assert cache.get("nope") is None


# --- /api/preflight/docker edge cases ---


class TestPreflightDockerEdge:

    def test_docker_inside_container(self, test_client, monkeypatch):
        import os.path as _ospath
        monkeypatch.setattr(_ospath, "exists", lambda p: p == "/.dockerenv")

        resp = test_client.get("/api/preflight/docker", headers=test_client.auth_headers)
        assert resp.status_code == 200
        data = resp.json()
        assert data["available"] is True
        assert "host" in data["version"]

    def test_docker_command_failed(self, test_client, monkeypatch):
        import asyncio
        import os.path as _ospath
        monkeypatch.setattr(_ospath, "exists", lambda p: False)

        mock_proc = AsyncMock()
        mock_proc.returncode = 1
        mock_proc.communicate = AsyncMock(return_value=(b"", b"error"))
        monkeypatch.setattr(asyncio, "create_subprocess_exec", AsyncMock(return_value=mock_proc))

        resp = test_client.get("/api/preflight/docker", headers=test_client.auth_headers)
        assert resp.status_code == 200
        data = resp.json()
        assert data["available"] is False
        assert "failed" in data["error"]

    def test_docker_os_error(self, test_client, monkeypatch):
        import asyncio
        import os.path as _ospath
        monkeypatch.setattr(_ospath, "exists", lambda p: False)
        monkeypatch.setattr(asyncio, "create_subprocess_exec", AsyncMock(side_effect=OSError("broken")))

        resp = test_client.get("/api/preflight/docker", headers=test_client.auth_headers)
        assert resp.status_code == 200
        assert resp.json()["available"] is False


# --- /api/preflight/gpu no-gpu fallback ---


class TestPreflightGpuNoGpu:

    def test_gpu_unavailable_generic(self, test_client, monkeypatch):
        monkeypatch.setattr("main.get_gpu_info", lambda: None)
        monkeypatch.setenv("GPU_BACKEND", "nvidia")

        resp = test_client.get("/api/preflight/gpu", headers=test_client.auth_headers)
        assert resp.status_code == 200
        data = resp.json()
        assert data["available"] is False
        assert "No GPU detected" in data["error"]


# --- /api/preflight/required-ports ---


class TestPreflightRequiredPorts:

    def test_returns_ports(self, test_client, monkeypatch):
        monkeypatch.setattr("main.SERVICES", {
            "svc-a": {"name": "A", "port": 8000, "external_port": 8000},
        })
        resp = test_client.get("/api/preflight/required-ports")
        assert resp.status_code == 200
        data = resp.json()
        assert any(p["port"] == 8000 for p in data["ports"])


# --- /api/preflight/ports ---


class TestPreflightPorts:

    def test_port_in_use(self, test_client, monkeypatch):
        import socket as _socket
        monkeypatch.setattr("main.SERVICES", {"svc": {"name": "S", "port": 8123, "external_port": 8123}})

        # Bind a port to make it "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", 18765))
        try:
            resp = test_client.post(
                "/api/preflight/ports",
                json={"ports": [18765]},
                headers=test_client.auth_headers,
            )
            assert resp.status_code == 200
            data = resp.json()
            assert data["available"] is False
            assert len(data["conflicts"]) == 1
        finally:
            sock.close()

    @pytest.mark.parametrize("port", [0, -1, 65536, "not-a-port"])
    def test_rejects_invalid_port_values(self, test_client, port):
        resp = test_client.post(
            "/api/preflight/ports",
            json={"ports": [port]},
            headers=test_client.auth_headers,
        )
        assert resp.status_code == 422

    def test_accepts_valid_port_range_edges(self, test_client):
        resp = test_client.post(
            "/api/preflight/ports",
            json={"ports": [1, 65535]},
            headers=test_client.auth_headers,
        )
        assert resp.status_code == 200


# --- /gpu endpoint (cached paths) ---


class TestGpuEndpoint:

    def test_gpu_cached_hit(self, test_client, monkeypatch):
        from models import GPUInfo
        import main
        gpu = GPUInfo(
            name="RTX 4090", memory_used_mb=2048, memory_total_mb=24576,
            memory_percent=8.3, utilization_percent=35, temperature_c=62,
            gpu_backend="nvidia",
        )
        main._cache.set("gpu_info", gpu, 60)
        resp = test_client.get("/gpu", headers=test_client.auth_headers)
        assert resp.status_code == 200
        assert resp.json()["name"] == "RTX 4090"

    def test_gpu_cached_falsy(self, test_client, monkeypatch):
        import main
        main._cache.set("gpu_info", None, 60)
        resp = test_client.get("/gpu", headers=test_client.auth_headers)
        assert resp.status_code == 503

    def test_gpu_no_cache_no_gpu(self, test_client, monkeypatch):
        import main
        main._cache._store.pop("gpu_info", None)
        monkeypatch.setattr("main.get_gpu_info", lambda: None)
        resp = test_client.get("/gpu", headers=test_client.auth_headers)
        assert resp.status_code == 503


# --- /services, /disk, /model, /bootstrap endpoints ---


class TestCoreEndpoints:

    def test_services_returns_list(self, test_client, monkeypatch):
        from models import ServiceStatus
        statuses = [ServiceStatus(id="s", name="S", port=80, external_port=80, status="healthy")]
        monkeypatch.setattr("main.get_cached_services", lambda: statuses)
        resp = test_client.get("/services", headers=test_client.auth_headers)
        assert resp.status_code == 200

    def test_services_fallback_live(self, test_client, monkeypatch):
        monkeypatch.setattr("main.get_cached_services", lambda: None)
        monkeypatch.setattr("main.get_all_services", AsyncMock(return_value=[]))
        resp = test_client.get("/services", headers=test_client.auth_headers)
        assert resp.status_code == 200
        assert resp.json() == []

    def test_disk_endpoint(self, test_client, monkeypatch):
        from models import DiskUsage
        monkeypatch.setattr("main.get_disk_usage", lambda: DiskUsage(path="/", used_gb=50, total_gb=500, percent=10))
        resp = test_client.get("/disk", headers=test_client.auth_headers)
        assert resp.status_code == 200
        assert resp.json()["total_gb"] == 500.0

    def test_model_endpoint(self, test_client, monkeypatch):
        from models import ModelInfo
        monkeypatch.setattr("main.get_model_info", lambda: ModelInfo(name="T", size_gb=1.0, context_length=4096))
        resp = test_client.get("/model", headers=test_client.auth_headers)
        assert resp.status_code == 200
        assert resp.json()["name"] == "T"

    def test_bootstrap_endpoint(self, test_client, monkeypatch):
        from models import BootstrapStatus
        monkeypatch.setattr("main.get_bootstrap_status", lambda: BootstrapStatus(active=False))
        resp = test_client.get("/bootstrap", headers=test_client.auth_headers)
        assert resp.status_code == 200
        assert resp.json()["active"] is False


# --- /status endpoint (FullStatus) ---


class TestStatusEndpoint:

    def test_returns_full_status(self, test_client, monkeypatch):
        from models import GPUInfo, DiskUsage, ModelInfo, BootstrapStatus
        monkeypatch.setattr("main.get_gpu_info", lambda: GPUInfo(
            name="RTX 4090", memory_used_mb=2048, memory_total_mb=24576,
            memory_percent=8.3, utilization_percent=35, temperature_c=62,
            gpu_backend="nvidia",
        ))
        monkeypatch.setattr("main.get_disk_usage", lambda: DiskUsage(path="/", used_gb=50, total_gb=500, percent=10))
        monkeypatch.setattr("main.get_model_info", lambda: ModelInfo(name="T", size_gb=1.0, context_length=4096))
        monkeypatch.setattr("main.get_bootstrap_status", lambda: BootstrapStatus(active=False))
        monkeypatch.setattr("main.get_uptime", lambda: 3600)
        monkeypatch.setattr("main.get_cached_services", lambda: [])
        monkeypatch.setattr("main.get_all_services", AsyncMock(return_value=[]))

        resp = test_client.get("/status", headers=test_client.auth_headers)
        assert resp.status_code == 200
        data = resp.json()
        assert data["gpu"]["name"] == "RTX 4090"
        assert data["uptime_seconds"] == 3600


# --- /api/status service serialization ---


class TestApiStatusServiceSerialization:

    def test_serialize_services_includes_service_id(self):
        from models import ServiceStatus
        services = [
            ServiceStatus(
                id="llama-server",
                name="llama-server (LLM Inference)",
                port=8080,
                external_port=11434,
                status="healthy",
            )
        ]

        serialized = _serialize_services(services, uptime=42)

        assert serialized == [{
            "id": "llama-server",
            "name": "llama-server (LLM Inference)",
            "status": "healthy",
            "port": 11434,
            "uptime": 42,
        }]

    def test_fallback_services_include_service_ids(self, monkeypatch):
        monkeypatch.setattr("main.SERVICES", {
            "dashboard-api": {
                "name": "Dashboard API (System Status)",
                "port": 3002,
                "external_port": 3002,
            }
        })

        serialized = _fallback_services()

        assert serialized == [{
            "id": "dashboard-api",
            "name": "Dashboard API (System Status)",
            "status": "unknown",
            "port": 3002,
            "uptime": None,
        }]


# --- /api/status fallback on exception ---


class TestApiStatusFallback:

    def test_fallback_on_oserror(self, test_client, monkeypatch):
        """Narrow exception class (OSError) falls through to the safe-fallback dict."""
        monkeypatch.setattr("main._build_api_status", AsyncMock(side_effect=OSError("network down")))

        resp = test_client.get("/api/status", headers=test_client.auth_headers)
        assert resp.status_code == 200
        data = resp.json()
        assert data["gpu"] is None
        assert data["tier"] == "Unknown"
        assert data["services"] == []

    def test_runtime_error_propagates_as_500(self, test_client, monkeypatch):
        """Programming errors (RuntimeError) inside _build_api_status must
        propagate so they surface in tests / monitoring rather than being
        silently masked as 200-with-zeros (CLAUDE.md "Let It Crash")."""
        from fastapi.testclient import TestClient
        from main import app

        monkeypatch.setattr(
            "main._build_api_status",
            AsyncMock(side_effect=RuntimeError("boom")),
        )
        client = TestClient(app, raise_server_exceptions=False)
        resp = client.get("/api/status", headers=test_client.auth_headers)
        assert resp.status_code == 500


# --- _build_api_status tier branches ---


class TestBuildApiStatusTiers:

    @pytest.mark.asyncio
    async def test_tier_entry(self, monkeypatch):
        from models import GPUInfo, BootstrapStatus
        gpu = GPUInfo(
            name="RTX 3060", memory_used_mb=1024, memory_total_mb=12288,
            memory_percent=8.3, utilization_percent=10, temperature_c=55,
            gpu_backend="nvidia",
        )
        monkeypatch.setattr("main.get_gpu_info", lambda: gpu)
        monkeypatch.setattr("main.get_all_services", AsyncMock(return_value=[]))
        monkeypatch.setattr("main.get_model_info", lambda: None)
        monkeypatch.setattr("main.get_bootstrap_status", lambda: BootstrapStatus(active=False))
        monkeypatch.setattr("main.get_loaded_model", AsyncMock(return_value=None))
        monkeypatch.setattr("main.get_llama_metrics", AsyncMock(return_value={"tokens_per_second": 0, "lifetime_tokens": 0}))
        monkeypatch.setattr("main.get_llama_context_size", AsyncMock(return_value=None))
        monkeypatch.setattr("main.get_uptime", lambda: 0)
        monkeypatch.setattr("main.get_cpu_metrics", lambda: {"percent": 0, "temp_c": None})
        monkeypatch.setattr("main.get_ram_metrics", lambda: {"used_gb": 0, "total_gb": 0, "percent": 0})

        result = await _build_api_status()
        assert result["tier"] == "Entry"

    @pytest.mark.asyncio
    async def test_tier_standard(self, monkeypatch):
        from models import GPUInfo, BootstrapStatus
        gpu = GPUInfo(
            name="RTX 4080", memory_used_mb=2048, memory_total_mb=16384,
            memory_percent=12.5, utilization_percent=20, temperature_c=55,
            gpu_backend="nvidia",
        )
        monkeypatch.setattr("main.get_gpu_info", lambda: gpu)
        monkeypatch.setattr("main.get_all_services", AsyncMock(return_value=[]))
        monkeypatch.setattr("main.get_model_info", lambda: None)
        monkeypatch.setattr("main.get_bootstrap_status", lambda: BootstrapStatus(active=False))
        monkeypatch.setattr("main.get_loaded_model", AsyncMock(return_value=None))
        monkeypatch.setattr("main.get_llama_metrics", AsyncMock(return_value={"tokens_per_second": 0, "lifetime_tokens": 0}))
        monkeypatch.setattr("main.get_llama_context_size", AsyncMock(return_value=None))
        monkeypatch.setattr("main.get_uptime", lambda: 0)
        monkeypatch.setattr("main.get_cpu_metrics", lambda: {"percent": 0, "temp_c": None})
        monkeypatch.setattr("main.get_ram_metrics", lambda: {"used_gb": 0, "total_gb": 0, "percent": 0})

        result = await _build_api_status()
        assert result["tier"] == "Standard"

    @pytest.mark.asyncio
    async def test_tier_minimal(self, monkeypatch):
        from models import GPUInfo, BootstrapStatus
        gpu = GPUInfo(
            name="GT 1030", memory_used_mb=256, memory_total_mb=2048,
            memory_percent=12.5, utilization_percent=5, temperature_c=40,
            gpu_backend="nvidia",
        )
        monkeypatch.setattr("main.get_gpu_info", lambda: gpu)
        monkeypatch.setattr("main.get_all_services", AsyncMock(return_value=[]))
        monkeypatch.setattr("main.get_model_info", lambda: None)
        monkeypatch.setattr("main.get_bootstrap_status", lambda: BootstrapStatus(active=False))
        monkeypatch.setattr("main.get_loaded_model", AsyncMock(return_value=None))
        monkeypatch.setattr("main.get_llama_metrics", AsyncMock(return_value={"tokens_per_second": 0, "lifetime_tokens": 0}))
        monkeypatch.setattr("main.get_llama_context_size", AsyncMock(return_value=None))
        monkeypatch.setattr("main.get_uptime", lambda: 0)
        monkeypatch.setattr("main.get_cpu_metrics", lambda: {"percent": 0, "temp_c": None})
        monkeypatch.setattr("main.get_ram_metrics", lambda: {"used_gb": 0, "total_gb": 0, "percent": 0})

        result = await _build_api_status()
        assert result["tier"] == "Minimal"

    @pytest.mark.asyncio
    async def test_no_gpu_returns_unknown_tier(self, monkeypatch):
        from models import BootstrapStatus
        monkeypatch.setattr("main.get_gpu_info", lambda: None)
        monkeypatch.setattr("main.get_all_services", AsyncMock(return_value=[]))
        monkeypatch.setattr("main.get_model_info", lambda: None)
        monkeypatch.setattr("main.get_bootstrap_status", lambda: BootstrapStatus(active=False))
        monkeypatch.setattr("main.get_loaded_model", AsyncMock(return_value=None))
        monkeypatch.setattr("main.get_llama_metrics", AsyncMock(return_value={"tokens_per_second": 0, "lifetime_tokens": 0}))
        monkeypatch.setattr("main.get_llama_context_size", AsyncMock(return_value=None))
        monkeypatch.setattr("main.get_uptime", lambda: 0)
        monkeypatch.setattr("main.get_cpu_metrics", lambda: {"percent": 0, "temp_c": None})
        monkeypatch.setattr("main.get_ram_metrics", lambda: {"used_gb": 0, "total_gb": 0, "percent": 0})

        result = await _build_api_status()
        assert result["tier"] == "Unknown"
        assert result["gpu"] is None

    @pytest.mark.asyncio
    async def test_gpu_with_power_draw(self, monkeypatch):
        from models import GPUInfo, BootstrapStatus
        gpu = GPUInfo(
            name="RTX 4090", memory_used_mb=2048, memory_total_mb=24576,
            memory_percent=8.3, utilization_percent=35, temperature_c=62,
            gpu_backend="nvidia", power_w=320.0,
        )
        monkeypatch.setattr("main.get_gpu_info", lambda: gpu)
        monkeypatch.setattr("main.get_all_services", AsyncMock(return_value=[]))
        monkeypatch.setattr("main.get_model_info", lambda: None)
        monkeypatch.setattr("main.get_bootstrap_status", lambda: BootstrapStatus(active=False))
        monkeypatch.setattr("main.get_loaded_model", AsyncMock(return_value=None))
        monkeypatch.setattr("main.get_llama_metrics", AsyncMock(return_value={"tokens_per_second": 0, "lifetime_tokens": 0}))
        monkeypatch.setattr("main.get_llama_context_size", AsyncMock(return_value=None))
        monkeypatch.setattr("main.get_uptime", lambda: 0)
        monkeypatch.setattr("main.get_cpu_metrics", lambda: {"percent": 0, "temp_c": None})
        monkeypatch.setattr("main.get_ram_metrics", lambda: {"used_gb": 0, "total_gb": 0, "percent": 0})

        result = await _build_api_status()
        assert result["gpu"]["powerDraw"] == 320.0

    @pytest.mark.asyncio
    async def test_active_bootstrap(self, monkeypatch):
        from models import GPUInfo, BootstrapStatus
        gpu = GPUInfo(
            name="RTX 4090", memory_used_mb=2048, memory_total_mb=24576,
            memory_percent=8.3, utilization_percent=35, temperature_c=62,
            gpu_backend="nvidia",
        )
        bs = BootstrapStatus(
            active=True, model_name="Qwen-32B", percent=50.0,
            downloaded_gb=8.0, total_gb=16.0, eta_seconds=120, speed_mbps=100.0,
        )
        monkeypatch.setattr("main.get_gpu_info", lambda: gpu)
        monkeypatch.setattr("main.get_all_services", AsyncMock(return_value=[]))
        monkeypatch.setattr("main.get_model_info", lambda: None)
        monkeypatch.setattr("main.get_bootstrap_status", lambda: bs)
        monkeypatch.setattr("main.get_loaded_model", AsyncMock(return_value=None))
        monkeypatch.setattr("main.get_llama_metrics", AsyncMock(return_value={"tokens_per_second": 0, "lifetime_tokens": 0}))
        monkeypatch.setattr("main.get_llama_context_size", AsyncMock(return_value=None))
        monkeypatch.setattr("main.get_uptime", lambda: 0)
        monkeypatch.setattr("main.get_cpu_metrics", lambda: {"percent": 0, "temp_c": None})
        monkeypatch.setattr("main.get_ram_metrics", lambda: {"used_gb": 0, "total_gb": 0, "percent": 0})

        result = await _build_api_status()
        assert result["bootstrap"]["active"] is True
        assert result["bootstrap"]["model"] == "Qwen-32B"
        assert result["bootstrap"]["percent"] == 50.0
