"""Tests for AsyncHTTPClient."""

from __future__ import annotations

import asyncio
import platform
import sys

import httpx
import pytest
import respx

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


pytestmark = pytest.mark.unit


class TestAsyncHTTPClientInit:
    """Test AsyncHTTPClient initialization."""

    def test_init_with_defaults(self):
        """Test client initialization with default config."""
        client = AsyncHTTPClient(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
        # httpx.AsyncClient should not be created yet (lazy initialization)
        assert client._client is None

    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 = AsyncHTTPClient(api_key="test-key", config=config)
        assert client._config.timeout == 60
        assert client._config.max_retries == 5
        assert client._config.http2 is False


class TestLazyInitialization:
    """Test lazy initialization of httpx.AsyncClient."""

    def test_client_not_initialized_on_init(self):
        """Test that httpx.AsyncClient is not created in __init__."""
        client = AsyncHTTPClient(api_key="test-key")

        # Client should not be initialized yet
        assert client._client is None
        assert client._client_lock is None

    @respx.mock
    @pytest.mark.asyncio()
    async def test_client_initialized_on_first_request(self):
        """Test that httpx.AsyncClient is created lazily on first request."""
        respx.get("https://api.pinecone.io/test").mock(
            return_value=httpx.Response(200, json={"result": "ok"})
        )

        client = AsyncHTTPClient(api_key="test-key")

        # Client should not be initialized yet
        assert client._client is None

        # Make first request
        response = await client.get("https://api.pinecone.io/test")

        # Now client should be initialized
        assert client._client is not None
        assert isinstance(client._client, httpx.AsyncClient)
        assert response.status_code == 200

        await client.close()

    @respx.mock
    @pytest.mark.asyncio()
    async def test_concurrent_initialization(self):
        """Test that concurrent requests only create one client instance."""
        respx.get("https://api.pinecone.io/test").mock(
            return_value=httpx.Response(200, json={"result": "ok"})
        )

        client = AsyncHTTPClient(api_key="test-key")

        # Make multiple concurrent requests
        tasks = [client.get("https://api.pinecone.io/test") for _ in range(10)]
        responses = await asyncio.gather(*tasks)

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

        # Only one client should have been created
        assert client._client is not None
        assert isinstance(client._client, httpx.AsyncClient)

        await client.close()

    @pytest.mark.asyncio()
    async def test_close_with_uninitialized_client(self):
        """Test that close() works even if no requests were made."""
        client = AsyncHTTPClient(api_key="test-key")

        # Client not initialized yet
        assert client._client is None

        # Should not raise an error
        await client.close()

        # Client should still be None
        assert client._client is None

    @respx.mock
    @pytest.mark.asyncio()
    async def test_context_manager_with_lazy_init(self):
        """Test that async context manager works correctly with lazy init."""
        respx.get("https://api.pinecone.io/test").mock(
            return_value=httpx.Response(200, json={"result": "ok"})
        )

        async with AsyncHTTPClient(api_key="test-key") as client:
            # Client not initialized yet
            assert client._client is None

            # Make request
            response = await client.get("https://api.pinecone.io/test")

            # Now client is initialized
            assert client._client is not None
            assert response.status_code == 200

        # Client should be closed after context manager exits
        # After close(), _client is set to None to ensure idempotency
        assert client._client is None

    @respx.mock
    @pytest.mark.asyncio()
    async def test_multiple_event_loops_simulation(self):
        """Test that client works when created in different context than used."""
        respx.get("https://api.pinecone.io/test").mock(
            return_value=httpx.Response(200, json={"result": "ok"})
        )

        # Create client outside of any async context (simulating module-level creation)
        client = AsyncHTTPClient(api_key="test-key")
        assert client._client is None

        # Use client in async context (simulating web framework request handler)
        async def handler():
            return await client.get("https://api.pinecone.io/test")

        response = await handler()

        # Client should work correctly
        assert response.status_code == 200
        assert client._client is not None

        await client.close()


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

    def test_user_agent_format(self):
        """Test User-Agent string format."""
        client = AsyncHTTPClient(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 = AsyncHTTPClient(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 = AsyncHTTPClient(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 = AsyncHTTPClient(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
    @pytest.mark.asyncio()
    async 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 = AsyncHTTPClient(api_key="test-key")
        response = await client.get("https://api.pinecone.io/test")

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

    @respx.mock
    @pytest.mark.asyncio()
    async 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 = AsyncHTTPClient(api_key="test-key")
        response = await client.post("https://api.pinecone.io/test", json={"data": "value"})

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

    @respx.mock
    @pytest.mark.asyncio()
    async 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 = AsyncHTTPClient(api_key="test-key")
        response = await client.put("https://api.pinecone.io/test", json={"data": "value"})

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

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

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

        assert response.status_code == 204
        await client.close()

    @respx.mock
    @pytest.mark.asyncio()
    async 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 = AsyncHTTPClient(api_key="test-key")
        response = await client.patch("https://api.pinecone.io/test", json={"data": "value"})

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


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

    @respx.mock
    @pytest.mark.asyncio()
    async 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 = AsyncHTTPClient(api_key="test-key")
        with pytest.raises(BadRequestError) as exc_info:
            await client.get("https://api.pinecone.io/test")

        assert exc_info.value.status_code == 400
        assert "Bad request" in exc_info.value.message
        await client.close()

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

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

        assert exc_info.value.status_code == 401
        assert "Invalid API key" in exc_info.value.message
        await client.close()

    @respx.mock
    @pytest.mark.asyncio()
    async 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 = AsyncHTTPClient(api_key="test-key")
        with pytest.raises(ForbiddenError) as exc_info:
            await client.get("https://api.pinecone.io/test")

        assert exc_info.value.status_code == 403
        await client.close()

    @respx.mock
    @pytest.mark.asyncio()
    async 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 = AsyncHTTPClient(api_key="test-key")
        with pytest.raises(NotFoundError) as exc_info:
            await client.get("https://api.pinecone.io/test")

        assert exc_info.value.status_code == 404
        assert "Not found" in exc_info.value.message
        await client.close()

    @respx.mock
    @pytest.mark.asyncio()
    async 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 = AsyncHTTPClient(api_key="test-key", config=config)
        with pytest.raises(RateLimitError) as exc_info:
            await 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
        await client.close()

    @respx.mock
    @pytest.mark.asyncio()
    async 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 = AsyncHTTPClient(api_key="test-key", config=config)
        with pytest.raises(ServerError) as exc_info:
            await client.get("https://api.pinecone.io/test")

        assert exc_info.value.status_code == 500
        assert "Internal server error" in exc_info.value.message
        await client.close()

    @respx.mock
    @pytest.mark.asyncio()
    async 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 = AsyncHTTPClient(api_key="test-key", config=config)
        with pytest.raises(ServerError) as exc_info:
            await client.get("https://api.pinecone.io/test")

        assert exc_info.value.status_code == 503
        await client.close()


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

    @respx.mock
    @pytest.mark.asyncio()
    async 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 = AsyncHTTPClient(api_key="test-key", config=config)
        response = await client.get("https://api.pinecone.io/test")

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

    @respx.mock
    @pytest.mark.asyncio()
    async 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 = AsyncHTTPClient(api_key="test-key", config=config)
        response = await client.get("https://api.pinecone.io/test")

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

    @respx.mock
    @pytest.mark.asyncio()
    async 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 = AsyncHTTPClient(api_key="test-key", config=config)
        response = await client.get("https://api.pinecone.io/test")

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

    @respx.mock
    @pytest.mark.asyncio()
    async 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 = AsyncHTTPClient(api_key="test-key", config=config)

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

        assert len(route.calls) == 3
        await client.close()

    @respx.mock
    @pytest.mark.asyncio()
    async 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 = AsyncHTTPClient(api_key="test-key", config=config)

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

        assert len(route.calls) == 3
        await client.close()

    @respx.mock
    @pytest.mark.asyncio()
    async 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 = AsyncHTTPClient(api_key="test-key")
        response = await client.get("https://api.pinecone.io/test")

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

    @respx.mock
    @pytest.mark.asyncio()
    async 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 = AsyncHTTPClient(api_key="test-key")
        with pytest.raises(BadRequestError):
            await client.get("https://api.pinecone.io/test")

        assert len(route.calls) == 1
        await client.close()


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

    @respx.mock
    @pytest.mark.asyncio()
    async 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 = AsyncHTTPClient(api_key="test-key", config=config)

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

        assert exc_info.value.timeout == 30.0
        await client.close()

    @respx.mock
    @pytest.mark.asyncio()
    async 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 = AsyncHTTPClient(api_key="test-key", config=config)

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

        await client.close()

    @respx.mock
    @pytest.mark.asyncio()
    async 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 = AsyncHTTPClient(api_key="test-key", config=config)
        response = await client.get("https://api.pinecone.io/test")

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

    @respx.mock
    @pytest.mark.asyncio()
    async 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 = AsyncHTTPClient(api_key="test-key", config=config)
        response = await client.get("https://api.pinecone.io/test")

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


class TestCancellation:
    """Test cancellation handling."""

    @pytest.mark.asyncio()
    async def test_cancellation_propagates(self):
        """Test that CancelledError is propagated correctly."""
        client = AsyncHTTPClient(api_key="test-key")

        # Create a task that will be cancelled
        async def make_request():
            # This will be cancelled before completing
            return await client.get("https://httpbin.org/delay/10")

        task = asyncio.create_task(make_request())
        await asyncio.sleep(0.01)  # Let task start
        task.cancel()

        # Task should propagate CancelledError
        with pytest.raises(asyncio.CancelledError):
            await task

        await client.close()

    @respx.mock
    @pytest.mark.asyncio()
    async def test_cancellation_during_retry(self):
        """Test cancellation during retry backoff."""

        # Mock endpoint that always returns 500
        respx.get("https://api.pinecone.io/test").mock(return_value=httpx.Response(500))

        config = HTTPConfig(max_retries=10, retry_backoff_factor=1.0)
        client = AsyncHTTPClient(api_key="test-key", config=config)

        # Create task that will retry multiple times
        task = asyncio.create_task(client.get("https://api.pinecone.io/test"))
        await asyncio.sleep(0.05)  # Let first attempt complete and start retry
        task.cancel()

        # Should propagate CancelledError, not ServerError
        with pytest.raises(asyncio.CancelledError):
            await task

        await client.close()


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

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

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

    @respx.mock
    @pytest.mark.asyncio()
    async 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 = AsyncHTTPClient(api_key="test-key")
        async with client:
            await client.get("https://api.pinecone.io/test")

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

    @respx.mock
    @pytest.mark.asyncio()
    async def test_manual_close(self):
        """Test manual close."""
        respx.get("https://api.pinecone.io/test").mock(
            return_value=httpx.Response(200, json={"result": "ok"})
        )

        client = AsyncHTTPClient(api_key="test-key")

        # Make request to initialize client
        await client.get("https://api.pinecone.io/test")
        assert client._client is not None
        assert not client._client.is_closed

        await 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 = AsyncHTTPClient(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 = AsyncHTTPClient(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 = AsyncHTTPClient(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 = AsyncHTTPClient(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 = AsyncHTTPClient(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 = AsyncHTTPClient(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 = AsyncHTTPClient(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)
