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

"""Tests for async HTTP/2 server connection."""

import asyncio
import pytest
from unittest import mock
from io import BytesIO

# Check if h2 is available for integration tests
try:
    import h2.connection
    import h2.config
    import h2.events
    H2_AVAILABLE = True
except ImportError:
    H2_AVAILABLE = False

from gunicorn.http2.errors import (
    HTTP2Error, HTTP2ConnectionError
)


pytestmark = pytest.mark.skipif(not H2_AVAILABLE, reason="h2 library not available")


class MockConfig:
    """Mock gunicorn configuration for HTTP/2."""

    def __init__(self):
        self.http2_max_concurrent_streams = 100
        self.http2_initial_window_size = 65535
        self.http2_max_frame_size = 16384
        self.http2_max_header_list_size = 65536


class MockAsyncReader:
    """Mock asyncio StreamReader for testing."""

    def __init__(self, data=b''):
        self._buffer = BytesIO(data)
        self._eof = False

    async def read(self, n=-1):
        data = self._buffer.read(n)
        if not data and self._eof:
            return b''
        return data

    def set_data(self, data):
        self._buffer = BytesIO(data)

    def set_eof(self):
        self._eof = True
        self._buffer = BytesIO(b'')


class MockAsyncWriter:
    """Mock asyncio StreamWriter for testing."""

    def __init__(self):
        self._buffer = bytearray()
        self._closed = False
        self._drained = False

    def write(self, data):
        if self._closed:
            raise OSError("Writer is closed")
        self._buffer.extend(data)

    async def drain(self):
        self._drained = True

    def close(self):
        self._closed = True

    async def wait_closed(self):
        pass

    def get_written_data(self):
        return bytes(self._buffer)

    def clear(self):
        self._buffer.clear()


def create_client_connection():
    """Create an h2 client connection for generating test frames."""
    config = h2.config.H2Configuration(client_side=True)
    conn = h2.connection.H2Connection(config=config)
    conn.initiate_connection()
    return conn


class TestAsyncHTTP2ConnectionInit:
    """Test AsyncHTTP2Connection initialization."""

    def test_basic_initialization(self):
        from gunicorn.http2.async_connection import AsyncHTTP2Connection

        cfg = MockConfig()
        reader = MockAsyncReader()
        writer = MockAsyncWriter()
        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))

        assert conn.cfg is cfg
        assert conn.reader is reader
        assert conn.writer is writer
        assert conn.client_addr == ('127.0.0.1', 12345)
        assert conn.streams == {}
        assert conn.is_closed is False
        assert conn._initialized is False

    def test_settings_from_config(self):
        from gunicorn.http2.async_connection import AsyncHTTP2Connection

        cfg = MockConfig()
        cfg.http2_max_concurrent_streams = 50

        reader = MockAsyncReader()
        writer = MockAsyncWriter()
        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))

        assert conn.max_concurrent_streams == 50


class TestAsyncHTTP2ConnectionInitiate:
    """Test async connection initiation."""

    @pytest.mark.asyncio
    async def test_initiate_connection(self):
        from gunicorn.http2.async_connection import AsyncHTTP2Connection

        cfg = MockConfig()
        reader = MockAsyncReader()
        writer = MockAsyncWriter()
        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))

        await conn.initiate_connection()

        assert conn._initialized is True
        written_data = writer.get_written_data()
        assert len(written_data) > 0

    @pytest.mark.asyncio
    async def test_initiate_connection_idempotent(self):
        from gunicorn.http2.async_connection import AsyncHTTP2Connection

        cfg = MockConfig()
        reader = MockAsyncReader()
        writer = MockAsyncWriter()
        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))

        await conn.initiate_connection()
        first_len = len(writer.get_written_data())

        await conn.initiate_connection()
        second_len = len(writer.get_written_data())

        assert first_len == second_len


class TestAsyncHTTP2ConnectionReceiveData:
    """Test async receiving and processing data."""

    @pytest.mark.asyncio
    async def test_receive_empty_data_closes_connection(self):
        from gunicorn.http2.async_connection import AsyncHTTP2Connection

        cfg = MockConfig()
        reader = MockAsyncReader()
        reader.set_eof()
        writer = MockAsyncWriter()
        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))
        await conn.initiate_connection()

        requests = await conn.receive_data()

        assert conn.is_closed is True
        assert requests == []

    @pytest.mark.asyncio
    async def test_receive_simple_get_request(self):
        from gunicorn.http2.async_connection import AsyncHTTP2Connection

        cfg = MockConfig()
        reader = MockAsyncReader()
        writer = MockAsyncWriter()
        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))
        await conn.initiate_connection()

        # Create client and exchange settings
        client = create_client_connection()
        client_preface = client.data_to_send()
        reader.set_data(client_preface)

        await conn.receive_data()

        server_data = writer.get_written_data()
        if server_data:
            client.receive_data(server_data)

        # Client sends GET request
        client.send_headers(
            stream_id=1,
            headers=[
                (':method', 'GET'),
                (':path', '/async-test'),
                (':scheme', 'https'),
                (':authority', 'localhost'),
            ],
            end_stream=True
        )
        reader.set_data(client.data_to_send())

        requests = await conn.receive_data()

        assert len(requests) == 1
        assert requests[0].method == 'GET'
        assert requests[0].path == '/async-test'

    @pytest.mark.asyncio
    async def test_receive_with_timeout(self):
        from gunicorn.http2.async_connection import AsyncHTTP2Connection

        cfg = MockConfig()
        reader = MockAsyncReader()
        writer = MockAsyncWriter()
        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))
        await conn.initiate_connection()

        client = create_client_connection()
        reader.set_data(client.data_to_send())

        # Should complete without timeout
        await conn.receive_data(timeout=5.0)

    @pytest.mark.asyncio
    async def test_receive_timeout_raises(self):
        from gunicorn.http2.async_connection import AsyncHTTP2Connection

        cfg = MockConfig()

        # Create a reader that blocks forever
        async def blocking_read(n):
            await asyncio.sleep(10)
            return b''

        reader = mock.Mock()
        reader.read = mock.AsyncMock(side_effect=blocking_read)
        writer = MockAsyncWriter()

        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))
        await conn.initiate_connection()

        # Timeout is converted to HTTP2ConnectionError by the implementation
        with pytest.raises((asyncio.TimeoutError, HTTP2ConnectionError)):
            await conn.receive_data(timeout=0.01)


class TestAsyncHTTP2ConnectionSendResponse:
    """Test async sending responses."""

    @pytest.mark.asyncio
    async def test_send_simple_response(self):
        from gunicorn.http2.async_connection import AsyncHTTP2Connection

        cfg = MockConfig()
        reader = MockAsyncReader()
        writer = MockAsyncWriter()
        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))
        await conn.initiate_connection()

        # Setup stream via request
        client = create_client_connection()
        reader.set_data(client.data_to_send())
        await conn.receive_data()

        client.receive_data(writer.get_written_data())

        client.send_headers(1, [
            (':method', 'GET'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'localhost'),
        ], end_stream=True)
        reader.set_data(client.data_to_send())
        await conn.receive_data()

        writer.clear()
        await conn.send_response(
            stream_id=1,
            status=200,
            headers=[('content-type', 'text/plain')],
            body=b'Async Hello!'
        )

        events = client.receive_data(writer.get_written_data())
        data_events = [e for e in events if isinstance(e, h2.events.DataReceived)]
        assert len(data_events) == 1
        assert data_events[0].data == b'Async Hello!'

    @pytest.mark.asyncio
    async def test_send_response_invalid_stream(self):
        """Test that sending response on invalid stream returns False."""
        from gunicorn.http2.async_connection import AsyncHTTP2Connection

        cfg = MockConfig()
        reader = MockAsyncReader()
        writer = MockAsyncWriter()
        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))
        await conn.initiate_connection()

        # Sending to a non-existent stream should return False gracefully
        result = await conn.send_response(stream_id=999, status=200, headers=[], body=None)
        assert result is False


class TestAsyncHTTP2ConnectionSendData:
    """Test async send_data method."""

    @pytest.mark.asyncio
    async def test_send_data(self):
        from gunicorn.http2.async_connection import AsyncHTTP2Connection

        cfg = MockConfig()
        reader = MockAsyncReader()
        writer = MockAsyncWriter()
        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))
        await conn.initiate_connection()

        # Setup stream
        client = create_client_connection()
        reader.set_data(client.data_to_send())
        await conn.receive_data()
        client.receive_data(writer.get_written_data())

        client.send_headers(1, [
            (':method', 'GET'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'localhost'),
        ], end_stream=True)
        reader.set_data(client.data_to_send())
        await conn.receive_data()

        # Send full response using send_response
        writer.clear()
        await conn.send_response(
            stream_id=1,
            status=200,
            headers=[('content-type', 'text/plain')],
            body=b'chunk1chunk2'
        )

        events = client.receive_data(writer.get_written_data())
        data_events = [e for e in events if isinstance(e, h2.events.DataReceived)]
        assert len(data_events) >= 1
        all_data = b''.join(e.data for e in data_events)
        assert all_data == b'chunk1chunk2'


def get_h2_header_value(headers_list, name):
    """Extract a header value from h2 headers list."""
    for header_name, header_value in headers_list:
        name_str = header_name.decode() if isinstance(header_name, bytes) else header_name
        if name_str == name:
            return header_value.decode() if isinstance(header_value, bytes) else header_value
    return None


class TestAsyncHTTP2ConnectionSendError:
    """Test async error response sending."""

    @pytest.mark.asyncio
    async def test_send_error(self):
        from gunicorn.http2.async_connection import AsyncHTTP2Connection

        cfg = MockConfig()
        reader = MockAsyncReader()
        writer = MockAsyncWriter()
        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))
        await conn.initiate_connection()

        client = create_client_connection()
        reader.set_data(client.data_to_send())
        await conn.receive_data()
        client.receive_data(writer.get_written_data())

        client.send_headers(1, [
            (':method', 'GET'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'localhost'),
        ], end_stream=True)
        reader.set_data(client.data_to_send())
        await conn.receive_data()

        writer.clear()
        await conn.send_error(stream_id=1, status_code=500, message="Internal Error")

        events = client.receive_data(writer.get_written_data())
        response_events = [e for e in events if isinstance(e, h2.events.ResponseReceived)]
        assert len(response_events) == 1
        headers_list = response_events[0].headers
        assert get_h2_header_value(headers_list, ':status') == '500'


class TestAsyncHTTP2ConnectionResetStream:
    """Test async stream reset."""

    @pytest.mark.asyncio
    async def test_reset_stream(self):
        from gunicorn.http2.async_connection import AsyncHTTP2Connection

        cfg = MockConfig()
        reader = MockAsyncReader()
        writer = MockAsyncWriter()
        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))
        await conn.initiate_connection()

        client = create_client_connection()
        reader.set_data(client.data_to_send())
        await conn.receive_data()
        client.receive_data(writer.get_written_data())

        client.send_headers(1, [
            (':method', 'GET'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'localhost'),
        ], end_stream=False)
        reader.set_data(client.data_to_send())
        await conn.receive_data()

        writer.clear()
        await conn.reset_stream(stream_id=1, error_code=0x8)

        events = client.receive_data(writer.get_written_data())
        reset_events = [e for e in events if isinstance(e, h2.events.StreamReset)]
        assert len(reset_events) == 1


class TestAsyncHTTP2ConnectionClose:
    """Test async connection close."""

    @pytest.mark.asyncio
    async def test_close_connection(self):
        from gunicorn.http2.async_connection import AsyncHTTP2Connection

        cfg = MockConfig()
        reader = MockAsyncReader()
        writer = MockAsyncWriter()
        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))
        await conn.initiate_connection()

        client = create_client_connection()
        reader.set_data(client.data_to_send())
        await conn.receive_data()

        writer.clear()
        await conn.close()

        assert conn.is_closed is True
        assert writer._closed is True

    @pytest.mark.asyncio
    async def test_close_idempotent(self):
        from gunicorn.http2.async_connection import AsyncHTTP2Connection

        cfg = MockConfig()
        reader = MockAsyncReader()
        writer = MockAsyncWriter()
        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))
        await conn.initiate_connection()

        await conn.close()
        await conn.close()  # Should not raise


class TestAsyncHTTP2ConnectionCleanup:
    """Test async stream cleanup."""

    @pytest.mark.asyncio
    async def test_cleanup_stream(self):
        from gunicorn.http2.async_connection import AsyncHTTP2Connection

        cfg = MockConfig()
        reader = MockAsyncReader()
        writer = MockAsyncWriter()
        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))
        await conn.initiate_connection()

        client = create_client_connection()
        reader.set_data(client.data_to_send())
        await conn.receive_data()
        client.receive_data(writer.get_written_data())

        client.send_headers(1, [
            (':method', 'GET'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'localhost'),
        ], end_stream=True)
        reader.set_data(client.data_to_send())
        await conn.receive_data()

        assert 1 in conn.streams
        conn.cleanup_stream(1)
        assert 1 not in conn.streams


class TestAsyncHTTP2ConnectionRepr:
    """Test async connection representation."""

    def test_repr(self):
        from gunicorn.http2.async_connection import AsyncHTTP2Connection

        cfg = MockConfig()
        reader = MockAsyncReader()
        writer = MockAsyncWriter()
        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))

        repr_str = repr(conn)
        assert "AsyncHTTP2Connection" in repr_str
        assert "streams=" in repr_str


class TestAsyncHTTP2ConnectionSocketErrors:
    """Test socket error handling in async connection."""

    @pytest.mark.asyncio
    async def test_read_error_raises_connection_error(self):
        from gunicorn.http2.async_connection import AsyncHTTP2Connection

        cfg = MockConfig()
        reader = mock.Mock()
        reader.read = mock.AsyncMock(side_effect=OSError("Connection reset"))
        writer = MockAsyncWriter()

        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))
        await conn.initiate_connection()

        with pytest.raises(HTTP2ConnectionError):
            await conn.receive_data()

    @pytest.mark.asyncio
    async def test_write_error_raises_connection_error(self):
        from gunicorn.http2.async_connection import AsyncHTTP2Connection

        cfg = MockConfig()
        reader = MockAsyncReader()
        writer = mock.Mock()
        writer.write = mock.Mock(side_effect=OSError("Broken pipe"))
        writer.drain = mock.AsyncMock()

        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))

        with pytest.raises(HTTP2ConnectionError):
            await conn.initiate_connection()


class TestAsyncHTTP2ConnectionPriority:
    """Test async HTTP/2 priority handling."""

    @pytest.mark.asyncio
    async def test_handle_priority_updated_existing_stream(self):
        """Test handling priority update for existing stream."""
        from gunicorn.http2.async_connection import AsyncHTTP2Connection

        cfg = MockConfig()
        reader = MockAsyncReader()
        writer = MockAsyncWriter()
        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))

        # Create a client connection to generate frames
        client_conn = create_client_connection()
        client_data = client_conn.data_to_send()

        # Set up reader with client preface
        reader.set_data(client_data)

        await conn.initiate_connection()
        await conn.receive_data()
        writer.clear()

        # Send a request to create a stream
        client_conn.send_headers(1, [
            (':method', 'GET'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'localhost'),
        ])
        request_data = client_conn.data_to_send()
        reader.set_data(request_data)
        await conn.receive_data()

        # Verify stream was created
        assert 1 in conn.streams
        stream = conn.streams[1]

        # Default priority values
        assert stream.priority_weight == 16
        assert stream.priority_depends_on == 0

        # Send a PRIORITY frame
        client_conn.prioritize(1, weight=128, depends_on=0, exclusive=False)
        priority_data = client_conn.data_to_send()
        reader.set_data(priority_data)
        await conn.receive_data()

        # Verify priority was updated
        assert stream.priority_weight == 128

    @pytest.mark.asyncio
    async def test_handle_priority_updated_nonexistent_stream(self):
        """Test that priority update for nonexistent stream is ignored."""
        from gunicorn.http2.async_connection import AsyncHTTP2Connection

        cfg = MockConfig()
        reader = MockAsyncReader()
        writer = MockAsyncWriter()
        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))

        # Create a client connection
        client_conn = create_client_connection()
        client_data = client_conn.data_to_send()

        reader.set_data(client_data)
        await conn.initiate_connection()
        await conn.receive_data()

        # Send a PRIORITY frame for a stream that doesn't exist
        client_conn.prioritize(99, weight=64, depends_on=0, exclusive=False)
        priority_data = client_conn.data_to_send()
        reader.set_data(priority_data)

        # Should not raise
        await conn.receive_data()


class TestAsyncHTTP2ConnectionTrailers:
    """Test async HTTP/2 response trailer support."""

    @pytest.mark.asyncio
    async def test_send_trailers_after_headers_and_body(self):
        """Test sending trailers after response headers and body."""
        from gunicorn.http2.async_connection import AsyncHTTP2Connection

        cfg = MockConfig()
        reader = MockAsyncReader()
        writer = MockAsyncWriter()
        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))

        # Create a client connection
        client_conn = create_client_connection()
        client_data = client_conn.data_to_send()
        reader.set_data(client_data)

        await conn.initiate_connection()
        await conn.receive_data()
        writer.clear()

        # Send a request
        client_conn.send_headers(1, [
            (':method', 'GET'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'localhost'),
        ], end_stream=True)
        reader.set_data(client_conn.data_to_send())
        await conn.receive_data()

        # Manually send headers without ending stream (for trailer support)
        stream = conn.streams[1]
        response_headers = [(':status', '200'), ('content-type', 'text/plain')]
        conn.h2_conn.send_headers(1, response_headers, end_stream=False)
        stream.send_headers(response_headers, end_stream=False)
        await conn._send_pending_data()

        # Send body without ending stream
        conn.h2_conn.send_data(1, b'Hello World', end_stream=False)
        stream.send_data(b'Hello World', end_stream=False)
        await conn._send_pending_data()

        # Send trailers
        trailers = [('grpc-status', '0'), ('grpc-message', 'OK')]
        await conn.send_trailers(1, trailers)

        # Verify stream is closed
        assert stream.response_complete is True
        assert stream.response_trailers == [('grpc-status', '0'), ('grpc-message', 'OK')]

    @pytest.mark.asyncio
    async def test_send_trailers_pseudo_header_raises(self):
        """Test that pseudo-headers in trailers raise error."""
        from gunicorn.http2.async_connection import AsyncHTTP2Connection

        cfg = MockConfig()
        reader = MockAsyncReader()
        writer = MockAsyncWriter()
        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))

        client_conn = create_client_connection()
        reader.set_data(client_conn.data_to_send())
        await conn.initiate_connection()
        await conn.receive_data()

        # Send a request
        client_conn.send_headers(1, [
            (':method', 'GET'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'localhost'),
        ], end_stream=True)
        reader.set_data(client_conn.data_to_send())
        await conn.receive_data()

        # Send response
        await conn.send_response(1, 200, [('content-type', 'text/plain')], None)

        # Try to send trailers with pseudo-header
        with pytest.raises(HTTP2Error) as exc_info:
            await conn.send_trailers(1, [(':status', '200')])
        assert "Pseudo-header" in str(exc_info.value)

    @pytest.mark.asyncio
    async def test_send_trailers_without_headers_returns_false(self):
        """Test that sending trailers without headers returns False."""
        from gunicorn.http2.async_connection import AsyncHTTP2Connection

        cfg = MockConfig()
        reader = MockAsyncReader()
        writer = MockAsyncWriter()
        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))

        client_conn = create_client_connection()
        reader.set_data(client_conn.data_to_send())
        await conn.initiate_connection()
        await conn.receive_data()

        # Send a request
        client_conn.send_headers(1, [
            (':method', 'GET'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'localhost'),
        ], end_stream=True)
        reader.set_data(client_conn.data_to_send())
        await conn.receive_data()

        # Try to send trailers without sending headers first - should return False
        result = await conn.send_trailers(1, [('trailer', 'value')])
        assert result is False


class TestAsyncHTTP2FlowControl:
    """Test async HTTP/2 flow control handling."""

    @pytest.mark.asyncio
    async def test_send_data_respects_zero_window(self):
        """Test that send_data returns False when flow control window is 0."""
        from gunicorn.http2.async_connection import AsyncHTTP2Connection

        cfg = MockConfig()
        reader = MockAsyncReader()
        writer = MockAsyncWriter()
        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))

        # Create client and send preface
        client_conn = create_client_connection()
        reader.set_data(client_conn.data_to_send())
        await conn.initiate_connection()
        await conn.receive_data()

        # Send a request
        client_conn.send_headers(1, [
            (':method', 'GET'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'localhost'),
        ], end_stream=True)
        reader.set_data(client_conn.data_to_send())
        await conn.receive_data()

        # Send response headers without ending stream
        conn.h2_conn.send_headers(1, [
            (':status', '200'),
            ('content-type', 'text/plain'),
        ], end_stream=False)
        await conn._send_pending_data()
        conn.streams[1].send_headers([(':status', '200')], end_stream=False)

        # Mock the flow control window to return 0
        original_window = conn.h2_conn.local_flow_control_window
        conn.h2_conn.local_flow_control_window = lambda stream_id: 0

        # Try to send data - should return False (not raise)
        result = await conn.send_data(1, b'Hello, World!')
        assert result is False

        # Restore
        conn.h2_conn.local_flow_control_window = original_window

    @pytest.mark.asyncio
    async def test_send_data_respects_flow_control(self):
        """Test that send_data chunks data according to flow control window."""
        from gunicorn.http2.async_connection import AsyncHTTP2Connection

        cfg = MockConfig()
        reader = MockAsyncReader()
        writer = MockAsyncWriter()
        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))

        # Create client and send preface
        client_conn = create_client_connection()
        reader.set_data(client_conn.data_to_send())
        await conn.initiate_connection()
        await conn.receive_data()

        # Send a request
        client_conn.send_headers(1, [
            (':method', 'GET'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'localhost'),
        ], end_stream=True)
        reader.set_data(client_conn.data_to_send())
        await conn.receive_data()

        # Send response headers without ending stream
        conn.h2_conn.send_headers(1, [
            (':status', '200'),
            ('content-type', 'text/plain'),
        ], end_stream=False)
        await conn._send_pending_data()
        conn.streams[1].send_headers([(':status', '200')], end_stream=False)

        # Send small data - should succeed within window
        small_data = b'Hello'
        await conn.send_data(1, small_data, end_stream=True)

        # Verify data was sent
        sent_data = writer.get_written_data()
        assert len(sent_data) > 0


class TestAsyncHTTP2StreamClosedHandling:
    """Test graceful handling of StreamClosedError in async connection."""

    @pytest.mark.asyncio
    async def test_send_response_on_closed_stream(self):
        """Test that send_response gracefully handles closed stream."""
        from gunicorn.http2.async_connection import AsyncHTTP2Connection

        cfg = MockConfig()
        reader = MockAsyncReader()
        writer = MockAsyncWriter()
        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))

        # Create client and send preface
        client_conn = create_client_connection()
        reader.set_data(client_conn.data_to_send())
        await conn.initiate_connection()
        await conn.receive_data()

        # Send a request
        client_conn.send_headers(1, [
            (':method', 'GET'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'localhost'),
        ], end_stream=True)
        reader.set_data(client_conn.data_to_send())
        await conn.receive_data()

        # Simulate client resetting the stream
        client_conn.reset_stream(1)
        reader.set_data(client_conn.data_to_send())
        await conn.receive_data()

        # Try to send response - should return False, not raise
        result = await conn.send_response(1, 200, [('content-type', 'text/plain')], b'Hello')
        assert result is False

    @pytest.mark.asyncio
    async def test_send_data_on_reset_stream(self):
        """Test that send_data gracefully handles reset stream."""
        from gunicorn.http2.async_connection import AsyncHTTP2Connection

        cfg = MockConfig()
        reader = MockAsyncReader()
        writer = MockAsyncWriter()
        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))

        # Create client and send preface
        client_conn = create_client_connection()
        reader.set_data(client_conn.data_to_send())
        await conn.initiate_connection()
        await conn.receive_data()

        # Send a request
        client_conn.send_headers(1, [
            (':method', 'GET'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'localhost'),
        ], end_stream=True)
        reader.set_data(client_conn.data_to_send())
        await conn.receive_data()

        # Send response headers without ending stream
        conn.h2_conn.send_headers(1, [
            (':status', '200'),
            ('content-type', 'text/plain'),
        ], end_stream=False)
        await conn._send_pending_data()
        conn.streams[1].send_headers([(':status', '200')], end_stream=False)

        # Simulate client resetting the stream
        client_conn.reset_stream(1)
        reader.set_data(client_conn.data_to_send())
        await conn.receive_data()

        # Try to send data - should return False, not raise
        result = await conn.send_data(1, b'Hello, World!', end_stream=True)
        assert result is False


class TestAsyncHTTP2WindowOverflowHandling:
    """Test window overflow handling in async connection."""

    @pytest.mark.asyncio
    async def test_window_overflow_sends_goaway(self):
        """Test that window overflow results in connection close."""
        from gunicorn.http2.async_connection import AsyncHTTP2Connection
        from gunicorn.http2.errors import HTTP2ErrorCode

        cfg = MockConfig()
        reader = MockAsyncReader()
        writer = MockAsyncWriter()
        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))

        # Create client and send preface
        client_conn = create_client_connection()
        reader.set_data(client_conn.data_to_send())
        await conn.initiate_connection()
        await conn.receive_data()

        # Mock increment_flow_control_window to raise ValueError (overflow)
        def raise_overflow(increment, stream_id=None):
            raise ValueError("Flow control window too large")

        conn.h2_conn.increment_flow_control_window = raise_overflow

        # Send a request with data to trigger the overflow
        client_conn.send_headers(1, [
            (':method', 'POST'),
            (':path', '/'),
            (':scheme', 'https'),
            (':authority', 'localhost'),
        ], end_stream=False)
        client_conn.send_data(1, b'test data', end_stream=True)
        reader.set_data(client_conn.data_to_send())
        await conn.receive_data()

        # Connection should be closed with FLOW_CONTROL_ERROR
        assert conn.is_closed is True


class TestAsyncHTTP2ProtocolErrorHandling:
    """Test protocol error handling sends proper GOAWAY."""

    @pytest.mark.asyncio
    async def test_protocol_error_sends_goaway(self):
        """Test that protocol errors result in GOAWAY being sent."""
        from gunicorn.http2.async_connection import AsyncHTTP2Connection
        from gunicorn.http2.errors import HTTP2ProtocolError, HTTP2ErrorCode

        cfg = MockConfig()
        reader = MockAsyncReader()
        writer = MockAsyncWriter()
        conn = AsyncHTTP2Connection(cfg, reader, writer, ('127.0.0.1', 12345))

        # Create client and send preface
        client_conn = create_client_connection()
        reader.set_data(client_conn.data_to_send())
        await conn.initiate_connection()
        await conn.receive_data()

        # Clear sent data to only capture new frames
        writer.clear()

        # Mock h2_conn.receive_data to raise ProtocolError
        def raise_protocol_error(data):
            raise h2.exceptions.ProtocolError("Test protocol error")

        conn.h2_conn.receive_data = raise_protocol_error

        # Set some dummy data for the reader
        reader.set_data(b'dummy data')

        # This should send GOAWAY and raise ProtocolError
        with pytest.raises(HTTP2ProtocolError) as exc_info:
            await conn.receive_data()

        assert "Test protocol error" in str(exc_info.value)

        # Verify something was sent (GOAWAY frame)
        sent_data = writer.get_written_data()
        assert len(sent_data) > 0
        # Connection should be marked as closed
        assert conn.is_closed is True
