"""
PDF Comment Agent — FastAPI route tests.

Uses the FastAPI :class:`TestClient` with dependency overrides so the tests
exercise HTTP parsing, validation, and serialisation only — never the real
pydantic-ai agent.
"""

from __future__ import annotations

from collections.abc import Iterator
from pathlib import Path

import pytest
from fastapi.testclient import TestClient

from stirling.api import app
from stirling.api.dependencies import get_pdf_comment_agent
from stirling.config import AppSettings, DocumentsBackend, load_settings
from stirling.contracts.pdf_comments import (
    PdfCommentInstruction,
    PdfCommentRequest,
    PdfCommentResponse,
)

# ---------------------------------------------------------------------------
# Stubs
# ---------------------------------------------------------------------------


class StubSettingsProvider:
    def __call__(self) -> AppSettings:
        return AppSettings(
            smart_model_name="test",
            fast_model_name="test",
            smart_model_max_tokens=8192,
            fast_model_max_tokens=2048,
            model_max_concurrency=32,
            documents_backend=DocumentsBackend.SQLITE,
            rag_embedding_model="test-embed",
            documents_sqlite_path=Path(":memory:"),
            documents_pgvector_dsn="",
            documents_pgvector_pool_min_size=1,
            documents_pgvector_pool_max_size=10,
            rag_chunk_size=512,
            rag_chunk_overlap=64,
            rag_default_top_k=5,
            rag_max_searches=5,
            chunked_reasoner_chars_per_slice=16_000,
            chunked_reasoner_concurrency=10,
            chunked_reasoner_worker_timeout_seconds=60.0,
            chunked_reasoner_notes_char_budget=250_000,
            max_pages=100,
            max_characters=100_000,
            require_user_id=False,
            posthog_enabled=False,
            posthog_api_key="",
            posthog_host="https://eu.i.posthog.com",
        )


class StubPdfCommentAgent:
    """Stub that echoes the session id and returns a canned comment."""

    def __init__(self, response: PdfCommentResponse | None = None) -> None:
        self._response = response
        self.generate_calls: list[PdfCommentRequest] = []

    async def generate(self, request: PdfCommentRequest) -> PdfCommentResponse:
        self.generate_calls.append(request)
        if self._response is not None:
            return self._response
        return PdfCommentResponse(
            session_id=request.session_id,
            comments=[
                PdfCommentInstruction(
                    chunk_id=request.chunks[0].id if request.chunks else "p0-c0",
                    comment_text="Stub comment.",
                )
            ],
            rationale="stubbed response",
        )


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


@pytest.fixture
def stub_agent() -> StubPdfCommentAgent:
    return StubPdfCommentAgent()


@pytest.fixture
def client(stub_agent: StubPdfCommentAgent) -> Iterator[TestClient]:
    app.dependency_overrides[load_settings] = StubSettingsProvider()
    app.dependency_overrides[get_pdf_comment_agent] = lambda: stub_agent
    yield TestClient(app, raise_server_exceptions=False)
    app.dependency_overrides.pop(load_settings, None)
    app.dependency_overrides.pop(get_pdf_comment_agent, None)


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


def _camel_request_body() -> dict[str, object]:
    return {
        "sessionId": "sess-1",
        "userMessage": "flag dates",
        "chunks": [
            {
                "id": "p0-c0",
                "page": 0,
                "x": 72.0,
                "y": 700.0,
                "width": 200.0,
                "height": 12.0,
                "text": "Signed on 5/6/2026",
            }
        ],
    }


def _snake_request_body() -> dict[str, object]:
    return {
        "session_id": "sess-snake",
        "user_message": "flag dates",
        "chunks": [
            {
                "id": "p0-c0",
                "page": 0,
                "x": 72.0,
                "y": 700.0,
                "width": 200.0,
                "height": 12.0,
                "text": "Snake case text",
            }
        ],
    }


# ---------------------------------------------------------------------------
# POST /api/v1/ai/pdf-comment-agent/generate
# ---------------------------------------------------------------------------


class TestGenerateEndpoint:
    def test_camel_case_body_returns_200(self, client: TestClient) -> None:
        resp = client.post("/api/v1/ai/pdf-comment-agent/generate", json=_camel_request_body())
        assert resp.status_code == 200

    def test_camel_case_body_response_has_expected_shape(self, client: TestClient) -> None:
        resp = client.post("/api/v1/ai/pdf-comment-agent/generate", json=_camel_request_body())
        body = resp.json()
        assert body["sessionId"] == "sess-1"
        assert isinstance(body["comments"], list)
        assert len(body["comments"]) == 1
        comment = body["comments"][0]
        assert comment["chunkId"] == "p0-c0"
        assert comment["commentText"] == "Stub comment."
        assert "rationale" in body

    def test_snake_case_body_is_still_accepted(self, client: TestClient) -> None:
        """ApiModel has validate_by_name=True & validate_by_alias=True, so snake_case
        payloads must still be accepted."""
        resp = client.post("/api/v1/ai/pdf-comment-agent/generate", json=_snake_request_body())
        assert resp.status_code == 200
        body = resp.json()
        # Response is always serialised in camelCase regardless of request form.
        assert body["sessionId"] == "sess-snake"

    def test_missing_required_field_returns_422(self, client: TestClient) -> None:
        body = _camel_request_body()
        del body["sessionId"]
        resp = client.post("/api/v1/ai/pdf-comment-agent/generate", json=body)
        assert resp.status_code == 422

    def test_agent_is_called_with_parsed_request(
        self,
        client: TestClient,
        stub_agent: StubPdfCommentAgent,
    ) -> None:
        client.post("/api/v1/ai/pdf-comment-agent/generate", json=_camel_request_body())
        assert len(stub_agent.generate_calls) == 1
        call = stub_agent.generate_calls[0]
        assert call.session_id == "sess-1"
        assert call.user_message == "flag dates"
        assert len(call.chunks) == 1
        assert call.chunks[0].id == "p0-c0"

    def test_agent_exception_surfaces_as_500(self) -> None:
        """If the agent raises (LLM outage, auth failure, OOM), the route must
        surface it as HTTP 500 so Java's AiEngineClient maps it to 502 — rather
        than silently returning an empty/successful response that the Java caller
        would mis-apply as 'zero comments to place'."""

        class FailingAgent:
            async def generate(self, _request: PdfCommentRequest) -> PdfCommentResponse:
                raise RuntimeError("model provider unreachable")

        app.dependency_overrides[load_settings] = StubSettingsProvider()
        app.dependency_overrides[get_pdf_comment_agent] = lambda: FailingAgent()
        try:
            with TestClient(app, raise_server_exceptions=False) as failing_client:
                resp = failing_client.post("/api/v1/ai/pdf-comment-agent/generate", json=_camel_request_body())
            assert resp.status_code == 500
        finally:
            app.dependency_overrides.pop(load_settings, None)
            app.dependency_overrides.pop(get_pdf_comment_agent, None)
