"""Tests for HTTPClient."""

from __future__ import annotations

import logging
import platform
import sys

import httpx
import pytest
import respx

from pinecone import __version__
from pinecone._internal.http.client import HTTPClient
from pinecone._internal.http.config import HTTPConfig
from pinecone.exceptions import (
    APIConnectionError,
    APIError,
    BadRequestError,
    ConflictError,
    ForbiddenError,
    InvalidConfigError,
    NotFoundError,
    PineconeError,
    RateLimitError,
    ServerError,
    TimeoutError,
    UnauthorizedError,
)


pytestmark = pytest.mark.unit


class TestHTTPClientInit:
    """Test HTTPClient initialization."""

    def test_init_with_defaults(self):
        """Test client initialization with default config."""
        client = HTTPClient(api_key="test-key")
        assert client._api_key == "test-key"
        assert client._config.timeout == 30
        assert client._config.max_retries == 3
        assert client._config.http2 is True

    def test_init_with_custom_config(self):
        """Test client initialization with custom config."""
        config = HTTPConfig(
            timeout=60,
            max_retries=5,
            max_connections=200,
            http2=False,
        )
        client = HTTPClient(api_key="test-key", config=config)
        assert client._config.timeout == 60
        assert client._config.max_retries == 5
        assert client._config.http2 is False


class TestUserAgent:
    """Test User-Agent generation."""

    def test_user_agent_format(self):
        """Test User-Agent string format."""
        client = HTTPClient(api_key="test-key")
        user_agent = client._user_agent()

        py_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
        os_name = platform.system()
        os_version = platform.release()
        expected = (
            f"pinecone-python-client/{__version__} python/{py_version} {os_name}/{os_version}"
        )

        assert user_agent == expected

    def test_headers_include_user_agent(self):
        """Test default headers include User-Agent."""
        client = HTTPClient(api_key="test-key")
        headers = client._default_headers()

        assert "User-Agent" in headers
        assert headers["User-Agent"].startswith("pinecone-python-client/")
        assert "python/" in headers["User-Agent"]

    def test_headers_include_api_key(self):
        """Test default headers include Api-Key."""
        client = HTTPClient(api_key="test-key")
        headers = client._default_headers()

        assert "Api-Key" in headers
        assert headers["Api-Key"] == "test-key"

    def test_headers_include_content_type(self):
        """Test default headers include Content-Type."""
        client = HTTPClient(api_key="test-key")
        headers = client._default_headers()

        assert "Content-Type" in headers
        assert headers["Content-Type"] == "application/json"


class TestSuccessfulRequests:
    """Test successful HTTP requests."""

    @respx.mock
    def test_get_request(self):
        """Test GET request."""
        respx.get("https://api.pinecone.io/test").mock(
            return_value=httpx.Response(200, json={"result": "ok"})
        )

        client = HTTPClient(api_key="test-key")
        response = client.get("https://api.pinecone.io/test")

        assert response.status_code == 200
        assert response.json() == {"result": "ok"}

    @respx.mock
    def test_post_request(self):
        """Test POST request."""
        respx.post("https://api.pinecone.io/test").mock(
            return_value=httpx.Response(201, json={"created": True})
        )

        client = HTTPClient(api_key="test-key")
        response = client.post("https://api.pinecone.io/test", json={"data": "value"})

        assert response.status_code == 201
        assert response.json() == {"created": True}

    @respx.mock
    def test_put_request(self):
        """Test PUT request."""
        respx.put("https://api.pinecone.io/test").mock(
            return_value=httpx.Response(200, json={"updated": True})
        )

        client = HTTPClient(api_key="test-key")
        response = client.put("https://api.pinecone.io/test", json={"data": "value"})

        assert response.status_code == 200
        assert response.json() == {"updated": True}

    @respx.mock
    def test_delete_request(self):
        """Test DELETE request."""
        respx.delete("https://api.pinecone.io/test").mock(return_value=httpx.Response(204))

        client = HTTPClient(api_key="test-key")
        response = client.delete("https://api.pinecone.io/test")

        assert response.status_code == 204

    @respx.mock
    def test_patch_request(self):
        """Test PATCH request."""
        respx.patch("https://api.pinecone.io/test").mock(
            return_value=httpx.Response(200, json={"patched": True})
        )

        client = HTTPClient(api_key="test-key")
        response = client.patch("https://api.pinecone.io/test", json={"data": "value"})

        assert response.status_code == 200
        assert response.json() == {"patched": True}


class TestErrorHandling:
    """Test HTTP error handling."""

    @respx.mock
    def test_400_bad_request(self):
        """Test 400 Bad Request raises BadRequestError."""
        respx.get("https://api.pinecone.io/test").mock(
            return_value=httpx.Response(400, json={"message": "Bad request"})
        )

        client = HTTPClient(api_key="test-key")
        with pytest.raises(BadRequestError) as exc_info:
            client.get("https://api.pinecone.io/test")

        assert exc_info.value.status_code == 400
        assert "Bad request" in exc_info.value.message

    @respx.mock
    def test_401_unauthorized(self):
        """Test 401 Unauthorized raises UnauthorizedError."""
        respx.get("https://api.pinecone.io/test").mock(return_value=httpx.Response(401))

        client = HTTPClient(api_key="test-key")
        with pytest.raises(UnauthorizedError) as exc_info:
            client.get("https://api.pinecone.io/test")

        assert exc_info.value.status_code == 401
        assert "Invalid API key" in exc_info.value.message

    @respx.mock
    def test_403_forbidden(self):
        """Test 403 Forbidden raises ForbiddenError."""
        respx.get("https://api.pinecone.io/test").mock(
            return_value=httpx.Response(403, json={"message": "Forbidden"})
        )

        client = HTTPClient(api_key="test-key")
        with pytest.raises(ForbiddenError) as exc_info:
            client.get("https://api.pinecone.io/test")

        assert exc_info.value.status_code == 403

    @respx.mock
    def test_404_not_found(self):
        """Test 404 Not Found raises NotFoundError."""
        respx.get("https://api.pinecone.io/test").mock(
            return_value=httpx.Response(404, json={"message": "Not found"})
        )

        client = HTTPClient(api_key="test-key")
        with pytest.raises(NotFoundError) as exc_info:
            client.get("https://api.pinecone.io/test")

        assert exc_info.value.status_code == 404
        assert "Not found" in exc_info.value.message

    @respx.mock
    def test_409_conflict(self):
        """Test 409 Conflict raises ConflictError."""
        respx.post("https://api.pinecone.io/indexes").mock(
            return_value=httpx.Response(409, json={"message": "Index already exists"})
        )

        client = HTTPClient(api_key="test-key")
        with pytest.raises(ConflictError) as exc_info:
            client.post("https://api.pinecone.io/indexes", content=b'{"name": "test"}')

        assert exc_info.value.status_code == 409
        assert "Index already exists" in exc_info.value.message

    @respx.mock
    def test_429_rate_limit(self):
        """Test 429 Rate Limit raises RateLimitError."""
        respx.get("https://api.pinecone.io/test").mock(
            return_value=httpx.Response(429, headers={"Retry-After": "60"})
        )

        # Use max_retries=1 to avoid long sleep times in test
        config = HTTPConfig(max_retries=1)
        client = HTTPClient(api_key="test-key", config=config)
        with pytest.raises(RateLimitError) as exc_info:
            client.get("https://api.pinecone.io/test")

        assert exc_info.value.status_code == 429
        assert exc_info.value.retry_after == 60
        assert "Retry after 60s" in exc_info.value.message

    @respx.mock
    def test_500_server_error(self):
        """Test 500 Server Error raises ServerError."""
        respx.get("https://api.pinecone.io/test").mock(
            return_value=httpx.Response(500, json={"message": "Internal server error"})
        )

        # Use max_retries=1 to avoid retries in test
        config = HTTPConfig(max_retries=1)
        client = HTTPClient(api_key="test-key", config=config)
        with pytest.raises(ServerError) as exc_info:
            client.get("https://api.pinecone.io/test")

        assert exc_info.value.status_code == 500
        assert "Internal server error" in exc_info.value.message

    @respx.mock
    def test_503_service_unavailable(self):
        """Test 503 Service Unavailable raises ServerError."""
        respx.get("https://api.pinecone.io/test").mock(return_value=httpx.Response(503))

        # Use max_retries=1 to avoid retries in test
        config = HTTPConfig(max_retries=1)
        client = HTTPClient(api_key="test-key", config=config)
        with pytest.raises(ServerError) as exc_info:
            client.get("https://api.pinecone.io/test")

        assert exc_info.value.status_code == 503


class TestRetryLogic:
    """Test retry logic with exponential backoff."""

    @respx.mock
    def test_retry_on_500(self):
        """Test retry on 500 error."""
        route = respx.get("https://api.pinecone.io/test").mock(
            side_effect=[
                httpx.Response(500),
                httpx.Response(200, json={"ok": True}),
            ]
        )

        # Use very short backoff for faster tests
        config = HTTPConfig(retry_backoff_factor=0.001, retry_max_wait=0.01)
        client = HTTPClient(api_key="test-key", config=config)
        response = client.get("https://api.pinecone.io/test")

        assert response.status_code == 200
        assert len(route.calls) == 2

    @respx.mock
    def test_retry_on_502(self):
        """Test retry on 502 error."""
        route = respx.get("https://api.pinecone.io/test").mock(
            side_effect=[
                httpx.Response(502),
                httpx.Response(200, json={"ok": True}),
            ]
        )

        # Use very short backoff for faster tests
        config = HTTPConfig(retry_backoff_factor=0.001, retry_max_wait=0.01)
        client = HTTPClient(api_key="test-key", config=config)
        response = client.get("https://api.pinecone.io/test")

        assert response.status_code == 200
        assert len(route.calls) == 2

    @respx.mock
    def test_retry_on_503(self):
        """Test retry on 503 error."""
        route = respx.get("https://api.pinecone.io/test").mock(
            side_effect=[
                httpx.Response(503),
                httpx.Response(503),
                httpx.Response(200, json={"ok": True}),
            ]
        )

        # Use very short backoff for faster tests
        config = HTTPConfig(retry_backoff_factor=0.001, retry_max_wait=0.01)
        client = HTTPClient(api_key="test-key", config=config)
        response = client.get("https://api.pinecone.io/test")

        assert response.status_code == 200
        assert len(route.calls) == 3

    @respx.mock
    def test_retry_exhausted(self):
        """Test all retries exhausted."""
        route = respx.get("https://api.pinecone.io/test").mock(return_value=httpx.Response(500))

        config = HTTPConfig(max_retries=3)
        client = HTTPClient(api_key="test-key", config=config)

        with pytest.raises(ServerError):
            client.get("https://api.pinecone.io/test")

        assert len(route.calls) == 3

    @respx.mock
    def test_retry_with_backoff(self):
        """Test exponential backoff timing."""
        route = respx.get("https://api.pinecone.io/test").mock(return_value=httpx.Response(500))

        # Use very short backoff for faster tests
        config = HTTPConfig(max_retries=3, retry_backoff_factor=0.001, retry_max_wait=0.01)
        client = HTTPClient(api_key="test-key", config=config)

        with pytest.raises(ServerError):
            client.get("https://api.pinecone.io/test")

        assert len(route.calls) == 3

    @respx.mock
    def test_retry_after_header(self):
        """Test respects Retry-After header."""
        route = respx.get("https://api.pinecone.io/test").mock(
            side_effect=[
                httpx.Response(429, headers={"Retry-After": "0.001"}),
                httpx.Response(200, json={"ok": True}),
            ]
        )

        client = HTTPClient(api_key="test-key")
        response = client.get("https://api.pinecone.io/test")

        assert response.status_code == 200
        assert len(route.calls) == 2

    @respx.mock
    def test_no_retry_on_400(self):
        """Test no retry on 400 error."""
        route = respx.get("https://api.pinecone.io/test").mock(
            return_value=httpx.Response(400, json={"message": "Bad request"})
        )

        client = HTTPClient(api_key="test-key")
        with pytest.raises(BadRequestError):
            client.get("https://api.pinecone.io/test")

        assert len(route.calls) == 1


class TestConnectionErrors:
    """Test connection error handling."""

    @respx.mock
    def test_timeout_error(self):
        """Test timeout raises TimeoutError."""
        respx.get("https://api.pinecone.io/test").mock(
            side_effect=httpx.TimeoutException("Timeout")
        )

        config = HTTPConfig(max_retries=3)
        client = HTTPClient(api_key="test-key", config=config)

        with pytest.raises(TimeoutError) as exc_info:
            client.get("https://api.pinecone.io/test")

        assert exc_info.value.timeout == 30.0

    @respx.mock
    def test_connection_error(self):
        """Test connection error raises ConnectionError."""
        respx.get("https://api.pinecone.io/test").mock(
            side_effect=httpx.ConnectError("Connection failed")
        )

        config = HTTPConfig(max_retries=3)
        client = HTTPClient(api_key="test-key", config=config)

        with pytest.raises(APIConnectionError):
            client.get("https://api.pinecone.io/test")

    @respx.mock
    def test_retry_on_timeout(self):
        """Test retry on timeout."""
        route = respx.get("https://api.pinecone.io/test").mock(
            side_effect=[
                httpx.TimeoutException("Timeout"),
                httpx.Response(200, json={"ok": True}),
            ]
        )

        # Use very short backoff for faster tests
        config = HTTPConfig(retry_backoff_factor=0.001, retry_max_wait=0.01)
        client = HTTPClient(api_key="test-key", config=config)
        response = client.get("https://api.pinecone.io/test")

        assert response.status_code == 200
        assert len(route.calls) == 2

    @respx.mock
    def test_retry_on_connection_error(self):
        """Test retry on connection error."""
        route = respx.get("https://api.pinecone.io/test").mock(
            side_effect=[
                httpx.ConnectError("Connection failed"),
                httpx.Response(200, json={"ok": True}),
            ]
        )

        # Use very short backoff for faster tests
        config = HTTPConfig(retry_backoff_factor=0.001, retry_max_wait=0.01)
        client = HTTPClient(api_key="test-key", config=config)
        response = client.get("https://api.pinecone.io/test")

        assert response.status_code == 200
        assert len(route.calls) == 2


class TestContextManager:
    """Test context manager support."""

    @respx.mock
    def test_context_manager(self):
        """Test client works as context manager."""
        respx.get("https://api.pinecone.io/test").mock(
            return_value=httpx.Response(200, json={"ok": True})
        )

        with HTTPClient(api_key="test-key") as client:
            response = client.get("https://api.pinecone.io/test")
            assert response.status_code == 200

    @respx.mock
    def test_context_manager_closes_client(self):
        """Test context manager closes client."""
        respx.get("https://api.pinecone.io/test").mock(return_value=httpx.Response(200))

        client = HTTPClient(api_key="test-key")
        with client:
            client.get("https://api.pinecone.io/test")

        # After close(), _client is set to None to ensure idempotency
        assert client._client is None

    def test_manual_close(self):
        """Test manual close."""
        client = HTTPClient(api_key="test-key")
        assert client._client is not None
        assert not client._client.is_closed

        client.close()
        # After close(), _client is set to None to ensure idempotency
        assert client._client is None


class TestBackoffCalculation:
    """Test exponential backoff calculation with jitter."""

    def test_backoff_first_attempt(self):
        """Test backoff time for first retry has jitter."""
        client = HTTPClient(api_key="test-key")
        wait_time = client._backoff_time(0)
        # Base delay is 2^0 = 1.0, jitter makes it 0.5-1.0
        assert 0.5 <= wait_time <= 1.0

    def test_backoff_second_attempt(self):
        """Test backoff time for second retry has jitter."""
        client = HTTPClient(api_key="test-key")
        wait_time = client._backoff_time(1)
        # Base delay is 2^1 = 2.0, jitter makes it 1.0-2.0
        assert 1.0 <= wait_time <= 2.0

    def test_backoff_third_attempt(self):
        """Test backoff time for third retry has jitter."""
        client = HTTPClient(api_key="test-key")
        wait_time = client._backoff_time(2)
        # Base delay is 2^2 = 4.0, jitter makes it 2.0-4.0
        assert 2.0 <= wait_time <= 4.0

    def test_backoff_max(self):
        """Test backoff respects maximum with jitter."""
        config = HTTPConfig(retry_max_wait=10)
        client = HTTPClient(api_key="test-key", config=config)
        wait_time = client._backoff_time(10)
        # Base delay capped at 10.0, jitter makes it 5.0-10.0
        assert 5.0 <= wait_time <= 10.0

    def test_backoff_with_retry_after_header(self):
        """Test backoff uses Retry-After header without jitter."""
        client = HTTPClient(api_key="test-key")
        response = httpx.Response(429, headers={"Retry-After": "30"})
        wait_time = client._backoff_time(0, response)
        # Retry-After header bypasses jitter
        assert wait_time == 30.0

    def test_backoff_custom_factor(self):
        """Test backoff with custom factor has jitter."""
        config = HTTPConfig(retry_backoff_factor=3.0)
        client = HTTPClient(api_key="test-key", config=config)
        wait_time = client._backoff_time(2)
        # Base delay is 3^2 = 9.0, jitter makes it 4.5-9.0
        assert 4.5 <= wait_time <= 9.0

    def test_backoff_jitter_varies(self):
        """Test that jitter produces different values across multiple calls."""
        client = HTTPClient(api_key="test-key")

        # Call backoff multiple times and collect results
        wait_times = [client._backoff_time(1) for _ in range(10)]

        # With jitter, we should get different values (extremely unlikely all are the same)
        assert len(set(wait_times)) > 1, "Jitter should produce varying backoff times"

        # All values should be in expected range (1.0-2.0 for attempt 1)
        assert all(1.0 <= t <= 2.0 for t in wait_times)


class TestErrorMetadata:
    """Test error metadata extraction from responses."""

    @respx.mock
    def test_error_with_request_id(self):
        """Test that request_id is extracted from response headers."""
        respx.get("https://api.pinecone.io/test").mock(
            return_value=httpx.Response(
                400,
                headers={"X-Request-Id": "req_abc123"},
                json={"message": "Bad request"},
            )
        )

        client = HTTPClient(api_key="test-key")
        with pytest.raises(BadRequestError) as exc_info:
            client.get("https://api.pinecone.io/test")

        assert exc_info.value.request_id == "req_abc123"
        assert exc_info.value.url == "https://api.pinecone.io/test"
        assert exc_info.value.method == "GET"

    @respx.mock
    def test_error_with_error_code(self):
        """Test that error_code is extracted from response body."""
        respx.post("https://api.pinecone.io/indexes").mock(
            return_value=httpx.Response(
                400,
                headers={"X-Request-Id": "req_xyz789"},
                json={
                    "message": "Invalid dimension",
                    "error": {"code": "INVALID_DIMENSION"},
                },
            )
        )

        client = HTTPClient(api_key="test-key")
        with pytest.raises(BadRequestError) as exc_info:
            client.post("https://api.pinecone.io/indexes")

        assert exc_info.value.error_code == "INVALID_DIMENSION"
        assert exc_info.value.request_id == "req_xyz789"
        assert "Invalid dimension" in exc_info.value.message

    @respx.mock
    def test_error_code_alternate_format(self):
        """Test error_code extraction from alternate JSON formats."""
        respx.post("https://api.pinecone.io/test").mock(
            return_value=httpx.Response(
                400,
                json={"message": "Error", "code": "TEST_ERROR"},
            )
        )

        client = HTTPClient(api_key="test-key")
        with pytest.raises(BadRequestError) as exc_info:
            client.post("https://api.pinecone.io/test")

        assert exc_info.value.error_code == "TEST_ERROR"

    @respx.mock
    def test_error_without_metadata(self):
        """Test that errors work without optional metadata."""
        respx.get("https://api.pinecone.io/test").mock(
            return_value=httpx.Response(404, text="Not found")
        )

        client = HTTPClient(api_key="test-key")
        with pytest.raises(NotFoundError) as exc_info:
            client.get("https://api.pinecone.io/test")

        # Metadata fields are None when not provided
        assert exc_info.value.request_id is None
        assert exc_info.value.error_code is None
        # But url and method are always populated
        assert exc_info.value.url == "https://api.pinecone.io/test"
        assert exc_info.value.method == "GET"

    @respx.mock
    def test_rate_limit_with_retry_after(self):
        """Test RateLimitError includes retry_after and metadata."""
        respx.get("https://api.pinecone.io/test").mock(
            return_value=httpx.Response(
                429,
                headers={"Retry-After": "30", "X-Request-Id": "req_rate123"},
            )
        )

        config = HTTPConfig(max_retries=1)
        client = HTTPClient(api_key="test-key", config=config)
        with pytest.raises(RateLimitError) as exc_info:
            client.get("https://api.pinecone.io/test")

        assert exc_info.value.retry_after == 30
        assert exc_info.value.request_id == "req_rate123"
        assert exc_info.value.url == "https://api.pinecone.io/test"


class TestErrorRepr:
    """Test __repr__ methods on exceptions."""

    def test_pinecone_error_repr(self):
        """Test PineconeError repr."""
        error = PineconeError("Test error")
        repr_str = repr(error)
        assert "PineconeError" in repr_str
        assert "Test error" in repr_str

    def test_api_error_repr_with_metadata(self):
        """Test APIError repr includes metadata."""
        error = APIError(
            "Test error",
            400,
            request_id="req_123",
            error_code="TEST_ERROR",
            url="https://api.test.com",
            method="POST",
        )
        repr_str = repr(error)
        assert "APIError" in repr_str
        assert "Test error" in repr_str
        assert "status_code=400" in repr_str
        assert "req_123" in repr_str
        assert "TEST_ERROR" in repr_str
        assert "https://api.test.com" in repr_str
        assert "POST" in repr_str

    def test_api_error_repr_minimal(self):
        """Test APIError repr without optional metadata."""
        error = APIError("Test error", 404)
        repr_str = repr(error)
        assert "APIError" in repr_str
        assert "Test error" in repr_str
        assert "status_code=404" in repr_str
        # Optional fields should not appear
        assert "request_id" not in repr_str
        assert "error_code" not in repr_str

    def test_timeout_error_repr(self):
        """Test TimeoutError repr includes timeout."""
        error = TimeoutError(30.0)
        repr_str = repr(error)
        assert "TimeoutError" in repr_str
        assert "30.0" in repr_str

    def test_rate_limit_error_repr(self):
        """Test RateLimitError repr includes retry_after."""
        error = RateLimitError(retry_after=60, request_id="req_rl123")
        repr_str = repr(error)
        assert "RateLimitError" in repr_str
        assert "retry_after=60" in repr_str
        assert "req_rl123" in repr_str

    def test_invalid_config_error_repr(self):
        """Test InvalidConfigError repr includes field and value."""
        error = InvalidConfigError("dimension", 0, "must be positive")
        repr_str = repr(error)
        assert "InvalidConfigError" in repr_str
        assert "dimension" in repr_str
        assert "field=" in repr_str
        assert "value=" in repr_str


class TestBackwardCompatibility:
    """Test that changes are backward compatible."""

    def test_api_error_old_signature(self):
        """Test APIError works with old signature (no metadata)."""
        # Old way: just message, status_code, response_body
        error = APIError("Test error", 400, "response body")
        assert error.message == "Test error"
        assert error.status_code == 400
        assert error.response_body == "response body"
        # New fields default to None
        assert error.request_id is None
        assert error.error_code is None
        assert error.url is None
        assert error.method is None

    def test_bad_request_error_old_signature(self):
        """Test BadRequestError works with old signature."""
        # Old way: message and response_body
        error = BadRequestError("Bad request", "response body")
        assert error.message == "Bad request"
        assert error.status_code == 400
        assert error.response_body == "response body"

    def test_unauthorized_error_default_message(self):
        """Test UnauthorizedError provides default message."""
        # New way: can be called with no args
        error = UnauthorizedError()
        assert "Invalid API key" in error.message
        assert error.status_code == 401

    def test_not_found_error_default_message(self):
        """Test NotFoundError provides default message."""
        # Can be called with no args
        error = NotFoundError()
        assert "not found" in error.message.lower()
        assert error.status_code == 404

        # Can still provide custom message
        error = NotFoundError("Index 'test' not found")
        assert "Index 'test' not found" in error.message


class TestDebugLogging:
    """Test DEBUG-level logging in HTTP client."""

    @respx.mock
    def test_debug_logging_enabled(self, caplog):
        """Test that DEBUG logging works when logger level is DEBUG."""
        from pinecone._internal.logging import get_logger

        logger = get_logger("pinecone._internal.http.client")
        logger._logger.setLevel(logging.DEBUG)

        respx.get("https://api.pinecone.io/test").mock(
            return_value=httpx.Response(200, json={"result": "ok"})
        )

        client = HTTPClient(api_key="test-key")
        with caplog.at_level(logging.DEBUG, logger="pinecone._internal.http.client"):
            response = client.get("https://api.pinecone.io/test")

        assert response.status_code == 200

        # Check that DEBUG logs were emitted
        log_messages = [record.message for record in caplog.records]
        assert any("HTTP request" in msg for msg in log_messages)
        assert any("HTTP response" in msg for msg in log_messages)

    @respx.mock
    def test_debug_logging_with_retry(self, caplog):
        """Test that DEBUG logging works during retries."""
        from pinecone._internal.logging import get_logger

        logger = get_logger("pinecone._internal.http.client")
        logger._logger.setLevel(logging.DEBUG)

        # First request fails with 500, second succeeds
        respx.get("https://api.pinecone.io/test").mock(
            side_effect=[
                httpx.Response(500),
                httpx.Response(200, json={"result": "ok"}),
            ]
        )

        config = HTTPConfig(max_retries=2)
        client = HTTPClient(api_key="test-key", config=config)
        with caplog.at_level(logging.DEBUG, logger="pinecone._internal.http.client"):
            response = client.get("https://api.pinecone.io/test")

        assert response.status_code == 200

        # Should have logged request/response for both attempts
        log_messages = [record.message for record in caplog.records]
        request_logs = [msg for msg in log_messages if "HTTP request" in msg]
        response_logs = [msg for msg in log_messages if "HTTP response" in msg]

        # Should have 2 request logs (initial + retry) and 2 response logs
        assert len(request_logs) >= 2
        assert len(response_logs) >= 2


class TestRequestErrorHandling:
    """Test httpx.RequestError exception handling."""

    @respx.mock
    def test_request_error_after_all_retries_exhausted(self):
        """Test that RequestError raises APIConnectionError after all retries."""
        # Mock a RequestError (generic httpx error)
        respx.get("https://api.pinecone.io/test").mock(
            side_effect=httpx.RequestError(
                "Request failed", request=httpx.Request("GET", "https://api.pinecone.io/test")
            )
        )

        config = HTTPConfig(max_retries=2)
        client = HTTPClient(api_key="test-key", config=config)

        with pytest.raises(APIConnectionError) as exc_info:
            client.get("https://api.pinecone.io/test")

        assert "Request failed after 2 attempts" in exc_info.value.message
        assert exc_info.value.__cause__ is not None

    @respx.mock
    def test_request_error_with_single_retry(self):
        """Test RequestError retry logic with single retry."""
        # First attempt fails with RequestError, second also fails
        respx.get("https://api.pinecone.io/test").mock(
            side_effect=httpx.RequestError(
                "Request failed", request=httpx.Request("GET", "https://api.pinecone.io/test")
            )
        )

        config = HTTPConfig(max_retries=1)
        client = HTTPClient(api_key="test-key", config=config)

        with pytest.raises(APIConnectionError) as exc_info:
            client.get("https://api.pinecone.io/test")

        assert "Request failed after 1 attempts" in exc_info.value.message
