from __future__ import annotations

import os
import platform
from json import JSONDecodeError, loads

import pytest

from urllib3 import AsyncHTTPSConnectionPool, HttpVersion
from urllib3.backend._async.hface import _HAS_HTTP3_SUPPORT  # type: ignore

from .. import TraefikTestCase


@pytest.mark.asyncio
class TestStreamResponse(TraefikTestCase):
    @pytest.mark.parametrize(
        "amt",
        [
            None,
            1,
            3,
            5,
            16,
            64,
            1024,
            16544,
        ],
    )
    async def test_h2n3_stream(self, amt: int | None) -> None:
        async with AsyncHTTPSConnectionPool(
            self.host,
            self.https_port,
            ca_certs=self.ca_authority,
            resolver=self.test_async_resolver.new(),
        ) as p:
            for i in range(3):
                resp = await p.request("GET", "/get", preload_content=False)

                assert resp.status == 200
                if _HAS_HTTP3_SUPPORT():
                    # colima is our only way to test HTTP/2 and HTTP/3 in GHA runners
                    # its known to have flaky behaviors. We can lose the connection easily...
                    # and our automatic downgrade to HTTP/2 makes the following assert
                    # problematic!
                    if (
                        os.environ.get("CI") is not None
                        and platform.system() == "Darwin"
                    ):
                        assert resp.version in {20, 30}
                    else:
                        assert resp.version == (20 if i == 0 else 30)
                else:
                    assert resp.version == 20

                chunks = []

                async for chunk in resp.stream(amt):
                    chunks.append(chunk)

                try:
                    payload_reconstructed = loads(b"".join(chunks))
                except JSONDecodeError as e:
                    print(e)
                    payload_reconstructed = None

                assert payload_reconstructed is not None, (
                    f"HTTP/{resp.version / 10} stream failure"
                )
                assert "args" in payload_reconstructed, (
                    f"HTTP/{resp.version / 10} stream failure"
                )

    async def test_h2n3_stream_gzip_amt_negative_one(self) -> None:
        # Async mirror of the regression test for issue #364.
        async with AsyncHTTPSConnectionPool(
            self.host,
            self.https_port,
            ca_certs=self.ca_authority,
            resolver=self.test_async_resolver.new(),
        ) as p:
            resp = await p.request("GET", "/gzip", preload_content=False)
            assert resp.status == 200
            assert resp.headers.get("Content-Encoding", "").lower() == "gzip"

            chunks: list[bytes] = []
            async for chunk in resp.stream(amt=-1):
                chunks.append(chunk)
                assert len(chunks) < 256, (
                    f"stream(amt=-1) iterates too many times (HTTP/{resp.version / 10})"
                )

            try:
                payload = loads(b"".join(chunks))
            except JSONDecodeError as e:
                pytest.fail(
                    f"HTTP/{resp.version / 10} gzip stream(amt=-1) "
                    f"did not yield decodable JSON: {e}"
                )
            assert payload.get("gzipped") is True

    @pytest.mark.parametrize("target_http", [11, 20, 30])
    async def test_sse_stream_amt_yields_per_event(self, target_http: int) -> None:
        # Async mirror of the regression test for
        # https://github.com/jawah/urllib3.future/issues/379
        # ``stream(amt)`` against a live stream (here SSE) must yield data as
        # it arrives, even when fewer than ``amt`` bytes are available, instead
        # of blocking until ``amt`` bytes accumulate or the stream ends.
        if target_http == 30 and _HAS_HTTP3_SUPPORT() is False:
            pytest.skip("Test requires http3 support")

        disabled_svn = {
            11: {HttpVersion.h2, HttpVersion.h3},
            20: {HttpVersion.h11, HttpVersion.h3},
            30: {HttpVersion.h11, HttpVersion.h2},
        }[target_http]

        async with AsyncHTTPSConnectionPool(
            self.host,
            self.https_port,
            ca_certs=self.ca_authority,
            resolver=self.test_async_resolver.new(),
            disabled_svn=disabled_svn,
            retries=False,
        ) as p:
            resp = await p.request(
                "GET", "/sse?delay=1s&count=5", preload_content=False
            )

            assert resp.status == 200
            assert resp.version == target_http

            chunks = []

            async for chunk in resp.stream(2**16):
                chunks.append(chunk)

            payload = b"".join(chunks)
            assert payload.count(b"event:") == 5

            # the whole stream is far smaller than ``amt``: the broken
            # behavior was a single monolithic yield delivered only once
            # the server closed the stream. Per-arrival delivery must give
            # us the first event alone in the first yield.
            assert chunks[0].count(b"event:") == 1
            assert len(chunks) >= 5

    async def test_read_zero(self) -> None:
        async with AsyncHTTPSConnectionPool(
            self.host,
            self.https_port,
            ca_certs=self.ca_authority,
            resolver=[self.test_async_resolver],
        ) as p:
            resp = await p.request("GET", "/get", preload_content=False)
            assert resp.status == 200

            assert await resp.read(0) == b""

            for i in range(5):
                assert len(await resp.read(1)) == 1

            assert await resp.read(0) == b""
            assert len(await resp.read()) > 0
