"""Unit tests for AsyncHTTPClient error handling and retry behavior."""

from __future__ import annotations

import asyncio
import time

from typing import TYPE_CHECKING, Any
from unittest.mock import AsyncMock, MagicMock, patch

import httpx
import pytest
import pytest_asyncio


if TYPE_CHECKING:
    from collections.abc import AsyncGenerator

from pinecone._internal.http import AsyncHTTPClient
from pinecone._internal.http.config import HTTPConfig
from pinecone.exceptions import (
    APIConnectionError,
    APIError,
    BadRequestError,
    ConnectionError as PineconeConnectionError,
    ForbiddenError,
    NotFoundError,
    RateLimitError,
    ServerError,
    TimeoutError as PineconeTimeoutError,
    UnauthorizedError,
)


pytestmark = pytest.mark.unit


class TestAsyncHTTPClientRetries:
    """Tests for retry behavior with backoff."""

    @pytest_asyncio.fixture()
    async def http_client(self) -> AsyncGenerator[AsyncHTTPClient, None]:
        """Create AsyncHTTPClient with test configuration."""
        config = HTTPConfig(max_retries=3, retry_backoff_factor=2.0)
        client = AsyncHTTPClient(api_key="test-key", config=config)
        # Initialize the client for tests that patch _client
        await client._ensure_client()
        yield client
        await client.close()

    @pytest.mark.asyncio()
    async def test_retry_on_429_with_backoff(self, http_client: AsyncHTTPClient):
        """Test retry on 429 with exponential backoff."""
        # Mock responses: 429, 429, then success
        mock_responses = [
            MagicMock(
                spec=httpx.Response,
                status_code=429,
                headers={"Retry-After": "1"},
                is_success=False,
            ),
            MagicMock(
                spec=httpx.Response,
                status_code=429,
                headers={},
                is_success=False,
            ),
            MagicMock(
                spec=httpx.Response,
                status_code=200,
                is_success=True,
                content=b'{"data": "success"}',
            ),
        ]

        with patch.object(http_client._client, "request", new_callable=AsyncMock) as mock_request:
            mock_request.side_effect = mock_responses

            with patch("asyncio.sleep", new_callable=AsyncMock) as mock_sleep:
                response = await http_client.request("POST", "https://api.test.com/endpoint")

                # Verify we got the success response
                assert response.status_code == 200
                assert mock_request.call_count == 3

                # Verify sleep was called for retries
                assert mock_sleep.call_count == 2
                # First retry should respect Retry-After header
                assert mock_sleep.call_args_list[0][0][0] == 1.0

    @pytest.mark.asyncio()
    async def test_retry_on_503_server_unavailable(self, http_client: AsyncHTTPClient):
        """Test retry on 503 status code."""
        mock_responses = [
            MagicMock(spec=httpx.Response, status_code=503, headers={}, is_success=False),
            MagicMock(spec=httpx.Response, status_code=200, headers={}, is_success=True),
        ]

        with patch.object(http_client._client, "request", new_callable=AsyncMock) as mock_request:
            mock_request.side_effect = mock_responses

            with patch("asyncio.sleep", new_callable=AsyncMock) as mock_sleep:
                response = await http_client.request("GET", "https://api.test.com/test")

                assert response.status_code == 200
                assert mock_request.call_count == 2
                assert mock_sleep.call_count == 1

    @pytest.mark.asyncio()
    async def test_retry_on_500_502_504(self, http_client: AsyncHTTPClient):
        """Test retry on other 5xx status codes."""
        for status_code in [500, 502, 504]:
            mock_responses = [
                MagicMock(
                    spec=httpx.Response, status_code=status_code, headers={}, is_success=False
                ),
                MagicMock(spec=httpx.Response, status_code=200, headers={}, is_success=True),
            ]

            with patch.object(
                http_client._client, "request", new_callable=AsyncMock
            ) as mock_request:
                mock_request.side_effect = mock_responses

                with patch("asyncio.sleep", new_callable=AsyncMock):
                    response = await http_client.request("GET", "https://api.test.com/test")

                    assert response.status_code == 200
                    assert mock_request.call_count == 2

    @pytest.mark.asyncio()
    async def test_exponential_backoff_timing(self, http_client: AsyncHTTPClient):
        """Test that backoff delays increase exponentially."""
        # Mock 3 retries before success
        mock_responses = [
            MagicMock(spec=httpx.Response, status_code=429, headers={}, is_success=False),
            MagicMock(spec=httpx.Response, status_code=429, headers={}, is_success=False),
            MagicMock(spec=httpx.Response, status_code=200, is_success=True),
        ]

        with patch.object(http_client._client, "request", new_callable=AsyncMock) as mock_request:
            mock_request.side_effect = mock_responses

            with patch("asyncio.sleep", new_callable=AsyncMock) as mock_sleep:
                await http_client.request("GET", "https://api.test.com/test")

                # Verify exponential backoff (with jitter, so values are in range)
                assert mock_sleep.call_count == 2
                first_delay = mock_sleep.call_args_list[0][0][0]
                second_delay = mock_sleep.call_args_list[1][0][0]

                # First retry: 2^0 * jitter = 0.5 to 1.0
                assert 0.5 <= first_delay <= 1.0
                # Second retry: 2^1 * jitter = 1.0 to 2.0
                assert 1.0 <= second_delay <= 2.0

    @pytest.mark.asyncio()
    async def test_retry_after_header_respected(self, http_client: AsyncHTTPClient):
        """Test that Retry-After header overrides exponential backoff."""
        mock_responses = [
            MagicMock(
                spec=httpx.Response,
                status_code=429,
                headers={"Retry-After": "5"},
                is_success=False,
            ),
            MagicMock(spec=httpx.Response, status_code=200, is_success=True),
        ]

        with patch.object(http_client._client, "request", new_callable=AsyncMock) as mock_request:
            mock_request.side_effect = mock_responses

            with patch("asyncio.sleep", new_callable=AsyncMock) as mock_sleep:
                await http_client.request("GET", "https://api.test.com/test")

                # Verify Retry-After was used
                assert mock_sleep.call_count == 1
                assert mock_sleep.call_args_list[0][0][0] == 5.0

    @pytest.mark.asyncio()
    async def test_max_retries_exhaustion(self, http_client: AsyncHTTPClient):
        """Test that exhausting max retries raises APIConnectionError."""
        # All attempts fail with 503
        mock_response = MagicMock(
            spec=httpx.Response,
            status_code=503,
            headers={},
            is_success=False,
            text="Service unavailable",
        )
        mock_response.json.side_effect = ValueError("No JSON")

        with patch.object(http_client._client, "request", new_callable=AsyncMock) as mock_request:
            mock_request.return_value = mock_response

            with patch("asyncio.sleep", new_callable=AsyncMock):
                # After last retry, should raise 503 error (not retry anymore)
                with pytest.raises(ServerError) as exc_info:
                    await http_client.request("GET", "https://api.test.com/test")

                assert exc_info.value.status_code == 503
                # Should attempt max_retries times (3)
                assert mock_request.call_count == 3

    @pytest.mark.asyncio()
    async def test_jitter_applied_to_backoff(self, http_client: AsyncHTTPClient):
        """Test that jitter is applied to prevent thundering herd."""
        # Create a simple test with two retries to verify jitter
        with (
            patch.object(http_client._client, "request", new_callable=AsyncMock) as mock_request,
            patch("asyncio.sleep", new_callable=AsyncMock) as mock_sleep,
        ):
            # Run the test multiple times to collect jitter samples
            delays = []
            for _ in range(5):
                mock_responses = [
                    MagicMock(
                        spec=httpx.Response,
                        status_code=503,
                        headers={},
                        is_success=False,
                        text="Unavailable",
                    ),
                    MagicMock(
                        spec=httpx.Response,
                        status_code=200,
                        headers={},
                        is_success=True,
                    ),
                ]
                # Set up json() to raise ValueError for error response
                mock_responses[0].json.side_effect = ValueError("No JSON")

                mock_request.reset_mock()
                mock_request.side_effect = mock_responses
                mock_sleep.reset_mock()

                await http_client.request("GET", "https://api.test.com/test")
                if mock_sleep.call_count > 0:
                    delays.append(mock_sleep.call_args_list[0][0][0])

            # With jitter (0.5-1.0 multiplier), delays should vary
            # All delays should be in expected range (base 1.0 * 0.5 to 1.0)
            assert all(0.5 <= d <= 1.0 for d in delays)
            # Should have at least some variation (not all identical)
            assert len(set(delays)) > 1


class TestAsyncHTTPClientErrorHandling:
    """Tests for HTTP error status code handling."""

    @pytest_asyncio.fixture()
    async def http_client(self) -> AsyncGenerator[AsyncHTTPClient, None]:
        """Create AsyncHTTPClient with test configuration."""
        config = HTTPConfig(max_retries=1)  # Disable retries for error tests
        client = AsyncHTTPClient(api_key="test-key", config=config)
        # Initialize the client for tests that patch _client
        await client._ensure_client()
        yield client
        await client.close()

    def _mock_error_response(self, status_code: int, message: str = "Error") -> MagicMock:
        """Create a mock error response."""
        response = MagicMock(spec=httpx.Response)
        response.status_code = status_code
        response.is_success = False
        response.text = message
        response.json.return_value = {"message": message}
        response.headers = {}
        return response

    @pytest.mark.asyncio()
    async def test_400_bad_request_error(self, http_client: AsyncHTTPClient):
        """Test 400 status raises BadRequestError."""
        mock_response = self._mock_error_response(400, "Invalid request")

        with patch.object(http_client._client, "request", new_callable=AsyncMock) as mock_request:
            mock_request.return_value = mock_response

            with pytest.raises(BadRequestError) as exc_info:
                await http_client.request("POST", "https://api.test.com/endpoint")

            assert exc_info.value.status_code == 400
            assert "Invalid request" in str(exc_info.value)
            assert exc_info.value.response_body == "Invalid request"

    @pytest.mark.asyncio()
    async def test_401_unauthorized_error(self, http_client: AsyncHTTPClient):
        """Test 401 status raises UnauthorizedError."""
        mock_response = self._mock_error_response(401, "Invalid API key")

        with patch.object(http_client._client, "request", new_callable=AsyncMock) as mock_request:
            mock_request.return_value = mock_response

            with pytest.raises(UnauthorizedError) as exc_info:
                await http_client.request("GET", "https://api.test.com/endpoint")

            assert exc_info.value.status_code == 401
            assert "Invalid API key" in str(exc_info.value)

    @pytest.mark.asyncio()
    async def test_403_forbidden_error(self, http_client: AsyncHTTPClient):
        """Test 403 status raises ForbiddenError."""
        mock_response = self._mock_error_response(403, "Access denied")

        with patch.object(http_client._client, "request", new_callable=AsyncMock) as mock_request:
            mock_request.return_value = mock_response

            with pytest.raises(ForbiddenError) as exc_info:
                await http_client.request("GET", "https://api.test.com/endpoint")

            assert exc_info.value.status_code == 403
            assert "Access denied" in str(exc_info.value)

    @pytest.mark.asyncio()
    async def test_404_not_found_error(self, http_client: AsyncHTTPClient):
        """Test 404 status raises NotFoundError."""
        mock_response = self._mock_error_response(404, "Resource not found")

        with patch.object(http_client._client, "request", new_callable=AsyncMock) as mock_request:
            mock_request.return_value = mock_response

            with pytest.raises(NotFoundError) as exc_info:
                await http_client.request("GET", "https://api.test.com/endpoint")

            assert exc_info.value.status_code == 404
            assert "Resource not found" in str(exc_info.value)

    @pytest.mark.asyncio()
    async def test_429_rate_limit_error_with_retry_after(self, http_client: AsyncHTTPClient):
        """Test 429 status raises RateLimitError with Retry-After."""
        mock_response = self._mock_error_response(429, "Rate limit exceeded")
        mock_response.headers = {"Retry-After": "60"}

        with patch.object(http_client._client, "request", new_callable=AsyncMock) as mock_request:
            mock_request.return_value = mock_response

            with pytest.raises(RateLimitError) as exc_info:
                await http_client.request("POST", "https://api.test.com/endpoint")

            assert exc_info.value.status_code == 429
            assert exc_info.value.retry_after == 60
            assert "60" in str(exc_info.value)

    @pytest.mark.asyncio()
    async def test_429_rate_limit_error_without_retry_after(self, http_client: AsyncHTTPClient):
        """Test 429 status raises RateLimitError without Retry-After."""
        mock_response = self._mock_error_response(429, "Rate limit exceeded")

        with patch.object(http_client._client, "request", new_callable=AsyncMock) as mock_request:
            mock_request.return_value = mock_response

            with pytest.raises(RateLimitError) as exc_info:
                await http_client.request("POST", "https://api.test.com/endpoint")

            assert exc_info.value.status_code == 429
            assert exc_info.value.retry_after is None

    @pytest.mark.asyncio()
    async def test_500_server_error(self, http_client: AsyncHTTPClient):
        """Test 500 status raises ServerError."""
        mock_response = self._mock_error_response(500, "Internal server error")

        with patch.object(http_client._client, "request", new_callable=AsyncMock) as mock_request:
            mock_request.return_value = mock_response

            with pytest.raises(ServerError) as exc_info:
                await http_client.request("GET", "https://api.test.com/endpoint")

            assert exc_info.value.status_code == 500
            assert "Internal server error" in str(exc_info.value)

    @pytest.mark.asyncio()
    async def test_503_server_error(self, http_client: AsyncHTTPClient):
        """Test 503 status raises ServerError."""
        mock_response = self._mock_error_response(503, "Service unavailable")

        with patch.object(http_client._client, "request", new_callable=AsyncMock) as mock_request:
            mock_request.return_value = mock_response

            with pytest.raises(ServerError) as exc_info:
                await http_client.request("GET", "https://api.test.com/endpoint")

            assert exc_info.value.status_code == 503
            assert "Service unavailable" in str(exc_info.value)

    @pytest.mark.asyncio()
    async def test_other_error_status_raises_api_error(self, http_client: AsyncHTTPClient):
        """Test other error status codes raise generic APIError."""
        mock_response = self._mock_error_response(418, "I'm a teapot")

        with patch.object(http_client._client, "request", new_callable=AsyncMock) as mock_request:
            mock_request.return_value = mock_response

            with pytest.raises(APIError) as exc_info:
                await http_client.request("GET", "https://api.test.com/endpoint")

            assert exc_info.value.status_code == 418
            assert "I'm a teapot" in str(exc_info.value)

    @pytest.mark.asyncio()
    async def test_error_message_from_json(self, http_client: AsyncHTTPClient):
        """Test error message extracted from JSON response."""
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.status_code = 400
        mock_response.is_success = False
        mock_response.text = '{"message": "Detailed error", "code": "INVALID_INPUT"}'
        mock_response.json.return_value = {"message": "Detailed error", "code": "INVALID_INPUT"}
        mock_response.headers = {}

        with patch.object(http_client._client, "request", new_callable=AsyncMock) as mock_request:
            mock_request.return_value = mock_response

            with pytest.raises(BadRequestError) as exc_info:
                await http_client.request("POST", "https://api.test.com/endpoint")

            assert "Detailed error" in str(exc_info.value)

    @pytest.mark.asyncio()
    async def test_error_message_fallback_to_text(self, http_client: AsyncHTTPClient):
        """Test error message falls back to text when JSON parsing fails."""
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.status_code = 500
        mock_response.is_success = False
        mock_response.text = "Plain text error message"
        mock_response.json.side_effect = ValueError("Invalid JSON")
        mock_response.headers = {}

        with patch.object(http_client._client, "request", new_callable=AsyncMock) as mock_request:
            mock_request.return_value = mock_response

            with pytest.raises(ServerError) as exc_info:
                await http_client.request("GET", "https://api.test.com/endpoint")

            assert "Plain text error message" in str(exc_info.value)

    @pytest.mark.asyncio()
    async def test_error_message_fallback_to_status_code(self, http_client: AsyncHTTPClient):
        """Test error message falls back to status code when no text available."""
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.status_code = 500
        mock_response.is_success = False
        mock_response.text = ""
        mock_response.json.side_effect = ValueError("Invalid JSON")
        mock_response.headers = {}

        with patch.object(http_client._client, "request", new_callable=AsyncMock) as mock_request:
            mock_request.return_value = mock_response

            with pytest.raises(ServerError) as exc_info:
                await http_client.request("GET", "https://api.test.com/endpoint")

            assert "500" in str(exc_info.value)


class TestAsyncHTTPClientTimeouts:
    """Tests for timeout handling."""

    @pytest_asyncio.fixture()
    async def http_client(self) -> AsyncGenerator[AsyncHTTPClient, None]:
        """Create AsyncHTTPClient with test configuration."""
        config = HTTPConfig(max_retries=3, timeout=30)
        client = AsyncHTTPClient(api_key="test-key", config=config)
        # Initialize the client for tests that patch _client
        await client._ensure_client()
        yield client
        await client.close()

    @pytest.mark.asyncio()
    async def test_timeout_triggers_retry(self, http_client: AsyncHTTPClient):
        """Test that TimeoutException triggers retry."""
        # First attempt times out, second succeeds
        mock_response = MagicMock(spec=httpx.Response, status_code=200, is_success=True)

        with patch.object(http_client._client, "request", new_callable=AsyncMock) as mock_request:
            mock_request.side_effect = [
                httpx.TimeoutException("Request timed out"),
                mock_response,
            ]

            with patch("asyncio.sleep", new_callable=AsyncMock) as mock_sleep:
                response = await http_client.request("GET", "https://api.test.com/endpoint")

                assert response.status_code == 200
                assert mock_request.call_count == 2
                assert mock_sleep.call_count == 1

    @pytest.mark.asyncio()
    async def test_timeout_during_retry_loop(self, http_client: AsyncHTTPClient):
        """Test timeout during retry loop continues retrying."""
        mock_response = MagicMock(spec=httpx.Response, status_code=200, is_success=True)

        with patch.object(http_client._client, "request", new_callable=AsyncMock) as mock_request:
            mock_request.side_effect = [
                httpx.TimeoutException("Timeout 1"),
                httpx.TimeoutException("Timeout 2"),
                mock_response,
            ]

            with patch("asyncio.sleep", new_callable=AsyncMock) as mock_sleep:
                response = await http_client.request("POST", "https://api.test.com/endpoint")

                assert response.status_code == 200
                assert mock_request.call_count == 3
                assert mock_sleep.call_count == 2

    @pytest.mark.asyncio()
    async def test_timeout_after_max_retries_raises_error(self, http_client: AsyncHTTPClient):
        """Test timeout after max retries raises TimeoutError."""
        with patch.object(http_client._client, "request", new_callable=AsyncMock) as mock_request:
            mock_request.side_effect = httpx.TimeoutException("Request timed out")

            with patch("asyncio.sleep", new_callable=AsyncMock):
                with pytest.raises(PineconeTimeoutError) as exc_info:
                    await http_client.request("GET", "https://api.test.com/endpoint")

                assert exc_info.value.timeout == 30.0
                assert "30" in str(exc_info.value)
                assert mock_request.call_count == 3

    @pytest.mark.asyncio()
    async def test_timeout_backoff_delays(self, http_client: AsyncHTTPClient):
        """Test backoff delays between timeout retries."""
        with patch.object(http_client._client, "request", new_callable=AsyncMock) as mock_request:
            mock_request.side_effect = httpx.TimeoutException("Timeout")

            with patch("asyncio.sleep", new_callable=AsyncMock) as mock_sleep:
                with pytest.raises(PineconeTimeoutError):
                    await http_client.request("GET", "https://api.test.com/endpoint")

                # Should have retried with backoff
                assert mock_sleep.call_count == 2
                # Verify delays are applied
                for call in mock_sleep.call_args_list:
                    assert call[0][0] > 0


class TestAsyncHTTPClientConnectionErrors:
    """Tests for connection error handling."""

    @pytest_asyncio.fixture()
    async def http_client(self) -> AsyncGenerator[AsyncHTTPClient, None]:
        """Create AsyncHTTPClient with test configuration."""
        config = HTTPConfig(max_retries=3)
        client = AsyncHTTPClient(api_key="test-key", config=config)
        # Initialize the client for tests that patch _client
        await client._ensure_client()
        yield client
        await client.close()

    @pytest.mark.asyncio()
    async def test_connect_error_triggers_retry(self, http_client: AsyncHTTPClient):
        """Test that ConnectError triggers retry."""
        mock_response = MagicMock(spec=httpx.Response, status_code=200, is_success=True)

        with patch.object(http_client._client, "request", new_callable=AsyncMock) as mock_request:
            mock_request.side_effect = [
                httpx.ConnectError("Connection failed"),
                mock_response,
            ]

            with patch("asyncio.sleep", new_callable=AsyncMock) as mock_sleep:
                response = await http_client.request("POST", "https://api.test.com/endpoint")

                assert response.status_code == 200
                assert mock_request.call_count == 2
                assert mock_sleep.call_count == 1

    @pytest.mark.asyncio()
    async def test_connection_error_during_retry_loop(self, http_client: AsyncHTTPClient):
        """Test connection errors during retry loop continue retrying."""
        mock_response = MagicMock(spec=httpx.Response, status_code=200, is_success=True)

        with patch.object(http_client._client, "request", new_callable=AsyncMock) as mock_request:
            mock_request.side_effect = [
                httpx.ConnectError("Connection failed 1"),
                httpx.ConnectError("Connection failed 2"),
                mock_response,
            ]

            with patch("asyncio.sleep", new_callable=AsyncMock) as mock_sleep:
                response = await http_client.request("GET", "https://api.test.com/endpoint")

                assert response.status_code == 200
                assert mock_request.call_count == 3
                assert mock_sleep.call_count == 2

    @pytest.mark.asyncio()
    async def test_connection_error_after_max_retries_raises_error(
        self, http_client: AsyncHTTPClient
    ):
        """Test connection error after max retries raises ConnectionError."""
        test_url = "https://api.test.com/endpoint"

        with patch.object(http_client._client, "request", new_callable=AsyncMock) as mock_request:
            mock_request.side_effect = httpx.ConnectError("Connection refused")

            with patch("asyncio.sleep", new_callable=AsyncMock):
                with pytest.raises(PineconeConnectionError) as exc_info:
                    await http_client.request("GET", test_url)

                assert test_url in str(exc_info.value)
                assert mock_request.call_count == 3

    @pytest.mark.asyncio()
    async def test_request_error_triggers_retry(self, http_client: AsyncHTTPClient):
        """Test that generic RequestError triggers retry."""
        mock_response = MagicMock(spec=httpx.Response, status_code=200, is_success=True)

        with patch.object(http_client._client, "request", new_callable=AsyncMock) as mock_request:
            mock_request.side_effect = [
                httpx.RequestError("Network error"),
                mock_response,
            ]

            with patch("asyncio.sleep", new_callable=AsyncMock):
                response = await http_client.request("POST", "https://api.test.com/endpoint")

                assert response.status_code == 200
                assert mock_request.call_count == 2

    @pytest.mark.asyncio()
    async def test_request_error_exhaustion_raises_api_connection_error(
        self, http_client: AsyncHTTPClient
    ):
        """Test request errors after max retries raise APIConnectionError."""
        with patch.object(http_client._client, "request", new_callable=AsyncMock) as mock_request:
            mock_request.side_effect = httpx.RequestError("Network failure")

            with patch("asyncio.sleep", new_callable=AsyncMock):
                with pytest.raises(APIConnectionError) as exc_info:
                    await http_client.request("GET", "https://api.test.com/endpoint")

                assert "3 attempts" in str(exc_info.value)
                assert mock_request.call_count == 3

    @pytest.mark.asyncio()
    async def test_connection_error_backoff_delays(self, http_client: AsyncHTTPClient):
        """Test backoff delays between connection error retries."""
        with patch.object(http_client._client, "request", new_callable=AsyncMock) as mock_request:
            mock_request.side_effect = httpx.ConnectError("Connection refused")

            with patch("asyncio.sleep", new_callable=AsyncMock) as mock_sleep:
                with pytest.raises(PineconeConnectionError):
                    await http_client.request("POST", "https://api.test.com/endpoint")

                # Should have retried with backoff
                assert mock_sleep.call_count == 2
                # Verify delays are applied and increase
                first_delay = mock_sleep.call_args_list[0][0][0]
                second_delay = mock_sleep.call_args_list[1][0][0]
                assert first_delay > 0
                assert second_delay > 0


class TestAsyncHTTPClientConcurrency:
    """Tests for true concurrency verification."""

    @pytest_asyncio.fixture()
    async def http_client(self) -> AsyncGenerator[AsyncHTTPClient, None]:
        """Create AsyncHTTPClient with test configuration."""
        config = HTTPConfig(max_connections=10, max_keepalive_connections=10)
        client = AsyncHTTPClient(api_key="test-key", config=config)
        # Initialize the client for tests that patch _client
        await client._ensure_client()
        yield client
        await client.close()

    @pytest.mark.asyncio()
    async def test_concurrent_requests_with_events(self, http_client: AsyncHTTPClient):
        """Test concurrent requests using events to verify overlap."""
        request_started = asyncio.Event()
        first_can_complete = asyncio.Event()
        request_count = 0

        async def mock_request_with_coordination(*args: Any, **kwargs: Any) -> httpx.Response:
            nonlocal request_count
            request_count += 1

            if request_count == 1:
                # First request signals it started
                request_started.set()
                # Wait for permission to complete
                await first_can_complete.wait()
            elif request_count == 2:
                # Second request waits for first to start (proving concurrency)
                await request_started.wait()
                # Allow first request to complete
                first_can_complete.set()

            return MagicMock(spec=httpx.Response, status_code=200, is_success=True)

        with patch.object(http_client._client, "request", new_callable=AsyncMock) as mock_request:
            mock_request.side_effect = mock_request_with_coordination

            # Launch two concurrent requests
            task1 = asyncio.create_task(http_client.request("GET", "https://api.test.com/1"))
            task2 = asyncio.create_task(http_client.request("GET", "https://api.test.com/2"))

            responses = await asyncio.gather(task1, task2)

            # Both should succeed
            assert all(r.status_code == 200 for r in responses)
            assert mock_request.call_count == 2

    @pytest.mark.asyncio()
    async def test_concurrent_requests_timing_verification(self, http_client: AsyncHTTPClient):
        """Test concurrent requests complete in parallel, not sequentially."""
        delay = 0.1  # Small delay per request
        start_times: list[float] = []
        end_times: list[float] = []

        async def mock_request_with_delay(*args: Any, **kwargs: Any) -> httpx.Response:
            start_times.append(time.perf_counter())
            await asyncio.sleep(delay)
            end_times.append(time.perf_counter())
            return MagicMock(spec=httpx.Response, status_code=200, is_success=True)

        with patch.object(http_client._client, "request", new_callable=AsyncMock) as mock_request:
            mock_request.side_effect = mock_request_with_delay

            # Launch 3 concurrent requests
            overall_start = time.perf_counter()
            tasks = [http_client.request("GET", f"https://api.test.com/{i}") for i in range(3)]
            await asyncio.gather(*tasks)
            overall_end = time.perf_counter()

            # If sequential, would take 3 * delay = 0.3s
            # If parallel, should take ~delay = 0.1s (plus overhead)
            total_time = overall_end - overall_start

            # Verify parallel execution (should be much less than sequential)
            assert total_time < (3 * delay * 0.8)  # Allow 20% margin

            # Verify requests overlapped (second started before first ended)
            assert start_times[1] < end_times[0]

    @pytest.mark.asyncio()
    async def test_concurrent_requests_with_mixed_outcomes(self, http_client: AsyncHTTPClient):
        """Test concurrent requests with different outcomes (success, retry, error)."""
        outcomes = [
            # First: immediate success
            MagicMock(spec=httpx.Response, status_code=200, headers={}, is_success=True),
            # Second: retry then success
            [
                MagicMock(spec=httpx.Response, status_code=503, headers={}, is_success=False),
                MagicMock(spec=httpx.Response, status_code=200, headers={}, is_success=True),
            ],
            # Third: timeout then success
            [
                httpx.TimeoutException("Timeout"),
                MagicMock(spec=httpx.Response, status_code=200, headers={}, is_success=True),
            ],
        ]

        # Track calls per URL to avoid race conditions
        call_counts: dict[str, int] = {"/1": 0, "/2": 0, "/3": 0}

        async def mock_request_side_effect(*args: Any, **kwargs: Any) -> httpx.Response:
            nonlocal call_counts
            url = args[1] if len(args) > 1 else kwargs.get("url", "")

            # Determine which request this is based on URL
            if "/1" in url:
                call_counts["/1"] += 1
                return outcomes[0]
            if "/2" in url:
                call_num = call_counts["/2"]
                call_counts["/2"] += 1
                return outcomes[1][0] if call_num == 0 else outcomes[1][1]
            # /3
            call_num = call_counts["/3"]
            call_counts["/3"] += 1
            result = outcomes[2][0] if call_num == 0 else outcomes[2][1]
            if isinstance(result, Exception):
                raise result
            return result

        with patch.object(http_client._client, "request", new_callable=AsyncMock) as mock_request:
            mock_request.side_effect = mock_request_side_effect

            with patch("asyncio.sleep", new_callable=AsyncMock):
                # Launch concurrent requests with different behaviors
                tasks = [
                    http_client.request("GET", f"https://api.test.com/{i}") for i in range(1, 4)
                ]
                responses = await asyncio.gather(*tasks)

                # All should eventually succeed
                assert len(responses) == 3
                assert all(r.status_code == 200 for r in responses)

    @pytest.mark.asyncio()
    async def test_connection_pooling_allows_concurrent_requests(
        self, http_client: AsyncHTTPClient
    ):
        """Test that connection pooling allows multiple simultaneous requests."""
        # Track concurrent requests
        active_requests = 0
        max_concurrent = 0

        async def mock_request_tracking(*args: Any, **kwargs: Any) -> httpx.Response:
            nonlocal active_requests, max_concurrent
            active_requests += 1
            max_concurrent = max(max_concurrent, active_requests)

            # Simulate some work
            await asyncio.sleep(0.05)

            active_requests -= 1
            return MagicMock(spec=httpx.Response, status_code=200, is_success=True)

        with patch.object(http_client._client, "request", new_callable=AsyncMock) as mock_request:
            mock_request.side_effect = mock_request_tracking

            # Launch 5 concurrent requests
            tasks = [http_client.request("GET", f"https://api.test.com/{i}") for i in range(5)]
            await asyncio.gather(*tasks)

            # Verify multiple requests were active simultaneously
            assert max_concurrent >= 2  # At least 2 were concurrent
            assert mock_request.call_count == 5
