"""End-to-end integration tests for LLMHosts Core dispatch through the HTTP API.

Tests the full flow: HTTP request → FastAPI route → dispatcher → Core engine → response.
Uses a mock Core engine (no real GGUF model needed) to validate the pipeline wiring.
"""

from __future__ import annotations

import json

import pytest
from httpx import ASGITransport, AsyncClient

from llmhosts.proxy.app import create_app
from llmhosts.proxy.dispatcher import BackendDispatcher

# ---------------------------------------------------------------------------
# Mock Core engine
# ---------------------------------------------------------------------------


class MockCoreEngine:
    """Simulates LLMHostsInferenceEngine with a loaded model."""

    is_loaded = True

    def generate(self, prompt: str, **kwargs) -> dict:
        return {
            "text": "I am a helpful AI assistant.",
            "tokens": [42, 43, 44, 45, 46, 47],
            "token_count": 6,
        }

    def generate_tokens(self, token_ids: list[int], **kwargs) -> list[int]:
        return [42, 43, 44, 45, 46]


class MockCoreEngineNoTokenizer:
    """Core engine without a tokenizer — falls back to raw token output."""

    is_loaded = True
    _tokenizer = None

    def generate(self, prompt: str, **kwargs) -> dict:
        raise RuntimeError("No tokenizer loaded")

    def generate_tokens(self, token_ids: list[int], **kwargs) -> list[int]:
        return [10, 20, 30]


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture()
async def core_client():
    """HTTP client with a mock Core engine wired into the dispatcher."""
    app = create_app()
    dispatcher = BackendDispatcher()
    await dispatcher.startup()

    # Inject mock engine — Core is the default local backend when loaded
    dispatcher._core_engine = MockCoreEngine()

    # Register the Core backend entry so _select_backend can find it
    from llmhosts.proxy.dispatcher import BackendEntry

    dispatcher._backends.append(
        BackendEntry(
            name="llmhosts-core",
            base_url="local://",
            backend_type="llmhosts-core",
            models=["qwen2.5:0.5b"],
            priority=-1,
            healthy=True,
        )
    )

    app.state.dispatcher = dispatcher

    transport = ASGITransport(app=app, raise_app_exceptions=False)
    async with AsyncClient(transport=transport, base_url="http://test") as ac:
        yield ac

    await dispatcher.shutdown()


# ---------------------------------------------------------------------------
# E2E HTTP → Core engine tests
# ---------------------------------------------------------------------------


@pytest.mark.integration
class TestCoreE2EHTTP:
    """Full HTTP path through the FastAPI routes to Core engine dispatch."""

    @pytest.mark.asyncio
    async def test_chat_completions_returns_openai_format(self, core_client: AsyncClient) -> None:
        """POST /v1/chat/completions → OpenAI ChatCompletion response via Core engine."""
        response = await core_client.post(
            "/v1/chat/completions",
            json={
                "model": "qwen2.5:0.5b",
                "messages": [{"role": "user", "content": "Hello, who are you?"}],
                "max_tokens": 50,
                "temperature": 0.0,
            },
        )

        assert response.status_code == 200
        body = response.json()

        # OpenAI ChatCompletion required fields
        assert body["object"] == "chat.completion"
        assert body["id"].startswith("chatcmpl-")
        assert isinstance(body["created"], int)
        assert body["model"] == "qwen2.5:0.5b"

        # Choices structure
        assert len(body["choices"]) == 1
        choice = body["choices"][0]
        assert choice["index"] == 0
        assert choice["message"]["role"] == "assistant"
        assert isinstance(choice["message"]["content"], str)
        assert len(choice["message"]["content"]) > 0
        assert choice["finish_reason"] == "stop"

        # Usage stats
        assert "usage" in body
        usage = body["usage"]
        assert usage["completion_tokens"] > 0
        assert usage["total_tokens"] > 0

    @pytest.mark.asyncio
    async def test_response_has_standard_headers(self, core_client: AsyncClient) -> None:
        """Response includes standard LLMHosts headers (privacy tier, request ID)."""
        response = await core_client.post(
            "/v1/chat/completions",
            json={
                "model": "qwen2.5:0.5b",
                "messages": [{"role": "user", "content": "Hi"}],
            },
        )

        assert response.status_code == 200
        # Standard headers always present
        assert response.headers.get("x-request-id") is not None
        assert response.headers.get("x-llmhosts-privacy-tier") is not None

    @pytest.mark.asyncio
    async def test_streaming_returns_sse(self, core_client: AsyncClient) -> None:
        """POST /v1/chat/completions with stream=true returns SSE chunks."""
        response = await core_client.post(
            "/v1/chat/completions",
            json={
                "model": "qwen2.5:0.5b",
                "messages": [{"role": "user", "content": "Hello"}],
                "stream": True,
            },
        )

        assert response.status_code == 200

        # Parse SSE lines
        lines = response.text.strip().split("\n")
        data_lines = [line for line in lines if line.startswith("data: ")]
        assert len(data_lines) >= 2  # at least one content chunk + [DONE]

        # Last data line should be [DONE]
        assert data_lines[-1] == "data: [DONE]"

        # First data line should be a valid SSE chunk
        first_chunk = json.loads(data_lines[0].removeprefix("data: "))
        assert first_chunk["object"] == "chat.completion.chunk"
        assert first_chunk["choices"][0]["delta"].get("content") is not None

    @pytest.mark.asyncio
    async def test_multi_message_conversation(self, core_client: AsyncClient) -> None:
        """Multi-turn conversation works through the full HTTP path."""
        response = await core_client.post(
            "/v1/chat/completions",
            json={
                "model": "qwen2.5:0.5b",
                "messages": [
                    {"role": "system", "content": "You are helpful."},
                    {"role": "user", "content": "Hello"},
                    {"role": "assistant", "content": "Hi there!"},
                    {"role": "user", "content": "How are you?"},
                ],
            },
        )

        assert response.status_code == 200
        body = response.json()
        assert body["choices"][0]["message"]["role"] == "assistant"
        assert len(body["choices"][0]["message"]["content"]) > 0


# ---------------------------------------------------------------------------
# Chat template tests
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestChatTemplateFormatting:
    """Verify model-aware chat template selection."""

    def _make_dispatcher(self) -> BackendDispatcher:
        d = BackendDispatcher.__new__(BackendDispatcher)
        return d

    def test_qwen_template(self) -> None:
        d = self._make_dispatcher()
        messages = [
            {"role": "system", "content": "You are helpful."},
            {"role": "user", "content": "Hello"},
        ]
        result = d._format_chat_prompt(messages, "qwen2.5-0.5b-instruct")

        assert "<|im_start|>system\nYou are helpful.<|im_end|>" in result
        assert "<|im_start|>user\nHello<|im_end|>" in result
        assert result.endswith("<|im_start|>assistant\n")

    def test_llama_template(self) -> None:
        d = self._make_dispatcher()
        messages = [
            {"role": "system", "content": "Be concise."},
            {"role": "user", "content": "Hello"},
        ]
        result = d._format_chat_prompt(messages, "llama-3.1-8b")

        assert "<<SYS>>" in result
        assert "Be concise." in result
        assert "[INST] Hello [/INST]" in result

    def test_mistral_template(self) -> None:
        d = self._make_dispatcher()
        messages = [{"role": "user", "content": "Hi"}]
        result = d._format_chat_prompt(messages, "mistral-7b-instruct")
        assert "[INST] Hi [/INST]" in result

    def test_generic_fallback_template(self) -> None:
        d = self._make_dispatcher()
        messages = [{"role": "user", "content": "Hello"}]
        result = d._format_chat_prompt(messages, "some-unknown-model")

        assert "<|user|>" in result
        assert "Hello" in result
        assert result.endswith("<|assistant|>\n")

    def test_empty_model_uses_generic(self) -> None:
        d = self._make_dispatcher()
        messages = [{"role": "user", "content": "Hi"}]
        result = d._format_chat_prompt(messages, "")
        assert "<|user|>" in result


# ---------------------------------------------------------------------------
# Tokenizer fallback path
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestCoreTokenizerFallback:
    """When no tokenizer is available, Core falls back to raw token output."""

    @pytest.mark.asyncio
    async def test_no_tokenizer_falls_back_to_raw_tokens(self) -> None:
        dispatcher = BackendDispatcher.__new__(BackendDispatcher)
        dispatcher._core_engine = MockCoreEngineNoTokenizer()
        dispatcher._config = type("Config", (), {"privacy_mode": None})()

        messages = [{"role": "user", "content": "Hello"}]
        body, headers = await dispatcher._dispatch_core(messages, model="test-model")

        # Should return stringified token IDs
        content = body["choices"][0]["message"]["content"]
        assert "10" in content
        assert "20" in content
        assert "30" in content
        assert headers["X-LLMHosts-Backend"] == "llmhosts-core"


# ---------------------------------------------------------------------------
# HuggingFace repo guessing
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestHFRepoGuessing:
    """Verify _guess_hf_repo maps model filenames to HuggingFace repos."""

    def test_qwen_0_5b(self) -> None:
        from llmhosts.inference.core import LLMHostsInferenceEngine

        assert (
            LLMHostsInferenceEngine._guess_hf_repo("qwen2.5-0.5b-instruct-q4_k_m.gguf") == "Qwen/Qwen2.5-0.5B-Instruct"
        )

    def test_qwen_7b(self) -> None:
        from llmhosts.inference.core import LLMHostsInferenceEngine

        assert LLMHostsInferenceEngine._guess_hf_repo("qwen2.5-7b-instruct-q4.gguf") == "Qwen/Qwen2.5-7B-Instruct"

    def test_llama_model(self) -> None:
        from llmhosts.inference.core import LLMHostsInferenceEngine

        assert (
            LLMHostsInferenceEngine._guess_hf_repo("llama-3-8b-instruct-q5.gguf") == "meta-llama/Llama-3.1-8B-Instruct"
        )

    def test_mistral_model(self) -> None:
        from llmhosts.inference.core import LLMHostsInferenceEngine

        assert (
            LLMHostsInferenceEngine._guess_hf_repo("mistral-7b-instruct-v0.3-q4.gguf")
            == "mistralai/Mistral-7B-Instruct-v0.3"
        )

    def test_unknown_model_returns_none(self) -> None:
        from llmhosts.inference.core import LLMHostsInferenceEngine

        assert LLMHostsInferenceEngine._guess_hf_repo("totally-unknown-model.gguf") is None
