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

"""HTTP/2 Docker integration tests.

These tests verify HTTP/2 functionality with real connections to gunicorn
running in Docker containers, both directly and through an nginx proxy.
"""

import asyncio
import ssl
import socket

import pytest


# Mark all tests in this module as requiring Docker
pytestmark = [
    pytest.mark.docker,
    pytest.mark.http2,
    pytest.mark.integration,
]


class TestDirectHTTP2Connection:
    """Test direct HTTP/2 connections to gunicorn."""

    def test_simple_get(self, h2_client, gunicorn_url):
        """Test basic GET request over HTTP/2."""
        response = h2_client.get(f"{gunicorn_url}/")
        assert response.status_code == 200
        assert response.http_version == "HTTP/2"
        assert response.text == "Hello HTTP/2!"

    def test_health_endpoint(self, h2_client, gunicorn_url):
        """Test health check endpoint."""
        response = h2_client.get(f"{gunicorn_url}/health")
        assert response.status_code == 200
        assert response.text == "OK"

    def test_post_with_body(self, h2_client, gunicorn_url):
        """Test POST request with body."""
        data = b"test data for echo"
        response = h2_client.post(f"{gunicorn_url}/echo", content=data)
        assert response.status_code == 200
        assert response.content == data

    def test_post_large_body(self, h2_client, gunicorn_url):
        """Test POST with larger body."""
        data = b"X" * 65536  # 64KB
        response = h2_client.post(f"{gunicorn_url}/echo", content=data)
        assert response.status_code == 200
        assert response.content == data
        assert len(response.content) == 65536

    def test_headers_endpoint(self, h2_client, gunicorn_url):
        """Test that custom headers are received."""
        response = h2_client.get(
            f"{gunicorn_url}/headers",
            headers={"X-Custom-Header": "test-value"}
        )
        assert response.status_code == 200
        headers = response.json()
        assert "HTTP_X_CUSTOM_HEADER" in headers
        assert headers["HTTP_X_CUSTOM_HEADER"] == "test-value"

    def test_version_endpoint(self, h2_client, gunicorn_url):
        """Test server protocol version."""
        response = h2_client.get(f"{gunicorn_url}/version")
        assert response.status_code == 200
        # HTTP/2 should report as HTTP/2.0 or similar
        assert "HTTP" in response.text

    def test_large_response(self, h2_client, gunicorn_url):
        """Test receiving large response over HTTP/2."""
        response = h2_client.get(f"{gunicorn_url}/large")
        assert response.status_code == 200
        assert len(response.content) == 1024 * 1024  # 1MB
        assert response.content == b"X" * (1024 * 1024)

    def test_different_methods(self, h2_client, gunicorn_url):
        """Test various HTTP methods."""
        for method in ["GET", "POST", "PUT", "DELETE", "PATCH"]:
            response = h2_client.request(method, f"{gunicorn_url}/method")
            assert response.status_code == 200
            assert response.text == method

    def test_status_codes(self, h2_client, gunicorn_url):
        """Test various HTTP status codes."""
        for code in [200, 201, 400, 404, 500]:
            response = h2_client.get(f"{gunicorn_url}/status?code={code}")
            assert response.status_code == code

    def test_not_found(self, h2_client, gunicorn_url):
        """Test 404 response."""
        response = h2_client.get(f"{gunicorn_url}/nonexistent")
        assert response.status_code == 404


class TestConcurrentStreams:
    """Test HTTP/2 multiplexing with concurrent streams."""

    @pytest.mark.asyncio
    async def test_concurrent_requests(self, async_h2_client, gunicorn_url):
        """Test multiple concurrent requests over single connection."""
        httpx = pytest.importorskip("httpx")

        async with httpx.AsyncClient(http2=True, verify=False, timeout=30.0) as client:
            # Send 10 concurrent requests
            tasks = [
                client.get(f"{gunicorn_url}/")
                for _ in range(10)
            ]
            responses = await asyncio.gather(*tasks)

        assert len(responses) == 10
        assert all(r.status_code == 200 for r in responses)
        assert all(r.http_version == "HTTP/2" for r in responses)
        assert all(r.text == "Hello HTTP/2!" for r in responses)

    @pytest.mark.asyncio
    async def test_concurrent_mixed_requests(self, async_h2_client, gunicorn_url):
        """Test concurrent requests to different endpoints."""
        httpx = pytest.importorskip("httpx")

        async with httpx.AsyncClient(http2=True, verify=False, timeout=30.0) as client:
            tasks = [
                client.get(f"{gunicorn_url}/"),
                client.get(f"{gunicorn_url}/headers"),
                client.get(f"{gunicorn_url}/version"),
                client.post(f"{gunicorn_url}/echo", content=b"test"),
                client.get(f"{gunicorn_url}/health"),
            ]
            responses = await asyncio.gather(*tasks)

        assert len(responses) == 5
        assert all(r.status_code == 200 for r in responses)

    @pytest.mark.asyncio
    async def test_many_concurrent_streams(self, async_h2_client, gunicorn_url):
        """Test many concurrent streams (up to HTTP/2 limit)."""
        httpx = pytest.importorskip("httpx")

        async with httpx.AsyncClient(http2=True, verify=False, timeout=60.0) as client:
            # Send 50 concurrent requests
            tasks = [
                client.get(f"{gunicorn_url}/")
                for _ in range(50)
            ]
            responses = await asyncio.gather(*tasks)

        assert len(responses) == 50
        assert all(r.status_code == 200 for r in responses)


class TestHTTP2BehindProxy:
    """Test HTTP/2 through nginx proxy."""

    def test_simple_get_via_proxy(self, h2_client, nginx_url):
        """Test basic GET through nginx proxy."""
        response = h2_client.get(f"{nginx_url}/")
        assert response.status_code == 200
        assert response.http_version == "HTTP/2"
        assert response.text == "Hello HTTP/2!"

    def test_post_via_proxy(self, h2_client, nginx_url):
        """Test POST through nginx proxy."""
        data = b"proxied data"
        response = h2_client.post(f"{nginx_url}/echo", content=data)
        assert response.status_code == 200
        assert response.content == data

    def test_headers_preserved(self, h2_client, nginx_url):
        """Test that custom headers pass through proxy."""
        response = h2_client.get(
            f"{nginx_url}/headers",
            headers={"X-Custom": "test-value"}
        )
        assert response.status_code == 200
        headers = response.json()
        assert "HTTP_X_CUSTOM" in headers
        assert headers["HTTP_X_CUSTOM"] == "test-value"

    def test_forwarded_headers(self, h2_client, nginx_url):
        """Test that proxy adds forwarded headers."""
        response = h2_client.get(f"{nginx_url}/headers")
        assert response.status_code == 200
        headers = response.json()
        # Nginx should add X-Forwarded-* headers
        assert "HTTP_X_FORWARDED_FOR" in headers
        assert "HTTP_X_FORWARDED_PROTO" in headers
        assert headers["HTTP_X_FORWARDED_PROTO"] == "https"

    def test_large_response_via_proxy(self, h2_client, nginx_url):
        """Test large response through proxy."""
        response = h2_client.get(f"{nginx_url}/large")
        assert response.status_code == 200
        assert len(response.content) == 1024 * 1024

    @pytest.mark.asyncio
    async def test_concurrent_via_proxy(self, async_h2_client, nginx_url):
        """Test concurrent requests through proxy."""
        httpx = pytest.importorskip("httpx")

        async with httpx.AsyncClient(http2=True, verify=False, timeout=30.0) as client:
            tasks = [
                client.get(f"{nginx_url}/")
                for _ in range(10)
            ]
            responses = await asyncio.gather(*tasks)

        assert len(responses) == 10
        assert all(r.status_code == 200 for r in responses)
        assert all(r.http_version == "HTTP/2" for r in responses)


class TestHTTP2Protocol:
    """Test HTTP/2 specific protocol behaviors."""

    def test_alpn_negotiation(self, gunicorn_url):
        """Verify ALPN negotiates h2 protocol."""
        ctx = ssl.create_default_context()
        ctx.check_hostname = False
        ctx.verify_mode = ssl.CERT_NONE
        ctx.set_alpn_protocols(['h2', 'http/1.1'])

        with socket.create_connection(('127.0.0.1', 8443), timeout=10) as sock:
            with ctx.wrap_socket(sock, server_hostname='localhost') as ssock:
                selected = ssock.selected_alpn_protocol()
                assert selected == 'h2', f"Expected h2, got {selected}"

    def test_alpn_http11_fallback(self, gunicorn_url):
        """Test that server accepts HTTP/1.1 via ALPN."""
        ctx = ssl.create_default_context()
        ctx.check_hostname = False
        ctx.verify_mode = ssl.CERT_NONE
        ctx.set_alpn_protocols(['http/1.1'])

        with socket.create_connection(('127.0.0.1', 8443), timeout=10) as sock:
            with ctx.wrap_socket(sock, server_hostname='localhost') as ssock:
                selected = ssock.selected_alpn_protocol()
                assert selected == 'http/1.1', f"Expected http/1.1, got {selected}"

    def test_http11_client_works(self, h1_client, gunicorn_url):
        """Test that HTTP/1.1 client can still connect."""
        response = h1_client.get(f"{gunicorn_url}/")
        assert response.status_code == 200
        assert response.http_version == "HTTP/1.1"
        assert response.text == "Hello HTTP/2!"

    def test_tls_version(self, gunicorn_url):
        """Verify TLS 1.2+ is used."""
        ctx = ssl.create_default_context()
        ctx.check_hostname = False
        ctx.verify_mode = ssl.CERT_NONE

        with socket.create_connection(('127.0.0.1', 8443), timeout=10) as sock:
            with ctx.wrap_socket(sock, server_hostname='localhost') as ssock:
                version = ssock.version()
                assert version in ('TLSv1.2', 'TLSv1.3'), f"Unexpected TLS version: {version}"


class TestHTTP2ErrorHandling:
    """Test HTTP/2 error handling."""

    def test_invalid_path(self, h2_client, gunicorn_url):
        """Test request to non-existent path."""
        response = h2_client.get(f"{gunicorn_url}/does/not/exist")
        assert response.status_code == 404
        assert response.http_version == "HTTP/2"

    def test_server_error(self, h2_client, gunicorn_url):
        """Test server error response."""
        response = h2_client.get(f"{gunicorn_url}/status?code=500")
        assert response.status_code == 500
        assert response.http_version == "HTTP/2"

    @pytest.mark.asyncio
    async def test_connection_reuse_after_error(self, async_h2_client, gunicorn_url):
        """Test that connection is reused after error response."""
        httpx = pytest.importorskip("httpx")

        async with httpx.AsyncClient(http2=True, verify=False, timeout=30.0) as client:
            # First request - error
            r1 = await client.get(f"{gunicorn_url}/status?code=500")
            assert r1.status_code == 500

            # Second request - should work on same connection
            r2 = await client.get(f"{gunicorn_url}/")
            assert r2.status_code == 200
            assert r2.text == "Hello HTTP/2!"


class TestHTTP2Headers:
    """Test HTTP/2 header handling."""

    def test_response_headers(self, h2_client, gunicorn_url):
        """Test that response headers are correctly received."""
        response = h2_client.get(f"{gunicorn_url}/")
        assert "content-type" in response.headers
        assert "content-length" in response.headers
        assert response.headers["x-request-path"] == "/"
        assert response.headers["x-request-method"] == "GET"

    def test_many_request_headers(self, h2_client, gunicorn_url):
        """Test sending many headers."""
        headers = {f"X-Custom-{i}": f"value-{i}" for i in range(20)}
        response = h2_client.get(f"{gunicorn_url}/headers", headers=headers)
        assert response.status_code == 200
        received = response.json()
        for i in range(20):
            key = f"HTTP_X_CUSTOM_{i}"
            assert key in received
            assert received[key] == f"value-{i}"

    def test_header_case_insensitivity(self, h2_client, gunicorn_url):
        """Test HTTP/2 header case handling."""
        response = h2_client.get(
            f"{gunicorn_url}/headers",
            headers={"X-Mixed-Case-Header": "test"}
        )
        assert response.status_code == 200
        # HTTP/2 lowercases headers, but WSGI uppercases them
        headers = response.json()
        assert "HTTP_X_MIXED_CASE_HEADER" in headers


class TestHTTP2Performance:
    """Performance-related HTTP/2 tests."""

    @pytest.mark.asyncio
    async def test_parallel_large_requests(self, async_h2_client, gunicorn_url):
        """Test parallel requests with large responses."""
        httpx = pytest.importorskip("httpx")

        async with httpx.AsyncClient(http2=True, verify=False, timeout=60.0) as client:
            tasks = [
                client.get(f"{gunicorn_url}/large")
                for _ in range(5)
            ]
            responses = await asyncio.gather(*tasks)

        assert len(responses) == 5
        assert all(r.status_code == 200 for r in responses)
        assert all(len(r.content) == 1024 * 1024 for r in responses)

    def test_connection_keepalive(self, h2_client, gunicorn_url):
        """Test that connections are kept alive."""
        # Multiple requests should reuse the same connection
        for _ in range(5):
            response = h2_client.get(f"{gunicorn_url}/")
            assert response.status_code == 200
            assert response.http_version == "HTTP/2"


class TestHTTP2EarlyHints:
    """Test HTTP 103 Early Hints support."""

    def test_early_hints_endpoint(self, h2_client, gunicorn_url):
        """Test that early hints endpoint returns 200."""
        response = h2_client.get(f"{gunicorn_url}/early-hints")
        assert response.status_code == 200
        assert response.text == "Early hints sent!"

    def test_early_hints_multiple_endpoint(self, h2_client, gunicorn_url):
        """Test multiple early hints endpoint returns 200."""
        response = h2_client.get(f"{gunicorn_url}/early-hints-multiple")
        assert response.status_code == 200
        assert response.text == "Multiple early hints sent!"

    def test_early_hints_via_proxy(self, h2_client, nginx_url):
        """Test early hints through nginx proxy."""
        response = h2_client.get(f"{nginx_url}/early-hints")
        assert response.status_code == 200
        assert response.text == "Early hints sent!"

    @pytest.mark.asyncio
    async def test_concurrent_early_hints(self, async_h2_client, gunicorn_url):
        """Test concurrent requests to early hints endpoint."""
        httpx = pytest.importorskip("httpx")

        async with httpx.AsyncClient(http2=True, verify=False, timeout=30.0) as client:
            tasks = [
                client.get(f"{gunicorn_url}/early-hints")
                for _ in range(10)
            ]
            responses = await asyncio.gather(*tasks)

        assert len(responses) == 10
        assert all(r.status_code == 200 for r in responses)
        assert all(r.text == "Early hints sent!" for r in responses)
