# -*- coding: utf-8 -
#
# This file is part of gunicorn released under the MIT license.
# See the NOTICE for more information.

"""Tests for HTTP/2 request and body classes."""

import pytest

from gunicorn.http2.request import HTTP2Request, HTTP2Body
from gunicorn.http2.stream import HTTP2Stream


class MockConnection:
    """Mock HTTP/2 connection for testing."""

    def __init__(self, initial_window_size=65535):
        self.initial_window_size = initial_window_size


class MockConfig:
    """Mock gunicorn configuration."""

    def __init__(self):
        pass


class TestHTTP2Body:
    """Test HTTP2Body class."""

    def test_init_with_data(self):
        body = HTTP2Body(b"Hello, World!")
        assert len(body) == 13

    def test_init_empty(self):
        body = HTTP2Body(b"")
        assert len(body) == 0

    def test_read_all(self):
        body = HTTP2Body(b"Test data")
        assert body.read() == b"Test data"
        assert body.read() == b""  # Already consumed

    def test_read_with_size(self):
        body = HTTP2Body(b"Hello, World!")
        assert body.read(5) == b"Hello"
        assert body.read(2) == b", "
        assert body.read(100) == b"World!"
        assert body.read(1) == b""

    def test_read_none_size(self):
        body = HTTP2Body(b"Test")
        assert body.read(None) == b"Test"

    def test_readline_basic(self):
        body = HTTP2Body(b"Line1\nLine2\nLine3")
        assert body.readline() == b"Line1\n"
        assert body.readline() == b"Line2\n"
        assert body.readline() == b"Line3"

    def test_readline_with_size(self):
        body = HTTP2Body(b"Hello\nWorld")
        assert body.readline(3) == b"Hel"
        assert body.readline(10) == b"lo\n"

    def test_readline_no_newline(self):
        body = HTTP2Body(b"No newline here")
        assert body.readline() == b"No newline here"

    def test_readline_empty(self):
        body = HTTP2Body(b"")
        assert body.readline() == b""

    def test_readline_crlf(self):
        body = HTTP2Body(b"Line1\r\nLine2")
        # BytesIO readline includes \r\n
        assert body.readline() == b"Line1\r\n"

    def test_readlines_basic(self):
        body = HTTP2Body(b"Line1\nLine2\nLine3")
        lines = body.readlines()
        assert lines == [b"Line1\n", b"Line2\n", b"Line3"]

    def test_readlines_with_hint(self):
        body = HTTP2Body(b"Line1\nLine2\nLine3\nLine4")
        # Hint affects how many lines are returned
        lines = body.readlines(hint=5)
        assert len(lines) >= 1

    def test_readlines_empty(self):
        body = HTTP2Body(b"")
        assert body.readlines() == []

    def test_iter(self):
        body = HTTP2Body(b"Line1\nLine2\nLine3")
        lines = list(body)
        assert lines == [b"Line1\n", b"Line2\n", b"Line3"]

    def test_len(self):
        body = HTTP2Body(b"12345")
        assert len(body) == 5

    def test_close(self):
        body = HTTP2Body(b"test")
        body.close()
        # Should not raise
        with pytest.raises(ValueError):
            body.read()


class TestHTTP2BodyReadStrategies:
    """Test different reading strategies matching HTTP/1.x patterns."""

    def test_read_all_at_once(self):
        data = b"A" * 1000
        body = HTTP2Body(data)
        result = body.read()
        assert result == data

    def test_read_chunked(self):
        data = b"A" * 100
        body = HTTP2Body(data)
        chunks = []
        while True:
            chunk = body.read(10)
            if not chunk:
                break
            chunks.append(chunk)
        assert b"".join(chunks) == data
        assert len(chunks) == 10

    def test_read_byte_by_byte(self):
        data = b"Hello"
        body = HTTP2Body(data)
        result = []
        for _ in range(len(data)):
            result.append(body.read(1))
        assert b"".join(result) == data

    def test_readline_all_lines(self):
        data = b"Line1\nLine2\nLine3\n"
        body = HTTP2Body(data)
        lines = []
        while True:
            line = body.readline()
            if not line:
                break
            lines.append(line)
        assert lines == [b"Line1\n", b"Line2\n", b"Line3\n"]


class TestHTTP2Request:
    """Test HTTP2Request class."""

    def _make_stream(self, headers, body=b""):
        """Helper to create a stream with headers and body."""
        conn = MockConnection()
        stream = HTTP2Stream(stream_id=1, connection=conn)
        stream.receive_headers(headers, end_stream=(len(body) == 0))
        if body:
            stream.request_body.write(body)
            stream.request_complete = True
        return stream

    def test_basic_get_request(self):
        stream = self._make_stream([
            (':method', 'GET'),
            (':path', '/test'),
            (':scheme', 'https'),
            (':authority', 'example.com'),
        ])
        cfg = MockConfig()
        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))

        assert req.method == 'GET'
        assert req.uri == '/test'
        assert req.path == '/test'
        assert req.scheme == 'https'
        assert req.version == (2, 0)

    def test_post_request_with_body(self):
        stream = self._make_stream(
            [
                (':method', 'POST'),
                (':path', '/submit'),
                (':scheme', 'https'),
                (':authority', 'api.example.com'),
                ('content-type', 'application/json'),
                ('content-length', '13'),
            ],
            body=b'{"key":"val"}'
        )
        cfg = MockConfig()
        req = HTTP2Request(stream, cfg, ('192.168.1.1', 54321))

        assert req.method == 'POST'
        assert req.body.read() == b'{"key":"val"}'
        assert req.content_type == 'application/json'
        assert req.content_length == 13

    def test_path_with_query_string(self):
        stream = self._make_stream([
            (':method', 'GET'),
            (':path', '/search?q=test&page=1'),
            (':scheme', 'https'),
            (':authority', 'example.com'),
        ])
        cfg = MockConfig()
        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))

        assert req.path == '/search'
        assert req.query == 'q=test&page=1'
        assert req.uri == '/search?q=test&page=1'

    def test_path_with_fragment(self):
        stream = self._make_stream([
            (':method', 'GET'),
            (':path', '/page#section'),
            (':scheme', 'https'),
            (':authority', 'example.com'),
        ])
        cfg = MockConfig()
        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))

        assert req.path == '/page'
        assert req.fragment == 'section'

    def test_headers_uppercase_conversion(self):
        """HTTP/2 headers are lowercase, should be converted to uppercase."""
        stream = self._make_stream([
            (':method', 'GET'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'example.com'),
            ('content-type', 'text/html'),
            ('accept-language', 'en-US'),
            ('x-custom-header', 'custom-value'),
        ])
        cfg = MockConfig()
        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))

        header_names = [h[0] for h in req.headers]
        assert 'CONTENT-TYPE' in header_names
        assert 'ACCEPT-LANGUAGE' in header_names
        assert 'X-CUSTOM-HEADER' in header_names

    def test_host_header_from_authority(self):
        """Host header should be generated from :authority pseudo-header."""
        stream = self._make_stream([
            (':method', 'GET'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'test.example.com:8080'),
        ])
        cfg = MockConfig()
        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))

        host = req.get_header('HOST')
        assert host == 'test.example.com:8080'

    def test_authority_overrides_host_header(self):
        """:authority MUST override Host header per RFC 9113 section 8.3.1."""
        stream = self._make_stream([
            (':method', 'GET'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'authority.example.com'),
            ('host', 'explicit.example.com'),
        ])
        cfg = MockConfig()
        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))

        # Count HOST headers - should be exactly one, from :authority
        host_headers = [h for h in req.headers if h[0] == 'HOST']
        assert len(host_headers) == 1
        assert host_headers[0][1] == 'authority.example.com'

    def test_get_header_case_insensitive(self):
        stream = self._make_stream([
            (':method', 'GET'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'example.com'),
            ('x-test-header', 'test-value'),
        ])
        cfg = MockConfig()
        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))

        assert req.get_header('X-TEST-HEADER') == 'test-value'
        assert req.get_header('x-test-header') == 'test-value'
        assert req.get_header('X-Test-Header') == 'test-value'

    def test_get_header_not_found(self):
        stream = self._make_stream([
            (':method', 'GET'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'example.com'),
        ])
        cfg = MockConfig()
        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))

        assert req.get_header('X-Not-Exists') is None

    def test_content_length_property(self):
        stream = self._make_stream([
            (':method', 'POST'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'example.com'),
            ('content-length', '42'),
        ])
        cfg = MockConfig()
        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))

        assert req.content_length == 42

    def test_content_length_none_when_missing(self):
        stream = self._make_stream([
            (':method', 'GET'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'example.com'),
        ])
        cfg = MockConfig()
        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))

        assert req.content_length is None

    def test_content_length_invalid_value(self):
        stream = self._make_stream([
            (':method', 'POST'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'example.com'),
            ('content-length', 'not-a-number'),
        ])
        cfg = MockConfig()
        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))

        assert req.content_length is None

    def test_content_type_property(self):
        stream = self._make_stream([
            (':method', 'POST'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'example.com'),
            ('content-type', 'application/json; charset=utf-8'),
        ])
        cfg = MockConfig()
        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))

        assert req.content_type == 'application/json; charset=utf-8'

    def test_content_type_none_when_missing(self):
        stream = self._make_stream([
            (':method', 'GET'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'example.com'),
        ])
        cfg = MockConfig()
        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))

        assert req.content_type is None


class TestHTTP2RequestConnectionState:
    """Test connection state methods."""

    def _make_stream(self, headers):
        conn = MockConnection()
        stream = HTTP2Stream(stream_id=1, connection=conn)
        stream.receive_headers(headers, end_stream=True)
        return stream

    def test_should_close_default_false(self):
        """HTTP/2 connections are persistent by default."""
        stream = self._make_stream([
            (':method', 'GET'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'example.com'),
        ])
        cfg = MockConfig()
        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))

        assert req.should_close() is False

    def test_force_close(self):
        stream = self._make_stream([
            (':method', 'GET'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'example.com'),
        ])
        cfg = MockConfig()
        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))

        req.force_close()
        assert req.should_close() is True
        assert req.must_close is True


class TestHTTP2RequestTrailers:
    """Test request trailers handling."""

    def test_no_trailers(self):
        conn = MockConnection()
        stream = HTTP2Stream(stream_id=1, connection=conn)
        stream.receive_headers([
            (':method', 'GET'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'example.com'),
        ], end_stream=True)

        cfg = MockConfig()
        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))

        assert req.trailers == []

    def test_with_trailers(self):
        conn = MockConnection()
        stream = HTTP2Stream(stream_id=1, connection=conn)
        stream.receive_headers([
            (':method', 'POST'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'example.com'),
        ], end_stream=False)
        stream.state = stream.state  # Keep state
        stream.trailers = [
            ('grpc-status', '0'),
            ('grpc-message', 'OK'),
        ]

        cfg = MockConfig()
        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))

        assert len(req.trailers) == 2
        assert ('GRPC-STATUS', '0') in req.trailers
        assert ('GRPC-MESSAGE', 'OK') in req.trailers


class TestHTTP2RequestMetadata:
    """Test request metadata properties."""

    def _make_stream(self, headers, stream_id=1):
        conn = MockConnection()
        stream = HTTP2Stream(stream_id=stream_id, connection=conn)
        stream.receive_headers(headers, end_stream=True)
        return stream

    def test_version_is_http2(self):
        stream = self._make_stream([
            (':method', 'GET'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'example.com'),
        ])
        cfg = MockConfig()
        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))

        assert req.version == (2, 0)

    def test_req_number_is_stream_id(self):
        stream = self._make_stream([
            (':method', 'GET'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'example.com'),
        ], stream_id=5)
        cfg = MockConfig()
        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))

        assert req.req_number == 5

    def test_peer_addr(self):
        stream = self._make_stream([
            (':method', 'GET'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'example.com'),
        ])
        cfg = MockConfig()
        req = HTTP2Request(stream, cfg, ('10.0.0.1', 54321))

        assert req.peer_addr == ('10.0.0.1', 54321)
        assert req.remote_addr == ('10.0.0.1', 54321)

    def test_proxy_protocol_info_none(self):
        """HTTP/2 doesn't use proxy protocol through data stream."""
        stream = self._make_stream([
            (':method', 'GET'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'example.com'),
        ])
        cfg = MockConfig()
        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))

        assert req.proxy_protocol_info is None


class TestHTTP2RequestRepr:
    """Test request string representation."""

    def test_repr_format(self):
        conn = MockConnection()
        stream = HTTP2Stream(stream_id=3, connection=conn)
        stream.receive_headers([
            (':method', 'POST'),
            (':path', '/api/users'),
            (':scheme', 'https'),
            (':authority', 'example.com'),
        ], end_stream=True)

        cfg = MockConfig()
        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))

        repr_str = repr(req)
        assert "HTTP2Request" in repr_str
        assert "method=POST" in repr_str
        assert "path=/api/users" in repr_str
        assert "stream_id=3" in repr_str


class TestHTTP2RequestDefaults:
    """Test default values when pseudo-headers are missing."""

    def test_default_method(self):
        conn = MockConnection()
        stream = HTTP2Stream(stream_id=1, connection=conn)
        stream.receive_headers([
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'example.com'),
        ], end_stream=True)

        cfg = MockConfig()
        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))

        assert req.method == 'GET'

    def test_default_scheme(self):
        conn = MockConnection()
        stream = HTTP2Stream(stream_id=1, connection=conn)
        stream.receive_headers([
            (':method', 'GET'),
            (':path', '/'),
            (':authority', 'example.com'),
        ], end_stream=True)

        cfg = MockConfig()
        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))

        assert req.scheme == 'https'

    def test_default_path(self):
        conn = MockConnection()
        stream = HTTP2Stream(stream_id=1, connection=conn)
        stream.receive_headers([
            (':method', 'GET'),
            (':scheme', 'https'),
            (':authority', 'example.com'),
        ], end_stream=True)

        cfg = MockConfig()
        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))

        assert req.uri == '/'
        assert req.path == '/'


class TestHTTP2RequestPriority:
    """Test HTTP2Request priority attributes."""

    def test_default_priority_values(self):
        """Test that request inherits default stream priority."""
        conn = MockConnection()
        stream = HTTP2Stream(stream_id=1, connection=conn)
        stream.receive_headers([
            (':method', 'GET'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'example.com'),
        ], end_stream=True)

        cfg = MockConfig()
        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))

        assert req.priority_weight == 16
        assert req.priority_depends_on == 0

    def test_custom_priority_values(self):
        """Test that request inherits custom stream priority."""
        conn = MockConnection()
        stream = HTTP2Stream(stream_id=3, connection=conn)

        # Update priority before creating request
        stream.update_priority(weight=200, depends_on=1)

        stream.receive_headers([
            (':method', 'POST'),
            (':path', '/api/data'),
            (':scheme', 'https'),
            (':authority', 'example.com'),
        ], end_stream=False)
        stream.receive_data(b'{"data": "test"}', end_stream=True)

        cfg = MockConfig()
        req = HTTP2Request(stream, cfg, ('192.168.1.100', 54321))

        assert req.priority_weight == 200
        assert req.priority_depends_on == 1

    def test_priority_reflects_stream_at_request_creation(self):
        """Test that priority reflects stream state when request is created."""
        conn = MockConnection()
        stream = HTTP2Stream(stream_id=1, connection=conn)
        stream.receive_headers([
            (':method', 'GET'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'example.com'),
        ], end_stream=True)

        cfg = MockConfig()

        # Create request with default priority
        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))
        assert req.priority_weight == 16

        # Update stream priority after request was created
        stream.update_priority(weight=256)

        # Request should still have old value (captured at creation time)
        assert req.priority_weight == 16

        # Stream has new value
        assert stream.priority_weight == 256


class MockWSGIConfig:
    """Mock gunicorn configuration with WSGI-required attributes."""

    def __init__(self):
        self.errorlog = '-'
        self.workers = 1


class TestHTTP2RequestWSGIEnviron:
    """Test HTTP/2 priority in WSGI environ."""

    def test_priority_in_wsgi_environ(self):
        """Test that HTTP/2 priority is added to WSGI environ."""
        from unittest import mock
        from gunicorn.http.wsgi import create

        conn = MockConnection()
        stream = HTTP2Stream(stream_id=1, connection=conn)
        stream.update_priority(weight=128, depends_on=3)
        stream.receive_headers([
            (':method', 'GET'),
            (':path', '/test'),
            (':scheme', 'https'),
            (':authority', 'example.com'),
        ], end_stream=True)

        cfg = MockConfig()
        req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345))

        # Create a mock socket
        mock_sock = mock.Mock()
        mock_sock.getsockname.return_value = ('127.0.0.1', 8443)

        # Use WSGI config for environ creation
        wsgi_cfg = MockWSGIConfig()

        # Create WSGI environ
        resp, environ = create(req, mock_sock, ('127.0.0.1', 12345), ('127.0.0.1', 8443), wsgi_cfg)

        # Verify priority is in environ
        assert environ.get('gunicorn.http2.priority_weight') == 128
        assert environ.get('gunicorn.http2.priority_depends_on') == 3

    def test_priority_not_in_environ_for_http1(self):
        """Test that HTTP/1 requests don't have priority keys."""
        from unittest import mock
        from gunicorn.http.wsgi import create

        # Create a mock HTTP/1 request (no priority attributes)
        mock_req = mock.Mock()
        mock_req.headers = [('HOST', 'example.com')]
        mock_req.scheme = 'https'
        mock_req.path = '/test'
        mock_req.query = ''
        mock_req.fragment = ''
        mock_req.method = 'GET'
        mock_req.uri = '/test'
        mock_req.version = (1, 1)
        mock_req._expected_100_continue = False
        mock_req.proxy_protocol_info = None
        mock_req.body = mock.Mock()

        # Remove priority attributes to simulate HTTP/1 request
        del mock_req.priority_weight
        del mock_req.priority_depends_on

        wsgi_cfg = MockWSGIConfig()

        mock_sock = mock.Mock()
        mock_sock.getsockname.return_value = ('127.0.0.1', 8443)

        resp, environ = create(mock_req, mock_sock, ('127.0.0.1', 12345), ('127.0.0.1', 8443), wsgi_cfg)

        # HTTP/1 requests should not have priority keys
        assert 'gunicorn.http2.priority_weight' not in environ
        assert 'gunicorn.http2.priority_depends_on' not in environ
