# Tests for aiohttp/protocol.py

import asyncio
import gzip
import platform
import re
import sys
import zlib
from collections.abc import Iterator
from contextlib import nullcontext
from typing import Any
from unittest import mock
from urllib.parse import quote

import pytest
from multidict import CIMultiDict
from yarl import URL

import aiohttp
from aiohttp import http_exceptions, streams
from aiohttp.base_protocol import BaseProtocol
from aiohttp.client_proto import ResponseHandler
from aiohttp.helpers import DEFAULT_CHUNK_SIZE
from aiohttp.http_parser import (
    NO_EXTENSIONS,
    DeflateBuffer,
    HeadersParser,
    HttpPayloadParser,
    HttpRequestParser,
    HttpRequestParserPy,
    HttpResponseParser,
    HttpResponseParserPy,
    PayloadState,
)
from aiohttp.http_writer import HttpVersion
from aiohttp.web_protocol import RequestHandler
from aiohttp.web_server import Server

try:
    try:
        import brotlicffi as brotli
    except ImportError:
        import brotli
except ImportError:
    brotli = None

try:
    if sys.version_info >= (3, 14):
        import compression.zstd as zstandard  # noqa: I900
    else:
        import backports.zstd as zstandard
except ImportError:
    zstandard = None  # type: ignore[assignment]

REQUEST_PARSERS = [HttpRequestParserPy]
RESPONSE_PARSERS = [HttpResponseParserPy]

try:
    from aiohttp.http_parser import HttpRequestParserC, HttpResponseParserC

    REQUEST_PARSERS.append(HttpRequestParserC)
    RESPONSE_PARSERS.append(HttpResponseParserC)
except ImportError:  # pragma: no cover
    pass


@pytest.fixture
def server() -> Any:
    return mock.create_autospec(
        Server,
        request_factory=mock.Mock(),
        request_handler=mock.AsyncMock(),
        instance=True,
    )


@pytest.fixture
def protocol() -> Any:
    return mock.create_autospec(
        BaseProtocol,
        spec_set=True,
        instance=True,
    )


def _gen_ids(parsers: list[Any]) -> list[str]:
    return [
        "py-parser" if parser.__module__ == "aiohttp.http_parser" else "c-parser"
        for parser in parsers
    ]


@pytest.fixture(params=REQUEST_PARSERS, ids=_gen_ids(REQUEST_PARSERS))
def parser(
    loop: asyncio.AbstractEventLoop,
    server: Server,
    request: pytest.FixtureRequest,
) -> Iterator[HttpRequestParser]:
    protocol = RequestHandler(server, loop=loop)

    # Parser implementations
    parser = request.param(
        protocol,
        loop,
        DEFAULT_CHUNK_SIZE,
        max_line_size=8190,
        max_headers=128,
        max_field_size=8190,
    )
    protocol._force_close = False
    protocol._parser = parser
    with mock.patch.object(protocol, "transport", True):
        yield parser


@pytest.fixture(params=REQUEST_PARSERS, ids=_gen_ids(REQUEST_PARSERS))
def request_cls(request: Any):
    # Request Parser class
    return request.param


@pytest.fixture(params=RESPONSE_PARSERS, ids=_gen_ids(RESPONSE_PARSERS))
def response(
    loop: asyncio.AbstractEventLoop,
    request: pytest.FixtureRequest,
) -> HttpResponseParser:
    protocol = ResponseHandler(loop)

    # Parser implementations
    parser = request.param(
        protocol,
        loop,
        DEFAULT_CHUNK_SIZE,
        max_line_size=8190,
        max_headers=128,
        max_field_size=8190,
        read_until_eof=True,
    )
    protocol._parser = parser
    return parser  # type: ignore[no-any-return]


@pytest.fixture(params=RESPONSE_PARSERS, ids=_gen_ids(RESPONSE_PARSERS))
def response_cls(request: Any):
    # Parser implementations
    return request.param


@pytest.fixture
def stream():
    return mock.Mock()


@pytest.mark.skipif(NO_EXTENSIONS, reason="Extensions available but not imported")
def test_c_parser_loaded():
    assert "HttpRequestParserC" in dir(aiohttp.http_parser)
    assert "HttpResponseParserC" in dir(aiohttp.http_parser)
    assert "RawRequestMessageC" in dir(aiohttp.http_parser)
    assert "RawResponseMessageC" in dir(aiohttp.http_parser)


def test_parse_headers(parser: Any) -> None:
    text = b"""GET /test HTTP/1.1\r
Host: a\r
test: a line\r
test2: data\r
\r
"""
    messages, upgrade, tail = parser.feed_data(text)
    assert len(messages) == 1
    msg = messages[0][0]

    assert list(msg.headers.items()) == [
        ("Host", "a"),
        ("test", "a line"),
        ("test2", "data"),
    ]
    assert msg.raw_headers == (
        (b"Host", b"a"),
        (b"test", b"a line"),
        (b"test2", b"data"),
    )
    assert not msg.should_close
    assert msg.compression is None
    assert not msg.upgrade


def test_reject_obsolete_line_folding(parser: Any) -> None:
    text = b"""GET /test HTTP/1.1\r
Host: a\r
test: line\r
 Content-Length: 48\r
test2: data\r
\r
"""
    with pytest.raises(http_exceptions.BadHttpMessage):
        parser.feed_data(text)


@pytest.mark.skipif(NO_EXTENSIONS, reason="Only tests C parser.")
def test_invalid_character(
    loop: asyncio.AbstractEventLoop,
    server: Server,
    request: pytest.FixtureRequest,
) -> None:
    protocol = RequestHandler(server, loop=loop)

    parser = HttpRequestParserC(
        protocol,
        loop,
        2**16,
        max_line_size=8190,
        max_field_size=8190,
    )
    protocol._parser = parser
    text = b"POST / HTTP/1.1\r\nHost: localhost:8080\r\nSet-Cookie: abc\x01def\r\n\r\n"
    error_detail = re.escape(
        r""":

    b'Set-Cookie: abc\x01def'
                     ^"""
    )
    with pytest.raises(http_exceptions.BadHttpMessage, match=error_detail):
        parser.feed_data(text)


@pytest.mark.skipif(NO_EXTENSIONS, reason="Only tests C parser.")
def test_invalid_linebreak(
    loop: asyncio.AbstractEventLoop,
    server: Server,
    request: pytest.FixtureRequest,
) -> None:
    protocol = RequestHandler(server, loop=loop)

    parser = HttpRequestParserC(
        protocol,
        loop,
        2**16,
        max_line_size=8190,
        max_field_size=8190,
    )
    protocol._parser = parser
    text = b"GET /world HTTP/1.1\r\nHost: 127.0.0.1\n\r\n"
    error_detail = re.escape(
        r""":

    b'Host: 127.0.0.1\n'
                     ^"""
    )
    with pytest.raises(http_exceptions.BadHttpMessage, match=error_detail):
        parser.feed_data(text)


def test_cve_2023_37276(parser: Any) -> None:
    text = b"""POST / HTTP/1.1\r\nHost: localhost:8080\r\nX-Abc: \rxTransfer-Encoding: chunked\r\n\r\n"""
    with pytest.raises(http_exceptions.BadHttpMessage):
        parser.feed_data(text)


@pytest.mark.parametrize(
    "rfc9110_5_6_2_token_delim",
    r'"(),/:;<=>?@[\]{}',
)
def test_bad_header_name(parser: Any, rfc9110_5_6_2_token_delim: str) -> None:
    text = f"POST / HTTP/1.1\r\nHost: a\r\nhead{rfc9110_5_6_2_token_delim}er: val\r\n\r\n".encode()
    expectation = pytest.raises(http_exceptions.BadHttpMessage)
    if rfc9110_5_6_2_token_delim == ":":
        # Inserting colon into header just splits name/value earlier.
        expectation = nullcontext()
    with expectation:
        parser.feed_data(text)


@pytest.mark.parametrize(
    "hdr",
    (
        "Content-Length: -5",  # https://www.rfc-editor.org/rfc/rfc9110.html#name-content-length
        "Content-Length: +256",
        "Content-Length: \N{superscript one}",
        "Content-Length: \N{mathematical double-struck digit one}",
        "Foo: abc\rdef",  # https://www.rfc-editor.org/rfc/rfc9110.html#section-5.5-5
        "Bar: abc\ndef",
        "Baz: abc\x00def",
        "Foo : bar",  # https://www.rfc-editor.org/rfc/rfc9112.html#section-5.1-2
        "Foo\t: bar",
        "\xffoo: bar",
        "Foo: abc\x01def",  # CTL bytes forbidden per RFC 9110 §5.5
        "Foo: abc\x7fdef",  # DEL is also a CTL byte
        "Foo: abc\x1fdef",
    ),
)
def test_bad_headers(parser: Any, hdr: str) -> None:
    text = f"POST / HTTP/1.1\r\nHost: a\r\n{hdr}\r\n\r\n".encode()
    with pytest.raises(http_exceptions.BadHttpMessage):
        parser.feed_data(text)


def test_ctl_host_header_bad_characters(parser: HttpRequestParser) -> None:
    """CTL byte in Host header must be rejected."""
    text = b"GET /test HTTP/1.1\r\nHost: trusted.example\x01@bad.test\r\n\r\n"
    with pytest.raises(http_exceptions.BadHttpMessage):
        parser.feed_data(text)


def test_unpaired_surrogate_in_header_py(
    loop: asyncio.AbstractEventLoop, server: Server
) -> None:
    protocol = RequestHandler(server, loop=loop)

    parser = HttpRequestParserPy(
        protocol,
        loop,
        2**16,
        max_line_size=8190,
        max_field_size=8190,
    )
    protocol._parser = parser
    text = b"POST / HTTP/1.1\r\nHost: a\r\n\xff\r\n\r\n"
    message = None
    try:
        parser.feed_data(text)
    except http_exceptions.InvalidHeader as e:
        message = e.message.encode("utf-8")
    assert message is not None


def test_content_length_transfer_encoding(parser: Any) -> None:
    text = (
        b"GET / HTTP/1.1\r\nHost: a\r\nContent-Length: 5\r\nTransfer-Encoding: a\r\n\r\n"
        + b"apple\r\n"
    )
    with pytest.raises(http_exceptions.BadHttpMessage):
        parser.feed_data(text)


@pytest.mark.parametrize(
    "hdr",
    (
        "Content-Length",
        "Host",
        "Transfer-Encoding",
    ),
)
def test_duplicate_singleton_header_rejected(
    parser: HttpRequestParser, hdr: str
) -> None:
    val1, val2 = ("1", "2") if hdr == "Content-Length" else ("value1", "value2")
    text = (
        f"GET /test HTTP/1.1\r\n"
        f"Host: example.com\r\n"
        f"{hdr}: {val1}\r\n"
        f"{hdr}: {val2}\r\n"
        "\r\n"
    ).encode()
    with pytest.raises(http_exceptions.BadHttpMessage, match="Duplicate"):
        parser.feed_data(text)


@pytest.mark.parametrize(
    "hdr",
    (
        "Content-Location",
        "Content-Range",
        "Content-Type",
        "ETag",
        "Max-Forwards",
        "Server",
        "User-Agent",
    ),
)
def test_duplicate_non_security_singleton_header_rejected_strict(
    parser: HttpRequestParser, hdr: str
) -> None:
    """Non-security singletons are rejected in strict mode (requests)."""
    text = (
        f"GET /test HTTP/1.1\r\n"
        f"Host: example.com\r\n"
        f"{hdr}: value1\r\n"
        f"{hdr}: value2\r\n"
        "\r\n"
    ).encode()
    with pytest.raises(http_exceptions.BadHttpMessage, match="Duplicate"):
        parser.feed_data(text)


@pytest.mark.parametrize(
    "hdr",
    (
        # Content-Length is excluded because llhttp rejects duplicates
        # at the C level before our singleton check runs.
        "Content-Location",
        "Content-Range",
        "Content-Type",
        "ETag",
        "Max-Forwards",
        "Server",
        "Transfer-Encoding",
        "User-Agent",
    ),
)
def test_duplicate_singleton_header_accepted_in_lax_mode(
    response: HttpResponseParser, hdr: str
) -> None:
    """All singleton duplicates are accepted in lax mode (response parser default)."""
    text = (f"HTTP/1.1 200 OK\r\n{hdr}: value1\r\n{hdr}: value2\r\n\r\n").encode()
    messages, upgrade, tail = response.feed_data(text)
    assert len(messages) == 1


def test_duplicate_host_header_rejected(parser: HttpRequestParser) -> None:
    text = (
        b"GET /admin HTTP/1.1\r\n"
        b"Host: admin.example\r\n"
        b"Host: public.example\r\n"
        b"\r\n"
    )
    with pytest.raises(http_exceptions.BadHttpMessage, match="Duplicate.*Host"):
        parser.feed_data(text)


def test_missing_host_header_rejected(parser: HttpRequestParser) -> None:
    text = b"GET /admin HTTP/1.1\r\n\r\n"
    with pytest.raises(http_exceptions.BadHttpMessage, match="Missing 'Host' header"):
        parser.feed_data(text)


@pytest.mark.parametrize(
    ("hdr1", "hdr2"),
    (
        ("content-length", "Content-Length"),
        ("Content-Length", "content-length"),
        ("transfer-encoding", "Transfer-Encoding"),
        ("Transfer-Encoding", "transfer-encoding"),
    ),
)
def test_duplicate_singleton_header_different_casing_rejected(
    parser: HttpRequestParser, hdr1: str, hdr2: str
) -> None:
    """Singleton check must be case-insensitive per RFC 9110."""
    val1, val2 = ("1", "2") if "content-length" in hdr1.lower() else ("v1", "v2")
    text = (
        f"GET /test HTTP/1.1\r\n"
        f"Host: example.com\r\n"
        f"{hdr1}: {val1}\r\n"
        f"{hdr2}: {val2}\r\n"
        "\r\n"
    ).encode()
    with pytest.raises(http_exceptions.BadHttpMessage, match="Duplicate"):
        parser.feed_data(text)


def test_duplicate_host_header_different_casing_rejected(
    parser: HttpRequestParser,
) -> None:
    """Duplicate Host with different casing must also be rejected."""
    text = (
        b"GET /test HTTP/1.1\r\n"
        b"host: evil.example\r\n"
        b"Host: good.example\r\n"
        b"\r\n"
    )
    with pytest.raises(http_exceptions.BadHttpMessage, match="Duplicate"):
        parser.feed_data(text)


def test_bad_chunked(parser: HttpRequestParser) -> None:
    """Test that invalid chunked encoding doesn't allow content-length to be used."""
    text = (
        b"GET / HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunked\r\n\r\n0_2e\r\n\r\n"
        + b"GET / HTTP/1.1\r\nHost: a\r\nContent-Length: 5\r\n\r\n0\r\n\r\n"
    )
    with pytest.raises(http_exceptions.BadHttpMessage, match="0_2e"):
        parser.feed_data(text)


def test_whitespace_before_header(parser: Any) -> None:
    text = b"GET / HTTP/1.1\r\nHost: a\r\n\tContent-Length: 1\r\n\r\nX"
    with pytest.raises(http_exceptions.BadHttpMessage):
        parser.feed_data(text)


@pytest.fixture
def xfail_c_parser_status(request) -> None:
    if isinstance(request.getfixturevalue("parser"), HttpRequestParserPy):
        return
    request.node.add_marker(
        pytest.mark.xfail(
            reason="Regression test for Py parser. May match C behaviour later.",
            raises=http_exceptions.BadStatusLine,
        )
    )


@pytest.mark.usefixtures("xfail_c_parser_status")
def test_parse_unusual_request_line(parser) -> None:
    text = b"#smol //a HTTP/1.3\r\n\r\n"
    messages, upgrade, tail = parser.feed_data(text)
    assert len(messages) == 1
    msg, _ = messages[0]
    assert msg.compression is None
    assert not msg.upgrade
    assert msg.method == "#smol"
    assert msg.path == "//a"
    assert msg.version == (1, 3)


def test_parse(parser) -> None:
    text = b"GET /test HTTP/1.1\r\nHost: a\r\n\r\n"
    messages, upgrade, tail = parser.feed_data(text)
    assert len(messages) == 1
    msg, _ = messages[0]
    assert msg.compression is None
    assert not msg.upgrade
    assert msg.method == "GET"
    assert msg.path == "/test"
    assert msg.version == (1, 1)
    assert msg.headers["Host"] == "a"


async def test_parse_body(parser) -> None:
    text = b"GET /test HTTP/1.1\r\nHost: a\r\nContent-Length: 4\r\n\r\nbody"
    messages, upgrade, tail = parser.feed_data(text)
    assert len(messages) == 1
    _, payload = messages[0]
    body = await payload.read(4)
    assert body == b"body"


async def test_parse_body_with_CRLF(parser) -> None:
    text = b"\r\nGET /test HTTP/1.1\r\nHost: a\r\nContent-Length: 4\r\n\r\nbody"
    messages, upgrade, tail = parser.feed_data(text)
    assert len(messages) == 1
    _, payload = messages[0]
    body = await payload.read(4)
    assert body == b"body"


def test_parse_delayed(parser) -> None:
    text = b"GET /test HTTP/1.1\r\nHost: a\r\n"
    messages, upgrade, tail = parser.feed_data(text)
    assert len(messages) == 0
    assert not upgrade

    messages, upgrade, tail = parser.feed_data(b"\r\n")
    assert len(messages) == 1
    msg = messages[0][0]
    assert msg.method == "GET"


def test_headers_multi_feed(parser) -> None:
    text1 = b"GET /test HTTP/1.1\r\n"
    text2 = b"Host: a\r\n"
    text3 = b"test: line"
    text4 = b" continue\r\n\r\n"

    messages, upgrade, tail = parser.feed_data(text1)
    assert len(messages) == 0

    messages, upgrade, tail = parser.feed_data(text2)
    assert len(messages) == 0

    messages, upgrade, tail = parser.feed_data(text3)
    assert len(messages) == 0

    messages, upgrade, tail = parser.feed_data(text4)
    assert len(messages) == 1

    msg = messages[0][0]
    assert list(msg.headers.items()) == [("Host", "a"), ("test", "line continue")]
    assert msg.raw_headers == ((b"Host", b"a"), (b"test", b"line continue"))
    assert not msg.should_close
    assert msg.compression is None
    assert not msg.upgrade


def test_headers_split_field(parser) -> None:
    text1 = b"GET /test HTTP/1.1\r\nHost: a\r\n"
    text2 = b"t"
    text3 = b"es"
    text4 = b"t: value\r\n\r\n"

    messages, upgrade, tail = parser.feed_data(text1)
    messages, upgrade, tail = parser.feed_data(text2)
    messages, upgrade, tail = parser.feed_data(text3)
    assert len(messages) == 0
    messages, upgrade, tail = parser.feed_data(text4)
    assert len(messages) == 1

    msg = messages[0][0]
    assert list(msg.headers.items()) == [("Host", "a"), ("test", "value")]
    assert msg.raw_headers == ((b"Host", b"a"), (b"test", b"value"))
    assert not msg.should_close
    assert msg.compression is None
    assert not msg.upgrade


def test_parse_headers_multi(parser) -> None:
    text = (
        b"GET /test HTTP/1.1\r\nHost: a\r\n"
        b"Set-Cookie: c1=cookie1\r\n"
        b"Set-Cookie: c2=cookie2\r\n\r\n"
    )

    messages, upgrade, tail = parser.feed_data(text)
    assert len(messages) == 1
    msg = messages[0][0]

    assert list(msg.headers.items()) == [
        ("Host", "a"),
        ("Set-Cookie", "c1=cookie1"),
        ("Set-Cookie", "c2=cookie2"),
    ]
    assert msg.raw_headers == (
        (b"Host", b"a"),
        (b"Set-Cookie", b"c1=cookie1"),
        (b"Set-Cookie", b"c2=cookie2"),
    )
    assert not msg.should_close
    assert msg.compression is None


def test_conn_default_1_0(parser) -> None:
    text = b"GET /test HTTP/1.0\r\n\r\n"
    messages, upgrade, tail = parser.feed_data(text)
    msg = messages[0][0]
    assert msg.should_close


def test_conn_default_1_1(parser) -> None:
    text = b"GET /test HTTP/1.1\r\nHost: a\r\n\r\n"
    messages, upgrade, tail = parser.feed_data(text)
    msg = messages[0][0]
    assert not msg.should_close


def test_conn_close(parser) -> None:
    text = b"GET /test HTTP/1.1\r\nHost: a\r\nconnection: close\r\n\r\n"
    messages, upgrade, tail = parser.feed_data(text)
    msg = messages[0][0]
    assert msg.should_close


def test_conn_close_1_0(parser) -> None:
    text = b"GET /test HTTP/1.0\r\nconnection: close\r\n\r\n"
    messages, upgrade, tail = parser.feed_data(text)
    msg = messages[0][0]
    assert msg.should_close


def test_conn_keep_alive_1_0(parser) -> None:
    text = b"GET /test HTTP/1.0\r\nconnection: keep-alive\r\n\r\n"
    messages, upgrade, tail = parser.feed_data(text)
    msg = messages[0][0]
    assert not msg.should_close


def test_conn_keep_alive_1_1(parser) -> None:
    text = b"GET /test HTTP/1.1\r\nHost: a\r\nconnection: keep-alive\r\n\r\n"
    messages, upgrade, tail = parser.feed_data(text)
    msg = messages[0][0]
    assert not msg.should_close


def test_conn_close_comma_list(parser) -> None:
    text = b"GET /test HTTP/1.1\r\nHost: a\r\nconnection: close, keep-alive\r\n\r\n"
    messages, upgrade, tail = parser.feed_data(text)
    msg = messages[0][0]
    assert msg.should_close


def test_conn_close_multiple_headers(parser) -> None:
    text = (
        b"GET /test HTTP/1.1\r\n"
        b"Host: a\r\n"
        b"connection: keep-alive\r\n"
        b"connection: close\r\n\r\n"
    )
    messages, upgrade, tail = parser.feed_data(text)
    msg = messages[0][0]
    assert msg.should_close


def test_conn_other_1_0(parser) -> None:
    text = b"GET /test HTTP/1.0\r\nconnection: test\r\n\r\n"
    messages, upgrade, tail = parser.feed_data(text)
    msg = messages[0][0]
    assert msg.should_close


def test_conn_other_1_1(parser) -> None:
    text = b"GET /test HTTP/1.1\r\nHost: a\r\nconnection: test\r\n\r\n"
    messages, upgrade, tail = parser.feed_data(text)
    msg = messages[0][0]
    assert not msg.should_close


def test_request_chunked(parser) -> None:
    text = b"GET /test HTTP/1.1\r\nHost: a\r\ntransfer-encoding: chunked\r\n\r\n"
    messages, upgrade, tail = parser.feed_data(text)
    msg, payload = messages[0]
    assert msg.chunked
    assert not upgrade
    assert isinstance(payload, streams.StreamReader)


def test_te_header_non_ascii(parser: HttpRequestParser) -> None:
    # K = Kelvin sign, not valid ascii.
    text = "GET /test HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunKed\r\n\r\n"
    with pytest.raises(http_exceptions.BadHttpMessage):
        parser.feed_data(text.encode())


def test_upgrade_header_non_ascii(parser: HttpRequestParser) -> None:
    # K = Kelvin sign, not valid ascii.
    text = "GET /test HTTP/1.1\r\nHost: a\r\nUpgrade: websocKet\r\n\r\n"
    messages, upgrade, tail = parser.feed_data(text.encode())
    assert not upgrade


def test_request_te_chunked_with_content_length(parser: HttpRequestParser) -> None:
    text = (
        b"GET /test HTTP/1.1\r\n"
        b"Host: a\r\n"
        b"content-length: 1234\r\n"
        b"transfer-encoding: chunked\r\n\r\n"
    )
    with pytest.raises(
        http_exceptions.BadHttpMessage,
        match="Transfer-Encoding can't be present with Content-Length",
    ):
        parser.feed_data(text)


def test_request_te_chunked123(parser: Any) -> None:
    text = b"GET /test HTTP/1.1\r\nHost: a\r\ntransfer-encoding: chunked123\r\n\r\n"
    with pytest.raises(
        http_exceptions.BadHttpMessage,
        match="Request has invalid `Transfer-Encoding`",
    ):
        parser.feed_data(text)


async def test_request_te_last_chunked(parser: Any) -> None:
    text = b"GET /test HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: not, chunked\r\n\r\n1\r\nT\r\n3\r\nest\r\n0\r\n\r\n"
    messages, upgrade, tail = parser.feed_data(text)
    # https://www.rfc-editor.org/rfc/rfc9112#section-6.3-2.4.3
    assert await messages[0][1].read() == b"Test"


def test_request_te_first_chunked(parser: Any) -> None:
    text = b"GET /test HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunked, not\r\n\r\n1\r\nT\r\n3\r\nest\r\n0\r\n\r\n"
    # https://www.rfc-editor.org/rfc/rfc9112#section-6.3-2.4.3
    with pytest.raises(
        http_exceptions.BadHttpMessage,
        match="nvalid `Transfer-Encoding`",
    ):
        parser.feed_data(text)


def test_conn_upgrade(parser: Any) -> None:
    text = (
        b"GET /test HTTP/1.1\r\n"
        b"Host: a\r\n"
        b"connection: upgrade\r\n"
        b"upgrade: websocket\r\n\r\n"
    )
    messages, upgrade, tail = parser.feed_data(text)
    msg = messages[0][0]
    assert not msg.should_close
    assert msg.upgrade
    assert upgrade


def test_conn_upgrade_comma_list(parser) -> None:
    text = (
        b"GET /test HTTP/1.1\r\n"
        b"host: a\r\n"
        b"connection: keep-alive, upgrade\r\n"
        b"upgrade: websocket\r\n\r\n"
    )
    messages, upgrade, tail = parser.feed_data(text)
    msg = messages[0][0]
    assert not msg.should_close
    assert msg.upgrade
    assert upgrade


def test_conn_upgrade_multiple_headers(parser) -> None:
    text = (
        b"GET /test HTTP/1.1\r\n"
        b"host: a\r\n"
        b"connection: keep-alive\r\n"
        b"connection: upgrade\r\n"
        b"upgrade: websocket\r\n\r\n"
    )
    messages, upgrade, tail = parser.feed_data(text)
    msg = messages[0][0]
    assert not msg.should_close
    assert msg.upgrade
    assert upgrade


def test_bad_upgrade(parser) -> None:
    """Test not upgraded if missing Upgrade header."""
    text = b"GET /test HTTP/1.1\r\nHost: a\r\nconnection: upgrade\r\n\r\n"
    messages, upgrade, tail = parser.feed_data(text)
    msg = messages[0][0]
    assert not msg.upgrade
    assert not upgrade


def test_compression_empty(parser) -> None:
    text = b"GET /test HTTP/1.1\r\nHost: a\r\ncontent-encoding: \r\n\r\n"
    messages, upgrade, tail = parser.feed_data(text)
    msg = messages[0][0]
    assert msg.compression is None


def test_compression_deflate(parser) -> None:
    text = b"GET /test HTTP/1.1\r\nHost: a\r\ncontent-encoding: deflate\r\n\r\n"
    messages, upgrade, tail = parser.feed_data(text)
    msg = messages[0][0]
    assert msg.compression == "deflate"


def test_compression_gzip(parser) -> None:
    text = b"GET /test HTTP/1.1\r\nHost: a\r\ncontent-encoding: gzip\r\n\r\n"
    messages, upgrade, tail = parser.feed_data(text)
    msg = messages[0][0]
    assert msg.compression == "gzip"


@pytest.mark.skipif(brotli is None, reason="brotli is not installed")
def test_compression_brotli(parser) -> None:
    text = b"GET /test HTTP/1.1\r\nHost: a\r\ncontent-encoding: br\r\n\r\n"
    messages, upgrade, tail = parser.feed_data(text)
    msg = messages[0][0]
    assert msg.compression == "br"


@pytest.mark.skipif(zstandard is None, reason="zstandard is not installed")
def test_compression_zstd(parser: HttpRequestParser) -> None:
    text = b"GET /test HTTP/1.1\r\nHost: a\r\ncontent-encoding: zstd\r\n\r\n"
    messages, upgrade, tail = parser.feed_data(text)
    msg = messages[0][0]
    assert msg.compression == "zstd"


@pytest.mark.parametrize(
    "enc",
    (
        "zﬆd".encode(),  # "ﬆ".upper() == "ST"
        "deﬂate".encode(),  # "ﬂ".upper() == "FL"
    ),
)
def test_compression_non_ascii(parser: HttpRequestParser, enc: bytes) -> None:
    text = b"GET /test HTTP/1.1\r\nHost: a\r\ncontent-encoding: " + enc + b"\r\n\r\n"
    messages, upgrade, tail = parser.feed_data(text)
    msg = messages[0][0]
    # Non-ascii input should not evaluate to a valid encoding scheme.
    assert msg.compression is None


def test_compression_unknown(parser: HttpRequestParser) -> None:
    text = b"GET /test HTTP/1.1\r\nHost: a\r\ncontent-encoding: compress\r\n\r\n"
    messages, upgrade, tail = parser.feed_data(text)
    msg = messages[0][0]
    assert msg.compression is None


def test_url_connect(parser: Any) -> None:
    text = b"CONNECT www.google.com HTTP/1.1\r\nHost: a\r\ncontent-length: 0\r\n\r\n"
    messages, upgrade, tail = parser.feed_data(text)
    msg, payload = messages[0]
    assert upgrade
    assert msg.url == URL.build(authority="www.google.com")


def test_headers_connect(parser: Any) -> None:
    text = b"CONNECT www.google.com HTTP/1.1\r\nHost: a\r\ncontent-length: 0\r\n\r\n"
    messages, upgrade, tail = parser.feed_data(text)
    msg, payload = messages[0]
    assert upgrade
    assert isinstance(payload, streams.StreamReader)


def test_url_absolute(parser: Any) -> None:
    text = (
        b"GET https://www.google.com/path/to.html HTTP/1.1\r\n"
        b"Host: a\r\n"
        b"content-length: 0\r\n\r\n"
    )
    messages, upgrade, tail = parser.feed_data(text)
    msg, payload = messages[0]
    assert not upgrade
    assert msg.method == "GET"
    assert msg.url == URL("https://www.google.com/path/to.html")


def test_headers_old_websocket_key1(parser: Any) -> None:
    text = b"GET /test HTTP/1.1\r\nHost: a\r\nSEC-WEBSOCKET-KEY1: line\r\n\r\n"

    with pytest.raises(http_exceptions.BadHttpMessage):
        parser.feed_data(text)


def test_headers_content_length_err_1(parser) -> None:
    text = b"GET /test HTTP/1.1\r\nHost: a\r\ncontent-length: line\r\n\r\n"

    with pytest.raises(http_exceptions.BadHttpMessage):
        parser.feed_data(text)


def test_headers_content_length_err_2(parser) -> None:
    text = b"GET /test HTTP/1.1\r\nHost: a\r\ncontent-length: -1\r\n\r\n"

    with pytest.raises(http_exceptions.BadHttpMessage):
        parser.feed_data(text)


_pad: dict[bytes, str] = {
    b"": "empty",
    # not a typo. Python likes triple zero
    b"\000": "NUL",
    b" ": "SP",
    b"  ": "SPSP",
    # not a typo: both 0xa0 and 0x0a in case of 8-bit fun
    b"\n": "LF",
    b"\xa0": "NBSP",
    b"\t ": "TABSP",
}


@pytest.mark.parametrize("hdr", [b"", b"foo"], ids=["name-empty", "with-name"])
@pytest.mark.parametrize("pad2", _pad.keys(), ids=["post-" + n for n in _pad.values()])
@pytest.mark.parametrize("pad1", _pad.keys(), ids=["pre-" + n for n in _pad.values()])
def test_invalid_header_spacing(parser, pad1: bytes, pad2: bytes, hdr: bytes) -> None:
    text = b"GET /test HTTP/1.1\r\nHost: a\r\n%s%s%s: value\r\n\r\n" % (pad1, hdr, pad2)
    expectation = pytest.raises(http_exceptions.BadHttpMessage)
    if pad1 == pad2 == b"" and hdr != b"":
        # one entry in param matrix is correct: non-empty name, not padded
        expectation = nullcontext()
    with expectation:
        parser.feed_data(text)


def test_empty_header_name(parser) -> None:
    text = b"GET /test HTTP/1.1\r\nHost: a\r\n:test\r\n\r\n"
    with pytest.raises(http_exceptions.BadHttpMessage):
        parser.feed_data(text)


def test_invalid_header(parser) -> None:
    text = b"GET /test HTTP/1.1\r\nHost: a\r\ntest line\r\n\r\n"
    with pytest.raises(http_exceptions.BadHttpMessage):
        parser.feed_data(text)


def test_invalid_name(parser) -> None:
    text = b"GET /test HTTP/1.1\r\nHost: a\r\ntest[]: line\r\n\r\n"

    with pytest.raises(http_exceptions.BadHttpMessage):
        parser.feed_data(text)


@pytest.mark.parametrize("size", [40960, 8191])
def test_max_header_field_size(parser, size) -> None:
    name = b"t" * size
    text = b"GET /test HTTP/1.1\r\n" + name + b":data\r\n\r\n"

    match = "400, message:\n  Got more than 8190 bytes when reading"
    with pytest.raises(http_exceptions.LineTooLong, match=match):
        for i in range(0, len(text), 5000):  # pragma: no branch
            parser.feed_data(text[i : i + 5000])


def test_max_header_size_under_limit(parser: HttpRequestParser) -> None:
    name = b"t" * 8185
    text = b"GET /test HTTP/1.1\r\nHost: a\r\n" + name + b":data\r\n\r\n"

    messages, upgrade, tail = parser.feed_data(text)
    msg = messages[0][0]
    assert msg.method == "GET"
    assert msg.path == "/test"
    assert msg.version == (1, 1)
    assert msg.headers == CIMultiDict([("Host", "a"), (name.decode(), "data")])
    assert msg.raw_headers == ((b"Host", b"a"), (name, b"data"))
    assert not msg.should_close
    assert msg.compression is None
    assert not msg.upgrade
    assert not msg.chunked
    assert msg.url == URL("/test")


@pytest.mark.parametrize("size", [40960, 8191])
def test_max_header_value_size(parser, size) -> None:
    name = b"t" * size
    text = b"GET /test HTTP/1.1\r\ndata:" + name + b"\r\n\r\n"

    match = "400, message:\n  Got more than 8190 bytes when reading"
    with pytest.raises(http_exceptions.LineTooLong, match=match):
        for i in range(0, len(text), 4000):  # pragma: no branch
            parser.feed_data(text[i : i + 4000])


def test_max_header_combined_size(parser: HttpRequestParser) -> None:
    k = b"t" * 4100
    text = b"GET /test HTTP/1.1\r\n" + k + b":" + k + b"\r\n\r\n"

    match = "400, message:\n  Got more than 8190 bytes when reading"
    with pytest.raises(http_exceptions.LineTooLong, match=match):
        parser.feed_data(text)


@pytest.mark.parametrize("size", [40960, 8191])
async def test_max_trailer_size(parser: HttpRequestParser, size: int) -> None:
    value = b"t" * size
    text = (
        b"GET /test HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunked\r\n\r\n"
        + hex(4000)[2:].encode()
        + b"\r\n"
        + b"b" * 4000
        + b"\r\n0\r\ntest: "
        + value
        + b"\r\n\r\n"
    )

    match = "400, message:\n  Got more than 8190 bytes when reading"
    with pytest.raises(http_exceptions.LineTooLong, match=match):
        payload = None
        for i in range(0, len(text), 3000):  # pragma: no branch
            messages, upgrade, tail = parser.feed_data(text[i : i + 3000])
            if messages:
                payload = messages[0][-1]
        # Trailers are not seen until payload is read.
        assert payload is not None
        await payload.read()


@pytest.mark.parametrize("headers,trailers", ((129, 0), (0, 129), (64, 65)))
async def test_max_headers(
    parser: HttpRequestParser, headers: int, trailers: int
) -> None:
    text = (
        b"GET /test HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunked"
        + b"".join(b"\r\nHeader-%d: Value" % i for i in range(headers))
        + b"\r\n\r\n4\r\ntest\r\n0"
        + b"".join(b"\r\nTrailer-%d: Value" % i for i in range(trailers))
        + b"\r\n\r\n"
    )

    match = "Too many (headers|trailers) received"
    with pytest.raises(http_exceptions.BadHttpMessage, match=match):
        messages, upgrade, tail = parser.feed_data(text)
        # Trailers are not seen until payload is read.
        await messages[0][-1].read()


def test_max_header_value_size_under_limit(parser: HttpRequestParser) -> None:
    value = b"A" * 8185
    text = b"GET /test HTTP/1.1\r\nHost: a\r\ndata:" + value + b"\r\n\r\n"

    messages, upgrade, tail = parser.feed_data(text)
    msg = messages[0][0]
    assert msg.method == "GET"
    assert msg.path == "/test"
    assert msg.version == (1, 1)
    assert msg.headers == CIMultiDict([("Host", "a"), ("data", value.decode())])
    assert msg.raw_headers == ((b"Host", b"a"), (b"data", value))
    assert not msg.should_close
    assert msg.compression is None
    assert not msg.upgrade
    assert not msg.chunked
    assert msg.url == URL("/test")


async def test_chunk_splits_after_pause(parser: HttpRequestParser) -> None:
    num_chunks = 20000  # comfortably above the 16385 pause threshold
    text = (
        b"GET /test HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunked\r\n\r\n"
        + b"1\r\nb\r\n" * num_chunks
        + b"0\r\n\r\n"
    )

    messages, upgrade, tail = parser.feed_data(text)
    payload = messages[0][-1]
    # Payload should have paused reading and stopped receiving new chunks after 16k.
    assert payload._http_chunk_splits is not None
    assert len(payload._http_chunk_splits) == 16385
    # We should still get the full result after read(), as it will continue processing.
    result = await payload.read()
    # Compare len first, as it's easier to debug in diff.
    assert len(result) == num_chunks
    assert result == b"b" * num_chunks


async def test_compressed_with_tail(response: HttpResponseParser) -> None:
    """Test compressed content-length body followed by a second response.

    With 2 responses arriving in one call and the first compressed, this should
    trigger decompression pausing with the second response being saved as the tail.
    Verify that the second response is resumed from the tail.
    """
    # Must be large enough to exceed high water mark.
    original = b"x" * 1024 * 1024
    compressed = zlib.compress(original)
    resp1 = (
        b"HTTP/1.1 200 OK\r\n"
        b"Content-Length: " + str(len(compressed)).encode() + b"\r\n"
        b"Content-Encoding: deflate\r\n"
        b"\r\n"
    ) + compressed
    resp2 = b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok"

    msgs, upgrade, tail = response.feed_data(resp1 + resp2)
    payload = msgs[0][-1]
    result = await payload.read()
    assert len(result) == len(original)
    assert result == original

    payload = response.protocol._buffer[0][0][-1]
    result = await payload.read()
    assert result == b"ok"


async def test_two_content_length_responses_in_one_call(
    response: HttpResponseParser,
) -> None:
    """Two complete responses in a single feed_data call.

    The first payload completes with tail data for the second, hitting the
    PAYLOAD_COMPLETE branch that resets the parser for the next message.
    """
    resp1 = b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"
    resp2 = b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nworld"

    msgs, upgrade, tail = response.feed_data(resp1 + resp2)
    assert len(msgs) == 2
    assert await msgs[0][-1].read() == b"hello"
    assert await msgs[1][-1].read() == b"world"


@pytest.mark.usefixtures("parametrize_zlib_backend")
async def test_compressed_zlib_64kb(response_cls: type[HttpResponseParser]) -> None:
    loop = asyncio.get_running_loop()
    protocol = ResponseHandler(loop)
    response = response_cls(
        protocol,
        loop,
        # 64KiB limit triggered a bug with isal implementation not returning all data.
        2**16,
        max_line_size=8190,
        max_headers=128,
        max_field_size=8190,
    )
    protocol._parser = response

    original = b"".join(
        bytes((*range(0, i), *range(i, 0, -1))) for _ in range(255) for i in range(255)
    )
    compressed = zlib.compress(original)
    headers = (
        b"HTTP/1.1 200 OK\r\n"
        b"Content-Length: " + str(len(compressed)).encode() + b"\r\n"
        b"Content-Encoding: deflate\r\n"
        b"\r\n"
    )

    msgs, upgrade, tail = response.feed_data(headers + compressed)
    payload = msgs[0][-1]
    result = await payload.read()
    assert len(result) == len(original)
    assert result == original


async def test_compressed_chunked_with_pending(response: HttpResponseParser) -> None:
    """Test chunked + compressed where the decompressor needs to resume from pause.

    We need to verify that chunked messages continue parsing correctly after
    a pause and resume in the decompression.
    """
    # Must be large enough to exceed high water mark.
    original = b"A" * 1024 * 1024
    compressed = zlib.compress(original)
    chunk_data = hex(len(compressed))[2:].encode() + b"\r\n" + compressed + b"\r\n"
    headers = (
        b"HTTP/1.1 200 OK\r\n"
        b"Transfer-Encoding: chunked\r\n"
        b"Content-Encoding: deflate\r\n"
        b"\r\n"
    )
    data = headers + chunk_data + b"0\r\n\r\n"

    msgs, upgrade, tail = response.feed_data(data)
    payload = msgs[0][-1]
    result = await payload.read()
    assert len(result) == len(original)
    assert result == original


async def test_compressed_until_eof_with_pending(response: HttpResponseParser) -> None:
    """Test read-until-eof + compressed with pause."""
    # Must be large enough to exceed high water mark.
    original = b"B" * 5 * 1024 * 1024
    compressed = zlib.compress(original)
    # No Content-Length or Transfer-Encoding means the parser must parse until EOF.
    headers = b"HTTP/1.1 200 OK\r\nContent-Encoding: deflate\r\n\r\n"

    msgs, upgrade, tail = response.feed_data(headers + compressed)
    response.feed_eof()
    payload = msgs[0][-1]

    # Check that .feed_eof() hasn't decompressed entire payload into memory.
    assert sum(len(b) for b in payload._buffer) <= (1024 * 1024)

    result = await payload.read()
    assert len(result) == len(original)
    assert result == original


async def test_compressed_until_eof_high_water(
    response_cls: type[HttpResponseParser],
) -> None:
    """Test read-until-eof + compressed with higher limit."""
    loop = asyncio.get_running_loop()
    protocol = ResponseHandler(loop)
    response = response_cls(
        protocol,
        loop,
        2**19,  # 512 KiB limit
        max_line_size=8190,
        max_headers=128,
        max_field_size=8190,
        read_until_eof=True,
    )
    protocol._parser = response

    # Must be large enough to exceed high water mark.
    original = b"B" * 5 * 1024 * 1024
    compressed = zlib.compress(original)
    # No Content-Length or Transfer-Encoding means the parser must parse until EOF.
    headers = b"HTTP/1.1 200 OK\r\nContent-Encoding: deflate\r\n\r\n"

    msgs, upgrade, tail = response.feed_data(headers + compressed)
    response.feed_eof()
    payload = msgs[0][-1]

    # Check that .feed_eof() hasn't decompressed entire payload into memory.
    assert sum(len(b) for b in payload._buffer) <= (2 * 1024 * 1024)
    # Individual chunks should have been decompressed at limit amount.
    assert all(len(b) == 512 * 1024 for b in payload._buffer)

    result = await payload.read()
    assert len(result) == len(original)
    assert result == original


async def test_compressed_256kb(response: HttpResponseParser) -> None:
    original = b"x" * 256 * 1024
    compressed = zlib.compress(original)
    headers = (
        b"HTTP/1.1 200 OK\r\n"
        b"Content-Length: " + str(len(compressed)).encode() + b"\r\n"
        b"Content-Encoding: deflate\r\n"
        b"\r\n"
    )

    messages, upgrade, tail = response.feed_data(headers + compressed)
    assert len(messages) == 1
    payload = messages[0][-1]
    result = await payload.read()
    assert len(result) == len(original)
    assert result == original


@pytest.mark.parametrize("size", [40965, 8191])
def test_max_header_value_size_continuation(response, size) -> None:
    name = b"T" * (size - 5)
    text = b"HTTP/1.1 200 Ok\r\ndata: test\r\n " + name + b"\r\n\r\n"

    match = "400, message:\n  Got more than 8190 bytes when reading"
    with pytest.raises(http_exceptions.LineTooLong, match=match):
        for i in range(0, len(text), 9000):  # pragma: no branch
            response.feed_data(text[i : i + 9000])


def test_max_header_value_size_continuation_under_limit(
    response: HttpResponseParser,
) -> None:
    value = b"A" * 8179
    text = b"HTTP/1.1 200 Ok\r\ndata: test\r\n " + value + b"\r\n\r\n"

    messages, upgrade, tail = response.feed_data(text)
    msg = messages[0][0]
    assert msg.code == 200
    assert msg.reason == "Ok"
    assert msg.version == (1, 1)
    assert msg.headers == CIMultiDict({"data": "test " + value.decode()})
    assert msg.raw_headers == ((b"data", b"test " + value),)
    assert msg.should_close
    assert msg.compression is None
    assert not msg.upgrade
    assert not msg.chunked


def test_http_request_parser(parser) -> None:
    text = b"GET /path HTTP/1.1\r\nHost: a\r\n\r\n"
    messages, upgrade, tail = parser.feed_data(text)
    msg = messages[0][0]

    assert msg.method == "GET"
    assert msg.path == "/path"
    assert msg.version == (1, 1)
    assert msg.headers == CIMultiDict({"Host": "a"})
    assert msg.raw_headers == ((b"Host", b"a"),)
    assert not msg.should_close
    assert msg.compression is None
    assert not msg.upgrade
    assert not msg.chunked
    assert msg.url == URL("/path")


def test_http_request_bad_status_line(parser) -> None:
    text = b"getpath \r\n\r\n"
    with pytest.raises(http_exceptions.BadStatusLine) as exc_info:
        parser.feed_data(text)
    # Check for accidentally escaped message.
    assert r"\n" not in exc_info.value.message


_num: dict[bytes, str] = {
    # dangerous: accepted by Python int()
    # unicodedata.category("\U0001D7D9") == 'Nd'
    "\N{mathematical double-struck digit one}".encode(): "utf8digit",
    # only added for interop tests, refused by Python int()
    # unicodedata.category("\U000000B9") == 'No'
    "\N{superscript one}".encode(): "utf8number",
    "\N{superscript one}".encode("latin-1"): "latin1number",
}


@pytest.mark.parametrize("nonascii_digit", _num.keys(), ids=_num.values())
def test_http_request_bad_status_line_number(
    parser: Any, nonascii_digit: bytes
) -> None:
    text = b"GET /digit HTTP/1." + nonascii_digit + b"\r\nHost: a\r\n\r\n"
    with pytest.raises(http_exceptions.BadStatusLine):
        parser.feed_data(text)


def test_http_request_bad_status_line_separator(parser: Any) -> None:
    # single code point, old, multibyte NFKC, multibyte NFKD
    utf8sep = "\N{arabic ligature sallallahou alayhe wasallam}".encode()
    text = b"GET /ligature HTTP/1" + utf8sep + b"1\r\nHost: a\r\n\r\n"
    with pytest.raises(http_exceptions.BadStatusLine):
        parser.feed_data(text)


def test_http_request_bad_status_line_whitespace(parser: Any) -> None:
    text = b"GET\n/path\fHTTP/1.1\r\nHost: a\r\n\r\n"
    with pytest.raises(http_exceptions.BadStatusLine):
        parser.feed_data(text)


def test_http_request_message_after_close(parser: HttpRequestParser) -> None:
    text = b"GET / HTTP/1.1\r\nHost: a\r\nConnection: close\r\n\r\nInvalid\r\n\r\n"
    with pytest.raises(
        http_exceptions.BadHttpMessage, match="Data after `Connection: close`"
    ):
        parser.feed_data(text)


def test_http_request_message_after_close_comma_list(parser: HttpRequestParser) -> None:
    text = b"GET / HTTP/1.1\r\nHost: a\r\nConnection: close, keep-alive\r\n\r\nInvalid\r\n\r\n"
    with pytest.raises(
        http_exceptions.BadHttpMessage, match="Data after `Connection: close`"
    ):
        parser.feed_data(text)


def test_http_request_upgrade(parser: HttpRequestParser) -> None:
    text = (
        b"GET /test HTTP/1.1\r\n"
        b"Host: a\r\n"
        b"connection: upgrade\r\n"
        b"upgrade: websocket\r\n\r\n"
        b"some raw data"
    )
    messages, upgrade, tail = parser.feed_data(text)
    msg = messages[0][0]
    assert not msg.should_close
    assert msg.upgrade
    assert upgrade
    assert tail == b"some raw data"


async def test_http_request_upgrade_unknown(parser: Any) -> None:
    text = (
        b"POST / HTTP/1.1\r\n"
        b"Host: a\r\n"
        b"Connection: Upgrade\r\n"
        b"Content-Length: 2\r\n"
        b"Upgrade: unknown\r\n"
        b"Content-Type: application/json\r\n\r\n"
        b"{}"
    )
    messages, upgrade, tail = parser.feed_data(text)

    msg = messages[0][0]
    assert not msg.should_close
    assert msg.upgrade
    assert not upgrade
    assert not msg.chunked
    assert tail == b""
    assert await messages[0][-1].read() == b"{}"


@pytest.fixture
def xfail_c_parser_url(request) -> None:
    if isinstance(request.getfixturevalue("parser"), HttpRequestParserPy):
        return
    request.node.add_marker(
        pytest.mark.xfail(
            reason="Regression test for Py parser. May match C behaviour later.",
            raises=http_exceptions.InvalidURLError,
        )
    )


@pytest.mark.usefixtures("xfail_c_parser_url")
def test_http_request_parser_utf8_request_line(parser) -> None:
    messages, upgrade, tail = parser.feed_data(
        # note the truncated unicode sequence
        b"GET /P\xc3\xbcnktchen\xa0\xef\xb7 HTTP/1.1\r\nHost: a\r\n" +
        # for easier grep: ASCII 0xA0 more commonly known as non-breaking space
        # note the leading and trailing spaces
        "sTeP:  \N{latin small letter sharp s}nek\t\N{no-break space}  "
        "\r\n\r\n".encode()
    )
    msg = messages[0][0]

    assert msg.method == "GET"
    assert msg.path == "/Pünktchen\udca0\udcef\udcb7"
    assert msg.version == (1, 1)
    assert msg.headers == CIMultiDict([("Host", "a"), ("STEP", "ßnek\t\xa0")])
    assert msg.raw_headers == ((b"Host", b"a"), (b"sTeP", "ßnek\t\xa0".encode()))
    assert not msg.should_close
    assert msg.compression is None
    assert not msg.upgrade
    assert not msg.chunked
    # python HTTP parser depends on Cython and CPython URL to match
    # .. but yarl.URL("/abs") is not equal to URL.build(path="/abs"), see #6409
    assert msg.url == URL.build(path="/Pünktchen\udca0\udcef\udcb7", encoded=True)


def test_http_request_parser_utf8(parser) -> None:
    text = "GET /path HTTP/1.1\r\nHost: a\r\nx-test:тест\r\n\r\n".encode()
    messages, upgrade, tail = parser.feed_data(text)
    msg = messages[0][0]

    assert msg.method == "GET"
    assert msg.path == "/path"
    assert msg.version == (1, 1)
    assert msg.headers == CIMultiDict([("Host", "a"), ("X-TEST", "тест")])
    assert msg.raw_headers == ((b"Host", b"a"), (b"x-test", "тест".encode()))
    assert not msg.should_close
    assert msg.compression is None
    assert not msg.upgrade
    assert not msg.chunked
    assert msg.url == URL("/path")


def test_http_request_parser_non_utf8(parser) -> None:
    text = "GET /path HTTP/1.1\r\nHost: a\r\nx-test:тест\r\n\r\n".encode("cp1251")
    msg = parser.feed_data(text)[0][0][0]

    assert msg.method == "GET"
    assert msg.path == "/path"
    assert msg.version == (1, 1)
    assert msg.headers == CIMultiDict(
        [
            ("Host", "a"),
            ("X-TEST", "тест".encode("cp1251").decode("utf8", "surrogateescape")),
        ]
    )
    assert msg.raw_headers == ((b"Host", b"a"), (b"x-test", "тест".encode("cp1251")))
    assert not msg.should_close
    assert msg.compression is None
    assert not msg.upgrade
    assert not msg.chunked
    assert msg.url == URL("/path")


def test_http_request_parser_two_slashes(parser) -> None:
    text = b"GET //path HTTP/1.1\r\nHost: a\r\n\r\n"
    msg = parser.feed_data(text)[0][0][0]

    assert msg.method == "GET"
    assert msg.path == "//path"
    assert msg.url.path == "//path"
    assert msg.version == (1, 1)
    assert not msg.should_close
    assert msg.compression is None
    assert not msg.upgrade
    assert not msg.chunked


@pytest.mark.parametrize(
    "rfc9110_5_6_2_token_delim",
    [bytes([i]) for i in rb'"(),/:;<=>?@[\]{}'],
)
def test_http_request_parser_bad_method(
    parser, rfc9110_5_6_2_token_delim: bytes
) -> None:
    with pytest.raises(http_exceptions.BadHttpMethod):
        parser.feed_data(
            rfc9110_5_6_2_token_delim + b'ET" /get HTTP/1.1\r\nHost: a\r\n\r\n'
        )


def test_http_request_parser_tls_handshake_on_http_port(
    parser: HttpRequestParser,
) -> None:
    with pytest.raises(http_exceptions.BadHttpMethod) as ctx:
        parser.feed_data(b"\x16\x03\x03\x01F\x01\r\n\r\n")

    assert "Received HTTPS traffic on an HTTP port" in str(ctx.value)


def test_http_request_parser_bad_version(parser: HttpRequestParser) -> None:
    with pytest.raises(http_exceptions.BadHttpMessage):
        parser.feed_data(b"GET //get HT/11\r\nHost: a\r\n\r\n")


def test_http_request_parser_bad_version_number(parser: Any) -> None:
    with pytest.raises(http_exceptions.BadHttpMessage):
        parser.feed_data(b"GET /test HTTP/1.32\r\nHost: a\r\n\r\n")


def test_http_request_parser_bad_ascii_uri(parser: Any) -> None:
    with pytest.raises(http_exceptions.InvalidURLError):
        parser.feed_data(b"GET ! HTTP/1.1\r\n\r\n")


def test_http_request_parser_bad_nonascii_uri(parser: Any) -> None:
    with pytest.raises(http_exceptions.InvalidURLError):
        parser.feed_data(b"GET \xff HTTP/1.1\r\n\r\n")


@pytest.mark.parametrize("size", [40965, 8191])
def test_http_request_max_status_line(parser, size) -> None:
    path = b"t" * (size - 5)
    match = "400, message:\n  Got more than 8190 bytes when reading"
    with pytest.raises(http_exceptions.LineTooLong, match=match):
        parser.feed_data(b"GET /path" + path + b" HTTP/1.1\r\n\r\n")


def test_http_request_max_status_line_under_limit(parser: HttpRequestParser) -> None:
    path = b"t" * 8172
    messages, upgraded, tail = parser.feed_data(
        b"GET /path" + path + b" HTTP/1.1\r\nHost: a\r\n\r\n"
    )
    msg = messages[0][0]

    assert msg.method == "GET"
    assert msg.path == "/path" + path.decode()
    assert msg.version == (1, 1)
    assert msg.headers == CIMultiDict({"Host": "a"})
    assert msg.raw_headers == ((b"Host", b"a"),)
    assert not msg.should_close
    assert msg.compression is None
    assert not msg.upgrade
    assert not msg.chunked
    assert msg.url == URL("/path" + path.decode())


def test_http_response_parser_utf8(response) -> None:
    text = "HTTP/1.1 200 Ok\r\nx-test:тест\r\n\r\n".encode()

    messages, upgraded, tail = response.feed_data(text)
    assert len(messages) == 1
    msg = messages[0][0]

    assert msg.version == (1, 1)
    assert msg.code == 200
    assert msg.reason == "Ok"
    assert msg.headers == CIMultiDict([("X-TEST", "тест")])
    assert msg.raw_headers == ((b"x-test", "тест".encode()),)
    assert not upgraded
    assert not tail


def test_http_response_parser_utf8_without_reason(response: Any) -> None:
    text = "HTTP/1.1 200 \r\nx-test:тест\r\n\r\n".encode()

    messages, upgraded, tail = response.feed_data(text)
    assert len(messages) == 1
    msg = messages[0][0]

    assert msg.version == (1, 1)
    assert msg.code == 200
    assert msg.reason == ""
    assert msg.headers == CIMultiDict([("X-TEST", "тест")])
    assert msg.raw_headers == ((b"x-test", "тест".encode()),)
    assert not upgraded
    assert not tail


def test_http_response_parser_obs_line_folding(response: Any) -> None:
    text = b"HTTP/1.1 200 Ok\r\ntest: line\r\n continue\r\n\r\n"

    messages, upgraded, tail = response.feed_data(text)
    assert len(messages) == 1
    msg = messages[0][0]

    assert msg.version == (1, 1)
    assert msg.code == 200
    assert msg.reason == "Ok"
    assert msg.headers == CIMultiDict([("TEST", "line continue")])
    assert msg.raw_headers == ((b"test", b"line continue"),)
    assert not upgraded
    assert not tail


@pytest.mark.dev_mode
def test_http_response_parser_strict_obs_line_folding(response: Any) -> None:
    text = b"HTTP/1.1 200 Ok\r\ntest: line\r\n continue\r\n\r\n"

    with pytest.raises(http_exceptions.BadHttpMessage):
        response.feed_data(text)


@pytest.mark.parametrize("size", [40962, 8191])
def test_http_response_parser_bad_status_line_too_long(response, size) -> None:
    reason = b"t" * (size - 2)
    match = "400, message:\n  Got more than 8190 bytes when reading"
    with pytest.raises(http_exceptions.LineTooLong, match=match):
        response.feed_data(b"HTTP/1.1 200 Ok" + reason + b"\r\n\r\n")


def test_http_response_parser_status_line_under_limit(
    response: HttpResponseParser,
) -> None:
    reason = b"O" * 8177
    messages, upgraded, tail = response.feed_data(
        b"HTTP/1.1 200 " + reason + b"\r\n\r\n"
    )
    msg = messages[0][0]
    assert msg.version == (1, 1)
    assert msg.code == 200
    assert msg.reason == reason.decode()


def test_http_response_parser_bad_version(response) -> None:
    with pytest.raises(http_exceptions.BadHttpMessage):
        response.feed_data(b"HT/11 200 Ok\r\n\r\n")


def test_http_response_parser_bad_version_number(response) -> None:
    with pytest.raises(http_exceptions.BadHttpMessage):
        response.feed_data(b"HTTP/12.3 200 Ok\r\n\r\n")


def test_http_response_parser_no_reason(response) -> None:
    msg = response.feed_data(b"HTTP/1.1 200\r\n\r\n")[0][0][0]

    assert msg.version == (1, 1)
    assert msg.code == 200
    assert msg.reason == ""


def test_http_response_parser_lenient_headers(response) -> None:
    messages, upgrade, tail = response.feed_data(
        b"HTTP/1.1 200 test\r\nFoo: abc\x01def\r\n\r\n"
    )
    msg = messages[0][0]

    assert msg.headers["Foo"] == "abc\x01def"


@pytest.mark.dev_mode
def test_http_response_parser_strict_headers(response) -> None:
    if isinstance(response, HttpResponseParserPy):
        pytest.xfail("Py parser is lenient. May update py-parser later.")
    with pytest.raises(http_exceptions.BadHttpMessage):
        response.feed_data(b"HTTP/1.1 200 test\r\nFoo: abc\x01def\r\n\r\n")


def test_http_response_parser_null_byte_in_header_value(
    response: HttpResponseParser,
) -> None:
    with pytest.raises(http_exceptions.InvalidHeader):
        response.feed_data(b"HTTP/1.1 200 OK\r\nFoo: abc\x00def\r\n\r\n")


def test_http_response_parser_bad_crlf(response: HttpResponseParser) -> None:
    """Still a lot of dodgy servers sending bad requests like this."""
    messages, upgrade, tail = response.feed_data(
        b"HTTP/1.0 200 OK\nFoo: abc\nBar: def\n\nBODY\n"
    )
    msg = messages[0][0]

    assert msg.headers["Foo"] == "abc"
    assert msg.headers["Bar"] == "def"


async def test_http_response_parser_bad_chunked_lax(response) -> None:
    text = (
        b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5 \r\nabcde\r\n0\r\n\r\n"
    )
    messages, upgrade, tail = response.feed_data(text)

    assert await messages[0][1].read(5) == b"abcde"


@pytest.mark.dev_mode
async def test_http_response_parser_bad_chunked_strict_py(
    loop: asyncio.AbstractEventLoop,
) -> None:
    protocol = ResponseHandler(loop)

    response = HttpResponseParserPy(
        protocol,
        loop,
        DEFAULT_CHUNK_SIZE,
        max_line_size=8190,
        max_field_size=8190,
    )
    protocol._parser = response
    text = (
        b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5 \r\nabcde\r\n0\r\n\r\n"
    )
    with pytest.raises(http_exceptions.TransferEncodingError, match="5"):
        response.feed_data(text)


@pytest.mark.dev_mode
@pytest.mark.skipif(
    "HttpRequestParserC" not in dir(aiohttp.http_parser),
    reason="C based HTTP parser not available",
)
async def test_http_response_parser_bad_chunked_strict_c(
    loop: asyncio.AbstractEventLoop,
) -> None:
    protocol = ResponseHandler(loop)

    response = HttpResponseParserC(
        protocol,
        loop,
        2**16,
        max_line_size=8190,
        max_field_size=8190,
    )
    protocol._parser = response
    text = (
        b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5 \r\nabcde\r\n0\r\n\r\n"
    )
    with pytest.raises(http_exceptions.BadHttpMessage):
        response.feed_data(text)


async def test_http_response_parser_notchunked(response) -> None:
    text = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: notchunked\r\n\r\n1\r\nT\r\n3\r\nest\r\n0\r\n\r\n"
    messages, upgrade, tail = response.feed_data(text)
    response.feed_eof()

    # https://www.rfc-editor.org/rfc/rfc9112#section-6.3-2.4.2
    assert await messages[0][1].read() == b"1\r\nT\r\n3\r\nest\r\n0\r\n\r\n"


async def test_http_response_parser_last_chunked(response) -> None:
    text = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: not, chunked\r\n\r\n1\r\nT\r\n3\r\nest\r\n0\r\n\r\n"
    messages, upgrade, tail = response.feed_data(text)

    # https://www.rfc-editor.org/rfc/rfc9112#section-6.3-2.4.2
    assert await messages[0][1].read() == b"Test"


def test_http_response_parser_bad(response) -> None:
    with pytest.raises(http_exceptions.BadHttpMessage):
        response.feed_data(b"HTT/1\r\n\r\n")


def test_http_response_parser_code_under_100(response) -> None:
    with pytest.raises(http_exceptions.BadStatusLine):
        response.feed_data(b"HTTP/1.1 99 test\r\n\r\n")


def test_http_response_parser_code_above_999(response) -> None:
    with pytest.raises(http_exceptions.BadStatusLine):
        response.feed_data(b"HTTP/1.1 9999 test\r\n\r\n")


def test_http_response_parser_code_not_int(response) -> None:
    with pytest.raises(http_exceptions.BadStatusLine):
        response.feed_data(b"HTTP/1.1 ttt test\r\n\r\n")


@pytest.mark.parametrize("nonascii_digit", _num.keys(), ids=_num.values())
def test_http_response_parser_code_not_ascii(response, nonascii_digit: bytes) -> None:
    with pytest.raises(http_exceptions.BadStatusLine):
        response.feed_data(b"HTTP/1.1 20" + nonascii_digit + b" test\r\n\r\n")


def test_http_request_chunked_payload(parser) -> None:
    text = b"GET /test HTTP/1.1\r\nHost: a\r\ntransfer-encoding: chunked\r\n\r\n"
    msg, payload = parser.feed_data(text)[0][0]

    assert msg.chunked
    assert not payload.is_eof()
    assert isinstance(payload, streams.StreamReader)

    parser.feed_data(b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n")

    assert b"dataline" == b"".join(d for d in payload._buffer)
    assert payload._http_chunk_splits is not None
    assert [4, 8] == list(payload._http_chunk_splits)
    assert payload.is_eof()


def test_http_request_chunked_payload_and_next_message(parser) -> None:
    text = b"GET /test HTTP/1.1\r\nHost: a\r\ntransfer-encoding: chunked\r\n\r\n"
    msg, payload = parser.feed_data(text)[0][0]

    messages, upgraded, tail = parser.feed_data(
        b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n"
        b"POST /test2 HTTP/1.1\r\n"
        b"Host: a\r\n"
        b"transfer-encoding: chunked\r\n\r\n"
    )

    assert b"dataline" == b"".join(d for d in payload._buffer)
    assert payload._http_chunk_splits is not None
    assert [4, 8] == list(payload._http_chunk_splits)
    assert payload.is_eof()

    assert len(messages) == 1
    msg2, payload2 = messages[0]

    assert msg2.method == "POST"
    assert msg2.chunked
    assert not payload2.is_eof()


def test_http_request_chunked_payload_chunks(parser) -> None:
    text = b"GET /test HTTP/1.1\r\nHost: a\r\ntransfer-encoding: chunked\r\n\r\n"
    msg, payload = parser.feed_data(text)[0][0]

    parser.feed_data(b"4\r\ndata\r")
    parser.feed_data(b"\n4")
    parser.feed_data(b"\r")
    parser.feed_data(b"\n")
    parser.feed_data(b"li")
    parser.feed_data(b"ne\r\n0\r\n")
    parser.feed_data(b"test: test\r\n")

    assert b"dataline" == b"".join(d for d in payload._buffer)
    assert payload._http_chunk_splits is not None
    assert [4, 8] == list(payload._http_chunk_splits)
    assert not payload.is_eof()

    parser.feed_data(b"\r\n")
    assert b"dataline" == b"".join(d for d in payload._buffer)
    assert [4, 8] == list(payload._http_chunk_splits)
    assert payload.is_eof()


def test_parse_chunked_payload_chunk_extension(parser) -> None:
    text = b"GET /test HTTP/1.1\r\nHost: a\r\ntransfer-encoding: chunked\r\n\r\n"
    msg, payload = parser.feed_data(text)[0][0]

    parser.feed_data(b"4;test\r\ndata\r\n4\r\nline\r\n0\r\ntest: test\r\n\r\n")

    assert b"dataline" == b"".join(d for d in payload._buffer)
    assert payload._http_chunk_splits is not None
    assert [4, 8] == list(payload._http_chunk_splits)
    assert payload.is_eof()


async def test_request_chunked_with_trailer(parser: HttpRequestParser) -> None:
    text = b"GET /test HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunked\r\n\r\n4\r\ntest\r\n0\r\ntest: trailer\r\nsecond: test trailer\r\n\r\n"
    messages, upgraded, tail = parser.feed_data(text)
    assert not tail
    msg, payload = messages[0]
    assert await payload.read() == b"test"

    # TODO: Add assertion of trailers when API added.


async def test_request_chunked_reject_bad_trailer(parser: HttpRequestParser) -> None:
    text = b"GET /test HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunked\r\n\r\n0\r\nbad\ntrailer\r\n\r\n"
    with pytest.raises(http_exceptions.BadHttpMessage, match=r"b'bad\\ntrailer'"):
        parser.feed_data(text)


def test_parse_no_length_or_te_on_post(
    loop: asyncio.AbstractEventLoop,
    server: Server,
    request_cls: type[HttpRequestParser],
) -> None:
    protocol = RequestHandler(server, loop=loop)
    parser = request_cls(protocol, loop, limit=DEFAULT_CHUNK_SIZE)
    protocol._parser = parser
    text = b"POST /test HTTP/1.1\r\nHost: a\r\n\r\n"
    msg, payload = parser.feed_data(text)[0][0]

    assert payload.is_eof()


def test_parse_payload_response_without_body(
    loop: asyncio.AbstractEventLoop,
    response_cls: type[HttpResponseParser],
) -> None:
    protocol = ResponseHandler(loop)
    parser = response_cls(protocol, loop, 2**16, response_with_body=False)
    protocol._parser = parser
    text = b"HTTP/1.1 200 Ok\r\ncontent-length: 10\r\n\r\n"
    msg, payload = parser.feed_data(text)[0][0]

    assert payload.is_eof()


async def test_parse_payload_response_with_invalid_body(
    protocol: BaseProtocol,
    response_cls: type[HttpResponseParser],
) -> None:
    loop = asyncio.get_running_loop()
    parser = response_cls(protocol, loop, 2**16, response_with_body=False)
    text = (
        b"HTTP/1.1 200 Ok\r\nTransfer-Encoding: chunked\r\n\r\n"
        b"7\r\nchunked\r\n0\r\n\r\n"
    )
    with pytest.raises(http_exceptions.BadHttpMessage, match="status line"):
        parser.feed_data(text)[0][0]


def test_parse_length_payload(response: HttpResponseParser) -> None:
    text = b"HTTP/1.1 200 Ok\r\ncontent-length: 4\r\n\r\n"
    msg, payload = response.feed_data(text)[0][0]
    assert not payload.is_eof()

    response.feed_data(b"da")
    response.feed_data(b"t")
    response.feed_data(b"aHT")

    assert payload.is_eof()
    assert b"data" == b"".join(d for d in payload._buffer)


def test_parse_no_length_payload(parser) -> None:
    text = b"PUT / HTTP/1.1\r\nHost: a\r\n\r\n"
    msg, payload = parser.feed_data(text)[0][0]
    assert payload.is_eof()


def test_parse_content_length_payload_multiple(response: Any) -> None:
    text = b"HTTP/1.1 200 OK\r\ncontent-length: 5\r\n\r\nfirst"
    msg, payload = response.feed_data(text)[0][0]
    assert msg.version == HttpVersion(major=1, minor=1)
    assert msg.code == 200
    assert msg.reason == "OK"
    assert msg.headers == CIMultiDict(
        [
            ("Content-Length", "5"),
        ]
    )
    assert msg.raw_headers == ((b"content-length", b"5"),)
    assert not msg.should_close
    assert msg.compression is None
    assert not msg.upgrade
    assert not msg.chunked
    assert payload.is_eof()
    assert b"first" == b"".join(d for d in payload._buffer)

    text = b"HTTP/1.1 200 OK\r\ncontent-length: 6\r\n\r\nsecond"
    msg, payload = response.feed_data(text)[0][0]
    assert msg.version == HttpVersion(major=1, minor=1)
    assert msg.code == 200
    assert msg.reason == "OK"
    assert msg.headers == CIMultiDict(
        [
            ("Content-Length", "6"),
        ]
    )
    assert msg.raw_headers == ((b"content-length", b"6"),)
    assert not msg.should_close
    assert msg.compression is None
    assert not msg.upgrade
    assert not msg.chunked
    assert payload.is_eof()
    assert b"second" == b"".join(d for d in payload._buffer)


def test_parse_content_length_than_chunked_payload(response: Any) -> None:
    text = b"HTTP/1.1 200 OK\r\ncontent-length: 5\r\n\r\nfirst"
    msg, payload = response.feed_data(text)[0][0]
    assert msg.version == HttpVersion(major=1, minor=1)
    assert msg.code == 200
    assert msg.reason == "OK"
    assert msg.headers == CIMultiDict(
        [
            ("Content-Length", "5"),
        ]
    )
    assert msg.raw_headers == ((b"content-length", b"5"),)
    assert not msg.should_close
    assert msg.compression is None
    assert not msg.upgrade
    assert not msg.chunked
    assert payload.is_eof()
    assert b"first" == b"".join(d for d in payload._buffer)

    text = (
        b"HTTP/1.1 200 OK\r\n"
        b"transfer-encoding: chunked\r\n\r\n"
        b"6\r\nsecond\r\n0\r\n\r\n"
    )
    msg, payload = response.feed_data(text)[0][0]
    assert msg.version == HttpVersion(major=1, minor=1)
    assert msg.code == 200
    assert msg.reason == "OK"
    assert msg.headers == CIMultiDict(
        [
            ("Transfer-Encoding", "chunked"),
        ]
    )
    assert msg.raw_headers == ((b"transfer-encoding", b"chunked"),)
    assert not msg.should_close
    assert msg.compression is None
    assert not msg.upgrade
    assert msg.chunked
    assert payload.is_eof()
    assert b"second" == b"".join(d for d in payload._buffer)


@pytest.mark.parametrize("code", (204, 304, 101, 102))
def test_parse_chunked_payload_empty_body_than_another_chunked(
    response: Any, code: int
) -> None:
    head = f"HTTP/1.1 {code} OK\r\n".encode()
    text = head + b"transfer-encoding: chunked\r\n\r\n"
    msg, payload = response.feed_data(text)[0][0]
    assert msg.version == HttpVersion(major=1, minor=1)
    assert msg.code == code
    assert msg.reason == "OK"
    assert msg.headers == CIMultiDict(
        [
            ("Transfer-Encoding", "chunked"),
        ]
    )
    assert msg.raw_headers == ((b"transfer-encoding", b"chunked"),)
    assert not msg.should_close
    assert msg.compression is None
    assert not msg.upgrade
    assert msg.chunked
    assert payload.is_eof()

    text = (
        b"HTTP/1.1 200 OK\r\n"
        b"transfer-encoding: chunked\r\n\r\n"
        b"6\r\nsecond\r\n0\r\n\r\n"
    )
    msg, payload = response.feed_data(text)[0][0]
    assert msg.version == HttpVersion(major=1, minor=1)
    assert msg.code == 200
    assert msg.reason == "OK"
    assert msg.headers == CIMultiDict(
        [
            ("Transfer-Encoding", "chunked"),
        ]
    )
    assert msg.raw_headers == ((b"transfer-encoding", b"chunked"),)
    assert not msg.should_close
    assert msg.compression is None
    assert not msg.upgrade
    assert msg.chunked
    assert payload.is_eof()
    assert b"second" == b"".join(d for d in payload._buffer)


async def test_parse_chunked_payload_split_chunks(response: Any) -> None:
    network_chunks = (
        b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n",
        b"5\r\nfi",
        b"rst",
        # This simulates a bug in lax mode caused when the \r\n separator, before the
        # next HTTP chunk, appears at the start of the next network chunk.
        b"\r\n",
        b"6",
        b"\r",
        b"\n",
        b"second\r",
        b"\n0\r\n\r\n",
    )
    reader = response.feed_data(network_chunks[0])[0][0][1]
    for c in network_chunks[1:]:
        response.feed_data(c)

    assert response.feed_eof() is None
    assert reader.is_eof()
    assert await reader.read() == b"firstsecond"


async def test_parse_chunked_payload_with_lf_in_extensions(
    parser: HttpRequestParser,
) -> None:
    """Test chunked payload that has a LF in the chunk extensions."""
    payload = (
        b"GET / HTTP/1.1\r\nHost: localhost:5001\r\n"
        b"Transfer-Encoding: chunked\r\n\r\n2;\nxx\r\n4c\r\n0\r\n\r\n"
        b"GET /admin HTTP/1.1\r\nHost: localhost:5001\r\n"
        b"Transfer-Encoding: chunked\r\n\r\n0\r\n\r\n"
    )
    with pytest.raises(http_exceptions.BadHttpMessage, match="\\\\nxx"):
        parser.feed_data(payload)


def test_partial_url(parser: HttpRequestParser) -> None:
    messages, upgrade, tail = parser.feed_data(b"GET /te")
    assert len(messages) == 0
    messages, upgrade, tail = parser.feed_data(b"st HTTP/1.1\r\nHost: a\r\n\r\n")
    assert len(messages) == 1

    msg, payload = messages[0]

    assert msg.method == "GET"
    assert msg.path == "/test"
    assert msg.version == (1, 1)
    assert payload.is_eof()


@pytest.mark.parametrize(
    ("uri", "path", "query", "fragment"),
    [
        ("/path%23frag", "/path#frag", {}, ""),
        ("/path%2523frag", "/path%23frag", {}, ""),
        ("/path?key=value%23frag", "/path", {"key": "value#frag"}, ""),
        ("/path?key=value%2523frag", "/path", {"key": "value%23frag"}, ""),
        ("/path#frag%20", "/path", {}, "frag "),
        ("/path#frag%2520", "/path", {}, "frag%20"),
    ],
)
def test_parse_uri_percent_encoded(parser, uri, path, query, fragment) -> None:
    text = (f"GET {uri} HTTP/1.1\r\nHost: a\r\n\r\n").encode()
    messages, upgrade, tail = parser.feed_data(text)
    msg = messages[0][0]

    assert msg.path == uri
    assert msg.url == URL(uri)
    assert msg.url.path == path
    assert msg.url.query == query
    assert msg.url.fragment == fragment


def test_parse_uri_utf8(parser) -> None:
    if not isinstance(parser, HttpRequestParserPy):
        pytest.xfail("Not valid HTTP. Maybe update py-parser to reject later.")
    text = ("GET /путь?ключ=знач#фраг HTTP/1.1\r\nHost: a\r\n\r\n").encode()
    messages, upgrade, tail = parser.feed_data(text)
    msg = messages[0][0]

    assert msg.path == "/путь?ключ=знач#фраг"
    assert msg.url.path == "/путь"
    assert msg.url.query == {"ключ": "знач"}
    assert msg.url.fragment == "фраг"


def test_parse_uri_utf8_percent_encoded(parser) -> None:
    text = (
        "GET %s HTTP/1.1\r\nHost: a\r\n\r\n"
        % quote("/путь?ключ=знач#фраг", safe="/?=#")
    ).encode()
    messages, upgrade, tail = parser.feed_data(text)
    msg = messages[0][0]

    assert msg.path == quote("/путь?ключ=знач#фраг", safe="/?=#")
    assert msg.url == URL("/путь?ключ=знач#фраг")
    assert msg.url.path == "/путь"
    assert msg.url.query == {"ключ": "знач"}
    assert msg.url.fragment == "фраг"


@pytest.mark.skipif(
    "HttpRequestParserC" not in dir(aiohttp.http_parser),
    reason="C based HTTP parser not available",
)
def test_parse_bad_method_for_c_parser_raises(
    loop: asyncio.AbstractEventLoop, server: Server
) -> None:
    protocol = RequestHandler(server, loop=loop)

    payload = b"GET1 /test HTTP/1.1\r\n\r\n"
    parser = HttpRequestParserC(
        protocol,
        loop,
        DEFAULT_CHUNK_SIZE,
        max_line_size=8190,
        max_headers=128,
        max_field_size=8190,
    )
    protocol._parser = parser

    with pytest.raises(aiohttp.http_exceptions.BadStatusLine):
        messages, upgrade, tail = parser.feed_data(payload)


class TestParsePayload:
    async def test_parse_eof_payload(self, protocol: BaseProtocol) -> None:
        out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
        p = HttpPayloadParser(out, headers_parser=HeadersParser())
        p.feed_data(b"data")
        p.feed_eof()

        assert out.is_eof()
        assert [(bytearray(b"data"))] == list(out._buffer)

    async def test_parse_length_payload_eof(self, protocol: BaseProtocol) -> None:
        out = aiohttp.StreamReader(
            protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
        )

        p = HttpPayloadParser(out, length=4, headers_parser=HeadersParser())
        p.feed_data(b"da")

        with pytest.raises(http_exceptions.ContentLengthError):
            p.feed_eof()

    async def test_parse_length_payload_eof_error_message(
        self, protocol: BaseProtocol
    ) -> None:
        """Test that ContentLengthError includes expected vs received bytes."""
        out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())

        # Expect 10 bytes, but only send 3
        p = HttpPayloadParser(out, length=10, headers_parser=HeadersParser())
        p.feed_data(b"abc")

        with pytest.raises(
            http_exceptions.ContentLengthError, match=r"received 3 of 10 bytes"
        ):
            p.feed_eof()

    async def test_parse_length_payload_eof_no_data(
        self, protocol: BaseProtocol
    ) -> None:
        """Test ContentLengthError when no data is received."""
        out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())

        # Expect 20 bytes, but send nothing
        p = HttpPayloadParser(out, length=20, headers_parser=HeadersParser())

        with pytest.raises(
            http_exceptions.ContentLengthError, match=r"received 0 of 20 bytes"
        ):
            p.feed_eof()

    async def test_parse_length_payload_partial_data(
        self, protocol: BaseProtocol
    ) -> None:
        """Test ContentLengthError with various amounts of partial data."""
        out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())

        # Expect 100 bytes, but only send 45
        p = HttpPayloadParser(out, length=100, headers_parser=HeadersParser())
        p.feed_data(b"a" * 25)
        p.feed_data(b"b" * 20)

        with pytest.raises(
            http_exceptions.ContentLengthError,
            match=r"received 45 of 100 bytes",
        ):
            p.feed_eof()

    async def test_parse_chunked_payload_size_error(
        self, protocol: BaseProtocol
    ) -> None:
        out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
        p = HttpPayloadParser(out, chunked=True, headers_parser=HeadersParser())
        with pytest.raises(http_exceptions.TransferEncodingError):
            p.feed_data(b"blah\r\n")
        assert isinstance(out.exception(), http_exceptions.TransferEncodingError)

    async def test_parse_chunked_payload_size_data_mismatch(
        self, protocol: BaseProtocol
    ) -> None:
        """Chunk-size does not match actual data: should raise, not hang.

        Regression test for #10596.
        """
        out = aiohttp.StreamReader(
            protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
        )
        p = HttpPayloadParser(out, chunked=True, headers_parser=HeadersParser())
        # Declared chunk-size is 4 but actual data is "Hello" (5 bytes).
        # After consuming 4 bytes, remaining starts with "o" not "\r\n".
        with pytest.raises(http_exceptions.TransferEncodingError):
            p.feed_data(b"4\r\nHello\r\n0\r\n\r\n")
        assert isinstance(out.exception(), http_exceptions.TransferEncodingError)

    async def test_parse_chunked_payload_size_data_mismatch_too_short(
        self, protocol: BaseProtocol
    ) -> None:
        """Chunk-size larger than data: declared 6 but only 5 bytes before CRLF.

        Regression test for #10596.
        """
        out = aiohttp.StreamReader(
            protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
        )
        p = HttpPayloadParser(out, chunked=True, headers_parser=HeadersParser())
        # Declared chunk-size is 6 but actual data before CRLF is "Hello" (5 bytes).
        # Parser reads 6 bytes: "Hello\r", then expects \r\n but sees "\n0\r\n..."
        with pytest.raises(http_exceptions.TransferEncodingError):
            p.feed_data(b"6\r\nHello\r\n0\r\n\r\n")
        assert isinstance(out.exception(), http_exceptions.TransferEncodingError)

    async def test_parse_chunked_payload_split_end(
        self, protocol: BaseProtocol
    ) -> None:
        out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
        p = HttpPayloadParser(out, chunked=True, headers_parser=HeadersParser())
        p.feed_data(b"4\r\nasdf\r\n0\r\n")
        p.feed_data(b"\r\n")

        assert out.is_eof()
        assert b"asdf" == b"".join(out._buffer)

    async def test_parse_chunked_payload_split_end2(
        self, protocol: BaseProtocol
    ) -> None:
        out = aiohttp.StreamReader(
            protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
        )
        p = HttpPayloadParser(out, chunked=True, headers_parser=HeadersParser())
        p.feed_data(b"4\r\nasdf\r\n0\r\n\r")
        p.feed_data(b"\n")

        assert out.is_eof()
        assert b"asdf" == b"".join(out._buffer)

    async def test_parse_chunked_payload_split_end_trailers(
        self, protocol: BaseProtocol
    ) -> None:
        out = aiohttp.StreamReader(
            protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
        )
        p = HttpPayloadParser(out, chunked=True, headers_parser=HeadersParser())
        p.feed_data(b"4\r\nasdf\r\n0\r\n")
        p.feed_data(b"Content-MD5: 912ec803b2ce49e4a541068d495ab570\r\n")
        p.feed_data(b"\r\n")

        assert out.is_eof()
        assert b"asdf" == b"".join(out._buffer)

    async def test_parse_chunked_payload_split_end_trailers2(
        self, protocol: BaseProtocol
    ) -> None:
        out = aiohttp.StreamReader(
            protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
        )
        p = HttpPayloadParser(out, chunked=True, headers_parser=HeadersParser())
        p.feed_data(b"4\r\nasdf\r\n0\r\n")
        p.feed_data(b"Content-MD5: 912ec803b2ce49e4a541068d495ab570\r\n\r")
        p.feed_data(b"\n")

        assert out.is_eof()
        assert b"asdf" == b"".join(out._buffer)

    async def test_parse_chunked_payload_split_end_trailers3(
        self, protocol: BaseProtocol
    ) -> None:
        out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
        p = HttpPayloadParser(out, chunked=True, headers_parser=HeadersParser())
        p.feed_data(b"4\r\nasdf\r\n0\r\nContent-MD5: ")
        p.feed_data(b"912ec803b2ce49e4a541068d495ab570\r\n\r\n")

        assert out.is_eof()
        assert b"asdf" == b"".join(out._buffer)

    async def test_parse_chunked_payload_split_end_trailers4(
        self, protocol: BaseProtocol
    ) -> None:
        out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
        p = HttpPayloadParser(out, chunked=True, headers_parser=HeadersParser())
        p.feed_data(b"4\r\nasdf\r\n0\r\nC")
        p.feed_data(b"ontent-MD5: 912ec803b2ce49e4a541068d495ab570\r\n\r\n")

        assert out.is_eof()
        assert b"asdf" == b"".join(out._buffer)

    async def test_http_payload_parser_length(self, protocol: BaseProtocol) -> None:
        out = aiohttp.StreamReader(
            protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
        )
        p = HttpPayloadParser(out, length=2, headers_parser=HeadersParser())
        state, tail = p.feed_data(b"1245")
        assert state is PayloadState.PAYLOAD_COMPLETE

        assert b"12" == out._buffer[0]
        assert b"45" == tail

    async def test_http_payload_parser_deflate(self, protocol: BaseProtocol) -> None:
        # c=compressobj(wbits=15); b''.join([c.compress(b'data'), c.flush()])
        COMPRESSED = b"x\x9cKI,I\x04\x00\x04\x00\x01\x9b"

        length = len(COMPRESSED)
        out = aiohttp.StreamReader(
            protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
        )
        p = HttpPayloadParser(
            out, length=length, compression="deflate", headers_parser=HeadersParser()
        )
        p.feed_data(COMPRESSED)
        assert b"data" == out._buffer[0]
        assert out.is_eof()

    async def test_http_payload_parser_deflate_no_hdrs(
        self, protocol: BaseProtocol
    ) -> None:
        """Tests incorrectly formed data (no zlib headers)."""
        # c=compressobj(wbits=-15); b''.join([c.compress(b'data'), c.flush()])
        COMPRESSED = b"KI,I\x04\x00"

        length = len(COMPRESSED)
        out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
        p = HttpPayloadParser(
            out, length=length, compression="deflate", headers_parser=HeadersParser()
        )
        p.feed_data(COMPRESSED)
        assert b"data" == out._buffer[0]
        assert out.is_eof()

    async def test_http_payload_parser_deflate_light(
        self, protocol: BaseProtocol
    ) -> None:
        # c=compressobj(wbits=9); b''.join([c.compress(b'data'), c.flush()])
        COMPRESSED = b"\x18\x95KI,I\x04\x00\x04\x00\x01\x9b"

        length = len(COMPRESSED)
        out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
        p = HttpPayloadParser(
            out, length=length, compression="deflate", headers_parser=HeadersParser()
        )
        p.feed_data(COMPRESSED)

        assert b"data" == out._buffer[0]
        assert out.is_eof()

    async def test_http_payload_parser_deflate_split(
        self, protocol: BaseProtocol
    ) -> None:
        out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
        p = HttpPayloadParser(
            out, compression="deflate", headers_parser=HeadersParser()
        )
        # Feeding one correct byte should be enough to choose exact
        # deflate decompressor
        p.feed_data(b"x")
        p.feed_data(b"\x9cKI,I\x04\x00\x04\x00\x01\x9b")
        p.feed_eof()
        assert b"data" == out._buffer[0]

    async def test_http_payload_parser_deflate_split_err(
        self, protocol: BaseProtocol
    ) -> None:
        out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
        p = HttpPayloadParser(
            out, compression="deflate", headers_parser=HeadersParser()
        )
        # Feeding one wrong byte should be enough to choose exact
        # deflate decompressor
        p.feed_data(b"K")
        p.feed_data(b"I,I\x04\x00")
        p.feed_eof()
        assert b"data" == out._buffer[0]

    async def test_http_payload_parser_length_zero(
        self, protocol: BaseProtocol
    ) -> None:
        out = aiohttp.StreamReader(
            protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
        )
        p = HttpPayloadParser(out, length=0, headers_parser=HeadersParser())
        assert p.done
        assert out.is_eof()

    @pytest.mark.skipif(brotli is None, reason="brotli is not installed")
    async def test_http_payload_brotli(self, protocol: BaseProtocol) -> None:
        compressed = brotli.compress(b"brotli data")
        out = aiohttp.StreamReader(
            protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
        )
        p = HttpPayloadParser(
            out,
            length=len(compressed),
            compression="br",
            headers_parser=HeadersParser(),
        )
        p.feed_data(compressed)
        assert b"brotli data" == out._buffer[0]
        assert out.is_eof()

    @pytest.mark.skipif(zstandard is None, reason="zstandard is not installed")
    async def test_http_payload_zstandard(self, protocol: BaseProtocol) -> None:
        compressed = zstandard.compress(b"zstd data")
        out = aiohttp.StreamReader(
            protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
        )
        p = HttpPayloadParser(
            out,
            length=len(compressed),
            compression="zstd",
            headers_parser=HeadersParser(),
        )
        p.feed_data(compressed)
        assert b"zstd data" == out._buffer[0]
        assert out.is_eof()

    @pytest.mark.skipif(zstandard is None, reason="zstandard is not installed")
    async def test_http_payload_zstandard_multi_frame(
        self, protocol: BaseProtocol
    ) -> None:
        frame1 = zstandard.compress(b"first")
        frame2 = zstandard.compress(b"second")
        payload = frame1 + frame2
        out = aiohttp.StreamReader(
            protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
        )
        p = HttpPayloadParser(
            out,
            length=len(payload),
            compression="zstd",
            headers_parser=HeadersParser(),
        )
        p.feed_data(payload)
        assert b"firstsecond" == b"".join(out._buffer)
        assert out.is_eof()

    @pytest.mark.skipif(zstandard is None, reason="zstandard is not installed")
    async def test_http_payload_zstandard_multi_frame_chunked(
        self, protocol: BaseProtocol
    ) -> None:
        frame1 = zstandard.compress(b"chunk1")
        frame2 = zstandard.compress(b"chunk2")
        out = aiohttp.StreamReader(
            protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
        )
        p = HttpPayloadParser(
            out,
            length=len(frame1) + len(frame2),
            compression="zstd",
            headers_parser=HeadersParser(),
        )
        p.feed_data(frame1)
        p.feed_data(frame2)
        assert b"chunk1chunk2" == b"".join(out._buffer)
        assert out.is_eof()

    @pytest.mark.skipif(zstandard is None, reason="zstandard is not installed")
    async def test_http_payload_zstandard_frame_split_mid_chunk(
        self, protocol: BaseProtocol
    ) -> None:
        frame1 = zstandard.compress(b"AAAA")
        frame2 = zstandard.compress(b"BBBB")
        combined = frame1 + frame2
        split_point = len(frame1) + 3  # 3 bytes into frame2
        out = aiohttp.StreamReader(
            protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
        )
        p = HttpPayloadParser(
            out,
            length=len(combined),
            compression="zstd",
            headers_parser=HeadersParser(),
        )
        p.feed_data(combined[:split_point])
        p.feed_data(combined[split_point:])
        assert b"AAAABBBB" == b"".join(out._buffer)
        assert out.is_eof()

    @pytest.mark.skipif(zstandard is None, reason="zstandard is not installed")
    async def test_http_payload_zstandard_many_small_frames(
        self, protocol: BaseProtocol
    ) -> None:
        parts = [f"part{i}".encode() for i in range(10)]
        payload = b"".join(zstandard.compress(p) for p in parts)
        out = aiohttp.StreamReader(
            protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
        )
        p = HttpPayloadParser(
            out,
            length=len(payload),
            compression="zstd",
            headers_parser=HeadersParser(),
        )
        p.feed_data(payload)
        assert b"".join(parts) == b"".join(out._buffer)
        assert out.is_eof()

    async def test_http_payload_gzip_multi_member(self, protocol: BaseProtocol) -> None:
        member1 = gzip.compress(b"first")
        member2 = gzip.compress(b"second")
        payload = member1 + member2
        out = aiohttp.StreamReader(
            protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
        )
        p = HttpPayloadParser(
            out,
            length=len(payload),
            compression="gzip",
            headers_parser=HeadersParser(),
        )
        p.feed_data(payload)
        assert b"firstsecond" == b"".join(out._buffer)
        assert out.is_eof()

    async def test_http_payload_gzip_multi_member_chunked(
        self, protocol: BaseProtocol
    ) -> None:
        member1 = gzip.compress(b"chunk1")
        member2 = gzip.compress(b"chunk2")
        out = aiohttp.StreamReader(
            protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
        )
        p = HttpPayloadParser(
            out,
            length=len(member1) + len(member2),
            compression="gzip",
            headers_parser=HeadersParser(),
        )
        p.feed_data(member1)
        p.feed_data(member2)
        assert b"chunk1chunk2" == b"".join(out._buffer)
        assert out.is_eof()

    async def test_http_payload_gzip_member_split_mid_chunk(
        self, protocol: BaseProtocol
    ) -> None:
        member1 = gzip.compress(b"AAAA")
        member2 = gzip.compress(b"BBBB")
        combined = member1 + member2
        split_point = len(member1) + 3  # 3 bytes into member2
        out = aiohttp.StreamReader(
            protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
        )
        p = HttpPayloadParser(
            out,
            length=len(combined),
            compression="gzip",
            headers_parser=HeadersParser(),
        )
        p.feed_data(combined[:split_point])
        p.feed_data(combined[split_point:])
        assert b"AAAABBBB" == b"".join(out._buffer)
        assert out.is_eof()

    async def test_http_payload_gzip_many_small_members(
        self, protocol: BaseProtocol
    ) -> None:
        parts = [f"part{i}".encode() for i in range(10)]
        payload = b"".join(gzip.compress(p) for p in parts)
        out = aiohttp.StreamReader(
            protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
        )
        p = HttpPayloadParser(
            out,
            length=len(payload),
            compression="gzip",
            headers_parser=HeadersParser(),
        )
        p.feed_data(payload)
        assert b"".join(parts) == b"".join(out._buffer)
        assert out.is_eof()


class TestDeflateBuffer:
    async def test_feed_data(self, protocol: BaseProtocol) -> None:
        buf = aiohttp.StreamReader(
            protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
        )
        dbuf = DeflateBuffer(buf, "deflate")

        dbuf.decompressor = mock.Mock()
        dbuf.decompressor.decompress_sync.return_value = b"line"

        # First byte should be b'x' in order code not to change the decoder.
        dbuf.feed_data(b"xxxx", 4)
        assert [b"line"] == list(buf._buffer)

    async def test_feed_data_err(self, protocol: BaseProtocol) -> None:
        buf = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_event_loop())
        dbuf = DeflateBuffer(buf, "deflate")

        exc = ValueError()
        dbuf.decompressor = mock.Mock()
        dbuf.decompressor.decompress_sync.side_effect = exc

        with pytest.raises(http_exceptions.ContentEncodingError):
            # Should be more than 4 bytes to trigger deflate FSM error.
            # Should start with b'x', otherwise code switch mocked decoder.
            dbuf.feed_data(b"xsomedata", 9)

    async def test_feed_eof(self, protocol: BaseProtocol) -> None:
        buf = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
        dbuf = DeflateBuffer(buf, "deflate")

        dbuf.decompressor = mock.Mock()
        dbuf.decompressor.data_available = False
        dbuf.decompressor.flush.return_value = b""

        dbuf.feed_eof()
        assert buf._eof

    async def test_feed_eof_err_deflate(self, protocol: BaseProtocol) -> None:
        buf = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
        dbuf = DeflateBuffer(buf, "deflate")

        dbuf.decompressor = mock.Mock()
        dbuf.decompressor.data_available = False
        dbuf.decompressor.flush.return_value = b""
        dbuf.decompressor.eof = False
        dbuf.size = 1  # Simulate that data was previously fed

        with pytest.raises(http_exceptions.ContentEncodingError):
            dbuf.feed_eof()

    async def test_feed_eof_no_err_gzip(self, protocol: BaseProtocol) -> None:
        buf = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
        dbuf = DeflateBuffer(buf, "gzip")

        dbuf.decompressor = mock.Mock()
        dbuf.decompressor.data_available = False
        dbuf.decompressor.flush.return_value = b""
        dbuf.decompressor.eof = False

        dbuf.feed_eof()
        assert buf._eof

    @pytest.mark.skipif(
        sys.platform in ("android", "ios"), reason="brotli not available"
    )
    async def test_feed_eof_no_err_brotli(self, protocol: BaseProtocol) -> None:
        buf = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
        dbuf = DeflateBuffer(buf, "br")

        dbuf.decompressor = mock.Mock()
        dbuf.decompressor.data_available = False
        dbuf.decompressor.flush.return_value = b""
        dbuf.decompressor.eof = False

        dbuf.feed_eof()
        assert buf._eof

    @pytest.mark.skipif(zstandard is None, reason="zstandard is not installed")
    async def test_feed_eof_no_err_zstandard(self, protocol: BaseProtocol) -> None:
        buf = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
        dbuf = DeflateBuffer(buf, "zstd")

        dbuf.decompressor = mock.Mock()
        dbuf.decompressor.data_available = False
        dbuf.decompressor.flush.return_value = b""
        dbuf.decompressor.eof = False

        dbuf.feed_eof()
        assert buf._eof

    async def test_empty_body(self, protocol: BaseProtocol) -> None:
        buf = aiohttp.StreamReader(
            protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
        )
        dbuf = DeflateBuffer(buf, "deflate")
        dbuf.feed_eof()

        assert buf.at_eof()

    @pytest.mark.skipif(platform.python_implementation() == "PyPy", reason="Broken")
    @pytest.mark.parametrize(
        "chunk_size",
        [1024, 2**14, 2**16],  # 1KB, 16KB, 64KB
        ids=["1KB", "16KB", "64KB"],
    )
    async def test_streaming_decompress_large_payload(
        self, protocol: BaseProtocol, chunk_size: int
    ) -> None:
        """Test that large payloads decompress correctly when streamed in chunks.

        This simulates real HTTP streaming where compressed data arrives in
        small network chunks. Each chunk's decompressed output should be within
        the max_decompress_size limit, allowing full recovery of the original data.
        """
        # Create a large payload (3MiB) that compresses well
        original = b"A" * (3 * 2**20)
        compressed = zlib.compress(original)

        buf = aiohttp.StreamReader(
            protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
        )
        dbuf = DeflateBuffer(buf, "deflate")

        # Feed compressed data in chunks (simulating network streaming)
        for i in range(0, len(compressed), chunk_size):  # pragma: no branch
            chunk = compressed[i : i + chunk_size]
            while dbuf.feed_data(chunk, len(chunk)):
                chunk = b""

        dbuf.feed_eof()

        # Read all decompressed data
        result = b"".join(buf._buffer)
        assert len(result) == len(original)
        assert result == original


def test_response_parser_incomplete_body_error_message(
    response: HttpResponseParser,
) -> None:
    """Test response parser error message for incomplete body."""
    # Response expects 50 bytes
    response.feed_data(b"HTTP/1.1 200 OK\r\nContent-Length: 50\r\n\r\n")
    # Send only 15 bytes
    response.feed_data(b"partial content")

    with pytest.raises(
        http_exceptions.ContentLengthError, match=r"received 15 of 50 bytes"
    ):
        response.feed_eof()


def test_response_parser_no_body_error_message(response: HttpResponseParser) -> None:
    """Test response parser error when no body is received."""
    # Response expects 25 bytes
    response.feed_data(b"HTTP/1.1 200 OK\r\nContent-Length: 25\r\n\r\n")
    # Send no body data

    with pytest.raises(
        http_exceptions.ContentLengthError, match=r"received 0 of 25 bytes"
    ):
        response.feed_eof()


def test_response_parser_partial_chunks_error_message(
    response: HttpResponseParser,
) -> None:
    """Test error message when body is sent in multiple chunks."""
    # Response expects 100 bytes
    response.feed_data(b"HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\n")
    # Send data in chunks totaling 60 bytes
    response.feed_data(b"a" * 20)
    response.feed_data(b"b" * 20)
    response.feed_data(b"c" * 20)

    with pytest.raises(
        http_exceptions.ContentLengthError, match=r"received 60 of 100 bytes"
    ):
        response.feed_eof()


def test_request_parser_incomplete_body_error_message(
    parser: HttpRequestParser,
) -> None:
    """Test request parser error message for incomplete body."""
    # Request with Content-Length but incomplete body
    parser.feed_data(b"POST /test HTTP/1.1\r\nHost: a\r\nContent-Length: 30\r\n\r\n")
    # Send only 10 bytes
    parser.feed_data(b"incomplete")

    with pytest.raises(
        http_exceptions.ContentLengthError, match=r"received 10 of 30 bytes"
    ):
        parser.feed_eof()


def test_response_content_length_zero_no_error(response: HttpResponseParser) -> None:
    """Test that Content-Length: 0 does not raise error on feed_eof."""
    # Response with Content-Length: 0
    response.feed_data(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")

    # This should NOT raise an error
    response.feed_eof()  # Should complete without exception
