"""Unit tests for AsyncInference namespace."""

from __future__ import annotations

import asyncio

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

import httpx
import orjson
import pytest

from pinecone.__interface__.inference import (
    AsyncInferenceNamespaceProtocol,
    EmbeddingResponseProtocol,
    ListModelsResponseProtocol,
    RerankResponseProtocol,
)
from pinecone._internal.constants import INFERENCE_API_VERSION
from pinecone._internal.http import AsyncHTTPClient
from pinecone.async_client.inference import AsyncInference
from pinecone.exceptions import (
    APIConnectionError,
    APIError,
    TimeoutError as PineconeTimeoutError,
)


pytestmark = pytest.mark.unit


class TestAsyncInferenceProtocolCompliance:
    """Tests for protocol compliance."""

    @pytest.mark.asyncio()
    async def test_async_inference_satisfies_protocol(self):
        """Test that AsyncInference class satisfies AsyncInferenceNamespaceProtocol."""
        http_client = MagicMock(spec=AsyncHTTPClient)
        inference = AsyncInference(http_client=http_client)

        assert isinstance(inference, AsyncInferenceNamespaceProtocol)


class TestAsyncInferenceEmbed:
    """Tests for the embed() method."""

    @pytest.fixture()
    def mock_http_client(self) -> MagicMock:
        """Create a mock async HTTP client."""
        mock = MagicMock(spec=AsyncHTTPClient)
        mock.post = AsyncMock()
        return mock

    @pytest.fixture()
    def inference(self, mock_http_client: MagicMock) -> AsyncInference:
        """Create an AsyncInference instance with mock HTTP client."""
        return AsyncInference(http_client=mock_http_client)

    @pytest.fixture()
    def embed_response_data(self) -> dict[str, Any]:
        """Sample embed API response data."""
        return {
            "data": [
                {"values": [0.1, 0.2, 0.3]},
                {"values": [0.4, 0.5, 0.6]},
            ],
            "model": "multilingual-e5-large",
            "vector_type": "dense",
            "usage": {"total_tokens": 10},
        }

    @pytest.mark.asyncio()
    async def test_embed_success(
        self,
        inference: AsyncInference,
        mock_http_client: MagicMock,
        embed_response_data: dict[str, Any],
    ):
        """Test successful async embedding generation."""
        # Setup mock response
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.content = _json_bytes(embed_response_data)
        mock_http_client.post.return_value = mock_response

        # Call embed
        result = await inference.embed(
            model="multilingual-e5-large",
            texts=["Hello world", "Goodbye world"],
        )

        # Verify response
        assert isinstance(result, EmbeddingResponseProtocol)
        assert result.model == "multilingual-e5-large"
        assert len(result.embeddings) == 2
        assert result.embeddings[0] == [0.1, 0.2, 0.3]
        assert result.embeddings[1] == [0.4, 0.5, 0.6]
        assert result.usage.total_tokens == 10

        # Verify HTTP call
        mock_http_client.post.assert_called_once()
        call_args = mock_http_client.post.call_args
        assert "embed" in call_args[0][0]
        assert call_args[1]["headers"]["X-Pinecone-Api-Version"] == INFERENCE_API_VERSION

    @pytest.mark.asyncio()
    async def test_embed_with_parameters(
        self,
        inference: AsyncInference,
        mock_http_client: MagicMock,
        embed_response_data: dict[str, Any],
    ):
        """Test embedding with optional parameters."""
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.content = _json_bytes(embed_response_data)
        mock_http_client.post.return_value = mock_response

        result = await inference.embed(
            model="multilingual-e5-large",
            texts=["Hello world"],
            parameters={"input_type": "query", "truncate": "END"},
        )

        assert isinstance(result, EmbeddingResponseProtocol)
        assert result.model == "multilingual-e5-large"

    @pytest.mark.asyncio()
    async def test_embed_empty_texts_raises_error(self, inference: AsyncInference):
        """Test that empty texts list raises ValueError."""
        with pytest.raises(ValueError) as exc_info:
            await inference.embed(model="test-model", texts=[])

        assert "texts" in str(exc_info.value)
        assert "non-empty" in str(exc_info.value)

    @pytest.mark.asyncio()
    async def test_embed_missing_model_raises_error(self, inference: AsyncInference):
        """Test that missing model raises ValueError."""
        with pytest.raises(ValueError) as exc_info:
            await inference.embed(model="", texts=["Hello"])

        assert "model" in str(exc_info.value)
        assert "non-empty" in str(exc_info.value)

    @pytest.mark.asyncio()
    async def test_embed_single_text(
        self,
        inference: AsyncInference,
        mock_http_client: MagicMock,
    ):
        """Test embedding a single text string."""
        response_data = {
            "data": [{"values": [0.1, 0.2, 0.3]}],
            "model": "test-model",
            "vector_type": "dense",
            "usage": {"total_tokens": 5},
        }
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.content = _json_bytes(response_data)
        mock_http_client.post.return_value = mock_response

        result = await inference.embed(model="test-model", texts=["Single text"])

        assert len(result.embeddings) == 1
        assert result.embeddings[0] == [0.1, 0.2, 0.3]


class TestAsyncInferenceRerank:
    """Tests for the rerank() method."""

    @pytest.fixture()
    def mock_http_client(self) -> MagicMock:
        """Create a mock async HTTP client."""
        mock = MagicMock(spec=AsyncHTTPClient)
        mock.post = AsyncMock()
        return mock

    @pytest.fixture()
    def inference(self, mock_http_client: MagicMock) -> AsyncInference:
        """Create an AsyncInference instance with mock HTTP client."""
        return AsyncInference(http_client=mock_http_client)

    @pytest.fixture()
    def rerank_response_data(self) -> dict[str, Any]:
        """Sample rerank API response data."""
        return {
            "data": [
                {"index": 0, "score": 0.95},
                {"index": 2, "score": 0.85},
                {"index": 1, "score": 0.45},
            ],
            "model": "bge-reranker-v2-m3",
            "usage": {"rerank_units": 3},
        }

    @pytest.mark.asyncio()
    async def test_rerank_success(
        self,
        inference: AsyncInference,
        mock_http_client: MagicMock,
        rerank_response_data: dict[str, Any],
    ):
        """Test successful async document reranking."""
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.content = _json_bytes(rerank_response_data)
        mock_http_client.post.return_value = mock_response

        result = await inference.rerank(
            model="bge-reranker-v2-m3",
            query="What is machine learning?",
            documents=[
                "Machine learning is a subset of AI",
                "Python is a programming language",
                "Deep learning uses neural networks",
            ],
        )

        assert isinstance(result, RerankResponseProtocol)
        assert result.model == "bge-reranker-v2-m3"
        assert len(result.results) == 3
        assert result.results[0].index == 0
        assert result.results[0].score == 0.95

        # Verify HTTP call
        mock_http_client.post.assert_called_once()
        call_args = mock_http_client.post.call_args
        assert "rerank" in call_args[0][0]
        assert call_args[1]["headers"]["X-Pinecone-Api-Version"] == INFERENCE_API_VERSION

    @pytest.mark.asyncio()
    async def test_rerank_with_top_n(
        self,
        inference: AsyncInference,
        mock_http_client: MagicMock,
    ):
        """Test reranking with top_n parameter."""
        response_data = {
            "data": [
                {"index": 0, "score": 0.95},
                {"index": 2, "score": 0.85},
            ],
            "model": "bge-reranker-v2-m3",
            "usage": {"rerank_units": 2},
        }
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.content = _json_bytes(response_data)
        mock_http_client.post.return_value = mock_response

        result = await inference.rerank(
            model="bge-reranker-v2-m3",
            query="What is AI?",
            documents=["Doc 1", "Doc 2", "Doc 3"],
            top_n=2,
        )

        assert len(result.results) == 2

    @pytest.mark.asyncio()
    async def test_rerank_with_return_documents(
        self,
        inference: AsyncInference,
        mock_http_client: MagicMock,
    ):
        """Test reranking with return_documents=True."""
        response_data = {
            "data": [
                {"index": 0, "score": 0.95, "document": "Machine learning is AI"},
            ],
            "model": "bge-reranker-v2-m3",
            "usage": {"rerank_units": 1},
        }
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.content = _json_bytes(response_data)
        mock_http_client.post.return_value = mock_response

        result = await inference.rerank(
            model="bge-reranker-v2-m3",
            query="What is AI?",
            documents=["Machine learning is AI"],
            return_documents=True,
        )

        assert len(result.results) == 1
        assert result.results[0].document == "Machine learning is AI"

    @pytest.mark.asyncio()
    async def test_rerank_empty_query_raises_error(self, inference: AsyncInference):
        """Test that empty query raises ValueError."""
        with pytest.raises(ValueError) as exc_info:
            await inference.rerank(
                model="test-model",
                query="",
                documents=["Doc 1"],
            )

        assert "query" in str(exc_info.value)
        assert "non-empty" in str(exc_info.value)

    @pytest.mark.asyncio()
    async def test_rerank_empty_documents_raises_error(self, inference: AsyncInference):
        """Test that empty documents list raises ValueError."""
        with pytest.raises(ValueError) as exc_info:
            await inference.rerank(
                model="test-model",
                query="What is AI?",
                documents=[],
            )

        assert "documents" in str(exc_info.value)
        assert "non-empty" in str(exc_info.value)

    @pytest.mark.asyncio()
    async def test_rerank_missing_model_raises_error(self, inference: AsyncInference):
        """Test that missing model raises ValueError."""
        with pytest.raises(ValueError) as exc_info:
            await inference.rerank(
                model="",
                query="What is AI?",
                documents=["Doc 1"],
            )

        assert "model" in str(exc_info.value)
        assert "non-empty" in str(exc_info.value)

    @pytest.mark.asyncio()
    async def test_rerank_top_n_too_small_raises_error(self, inference: AsyncInference):
        """Test that top_n < 1 raises ValueError."""
        with pytest.raises(ValueError) as exc_info:
            await inference.rerank(
                model="test-model",
                query="What is AI?",
                documents=["Doc 1", "Doc 2"],
                top_n=0,
            )

        assert "top_n" in str(exc_info.value)
        assert "between 1 and 2" in str(exc_info.value)

    @pytest.mark.asyncio()
    async def test_rerank_top_n_too_large_raises_error(self, inference: AsyncInference):
        """Test that top_n > len(documents) raises ValueError."""
        with pytest.raises(ValueError) as exc_info:
            await inference.rerank(
                model="test-model",
                query="What is AI?",
                documents=["Doc 1", "Doc 2"],
                top_n=5,
            )

        assert "top_n" in str(exc_info.value)
        assert "between 1 and 2" in str(exc_info.value)

    @pytest.mark.asyncio()
    async def test_rerank_top_n_equal_to_documents_succeeds(
        self,
        inference: AsyncInference,
        mock_http_client: MagicMock,
    ):
        """Test that top_n == len(documents) is valid."""
        response_data = {
            "data": [
                {"index": 0, "score": 0.95},
                {"index": 1, "score": 0.85},
            ],
            "model": "test-model",
            "usage": {"rerank_units": 2},
        }
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.content = _json_bytes(response_data)
        mock_http_client.post.return_value = mock_response

        result = await inference.rerank(
            model="test-model",
            query="What is AI?",
            documents=["Doc 1", "Doc 2"],
            top_n=2,
        )

        assert len(result.results) == 2


class TestAsyncInferenceConfiguration:
    """Tests for AsyncInference configuration."""

    @pytest.mark.asyncio()
    async def test_custom_base_url(self):
        """Test that custom base_url is used in requests."""
        mock_http_client = MagicMock(spec=AsyncHTTPClient)
        mock_http_client.post = AsyncMock()
        inference = AsyncInference(
            http_client=mock_http_client,
            base_url="https://custom.pinecone.io",
        )

        # Setup mock response
        response_data = {
            "data": [{"values": [0.1, 0.2]}],
            "model": "test-model",
            "vector_type": "dense",
            "usage": {"total_tokens": 5},
        }
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.content = _json_bytes(response_data)
        mock_http_client.post.return_value = mock_response

        await inference.embed(model="test-model", texts=["Hello"])

        # Verify custom base URL is used
        call_args = mock_http_client.post.call_args
        assert call_args[0][0] == "https://custom.pinecone.io/embed"

    def test_repr_with_default_base_url(self):
        """Test __repr__ with default base_url."""
        mock_http_client = MagicMock(spec=AsyncHTTPClient)
        inference = AsyncInference(http_client=mock_http_client)

        assert repr(inference) == "AsyncInference(base_url='https://api.pinecone.io')"

    def test_repr_with_custom_base_url(self):
        """Test __repr__ with custom base_url."""
        mock_http_client = MagicMock(spec=AsyncHTTPClient)
        inference = AsyncInference(
            http_client=mock_http_client,
            base_url="https://custom.pinecone.io",
        )

        assert repr(inference) == "AsyncInference(base_url='https://custom.pinecone.io')"


class TestAsyncInferenceConcurrency:
    """Tests for concurrent async operations."""

    @pytest.mark.asyncio()
    async def test_concurrent_embed_calls(self):
        """Test multiple concurrent embed calls."""
        mock_http_client = MagicMock(spec=AsyncHTTPClient)
        mock_http_client.post = AsyncMock()
        inference = AsyncInference(http_client=mock_http_client)

        # Setup mock responses
        response_data = {
            "data": [{"values": [0.1, 0.2]}],
            "model": "test-model",
            "vector_type": "dense",
            "usage": {"total_tokens": 5},
        }
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.content = _json_bytes(response_data)
        mock_http_client.post.return_value = mock_response

        # Make 3 concurrent calls
        tasks = [inference.embed(model="test-model", texts=[f"Text {i}"]) for i in range(3)]
        results = await asyncio.gather(*tasks)

        # Verify all calls succeeded
        assert len(results) == 3
        assert all(len(r.embeddings) == 1 for r in results)
        assert mock_http_client.post.call_count == 3

    @pytest.mark.asyncio()
    async def test_concurrent_rerank_calls(self):
        """Test multiple concurrent rerank calls."""
        mock_http_client = MagicMock(spec=AsyncHTTPClient)
        mock_http_client.post = AsyncMock()
        inference = AsyncInference(http_client=mock_http_client)

        # Setup mock responses
        response_data = {
            "data": [{"index": 0, "score": 0.95}],
            "model": "test-model",
            "usage": {"rerank_units": 1},
        }
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.content = _json_bytes(response_data)
        mock_http_client.post.return_value = mock_response

        # Make 3 concurrent calls
        tasks = [
            inference.rerank(model="test-model", query=f"Query {i}", documents=["Doc 1"])
            for i in range(3)
        ]
        results = await asyncio.gather(*tasks)

        # Verify all calls succeeded
        assert len(results) == 3
        assert all(len(r.results) == 1 for r in results)
        assert mock_http_client.post.call_count == 3


class TestAsyncInferenceCancellation:
    """Tests for cancellation handling."""

    @pytest.mark.asyncio()
    async def test_embed_cancellation_propagates(self):
        """Test that cancellation is propagated correctly during embed."""
        mock_http_client = MagicMock(spec=AsyncHTTPClient)

        # Simulate a cancellation during HTTP request
        async def cancel_during_request(*args, **kwargs):
            await asyncio.sleep(0.1)
            raise asyncio.CancelledError

        mock_http_client.post = AsyncMock(side_effect=cancel_during_request)
        inference = AsyncInference(http_client=mock_http_client)

        # Embed should propagate CancelledError
        with pytest.raises(asyncio.CancelledError):
            await inference.embed(model="test-model", texts=["Hello world"])

    @pytest.mark.asyncio()
    async def test_rerank_cancellation_propagates(self):
        """Test that cancellation is propagated correctly during rerank."""
        mock_http_client = MagicMock(spec=AsyncHTTPClient)

        # Simulate a cancellation during HTTP request
        async def cancel_during_request(*args, **kwargs):
            await asyncio.sleep(0.1)
            raise asyncio.CancelledError

        mock_http_client.post = AsyncMock(side_effect=cancel_during_request)
        inference = AsyncInference(http_client=mock_http_client)

        # Rerank should propagate CancelledError
        with pytest.raises(asyncio.CancelledError):
            await inference.rerank(
                model="test-model",
                query="What is AI?",
                documents=["Doc 1"],
            )

    @pytest.mark.asyncio()
    async def test_cancelled_task_cleanup(self):
        """Test that resources are cleaned up when task is cancelled."""
        mock_http_client = MagicMock(spec=AsyncHTTPClient)

        # Simulate a long-running request
        async def long_request(*args, **kwargs):
            await asyncio.sleep(10)
            mock_response = MagicMock(spec=httpx.Response)
            mock_response.content = _json_bytes(
                {
                    "data": [{"values": [0.1, 0.2]}],
                    "model": "test-model",
                    "vector_type": "dense",
                    "usage": {"total_tokens": 5},
                }
            )
            return mock_response

        mock_http_client.post = AsyncMock(side_effect=long_request)
        inference = AsyncInference(http_client=mock_http_client)

        # Create and cancel task
        task = asyncio.create_task(inference.embed(model="test-model", texts=["Hello"]))
        await asyncio.sleep(0.01)  # Let task start
        task.cancel()

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


class TestAsyncInferenceTimeout:
    """Tests for timeout parameter support in async inference."""

    @pytest.fixture()
    def mock_http_client(self) -> MagicMock:
        """Create a mock async HTTP client."""
        return MagicMock(spec=AsyncHTTPClient)

    @pytest.fixture()
    def inference(self, mock_http_client: MagicMock) -> AsyncInference:
        """Create an AsyncInference instance with mock HTTP client."""
        return AsyncInference(http_client=mock_http_client)

    @pytest.fixture()
    def embed_response_data(self) -> dict[str, Any]:
        """Sample embed API response data."""
        return {
            "data": [{"values": [0.1, 0.2, 0.3]}],
            "model": "multilingual-e5-large",
            "vector_type": "dense",
            "usage": {"total_tokens": 5},
        }

    @pytest.fixture()
    def rerank_response_data(self) -> dict[str, Any]:
        """Sample rerank API response data."""
        return {
            "data": [{"index": 0, "score": 0.95}],
            "model": "bge-reranker-v2-m3",
            "usage": {"total_tokens": 10},
        }

    @pytest.mark.asyncio()
    async def test_embed_with_timeout(
        self,
        inference: AsyncInference,
        mock_http_client: MagicMock,
        embed_response_data: dict[str, Any],
    ):
        """Test that timeout parameter is passed to HTTP client."""
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.content = _json_bytes(embed_response_data)
        mock_http_client.post = AsyncMock(return_value=mock_response)

        await inference.embed(
            model="multilingual-e5-large",
            texts=["Hello world"],
            timeout=5.0,
        )

        # Verify timeout was passed to HTTP client
        call_args = mock_http_client.post.call_args
        assert call_args[1]["timeout"] == 5.0

    @pytest.mark.asyncio()
    async def test_embed_without_timeout(
        self,
        inference: AsyncInference,
        mock_http_client: MagicMock,
        embed_response_data: dict[str, Any],
    ):
        """Test that timeout parameter is omitted when None."""
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.content = _json_bytes(embed_response_data)
        mock_http_client.post = AsyncMock(return_value=mock_response)

        await inference.embed(
            model="multilingual-e5-large",
            texts=["Hello world"],
        )

        # Verify timeout was not passed to HTTP client
        call_args = mock_http_client.post.call_args
        assert "timeout" not in call_args[1]

    @pytest.mark.asyncio()
    async def test_rerank_with_timeout(
        self,
        inference: AsyncInference,
        mock_http_client: MagicMock,
        rerank_response_data: dict[str, Any],
    ):
        """Test that timeout parameter is passed to HTTP client for rerank."""
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.content = _json_bytes(rerank_response_data)
        mock_http_client.post = AsyncMock(return_value=mock_response)

        await inference.rerank(
            model="bge-reranker-v2-m3",
            query="What is AI?",
            documents=["AI is..."],
            timeout=10.0,
        )

        # Verify timeout was passed to HTTP client
        call_args = mock_http_client.post.call_args
        assert call_args[1]["timeout"] == 10.0

    @pytest.mark.asyncio()
    async def test_rerank_without_timeout(
        self,
        inference: AsyncInference,
        mock_http_client: MagicMock,
        rerank_response_data: dict[str, Any],
    ):
        """Test that timeout parameter is omitted when None for rerank."""
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.content = _json_bytes(rerank_response_data)
        mock_http_client.post = AsyncMock(return_value=mock_response)

        await inference.rerank(
            model="bge-reranker-v2-m3",
            query="What is AI?",
            documents=["AI is..."],
        )

        # Verify timeout was not passed to HTTP client
        call_args = mock_http_client.post.call_args
        assert "timeout" not in call_args[1]


class TestAsyncInferenceListModels:
    """Tests for the list_models() method."""

    @pytest.fixture()
    def mock_http_client(self) -> MagicMock:
        """Create a mock async HTTP client."""
        mock = MagicMock(spec=AsyncHTTPClient)
        mock.get = AsyncMock()
        return mock

    @pytest.fixture()
    def inference(self, mock_http_client: MagicMock) -> AsyncInference:
        """Create an AsyncInference instance with mock HTTP client."""
        return AsyncInference(http_client=mock_http_client)

    @pytest.fixture()
    def list_models_response_data(self) -> dict[str, Any]:
        """Sample list_models API response data."""
        return {
            "models": [
                {
                    "model": "multilingual-e5-large",
                    "short_description": "A high-performance dense embedding model",
                    "type": "embed",
                    "vector_type": "dense",
                    "default_dimension": 1024,
                    "modality": "text",
                    "max_sequence_length": 512,
                    "max_batch_size": 96,
                    "provider_name": "Microsoft",
                    "supported_dimensions": [1024],
                    "supported_metrics": ["cosine", "euclidean"],
                    "supported_parameters": [
                        {
                            "parameter": "input_type",
                            "required": True,
                            "type": "one_of",
                            "value_type": "string",
                            "allowed_values": ["query", "passage"],
                        },
                        {
                            "parameter": "truncate",
                            "required": False,
                            "default": "END",
                            "type": "one_of",
                            "value_type": "string",
                            "allowed_values": ["END", "NONE"],
                        },
                    ],
                },
                {
                    "model": "bge-reranker-v2-m3",
                    "short_description": "A high-performance multilingual reranking model",
                    "type": "rerank",
                    "modality": "text",
                    "max_sequence_length": 1024,
                    "max_batch_size": 100,
                    "provider_name": "BAAI",
                    "supported_parameters": [
                        {
                            "parameter": "truncate",
                            "required": False,
                            "default": "NONE",
                            "type": "one_of",
                            "value_type": "string",
                            "allowed_values": ["END", "NONE"],
                        }
                    ],
                },
            ]
        }

    @pytest.mark.asyncio()
    async def test_list_models_success(
        self,
        inference: AsyncInference,
        mock_http_client: MagicMock,
        list_models_response_data: dict[str, Any],
    ):
        """Test successful async list_models operation."""
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.content = _json_bytes(list_models_response_data)
        mock_http_client.get = AsyncMock(return_value=mock_response)

        result = await inference.list_models()

        assert isinstance(result, ListModelsResponseProtocol)
        assert len(result.models) == 2

        # Check first model (embed)
        embed_model = result.models[0]
        assert embed_model.model == "multilingual-e5-large"
        assert embed_model.type == "embed"
        assert embed_model.vector_type == "dense"
        assert embed_model.default_dimension == 1024
        assert embed_model.provider_name == "Microsoft"
        assert len(embed_model.supported_parameters) == 2

        # Check parameter structure
        input_type_param = embed_model.supported_parameters[0]
        assert input_type_param.parameter == "input_type"
        assert input_type_param.required is True
        assert input_type_param.type == "one_of"
        assert input_type_param.value_type == "string"
        assert input_type_param.allowed_values == ["query", "passage"]

        # Check second model (rerank)
        rerank_model = result.models[1]
        assert rerank_model.model == "bge-reranker-v2-m3"
        assert rerank_model.type == "rerank"
        assert rerank_model.vector_type is None  # rerank models don't have vector_type

        # Verify HTTP call
        mock_http_client.get.assert_called_once()
        call_args = mock_http_client.get.call_args
        assert "models" in call_args[0][0]
        assert call_args[1]["headers"]["X-Pinecone-Api-Version"] == INFERENCE_API_VERSION

    @pytest.mark.asyncio()
    async def test_list_models_with_type_filter(
        self,
        inference: AsyncInference,
        mock_http_client: MagicMock,
    ):
        """Test async list_models with type filter."""
        response_data = {
            "models": [
                {
                    "model": "multilingual-e5-large",
                    "short_description": "Embedding model",
                    "type": "embed",
                    "vector_type": "dense",
                    "default_dimension": 1024,
                    "modality": "text",
                    "max_sequence_length": 512,
                    "max_batch_size": 96,
                    "provider_name": "Microsoft",
                    "supported_dimensions": [1024],
                    "supported_metrics": ["cosine"],
                    "supported_parameters": [],
                }
            ]
        }
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.content = _json_bytes(response_data)
        mock_http_client.get = AsyncMock(return_value=mock_response)

        result = await inference.list_models(model_type="embed")

        assert len(result.models) == 1
        assert result.models[0].type == "embed"

        # Verify query params were sent
        call_args = mock_http_client.get.call_args
        assert call_args[1]["params"]["type"] == "embed"

    @pytest.mark.asyncio()
    async def test_list_models_with_vector_type_filter(
        self,
        inference: AsyncInference,
        mock_http_client: MagicMock,
    ):
        """Test async list_models with vector_type filter."""
        response_data = {
            "models": [
                {
                    "model": "pinecone-sparse-english-v0",
                    "short_description": "Sparse embedding model",
                    "type": "embed",
                    "vector_type": "sparse",
                    "modality": "text",
                    "max_sequence_length": 512,
                    "max_batch_size": 96,
                    "provider_name": "Pinecone",
                    "supported_metrics": ["dotproduct"],
                    "supported_parameters": [],
                }
            ]
        }
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.content = _json_bytes(response_data)
        mock_http_client.get = AsyncMock(return_value=mock_response)

        result = await inference.list_models(model_type="embed", vector_type="sparse")

        assert len(result.models) == 1
        assert result.models[0].vector_type == "sparse"

        # Verify query params were sent
        call_args = mock_http_client.get.call_args
        assert call_args[1]["params"]["type"] == "embed"
        assert call_args[1]["params"]["vector_type"] == "sparse"

    @pytest.mark.asyncio()
    async def test_list_models_with_timeout(
        self,
        inference: AsyncInference,
        mock_http_client: MagicMock,
        list_models_response_data: dict[str, Any],
    ):
        """Test that timeout parameter is passed to HTTP client."""
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.content = _json_bytes(list_models_response_data)
        mock_http_client.get = AsyncMock(return_value=mock_response)

        await inference.list_models(timeout=5.0)

        # Verify timeout was passed to HTTP client
        call_args = mock_http_client.get.call_args
        assert call_args[1]["timeout"] == 5.0

    @pytest.mark.asyncio()
    async def test_list_models_without_filters(
        self,
        inference: AsyncInference,
        mock_http_client: MagicMock,
        list_models_response_data: dict[str, Any],
    ):
        """Test async list_models without any filters."""
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.content = _json_bytes(list_models_response_data)
        mock_http_client.get = AsyncMock(return_value=mock_response)

        result = await inference.list_models()

        assert len(result.models) == 2

        # Verify no params were sent
        call_args = mock_http_client.get.call_args
        assert "params" not in call_args[1] or call_args[1]["params"] is None


class TestAsyncInferenceErrorContext:
    """Tests for error context in async _make_inference_request."""

    @pytest.fixture()
    def mock_http_client(self) -> MagicMock:
        """Create a mock HTTP client."""
        return MagicMock(spec=AsyncHTTPClient)

    @pytest.fixture()
    def inference(self, mock_http_client: MagicMock) -> AsyncInference:
        """Create an AsyncInference instance with mock HTTP client."""
        return AsyncInference(http_client=mock_http_client)

    @pytest.mark.asyncio()
    async def test_embed_api_error_includes_context(
        self,
        inference: AsyncInference,
        mock_http_client: MagicMock,
    ):
        """Test that APIError from embed includes operation context."""
        # Setup mock to raise APIError
        original_error = APIError(
            "Invalid model", status_code=400, response_body='{"error": "invalid"}'
        )
        mock_http_client.post = AsyncMock(side_effect=original_error)

        # Call embed and verify error message includes context
        with pytest.raises(APIError) as exc_info:
            await inference.embed(model="test-model", texts=["Hello"])

        assert "Inference embed request failed" in str(exc_info.value)
        assert "Invalid model" in str(exc_info.value)
        assert exc_info.value.status_code == 400
        assert exc_info.value.response_body == '{"error": "invalid"}'
        assert exc_info.value.__cause__ is original_error

    @pytest.mark.asyncio()
    async def test_rerank_api_error_includes_context(
        self,
        inference: AsyncInference,
        mock_http_client: MagicMock,
    ):
        """Test that APIError from rerank includes operation context."""
        # Setup mock to raise APIError
        original_error = APIError("Rate limit exceeded", status_code=429)
        mock_http_client.post = AsyncMock(side_effect=original_error)

        # Call rerank and verify error message includes context
        with pytest.raises(APIError) as exc_info:
            await inference.rerank(
                model="test-model",
                query="What is AI?",
                documents=["AI is..."],
            )

        assert "Inference rerank request failed" in str(exc_info.value)
        assert "Rate limit exceeded" in str(exc_info.value)
        assert exc_info.value.status_code == 429
        assert exc_info.value.__cause__ is original_error

    @pytest.mark.asyncio()
    async def test_embed_connection_error_includes_context(
        self,
        inference: AsyncInference,
        mock_http_client: MagicMock,
    ):
        """Test that APIConnectionError from embed includes operation context."""
        # Setup mock to raise APIConnectionError
        original_error = APIConnectionError("Failed to connect")
        mock_http_client.post = AsyncMock(side_effect=original_error)

        # Call embed and verify error message includes context
        with pytest.raises(APIConnectionError) as exc_info:
            await inference.embed(model="test-model", texts=["Hello"])

        assert "Inference embed request failed" in str(exc_info.value)
        assert "Failed to connect" in str(exc_info.value)
        assert exc_info.value.__cause__ is original_error

    @pytest.mark.asyncio()
    async def test_rerank_timeout_error_includes_context(
        self,
        inference: AsyncInference,
        mock_http_client: MagicMock,
    ):
        """Test that TimeoutError from rerank includes operation context."""
        # Setup mock to raise TimeoutError (subclass of APIConnectionError)
        original_error = PineconeTimeoutError(timeout=30.0)
        mock_http_client.post = AsyncMock(side_effect=original_error)

        # Call rerank and verify error message includes context
        with pytest.raises(APIConnectionError) as exc_info:
            await inference.rerank(
                model="test-model",
                query="What is AI?",
                documents=["AI is..."],
            )

        assert "Inference rerank request failed" in str(exc_info.value)
        assert "timed out" in str(exc_info.value).lower()
        assert exc_info.value.__cause__ is original_error


class TestAsyncInferenceIntrospection:
    """Test suite for AsyncInference interactive discoverability."""

    @pytest.fixture()
    def inference(self) -> AsyncInference:
        """Create an AsyncInference instance for testing."""
        mock_http_client = AsyncMock(spec=AsyncHTTPClient)
        return AsyncInference(http_client=mock_http_client)

    def test_dir_includes_all_eagerly_bound_methods(self, inference: AsyncInference):
        """Test that dir() includes all methods from subpackage files after eager binding.

        This test verifies that the eager method binding in __init__ correctly
        makes all methods discoverable via dir(). Methods are bound from separate
        files (_embed.py, _rerank.py, _list_models.py) to enable parallel development.
        """
        methods = dir(inference)

        # Verify all public methods from subpackage files are bound and discoverable
        expected_methods = ["embed", "list_models", "rerank"]
        for method in expected_methods:
            assert method in methods, f"Method '{method}' should be in dir() output"

        # Verify methods are actually callable and bound to the instance
        for method_name in expected_methods:
            method = getattr(inference, method_name)
            assert callable(method), f"{method_name} should be callable"
            assert hasattr(method, "__self__"), f"{method_name} should be a bound method"
            assert method.__self__ is inference, f"{method_name} should be bound to instance"

        # Verify the exact set matches (no extras, no missing)
        assert set(methods) == set(expected_methods), (
            "dir() should return exactly the public methods"
        )

    def test_dir_excludes_private_attributes(self, inference: AsyncInference):
        """Test that dir() excludes private attributes and helper methods."""
        methods = dir(inference)

        # Should NOT include private attributes
        assert "_http" not in methods
        assert "_base_url" not in methods
        assert "_adapter" not in methods
        assert "_make_inference_request" not in methods

        # Should NOT include dunder methods
        assert "__init__" not in methods
        assert "__repr__" not in methods


def _json_bytes(data: dict[str, Any]) -> bytes:
    """Convert dict to JSON bytes for mock responses."""
    return orjson.dumps(data)
