"""Contract tests for the suite's streaming ASGI bridge.

These pin what `StreamingASGITransport` itself guarantees — chunk-by-chunk delivery, disconnect
propagation, and failure handling — against minimal hand-written ASGI applications, so the MCP
transport tests built on top of it never have to wonder what the harness provides. They are
harness self-tests, not interaction-model tests, and are exempted from the requirement-coverage
contract in `test_coverage.py`.
"""

import anyio
import httpx
import pytest
from starlette.types import Message, Receive, Scope, Send

from tests.interaction.transports._bridge import StreamingASGITransport

pytestmark = pytest.mark.anyio


async def test_response_chunks_arrive_as_the_application_sends_them() -> None:
    """Each body chunk is delivered as sent, empty chunks are skipped, and the stream ends with the application."""

    async def chunked_app(scope: Scope, receive: Receive, send: Send) -> None:
        assert scope["type"] == "http"
        assert (await receive())["type"] == "http.request"
        await send({"type": "http.response.start", "status": 200, "headers": [(b"content-type", b"text/plain")]})
        await send({"type": "http.response.body", "body": b"first", "more_body": True})
        await send({"type": "http.response.body", "body": b"", "more_body": True})
        await send({"type": "http.response.body", "body": b"second", "more_body": False})

    async with (
        httpx.AsyncClient(transport=StreamingASGITransport(chunked_app), base_url="http://bridge") as http,
        http.stream("GET", "/chunks") as response,
    ):
        with anyio.fail_after(5):
            chunks = [chunk async for chunk in response.aiter_raw()]

    assert response.status_code == 200
    assert response.headers["content-type"] == "text/plain"
    assert chunks == [b"first", b"second"]


async def test_closing_the_response_delivers_a_disconnect_to_the_application() -> None:
    """A client that closes the response early is seen by the application as an http.disconnect."""
    seen_after_request: list[Message] = []
    disconnect_seen = anyio.Event()

    async def waiting_app(scope: Scope, receive: Receive, send: Send) -> None:
        assert scope["type"] == "http"
        assert (await receive())["type"] == "http.request"
        await send({"type": "http.response.start", "status": 200, "headers": []})
        seen_after_request.append(await receive())
        disconnect_seen.set()

    async with httpx.AsyncClient(transport=StreamingASGITransport(waiting_app), base_url="http://bridge") as http:
        async with http.stream("GET", "/wait") as response:
            assert response.status_code == 200
        # Leaving the stream block closes the response while the application is still mid-response.
        with anyio.fail_after(5):
            await disconnect_seen.wait()

    assert seen_after_request == [{"type": "http.disconnect"}]


async def test_an_application_failure_before_the_response_starts_fails_the_request() -> None:
    """An exception raised before http.response.start reaches the caller as that same exception."""

    async def broken_app(scope: Scope, receive: Receive, send: Send) -> None:
        raise RuntimeError("the demo application is broken")

    async with httpx.AsyncClient(transport=StreamingASGITransport(broken_app), base_url="http://bridge") as http:
        with pytest.raises(RuntimeError, match="the demo application is broken"):
            await http.get("/broken")


async def test_disabling_cancel_on_close_lets_the_application_finish_after_disconnect() -> None:
    """With cancel_on_close=False, an application that runs cleanup after seeing http.disconnect
    completes that cleanup before the transport finishes closing."""
    cleanup_ran = anyio.Event()

    async def lingering_app(scope: Scope, receive: Receive, send: Send) -> None:
        assert scope["type"] == "http"
        await receive()
        await send({"type": "http.response.start", "status": 200, "headers": []})
        assert (await receive())["type"] == "http.disconnect"
        cleanup_ran.set()

    transport = StreamingASGITransport(lingering_app, cancel_on_close=False)
    with anyio.fail_after(5):
        async with httpx.AsyncClient(transport=transport, base_url="http://bridge") as http:
            async with http.stream("GET", "/linger") as response:
                assert response.status_code == 200
            assert not cleanup_ran.is_set()
    assert cleanup_ran.is_set()
