"""PAPER-38 — a mid-body failure must surface its cause, not a generator bug.

With any retry policy configured, a failure *during the body read* was thrown
back into `stream()`'s generator at `yield response`. The retryable handlers
caught it and `continue`d, so the generator re-entered its loop and tried to
`yield` a second time — which `asynccontextmanager.__aexit__` turns into::

    RuntimeError: generator didn't stop after athrow()

The real `httpx.ReadError` was gone. A consumer catching `HttpTransportError`
around a streamed download saw nothing; one catching `RuntimeError` could not
tell a transport blip from a library bug. `download()` iterates the body inside
the `async with`, so **every resumable download was on this path**.

The library's own contract already said retries are off the table once the
response is yielded. These tests pin the code to it.
"""

from __future__ import annotations

import httpx
import pytest

from mgf.http import AsyncHttpClient, HttpTransportError, RetryPolicy


class _FailingStream(httpx.AsyncByteStream):
    """Headers arrive fine; the body yields one chunk and then dies.

    An httpx MockTransport response needs a real `AsyncByteStream` on the async
    path — a sync generator is silently the wrong shape and fails far from here.
    One good chunk first, so the failure is genuinely MID-body: that is the whole
    distinction PAPER-38 turns on.
    """

    async def __aiter__(self):
        yield b"partial"
        raise httpx.ReadError("connection dropped mid-body")


def _fails_mid_body(_request: httpx.Request) -> httpx.Response:
    return httpx.Response(200, stream=_FailingStream())


@pytest.mark.parametrize("max_attempts", [0, 1, 2, 3])
@pytest.mark.asyncio
async def test_mid_body_failure_reports_the_cause_at_every_retry_setting(
    max_attempts: int,
) -> None:
    """The defect was invisible at `max_attempts=0` and appeared at every other
    value — so the parametrisation IS the regression: a fix that only works at
    one setting reproduces the bug the paper describes."""
    client = AsyncHttpClient(timeout=5.0, retry=RetryPolicy(max_attempts=max_attempts))
    client._client._transport = httpx.MockTransport(_fails_mid_body)
    try:
        with pytest.raises(HttpTransportError) as ei:
            async with client.stream("GET", "https://h/big") as resp:
                async for _chunk in resp.aiter_bytes():
                    pass
        assert not isinstance(ei.value, RuntimeError)
        assert isinstance(ei.value.__cause__, httpx.ReadError), (
            f"the real cause was destroyed; __cause__ is {type(ei.value.__cause__).__name__}"
        )
    finally:
        await client.aclose()


@pytest.mark.asyncio
async def test_the_runtime_error_is_gone() -> None:
    """Named for the symptom a consumer actually saw, so a regression is
    recognisable from the failure output alone."""
    client = AsyncHttpClient(timeout=5.0, retry=RetryPolicy(max_attempts=2))
    client._client._transport = httpx.MockTransport(_fails_mid_body)
    try:
        with pytest.raises(Exception) as ei:
            async with client.stream("GET", "https://h/big") as resp:
                async for _chunk in resp.aiter_bytes():
                    pass
        assert "generator didn't stop after athrow" not in str(ei.value)
    finally:
        await client.aclose()


@pytest.mark.asyncio
async def test_a_pre_yield_failure_still_retries() -> None:
    """The guard must NOT disable retries — only retries after the handover.
    A connect failure never reaches `yield`, so all attempts are still spent."""
    calls: list[int] = []

    def _connect_fails(_request: httpx.Request) -> httpx.Response:
        calls.append(1)
        raise httpx.ConnectError("refused")

    client = AsyncHttpClient(
        timeout=5.0, retry=RetryPolicy(max_attempts=2, base_backoff_seconds=0.001)
    )
    client._client._transport = httpx.MockTransport(_connect_fails)
    try:
        with pytest.raises(HttpTransportError):
            async with client.stream("GET", "https://h/x") as _resp:
                pass
        assert len(calls) == 3, f"retries were disabled by the fix: {len(calls)} attempt(s)"
    finally:
        await client.aclose()
