#
# This file is part of gunicorn released under the MIT license.
# See the NOTICE for more information.

"""Tests for ASGI callback parsers.

Tests both PythonProtocol and H1CProtocol (if available) to ensure
consistent behavior across implementations.
"""

from gunicorn.asgi.parser import PythonProtocol


def get_parser_class(http_parser):
    """Get the appropriate parser class for the test parameter."""
    if http_parser == "fast":
        from gunicorn_h1c import H1CProtocol
        return H1CProtocol
    return PythonProtocol


def normalize_headers(headers):
    """Normalize headers to lowercase names for comparison.

    H1CProtocol preserves original case, PythonProtocol lowercases.
    """
    return {name.lower(): value for name, value in headers}


class TestRequestLineParsing:
    """Test request line parsing for both implementations."""

    def test_simple_get(self, http_parser):
        """Parse a simple GET request."""
        parser_class = get_parser_class(http_parser)
        events = []

        parser = parser_class(
            on_message_begin=lambda: events.append('begin'),
            on_url=lambda url: events.append(('url', url)),
            on_headers_complete=lambda: events.append('headers_complete'),
            on_message_complete=lambda: events.append('complete'),
        )

        parser.feed(b"GET /path HTTP/1.1\r\n\r\n")

        assert parser.method == b"GET"
        assert parser.path == b"/path"
        assert parser.http_version == (1, 1)
        assert parser.is_complete
        assert 'begin' in events
        assert ('url', b'/path') in events
        assert 'complete' in events

    def test_post_with_query(self, http_parser):
        """Parse a POST request with query string."""
        parser_class = get_parser_class(http_parser)

        parser = parser_class()
        parser.feed(b"POST /api/data?foo=bar&baz=qux HTTP/1.1\r\n\r\n")

        assert parser.method == b"POST"
        assert parser.path == b"/api/data?foo=bar&baz=qux"
        assert parser.http_version == (1, 1)
        assert parser.is_complete

    def test_http_10_version(self, http_parser):
        """Parse HTTP/1.0 request."""
        parser_class = get_parser_class(http_parser)

        parser = parser_class()
        parser.feed(b"GET / HTTP/1.0\r\n\r\n")

        assert parser.method == b"GET"
        assert parser.http_version == (1, 0)
        assert parser.is_complete

    def test_various_methods(self, http_parser):
        """Test parsing various HTTP methods."""
        parser_class = get_parser_class(http_parser)
        methods = [b"GET", b"POST", b"PUT", b"DELETE", b"PATCH", b"HEAD", b"OPTIONS"]

        for method in methods:
            parser = parser_class()
            parser.feed(method + b" / HTTP/1.1\r\n\r\n")
            assert parser.method == method


class TestHeaderParsing:
    """Test header parsing for both implementations."""

    def test_single_header(self, http_parser):
        """Parse a request with single header."""
        parser_class = get_parser_class(http_parser)
        headers = []

        parser = parser_class(
            on_header=lambda n, v: headers.append((n, v)),
        )
        parser.feed(b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n")

        assert len(parser.headers) == 1
        header_dict = normalize_headers(parser.headers)
        assert header_dict[b"host"] == b"localhost"
        callback_dict = normalize_headers(headers)
        assert callback_dict[b"host"] == b"localhost"

    def test_multiple_headers(self, http_parser):
        """Parse a request with multiple headers."""
        parser_class = get_parser_class(http_parser)

        parser = parser_class()
        parser.feed(
            b"GET / HTTP/1.1\r\n"
            b"Host: localhost\r\n"
            b"User-Agent: TestClient\r\n"
            b"Accept: */*\r\n"
            b"\r\n"
        )

        assert len(parser.headers) == 3
        header_dict = normalize_headers(parser.headers)
        assert header_dict[b"host"] == b"localhost"
        assert header_dict[b"user-agent"] == b"TestClient"
        assert header_dict[b"accept"] == b"*/*"

    def test_header_with_spaces(self, http_parser):
        """Parse headers with leading/trailing spaces in values."""
        parser_class = get_parser_class(http_parser)

        parser = parser_class()
        parser.feed(
            b"GET / HTTP/1.1\r\n"
            b"Host:   localhost  \r\n"
            b"\r\n"
        )

        header_dict = normalize_headers(parser.headers)
        assert header_dict[b"host"] == b"localhost"

    def test_empty_header_value(self, http_parser):
        """Parse header with empty value."""
        parser_class = get_parser_class(http_parser)

        parser = parser_class()
        parser.feed(
            b"GET / HTTP/1.1\r\n"
            b"Host: localhost\r\n"
            b"X-Empty:\r\n"
            b"\r\n"
        )

        header_dict = normalize_headers(parser.headers)
        assert header_dict[b"x-empty"] == b""

    def test_large_header_value(self, http_parser):
        """Parse header with large value."""
        parser_class = get_parser_class(http_parser)

        large_value = b"x" * 4096

        parser = parser_class()
        parser.feed(
            b"GET / HTTP/1.1\r\n"
            b"Host: localhost\r\n"
            b"X-Large: " + large_value + b"\r\n"
            b"\r\n"
        )

        header_dict = normalize_headers(parser.headers)
        assert header_dict[b"x-large"] == large_value


class TestBodyHandling:
    """Test body parsing for both implementations."""

    def test_content_length_body(self, http_parser):
        """Parse request with Content-Length body."""
        parser_class = get_parser_class(http_parser)
        body_chunks = []

        parser = parser_class(
            on_body=lambda chunk: body_chunks.append(chunk),
        )
        parser.feed(
            b"POST /data HTTP/1.1\r\n"
            b"Host: localhost\r\n"
            b"Content-Length: 13\r\n"
            b"\r\n"
            b"Hello, World!"
        )

        assert parser.content_length == 13
        assert not parser.is_chunked
        assert b"".join(body_chunks) == b"Hello, World!"
        assert parser.is_complete

    def test_content_length_incremental(self, http_parser):
        """Parse body arriving in multiple chunks."""
        parser_class = get_parser_class(http_parser)
        body_chunks = []

        parser = parser_class(
            on_body=lambda chunk: body_chunks.append(chunk),
        )

        # Send headers
        parser.feed(
            b"POST /data HTTP/1.1\r\n"
            b"Host: localhost\r\n"
            b"Content-Length: 10\r\n"
            b"\r\n"
        )
        assert not parser.is_complete

        # Send body in parts
        parser.feed(b"Hello")
        assert not parser.is_complete
        parser.feed(b"World")
        assert parser.is_complete

        assert b"".join(body_chunks) == b"HelloWorld"

    def test_chunked_encoding(self, http_parser):
        """Parse chunked transfer-encoded body."""
        parser_class = get_parser_class(http_parser)
        body_chunks = []

        parser = parser_class(
            on_body=lambda chunk: body_chunks.append(chunk),
        )
        parser.feed(
            b"POST /data HTTP/1.1\r\n"
            b"Host: localhost\r\n"
            b"Transfer-Encoding: chunked\r\n"
            b"\r\n"
            b"5\r\n"
            b"Hello\r\n"
            b"6\r\n"
            b"World!\r\n"
            b"0\r\n"
            b"\r\n"
        )

        assert parser.is_chunked
        assert b"".join(body_chunks) == b"HelloWorld!"
        assert parser.is_complete

    def test_chunked_with_extensions(self, http_parser):
        """Parse chunked body with chunk extensions."""
        parser_class = get_parser_class(http_parser)
        body_chunks = []

        parser = parser_class(
            on_body=lambda chunk: body_chunks.append(chunk),
        )
        parser.feed(
            b"POST /data HTTP/1.1\r\n"
            b"Host: localhost\r\n"
            b"Transfer-Encoding: chunked\r\n"
            b"\r\n"
            b"5;ext=value\r\n"
            b"Hello\r\n"
            b"0\r\n"
            b"\r\n"
        )

        assert b"".join(body_chunks) == b"Hello"
        assert parser.is_complete

    def test_no_body_get(self, http_parser):
        """GET request has no body."""
        parser_class = get_parser_class(http_parser)
        body_chunks = []

        parser = parser_class(
            on_body=lambda chunk: body_chunks.append(chunk),
        )
        parser.feed(
            b"GET / HTTP/1.1\r\n"
            b"Host: localhost\r\n"
            b"\r\n"
        )

        assert parser.content_length is None
        assert not parser.is_chunked
        assert body_chunks == []
        assert parser.is_complete


class TestConnectionHandling:
    """Test connection handling and keep-alive for both implementations."""

    def test_http11_keepalive_default(self, http_parser):
        """HTTP/1.1 defaults to keep-alive."""
        parser_class = get_parser_class(http_parser)

        parser = parser_class()
        parser.feed(
            b"GET / HTTP/1.1\r\n"
            b"Host: localhost\r\n"
            b"\r\n"
        )

        assert parser.should_keep_alive is True

    def test_http11_connection_close(self, http_parser):
        """HTTP/1.1 with Connection: close."""
        parser_class = get_parser_class(http_parser)

        parser = parser_class()
        parser.feed(
            b"GET / HTTP/1.1\r\n"
            b"Host: localhost\r\n"
            b"Connection: close\r\n"
            b"\r\n"
        )

        assert parser.should_keep_alive is False

    def test_http10_no_keepalive(self, http_parser):
        """HTTP/1.0 defaults to no keep-alive."""
        parser_class = get_parser_class(http_parser)

        parser = parser_class()
        parser.feed(
            b"GET / HTTP/1.0\r\n"
            b"Host: localhost\r\n"
            b"\r\n"
        )

        assert parser.should_keep_alive is False

    def test_http10_with_keepalive(self, http_parser):
        """HTTP/1.0 with Connection: keep-alive."""
        parser_class = get_parser_class(http_parser)

        parser = parser_class()
        parser.feed(
            b"GET / HTTP/1.0\r\n"
            b"Host: localhost\r\n"
            b"Connection: keep-alive\r\n"
            b"\r\n"
        )

        assert parser.should_keep_alive is True


class TestParserReset:
    """Test parser reset for keep-alive connections."""

    def test_reset_after_request(self, http_parser):
        """Parser can be reset for a new request."""
        parser_class = get_parser_class(http_parser)
        complete_count = [0]

        parser = parser_class(
            on_message_complete=lambda: complete_count.__setitem__(0, complete_count[0] + 1),
        )

        # First request
        parser.feed(b"GET /first HTTP/1.1\r\n\r\n")
        assert parser.path == b"/first"
        assert parser.is_complete

        # Reset and send second request
        parser.reset()
        assert not parser.is_complete
        # H1CProtocol resets to b'', PythonProtocol to None
        assert not parser.method

        parser.feed(b"GET /second HTTP/1.1\r\n\r\n")
        assert parser.path == b"/second"
        assert parser.is_complete

        assert complete_count[0] == 2


class TestCallbackBehavior:
    """Test callback behavior consistency."""

    def test_all_callbacks_fire(self, http_parser):
        """All callbacks fire in correct order."""
        parser_class = get_parser_class(http_parser)
        events = []

        parser = parser_class(
            on_message_begin=lambda: events.append('begin'),
            on_url=lambda url: events.append(('url', url)),
            on_header=lambda n, v: events.append(('header', n.lower(), v)),
            on_headers_complete=lambda: events.append('headers_complete'),
            on_body=lambda chunk: events.append(('body', chunk)),
            on_message_complete=lambda: events.append('complete'),
        )

        parser.feed(
            b"POST / HTTP/1.1\r\n"
            b"Host: localhost\r\n"
            b"Content-Length: 4\r\n"
            b"\r\n"
            b"test"
        )

        assert events[0] == 'begin'
        assert events[1] == ('url', b'/')
        assert ('header', b'host', b'localhost') in events
        assert ('header', b'content-length', b'4') in events
        assert 'headers_complete' in events
        assert ('body', b'test') in events
        assert events[-1] == 'complete'

    def test_skip_body_on_headers_complete(self, http_parser):
        """Return True from on_headers_complete skips body parsing."""
        parser_class = get_parser_class(http_parser)
        body_chunks = []

        parser = parser_class(
            on_headers_complete=lambda: True,  # Skip body
            on_body=lambda chunk: body_chunks.append(chunk),
        )

        parser.feed(
            b"POST / HTTP/1.1\r\n"
            b"Host: localhost\r\n"
            b"Content-Length: 10\r\n"
            b"\r\n"
            b"0123456789"
        )

        assert parser.is_complete
        assert body_chunks == []  # Body was skipped


class TestCallbackRequest:
    """Test CallbackRequest building from parser state."""

    def test_non_ascii_path_decoding(self, http_parser):
        """Test that percent-encoded UTF-8 paths are decoded correctly.

        Per ASGI spec:
        - path: percent-decoded UTF-8 string
        - raw_path: original bytes as received
        """
        from gunicorn.asgi.parser import CallbackRequest

        parser_class = get_parser_class(http_parser)
        parser = parser_class()

        # ö = %C3%B6 in UTF-8 percent-encoded
        parser.feed(b"GET /%C3%B6/ HTTP/1.1\r\nHost: test\r\n\r\n")

        request = CallbackRequest.from_parser(parser)

        # path should be percent-decoded UTF-8 string
        assert request.path == "/\u00f6/"  # /ö/
        # raw_path should be original bytes
        assert request.raw_path == b"/%C3%B6/"

    def test_non_ascii_path_with_query(self, http_parser):
        """Test percent-encoded path with query string."""
        from gunicorn.asgi.parser import CallbackRequest

        parser_class = get_parser_class(http_parser)
        parser = parser_class()

        # Japanese: /日本/ = /%E6%97%A5%E6%9C%AC/
        parser.feed(b"GET /%E6%97%A5%E6%9C%AC/?q=test HTTP/1.1\r\nHost: test\r\n\r\n")

        request = CallbackRequest.from_parser(parser)

        assert request.path == "/\u65e5\u672c/"  # /日本/
        assert request.raw_path == b"/%E6%97%A5%E6%9C%AC/"
        assert request.query == "q=test"

    def test_invalid_utf8_path(self, http_parser):
        """Test that invalid UTF-8 sequences use replacement character."""
        from gunicorn.asgi.parser import CallbackRequest

        parser_class = get_parser_class(http_parser)
        parser = parser_class()

        # %FF is invalid UTF-8
        parser.feed(b"GET /%FF HTTP/1.1\r\nHost: test\r\n\r\n")

        request = CallbackRequest.from_parser(parser)

        # Should use replacement character for invalid bytes
        assert "\ufffd" in request.path
        assert request.raw_path == b"/%FF"

    def test_simple_ascii_path(self, http_parser):
        """Test that simple ASCII paths work unchanged."""
        from gunicorn.asgi.parser import CallbackRequest

        parser_class = get_parser_class(http_parser)
        parser = parser_class()

        parser.feed(b"GET /api/users HTTP/1.1\r\nHost: test\r\n\r\n")

        request = CallbackRequest.from_parser(parser)

        assert request.path == "/api/users"
        assert request.raw_path == b"/api/users"

    def test_percent_encoded_ascii(self, http_parser):
        """Test percent-encoded ASCII characters."""
        from gunicorn.asgi.parser import CallbackRequest

        parser_class = get_parser_class(http_parser)
        parser = parser_class()

        # Space encoded as %20
        parser.feed(b"GET /hello%20world HTTP/1.1\r\nHost: test\r\n\r\n")

        request = CallbackRequest.from_parser(parser)

        assert request.path == "/hello world"
        assert request.raw_path == b"/hello%20world"


class TestChunkExtensionValidation:
    """Test chunk extension validation per RFC 9112."""

    def test_valid_chunk_extension(self, http_parser):
        """Valid chunk extensions should be accepted."""
        parser_class = get_parser_class(http_parser)
        body_chunks = []

        parser = parser_class(
            on_body=lambda chunk: body_chunks.append(chunk),
        )
        parser.feed(
            b"POST /data HTTP/1.1\r\n"
            b"Host: localhost\r\n"
            b"Transfer-Encoding: chunked\r\n"
            b"\r\n"
            b"5;name=value\r\n"
            b"Hello\r\n"
            b"0\r\n"
            b"\r\n"
        )

        assert b"".join(body_chunks) == b"Hello"
        assert parser.is_complete

    def test_chunk_extension_with_quoted_string(self, http_parser):
        """Chunk extensions with quoted values should be accepted."""
        parser_class = get_parser_class(http_parser)
        body_chunks = []

        parser = parser_class(
            on_body=lambda chunk: body_chunks.append(chunk),
        )
        parser.feed(
            b"POST /data HTTP/1.1\r\n"
            b"Host: localhost\r\n"
            b"Transfer-Encoding: chunked\r\n"
            b"\r\n"
            b'5;name="quoted value"\r\n'
            b"Hello\r\n"
            b"0\r\n"
            b"\r\n"
        )

        assert b"".join(body_chunks) == b"Hello"
        assert parser.is_complete

    def test_chunk_extension_bare_cr_rejected(self, http_parser):
        """Chunk extensions with bare CR should be rejected per RFC 9112."""
        import pytest
        from gunicorn.asgi.parser import InvalidChunkExtension

        parser_class = get_parser_class(http_parser)

        # Build the exception types to catch
        exceptions_to_catch = [InvalidChunkExtension]
        if http_parser == "fast":
            import gunicorn_h1c
            if hasattr(gunicorn_h1c, 'InvalidChunkExtension'):
                exceptions_to_catch.append(gunicorn_h1c.InvalidChunkExtension)

        parser = parser_class()
        parser.feed(
            b"POST /data HTTP/1.1\r\n"
            b"Host: localhost\r\n"
            b"Transfer-Encoding: chunked\r\n"
            b"\r\n"
        )

        # Chunk extension with bare CR (not followed by LF)
        with pytest.raises(tuple(exceptions_to_catch)):
            parser.feed(b"5;ext=val\rue\r\nHello\r\n0\r\n\r\n")

    def test_multiple_chunk_extensions(self, http_parser):
        """Multiple chunk extensions should be accepted."""
        parser_class = get_parser_class(http_parser)
        body_chunks = []

        parser = parser_class(
            on_body=lambda chunk: body_chunks.append(chunk),
        )
        parser.feed(
            b"POST /data HTTP/1.1\r\n"
            b"Host: localhost\r\n"
            b"Transfer-Encoding: chunked\r\n"
            b"\r\n"
            b"5;a=1;b=2;c=3\r\n"
            b"Hello\r\n"
            b"0\r\n"
            b"\r\n"
        )

        assert b"".join(body_chunks) == b"Hello"
        assert parser.is_complete
