"""End-to-end tests for the /mcp HTTP endpoint mounted on the Quart app.

These exercise the ASGI dispatch + auth middleware by driving the app's ASGI
callable directly. We avoid Quart's test_client() because it expects Quart's
request pipeline to set `app._preserved_context` as a side effect, but our
ASGI middleware forwards /mcp requests to FastMCP without touching the Quart
pipeline (correct production behavior), which causes test_client to fail on
teardown.

The api_keys lookup is mocked so the tests don't require a database, matching
the pattern in test_api_keys.py.
"""
import json
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from scribe.app import create_app


async def _send_request(
    app, method: str, path: str,
    headers: dict | None = None, body: bytes | None = None,
) -> tuple[int, bytes]:
    """Drive the app's ASGI callable directly and collect the response."""
    raw_headers = [(k.lower().encode(), v.encode()) for k, v in (headers or {}).items()]
    if body is not None:
        raw_headers.append((b"content-length", str(len(body)).encode()))
    scope = {
        "type": "http",
        "asgi": {"version": "3.0", "spec_version": "2.3"},
        "http_version": "1.1",
        "method": method,
        "scheme": "http",
        "path": path,
        "raw_path": path.encode(),
        "query_string": b"",
        "root_path": "",
        "headers": raw_headers,
        "client": ("testclient", 50000),
        "server": ("testserver", 80),
    }
    receive_messages = [{
        "type": "http.request", "body": body or b"", "more_body": False,
    }]
    sent: list[dict] = []

    async def receive():
        if receive_messages:
            return receive_messages.pop(0)
        return {"type": "http.disconnect"}

    async def send(message):
        sent.append(message)

    await app.asgi_app(scope, receive, send)

    start = next(m for m in sent if m["type"] == "http.response.start")
    body_chunks = b"".join(
        m.get("body", b"") for m in sent if m["type"] == "http.response.body"
    )
    return start["status"], body_chunks


@pytest.mark.asyncio
async def test_mcp_endpoint_unauthenticated_returns_401():
    app = create_app()
    status, _ = await _send_request(app, "POST", "/mcp")
    assert status == 401


@pytest.mark.asyncio
async def test_mcp_endpoint_invalid_token_returns_401():
    app = create_app()
    with patch(
        "scribe.mcp.auth.lookup_key",
        AsyncMock(return_value=None),
    ):
        status, _ = await _send_request(
            app, "POST", "/mcp",
            headers={"Authorization": "Bearer fmcp_invalid"},
        )
    assert status == 401


@pytest.mark.asyncio
async def test_mcp_endpoint_valid_token_passes_auth():
    """With a valid Bearer, the request must successfully reach FastMCP's
    initialize handler. Asserting `!= 401` is too weak: it lets a 404 from
    a path-mismatch (the original bug) through. FastMCP responds 200 to a
    well-formed initialize handshake.

    FastMCP's session manager normally starts via Quart's @before_serving
    hook in production. This raw-ASGI test doesn't go through Quart's
    serving lifecycle, so we manually enter the session manager."""
    fake_key = MagicMock()
    fake_key.user_id = 7
    fake_key.scope = "write"
    app = create_app()
    initialize_body = json.dumps({
        "jsonrpc": "2.0", "id": 1, "method": "initialize",
        "params": {
            "protocolVersion": "2024-11-05",
            "capabilities": {},
            "clientInfo": {"name": "test", "version": "0"},
        },
    }).encode()
    async with app.mcp_instance.session_manager.run():
        with patch(
            "scribe.mcp.auth.lookup_key",
            AsyncMock(return_value=fake_key),
        ):
            status, _ = await _send_request(
                app, "POST", "/mcp",
                headers={
                    # FastMCP's transport_security module enforces a Host
                    # header (DNS-rebinding protection); without it the
                    # request gets a 421 Misdirected Request.
                    "Host": "testserver",
                    "Authorization": "Bearer fmcp_valid",
                    "Content-Type": "application/json",
                    "Accept": "application/json, text/event-stream",
                },
                body=initialize_body,
            )
    assert status == 200, f"expected 200 from FastMCP initialize, got {status}"


# Note: there's no explicit "non-/mcp paths bypass the middleware" test here
# because driving Quart's full request pipeline through a hand-rolled ASGI
# scope (no lifespan startup, no hypercorn state) doesn't produce a response.
# The bypass behavior is implicit: if the middleware ate non-/mcp requests,
# the rest of the test suite (~250 tests hitting /api/*) would break.
