"""Tests for vLLM discovery and engine management."""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from llmhosts.discovery.engine import EngineManager, EngineType, InferenceEngine
from llmhosts.discovery.ollama import OllamaDiscovery
from llmhosts.discovery.vllm import VLLMDiscovery, VLLMMetrics, VLLMModel

# -------------------------------------------------------------------
# VLLMModel
# -------------------------------------------------------------------


@pytest.mark.unit
def test_vllm_model_fields() -> None:
    """VLLMModel has required fields with defaults."""
    m = VLLMModel(id="meta-llama/Llama-3.2-8B")
    assert m.id == "meta-llama/Llama-3.2-8B"
    assert m.object == "model"
    assert m.owned_by == "vllm"
    assert m.max_model_len is None

    m2 = VLLMModel(id="x", max_model_len=4096)
    assert m2.max_model_len == 4096


# -------------------------------------------------------------------
# InferenceEngine
# -------------------------------------------------------------------


@pytest.mark.unit
def test_inference_engine_defaults() -> None:
    """InferenceEngine has sensible defaults."""
    e = InferenceEngine(engine_type=EngineType.OLLAMA, host="http://localhost:11434")
    assert e.engine_type == EngineType.OLLAMA
    assert e.host == "http://localhost:11434"
    assert e.available is False
    assert e.models == []
    assert e.throughput_tps is None


# -------------------------------------------------------------------
# EngineType
# -------------------------------------------------------------------


@pytest.mark.unit
def test_engine_type_enum_values() -> None:
    """EngineType has expected values."""
    assert EngineType.OLLAMA.value == "ollama"
    assert EngineType.VLLM.value == "vllm"
    assert EngineType.AUTO.value == "auto"


# -------------------------------------------------------------------
# VLLMDiscovery - mock HTTP
# -------------------------------------------------------------------


@pytest.mark.unit
@pytest.mark.asyncio
async def test_vllm_discovery_available() -> None:
    """VLLMDiscovery.is_available returns True when vLLM responds 200."""
    with patch("llmhosts.discovery.vllm.httpx.AsyncClient") as mock_client_cls:
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_client = AsyncMock()
        mock_client.get = AsyncMock(return_value=mock_resp)
        mock_client.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client.__aexit__ = AsyncMock(return_value=None)
        mock_client.aclose = AsyncMock()
        mock_client_cls.return_value = mock_client

        async with VLLMDiscovery(host="http://test:8000") as vllm:
            result = await vllm.is_available()
        assert result is True


@pytest.mark.unit
@pytest.mark.asyncio
async def test_vllm_discovery_unavailable() -> None:
    """VLLMDiscovery.is_available returns False when vLLM is unreachable."""
    import httpx

    with patch("llmhosts.discovery.vllm.httpx.AsyncClient") as mock_client_cls:
        mock_client = AsyncMock()
        mock_client.get = AsyncMock(side_effect=httpx.ConnectError("connection refused"))
        mock_client.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client.__aexit__ = AsyncMock(return_value=None)
        mock_client.aclose = AsyncMock()
        mock_client_cls.return_value = mock_client

        async with VLLMDiscovery(host="http://test:8000") as vllm:
            result = await vllm.is_available()
        assert result is False


@pytest.mark.unit
@pytest.mark.asyncio
async def test_vllm_list_models() -> None:
    """VLLMDiscovery.list_models parses OpenAI /v1/models response."""
    with patch("llmhosts.discovery.vllm.httpx.AsyncClient") as mock_client_cls:
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.json.return_value = {
            "data": [
                {"id": "model-a", "object": "model", "owned_by": "vllm", "root": {"max_model_len": 4096}},
                {"id": "model-b", "object": "model", "owned_by": "vllm"},
            ]
        }
        mock_client = AsyncMock()
        mock_client.get = AsyncMock(return_value=mock_resp)
        mock_client.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client.__aexit__ = AsyncMock(return_value=None)
        mock_client.aclose = AsyncMock()
        mock_client_cls.return_value = mock_client

        async with VLLMDiscovery(host="http://test:8000") as vllm:
            models = await vllm.list_models()

        assert len(models) == 2
        assert models[0].id == "model-a"
        assert models[0].max_model_len == 4096
        assert models[1].id == "model-b"
        assert models[1].max_model_len is None


@pytest.mark.unit
@pytest.mark.asyncio
async def test_vllm_metrics() -> None:
    """VLLMDiscovery.get_metrics parses Prometheus /metrics response."""
    with patch("llmhosts.discovery.vllm.httpx.AsyncClient") as mock_client_cls:
        metrics_text = """
vllm:num_requests_running{model_name="x"} 3.0
vllm:num_requests_waiting{model_name="x"} 1.5
"""
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.text = metrics_text
        mock_client = AsyncMock()
        mock_client.get = AsyncMock(return_value=mock_resp)
        mock_client.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client.__aexit__ = AsyncMock(return_value=None)
        mock_client.aclose = AsyncMock()
        mock_client_cls.return_value = mock_client

        async with VLLMDiscovery(host="http://test:8000") as vllm:
            metrics = await vllm.get_metrics()

        assert metrics.active_requests == 3
        assert metrics.avg_queue_depth == 1.5
        assert metrics.throughput_tps == 0.0


@pytest.mark.unit
@pytest.mark.asyncio
async def test_vllm_health_check() -> None:
    """VLLMDiscovery.health_check returns VLLMHealth with correct fields."""
    with patch("llmhosts.discovery.vllm.httpx.AsyncClient") as mock_client_cls:
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.json.return_value = {"data": [{"id": "m1"}]}
        mock_ver_resp = MagicMock()
        mock_ver_resp.status_code = 200
        mock_ver_resp.text = "0.4.0"

        mock_client = AsyncMock()
        # is_available -> get /v1/models, list_models -> get /v1/models, version -> get /version
        mock_client.get = AsyncMock(side_effect=[mock_resp, mock_resp, mock_ver_resp])
        mock_client.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client.__aexit__ = AsyncMock(return_value=None)
        mock_client.aclose = AsyncMock()
        mock_client_cls.return_value = mock_client

        async with VLLMDiscovery(host="http://test:8000") as vllm:
            health = await vllm.health_check()

        assert health.healthy is True
        assert health.models_loaded == 1
        assert health.version == "0.4.0"


# -------------------------------------------------------------------
# EngineManager
# -------------------------------------------------------------------


@pytest.mark.unit
@pytest.mark.asyncio
async def test_engine_manager_discover_all() -> None:
    """EngineManager.discover_all returns both Ollama and vLLM engines."""
    from llmhosts.config import LLMHostsConfig

    mock_cfg = LLMHostsConfig.model_validate(
        {
            "ollama": {"host": "http://localhost:11434"},
            "engine": {"type": "ollama", "vllm_host": "http://localhost:8000"},
        }
    )

    with (
        patch.object(OllamaDiscovery, "is_available", new_callable=AsyncMock, return_value=True),
        patch.object(OllamaDiscovery, "list_models", new_callable=AsyncMock, return_value=[]),
        patch.object(VLLMDiscovery, "is_available", new_callable=AsyncMock, return_value=False),
        patch.object(VLLMDiscovery, "list_models", new_callable=AsyncMock, return_value=[]),
        patch.object(VLLMDiscovery, "get_metrics", new_callable=AsyncMock, return_value=VLLMMetrics()),
    ):
        mgr = EngineManager(config=mock_cfg)
        engines = await mgr.discover_all()

    assert len(engines) == 2
    ollama_eng = next(e for e in engines if e.engine_type == EngineType.OLLAMA)
    vllm_eng = next(e for e in engines if e.engine_type == EngineType.VLLM)
    assert ollama_eng.available is True
    assert ollama_eng.host == "http://localhost:11434"
    assert vllm_eng.available is False
    assert vllm_eng.host == "http://localhost:8000"


@pytest.mark.unit
@pytest.mark.asyncio
async def test_engine_manager_switch_engine(tmp_path: str) -> None:
    """EngineManager.switch_engine persists to config file."""
    from pathlib import Path

    from llmhosts.config import LLMHostsConfig

    llmhosts_dir = Path(tmp_path) / ".llmhosts"
    llmhosts_dir.mkdir(parents=True, exist_ok=True)
    cfg_path = llmhosts_dir / "config.toml"
    cfg_path.write_text(
        '[ollama]\nhost = "http://localhost:11434"\n[engine]\ntype = "ollama"\nvllm_host = "http://localhost:8000"'
    )

    mock_cfg = LLMHostsConfig.model_validate(
        {
            "ollama": {"host": "http://localhost:11434"},
            "engine": {"type": "ollama", "vllm_host": "http://localhost:8000"},
        }
    )

    with patch("llmhosts.discovery.engine._config_path", return_value=cfg_path):
        mgr = EngineManager(config=mock_cfg)
        result = await mgr.switch_engine(EngineType.VLLM)

    assert result is True
    content = cfg_path.read_text()
    assert "vllm" in content
    assert 'type = "vllm"' in content or "type = 'vllm'" in content
