"""Happy-path proxy tests -- mock backend verifies full request lifecycle.

Covers OpenAI and Anthropic dialects (streaming + non-streaming), model listing
with populated backends, and server startup orchestration.
"""

from __future__ import annotations

import json
from typing import TYPE_CHECKING, Any
from unittest.mock import AsyncMock, MagicMock

import httpx
import pytest
from httpx import ASGITransport, AsyncClient

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

if TYPE_CHECKING:
    from fastapi import FastAPI

# ---------------------------------------------------------------------------
# Canned backend responses (OpenAI-compatible format)
# ---------------------------------------------------------------------------

MOCK_MODELS = ["llama3.2:8b", "mistral:7b"]

OPENAI_CHAT_RESPONSE: dict[str, Any] = {
    "id": "chatcmpl-test-001",
    "object": "chat.completion",
    "created": 1700000000,
    "model": "llama3.2:8b",
    "choices": [
        {
            "index": 0,
            "message": {"role": "assistant", "content": "Hello! How can I help you?"},
            "finish_reason": "stop",
        }
    ],
    "usage": {
        "prompt_tokens": 10,
        "completion_tokens": 8,
        "total_tokens": 18,
    },
}

OPENAI_STREAM_LINES: list[str] = [
    'data: {"id":"chatcmpl-test-001","object":"chat.completion.chunk","created":1700000000,"model":"llama3.2:8b","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}',
    'data: {"id":"chatcmpl-test-001","object":"chat.completion.chunk","created":1700000000,"model":"llama3.2:8b","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]}',
    'data: {"id":"chatcmpl-test-001","object":"chat.completion.chunk","created":1700000000,"model":"llama3.2:8b","choices":[{"index":0,"delta":{"content":"!"},"finish_reason":null}]}',
    'data: {"id":"chatcmpl-test-001","object":"chat.completion.chunk","created":1700000000,"model":"llama3.2:8b","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}',
    "data: [DONE]",
]


# ---------------------------------------------------------------------------
# Helpers -- fake httpx responses
# ---------------------------------------------------------------------------


def _make_httpx_response(data: dict[str, Any], status_code: int = 200) -> httpx.Response:
    """Build a real ``httpx.Response`` with JSON body."""
    return httpx.Response(
        status_code=status_code,
        json=data,
        request=httpx.Request("POST", "http://mock-backend:11434/v1/chat/completions"),
    )


class _FakeStreamResponse:
    """Async context manager mimicking ``httpx.AsyncClient.stream()``."""

    def __init__(self, lines: list[str]) -> None:
        self._lines = lines
        self.status_code = 200

    async def __aenter__(self) -> _FakeStreamResponse:
        return self

    async def __aexit__(self, *args: object) -> None:
        pass

    def raise_for_status(self) -> None:
        pass

    async def aiter_lines(self):
        for line in self._lines:
            yield line


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


@pytest.fixture()
def app() -> FastAPI:
    """Fresh FastAPI app without lifespan (same pattern as conftest.py)."""
    return create_app()


@pytest.fixture()
async def mock_ollama_client(app: FastAPI):
    """App with a mock backend returning canned OpenAI-compatible responses.

    Manually registers a fake backend and patches the underlying httpx client
    so no real network calls are made.  Core engine is not loaded, so the
    dispatcher falls through to the OpenAI-compatible dispatch path.
    """
    dispatcher = BackendDispatcher()
    # Ensure Core is not loaded so requests go through the backend dispatch
    dispatcher._core_engine = None
    await dispatcher.startup()

    # Register a backend (uses OpenAI-compatible API at /v1/chat/completions)
    dispatcher._backends.append(
        BackendEntry(
            name="mock-backend",
            base_url="http://mock-backend:11434",
            backend_type="generic",
            models=MOCK_MODELS,
            priority=0,
            healthy=True,
        )
    )

    # Patch the per-backend pool
    mock_backend_url = "http://mock-backend:11434"
    pool = dispatcher._get_or_create_pool(
        BackendEntry(name="mock-backend", base_url=mock_backend_url, backend_type="generic")
    )
    original_post = pool.post
    original_stream = pool.stream

    async def _mock_post(url: str, **kwargs: Any) -> httpx.Response:
        if "/v1/chat/completions" in url:
            return _make_httpx_response(OPENAI_CHAT_RESPONSE)
        return await original_post(url, **kwargs)

    def _mock_stream(method: str, url: str, **kwargs: Any) -> _FakeStreamResponse:
        if "/v1/chat/completions" in url:
            return _FakeStreamResponse(OPENAI_STREAM_LINES)
        return original_stream(method, url, **kwargs)  # type: ignore[return-value]

    pool.post = AsyncMock(side_effect=_mock_post)  # type: ignore[method-assign]
    pool.stream = MagicMock(side_effect=_mock_stream)  # type: ignore[method-assign]

    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()


# ===================================================================
# OpenAI format -- success paths
# ===================================================================


@pytest.mark.unit
async def test_openai_chat_completion_success(mock_ollama_client: AsyncClient) -> None:
    """POST /v1/chat/completions returns valid OpenAI response when backend is available."""
    resp = await mock_ollama_client.post(
        "/v1/chat/completions",
        json={
            "model": "llama3.2:8b",
            "messages": [{"role": "user", "content": "Hello"}],
        },
    )
    assert resp.status_code == 200
    data = resp.json()

    # Top-level envelope fields
    assert "id" in data
    assert data["object"] == "chat.completion"
    assert "created" in data
    assert data["model"] == "llama3.2:8b"

    # Choices
    assert len(data["choices"]) == 1
    choice = data["choices"][0]
    assert choice["index"] == 0
    assert choice["message"]["role"] == "assistant"
    assert choice["message"]["content"] == "Hello! How can I help you?"
    assert choice["finish_reason"] == "stop"

    # Usage
    assert data["usage"]["prompt_tokens"] == 10
    assert data["usage"]["completion_tokens"] == 8
    assert data["usage"]["total_tokens"] == 18


@pytest.mark.unit
async def test_openai_response_has_correct_format(mock_ollama_client: AsyncClient) -> None:
    """Response matches OpenAI ChatCompletion schema exactly."""
    resp = await mock_ollama_client.post(
        "/v1/chat/completions",
        json={
            "model": "llama3.2:8b",
            "messages": [{"role": "user", "content": "Hi"}],
        },
    )
    assert resp.status_code == 200
    data = resp.json()

    # id starts with chatcmpl-
    assert data["id"].startswith("chatcmpl-"), f"id should start with 'chatcmpl-', got {data['id']}"

    # object field must be exactly "chat.completion"
    assert data["object"] == "chat.completion"

    # created is a unix timestamp (integer > 0)
    assert isinstance(data["created"], int)
    assert data["created"] > 0

    # model field echoes the requested model
    assert data["model"] == "llama3.2:8b"

    # choices is a list of exactly 1 item (non-streaming, n=1)
    assert isinstance(data["choices"], list)
    assert len(data["choices"]) == 1

    # usage has all three token counts
    usage = data["usage"]
    assert "prompt_tokens" in usage
    assert "completion_tokens" in usage
    assert "total_tokens" in usage


@pytest.mark.unit
async def test_openai_streaming_success(mock_ollama_client: AsyncClient) -> None:
    """POST /v1/chat/completions with stream=true returns SSE."""
    resp = await mock_ollama_client.post(
        "/v1/chat/completions",
        json={
            "model": "llama3.2:8b",
            "messages": [{"role": "user", "content": "Hi"}],
            "stream": True,
        },
    )
    assert resp.status_code == 200
    assert "text/event-stream" in resp.headers["content-type"]

    raw = resp.text
    lines = [ln for ln in raw.split("\n") if ln.startswith("data: ")]
    assert len(lines) >= 3, f"Expected at least 3 SSE data lines, got {len(lines)}"

    # Last line must be the [DONE] sentinel
    assert lines[-1].strip() == "data: [DONE]"

    # Parse the first data line -- should carry role:"assistant"
    first_payload = json.loads(lines[0].removeprefix("data: "))
    assert first_payload["object"] == "chat.completion.chunk"
    assert first_payload["choices"][0]["delta"]["role"] == "assistant"

    # Content lines should carry actual text tokens
    content_chunks: list[str] = []
    for line in lines[1:-1]:
        payload = json.loads(line.removeprefix("data: "))
        delta = payload["choices"][0]["delta"]
        if delta.get("content"):
            content_chunks.append(delta["content"])

    # We expect at least "Hello" and "!" from the mock stream
    assert len(content_chunks) >= 1, "Expected at least one content token in the stream"
    combined = "".join(content_chunks)
    assert "Hello" in combined, f"Expected 'Hello' in streamed content, got '{combined}'"


# ===================================================================
# Anthropic format -- success paths
# ===================================================================


@pytest.mark.unit
async def test_anthropic_messages_success(mock_ollama_client: AsyncClient) -> None:
    """POST /v1/messages returns valid Anthropic response when backend is available."""
    resp = await mock_ollama_client.post(
        "/v1/messages",
        json={
            "model": "llama3.2:8b",
            "messages": [{"role": "user", "content": "Hello"}],
            "max_tokens": 256,
        },
    )
    assert resp.status_code == 200
    data = resp.json()

    # Top-level envelope
    assert "id" in data
    assert data["type"] == "message"
    assert data["role"] == "assistant"

    # Content blocks
    assert isinstance(data["content"], list)
    assert len(data["content"]) >= 1
    assert data["content"][0]["type"] == "text"
    assert data["content"][0]["text"] == "Hello! How can I help you?"

    # Usage
    assert data["usage"]["input_tokens"] == 10
    assert data["usage"]["output_tokens"] == 8


@pytest.mark.unit
async def test_anthropic_response_has_correct_format(mock_ollama_client: AsyncClient) -> None:
    """Response matches Anthropic Messages schema exactly."""
    resp = await mock_ollama_client.post(
        "/v1/messages",
        json={
            "model": "llama3.2:8b",
            "messages": [{"role": "user", "content": "Hi"}],
            "max_tokens": 100,
        },
    )
    assert resp.status_code == 200
    data = resp.json()

    # id starts with msg_
    assert data["id"].startswith("msg_"), f"id should start with 'msg_', got {data['id']}"

    # type must be "message"
    assert data["type"] == "message"

    # content is a list of content blocks
    assert isinstance(data["content"], list)
    for block in data["content"]:
        assert "type" in block, "Each content block must have a 'type' field"

    # stop_reason present
    assert data["stop_reason"] in ("end_turn", "max_tokens", "stop_sequence", "tool_use")

    # model echoed
    assert data["model"] == "llama3.2:8b"


@pytest.mark.unit
async def test_anthropic_streaming_success(mock_ollama_client: AsyncClient) -> None:
    """POST /v1/messages with stream=true returns Anthropic SSE."""
    resp = await mock_ollama_client.post(
        "/v1/messages",
        json={
            "model": "llama3.2:8b",
            "messages": [{"role": "user", "content": "Hi"}],
            "max_tokens": 100,
            "stream": True,
        },
    )
    assert resp.status_code == 200
    assert "text/event-stream" in resp.headers["content-type"]

    raw = resp.text
    # Anthropic SSE uses "event: <type>\ndata: <json>\n\n"
    event_lines = [ln for ln in raw.split("\n") if ln.startswith("event: ")]
    event_types = [ln.removeprefix("event: ").strip() for ln in event_lines]

    # Required event sequence
    assert "message_start" in event_types, "Stream must include message_start event"
    assert "content_block_start" in event_types, "Stream must include content_block_start event"
    assert "content_block_delta" in event_types, "Stream must include content_block_delta event"
    assert "message_stop" in event_types, "Stream must include message_stop event"

    # Verify content_block_delta carries actual text
    data_lines = [ln for ln in raw.split("\n") if ln.startswith("data: ")]
    deltas: list[str] = []
    for dl in data_lines:
        payload = json.loads(dl.removeprefix("data: "))
        if payload.get("type") == "content_block_delta":
            text = payload.get("delta", {}).get("text", "")
            if text:
                deltas.append(text)

    assert len(deltas) >= 1, "Expected at least one content_block_delta with text"
    combined = "".join(deltas)
    assert "Hello" in combined, f"Expected 'Hello' in streamed deltas, got '{combined}'"


# ===================================================================
# Model listing with backends
# ===================================================================


@pytest.mark.unit
async def test_models_lists_backend_models(mock_ollama_client: AsyncClient) -> None:
    """GET /v1/models shows backend models when available."""
    resp = await mock_ollama_client.get("/v1/models")
    assert resp.status_code == 200
    data = resp.json()

    assert data["object"] == "list"
    model_ids = [m["id"] for m in data["data"]]
    assert "llama3.2:8b" in model_ids, f"Expected llama3.2:8b in models, got {model_ids}"
    assert "mistral:7b" in model_ids, f"Expected mistral:7b in models, got {model_ids}"

    # Each model entry must have the standard fields
    for model_entry in data["data"]:
        assert "id" in model_entry
        assert model_entry["object"] == "model"
        assert "created" in model_entry
        assert "owned_by" in model_entry


# ===================================================================
# Core engine registration during startup
# ===================================================================


@pytest.mark.unit
async def test_core_engine_registration_on_startup() -> None:
    """BackendDispatcher registers Core engine as backend when a model is loaded."""
    dispatcher = BackendDispatcher()

    # Mock a loaded Core engine
    mock_engine = type("MockEngine", (), {"is_loaded": True})()
    dispatcher._core_engine = mock_engine

    await dispatcher.startup()

    # Core should have been registered as a backend
    core_backends = [b for b in dispatcher._backends if b.name == "llmhosts-core"]
    assert len(core_backends) == 1
    backend = core_backends[0]
    assert backend.backend_type == "llmhosts-core"
    assert backend.healthy is True
    assert backend.priority == -1  # highest priority

    await dispatcher.shutdown()


# ===================================================================
# Server startup
# ===================================================================


@pytest.mark.unit
async def test_server_startup_creates_app() -> None:
    """LLMHostsServer.startup() returns a StartupReport."""
    from llmhosts.config import LLMHostsConfig
    from llmhosts.server import LLMHostsServer, StartupReport

    config = LLMHostsConfig()
    # Disable auto_discover so we don't hit the real Ollama endpoint
    config.ollama.auto_discover = False

    server = LLMHostsServer(config)
    report = await server.startup()

    assert isinstance(report, StartupReport)
    # With auto_discover=False, ollama should be unavailable
    assert report.ollama_available is False
    # But we should still get a list of endpoints
    assert len(report.endpoints) > 0
    # Warnings should mention disabled discovery
    assert any("disabled" in w.lower() for w in report.warnings), (
        f"Expected a warning about disabled Ollama discovery, got {report.warnings}"
    )
    # Router and cache tiers should have values
    assert report.router_tier != ""
    assert report.cache_tier != ""

    await server.shutdown()
