"""Unit tests for AsyncPinecone client."""

from __future__ import annotations

import asyncio
import os
import warnings

from unittest.mock import AsyncMock, MagicMock, patch

import httpx
import orjson
import pytest
import respx

from pinecone import AsyncPinecone
from pinecone._internal.http import AsyncHTTPClient
from pinecone.async_client.inference import AsyncInference
from pinecone.async_index import AsyncIndex


pytestmark = pytest.mark.unit


class TestAsyncPineconeInitialization:
    """Tests for AsyncPinecone initialization."""

    def test_init_with_api_key(self):
        """Test initialization with explicit api_key."""
        pc = AsyncPinecone(api_key="test-api-key")

        assert pc._api_key == "test-api-key"
        assert pc._base_url == "https://api.pinecone.io"
        assert isinstance(pc._http_client, AsyncHTTPClient)
        # Inference namespace is lazily initialized
        assert pc._inference is None
        # Accessing property creates it
        assert isinstance(pc.inference, AsyncInference)
        assert pc._inference is not None

    def test_init_with_custom_base_url(self):
        """Test initialization with custom base_url."""
        pc = AsyncPinecone(
            api_key="test-api-key",
            base_url="https://custom.pinecone.io",
        )

        assert pc._base_url == "https://custom.pinecone.io"

    def test_init_with_env_var(self):
        """Test initialization with PINECONE_API_KEY environment variable."""
        with patch.dict(os.environ, {"PINECONE_API_KEY": "env-api-key"}):
            pc = AsyncPinecone()
            assert pc._api_key == "env-api-key"

    def test_init_without_api_key_raises_error(self):
        """Test that missing api_key raises ValueError."""
        with patch.dict(os.environ, {}, clear=True):
            with pytest.raises(ValueError) as exc_info:
                AsyncPinecone()

            assert "api_key is required" in str(exc_info.value)
            assert "PINECONE_API_KEY" in str(exc_info.value)

    def test_repr(self):
        """Test string representation."""
        pc = AsyncPinecone(api_key="test-key")
        assert repr(pc) == "AsyncPinecone(base_url='https://api.pinecone.io')"

        pc = AsyncPinecone(api_key="test-key", base_url="https://custom.io")
        assert repr(pc) == "AsyncPinecone(base_url='https://custom.io')"


class TestAsyncPineconeInferenceProperty:
    """Tests for the inference property."""

    def test_inference_property_returns_async_inference(self):
        """Test that inference property returns AsyncInference instance."""
        pc = AsyncPinecone(api_key="test-api-key")

        inference = pc.inference

        assert isinstance(inference, AsyncInference)
        assert inference is pc._inference

    def test_inference_property_consistent(self):
        """Test that inference property returns same instance."""
        pc = AsyncPinecone(api_key="test-api-key")

        inference1 = pc.inference
        inference2 = pc.inference

        assert inference1 is inference2


class TestAsyncPineconeContextManager:
    """Tests for async context manager functionality."""

    @pytest.mark.asyncio()
    async def test_async_context_manager(self):
        """Test async context manager usage."""
        async with AsyncPinecone(api_key="test-api-key") as pc:
            assert isinstance(pc, AsyncPinecone)
            assert isinstance(pc.inference, AsyncInference)

    @pytest.mark.asyncio()
    async def test_context_manager_calls_close(self):
        """Test that context manager calls close on exit."""
        pc = AsyncPinecone(api_key="test-api-key")
        pc._http_client.close = AsyncMock()

        async with pc:
            pass

        pc._http_client.close.assert_called_once()

    @pytest.mark.asyncio()
    async def test_context_manager_closes_on_exception(self):
        """Test that context manager closes even on exception."""
        pc = AsyncPinecone(api_key="test-api-key")
        pc._http_client.close = AsyncMock()

        with pytest.raises(ValueError):
            async with pc:
                raise ValueError("Test error")

        pc._http_client.close.assert_called_once()


class TestAsyncPineconeClose:
    """Tests for close() method."""

    @pytest.mark.asyncio()
    async def test_close_closes_http_client(self):
        """Test that close() closes the HTTP client."""
        pc = AsyncPinecone(api_key="test-api-key")
        pc._http_client.close = AsyncMock()

        await pc.close()

        pc._http_client.close.assert_called_once()

    @pytest.mark.asyncio()
    async def test_manual_close_usage(self):
        """Test manual close without context manager."""
        pc = AsyncPinecone(api_key="test-api-key")
        pc._http_client.close = AsyncMock()

        try:
            # Use client
            assert isinstance(pc.inference, AsyncInference)
        finally:
            await pc.close()

        pc._http_client.close.assert_called_once()

    @pytest.mark.asyncio()
    async def test_close_closes_indexes_http_client(self):
        """Test that close() closes the async HTTP client used by indexes namespace."""
        pc = AsyncPinecone(api_key="test-api-key")
        pc._http_client.close = AsyncMock()

        # Access indexes property to initialize the namespace
        _ = pc.indexes

        # Verify indexes namespace exists and uses the same async HTTP client
        assert pc._indexes is not None
        assert pc._indexes._http is pc._http_client

        await pc.close()

        # Async HTTP client should be closed
        pc._http_client.close.assert_called_once()


class TestAsyncPineconeIntegration:
    """Integration tests with mocked HTTP client."""

    @pytest.mark.asyncio()
    async def test_embed_through_client(self):
        """Test embedding through AsyncPinecone client."""
        pc = AsyncPinecone(api_key="test-api-key")

        # Mock the HTTP client
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.content = orjson.dumps(
            {
                "data": [{"values": [0.1, 0.2, 0.3]}],
                "model": "test-model",
                "vector_type": "dense",
                "usage": {"total_tokens": 5},
            }
        )
        pc._http_client.post = AsyncMock(return_value=mock_response)

        # Call embed
        result = await pc.inference.embed(model="test-model", texts=["Hello world"])

        # Verify
        assert len(result.embeddings) == 1
        assert result.embeddings[0] == [0.1, 0.2, 0.3]
        pc._http_client.post.assert_called_once()

    @pytest.mark.asyncio()
    async def test_rerank_through_client(self):
        """Test reranking through AsyncPinecone client."""
        pc = AsyncPinecone(api_key="test-api-key")

        # Mock the HTTP client
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.content = orjson.dumps(
            {
                "data": [{"index": 0, "score": 0.95}],
                "model": "test-model",
                "usage": {"rerank_units": 1},
            }
        )
        pc._http_client.post = AsyncMock(return_value=mock_response)

        # Call rerank
        result = await pc.inference.rerank(
            model="test-model", query="What is AI?", documents=["AI is artificial intelligence"]
        )

        # Verify
        assert len(result.results) == 1
        assert result.results[0].score == 0.95
        pc._http_client.post.assert_called_once()

    @pytest.mark.asyncio()
    async def test_concurrent_operations(self):
        """Test concurrent operations through AsyncPinecone client."""
        async with AsyncPinecone(api_key="test-api-key") as pc:
            # Mock responses
            embed_response = MagicMock(spec=httpx.Response)
            embed_response.content = orjson.dumps(
                {
                    "data": [{"values": [0.1, 0.2]}],
                    "model": "test-model",
                    "vector_type": "dense",
                    "usage": {"total_tokens": 5},
                }
            )

            rerank_response = MagicMock(spec=httpx.Response)
            rerank_response.content = orjson.dumps(
                {
                    "data": [{"index": 0, "score": 0.95}],
                    "model": "test-model",
                    "usage": {"rerank_units": 1},
                }
            )

            # Mock HTTP client to return different responses
            responses = [embed_response, rerank_response]
            pc._http_client.post = AsyncMock(side_effect=responses)

            # Make concurrent calls
            embed_task = pc.inference.embed(model="test-model", texts=["Hello"])
            rerank_task = pc.inference.rerank(model="test-model", query="Query", documents=["Doc"])

            embed_result, rerank_result = await asyncio.gather(embed_task, rerank_task)

            # Verify both completed
            assert len(embed_result.embeddings) == 1
            assert len(rerank_result.results) == 1
            assert pc._http_client.post.call_count == 2


class TestAsyncPineconeEnvironmentConfiguration:
    """Tests for environment-based configuration."""

    def test_api_key_from_env_preferred_over_missing(self):
        """Test that env var is used when parameter is None."""
        with patch.dict(os.environ, {"PINECONE_API_KEY": "env-key"}):
            pc = AsyncPinecone()
            assert pc._api_key == "env-key"

    def test_explicit_api_key_preferred_over_env(self):
        """Test that explicit api_key overrides environment variable."""
        with patch.dict(os.environ, {"PINECONE_API_KEY": "env-key"}):
            pc = AsyncPinecone(api_key="explicit-key")
            assert pc._api_key == "explicit-key"

    def test_empty_string_api_key_uses_env(self):
        """Test that empty string api_key falls back to environment variable."""
        with patch.dict(os.environ, {"PINECONE_API_KEY": "env-key"}):
            # Empty string is falsy, so it falls back to env var
            pc = AsyncPinecone(api_key="")
            assert pc._api_key == "env-key"

    def test_none_api_key_uses_env(self):
        """Test that None api_key uses environment variable."""
        with patch.dict(os.environ, {"PINECONE_API_KEY": "env-key"}):
            pc = AsyncPinecone(api_key=None)
            assert pc._api_key == "env-key"


class TestAsyncPineconeTimeout:
    """Tests for timeout configuration."""

    def test_client_with_custom_timeout(self):
        """Test client with custom timeout."""
        pc = AsyncPinecone(api_key="test-key", timeout=60)

        assert pc is not None
        assert pc._http_client._config.timeout == 60

    def test_client_with_float_timeout(self):
        """Test client with float timeout."""
        pc = AsyncPinecone(api_key="test-key", timeout=45.5)

        assert pc is not None
        assert pc._http_client._config.timeout == 45.5

    def test_client_with_granular_timeout(self):
        """Test client with granular timeout configuration."""
        timeout_config = {"connect": 5, "read": 120, "write": 30, "pool": 10}
        pc = AsyncPinecone(api_key="test-key", timeout=timeout_config)

        assert pc is not None
        assert pc._http_client._config.timeout == timeout_config

    def test_client_with_partial_granular_timeout(self):
        """Test client with partial granular timeout configuration."""
        timeout_config = {"connect": 5, "read": 60}
        pc = AsyncPinecone(api_key="test-key", timeout=timeout_config)

        assert pc is not None
        assert pc._http_client._config.timeout == timeout_config

    def test_client_with_default_timeout(self):
        """Test client uses default timeout when not specified."""
        pc = AsyncPinecone(api_key="test-key")

        assert pc is not None
        assert pc._http_client._config.timeout == 30


class TestAsyncPineconeResourceWarnings:
    """Tests for resource leak warnings."""

    @respx.mock
    @pytest.mark.asyncio()
    async def test_del_warns_if_client_not_closed(self):
        """Test that __del__ warns if client wasn't closed after use."""
        respx.post("https://api.pinecone.io/embed").mock(
            return_value=httpx.Response(
                200,
                json={
                    "model": "multilingual-e5-large",
                    "data": [{"values": [0.1, 0.2, 0.3]}],
                    "usage": {"total_tokens": 5},
                },
            )
        )

        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            pc = AsyncPinecone(api_key="test-api-key")
            # Use the client to initialize httpx.AsyncClient
            await pc.inference.embed(model="multilingual-e5-large", texts=["test"])
            # Delete without closing
            del pc

            # Should have warnings from both AsyncPinecone and AsyncHTTPClient
            resource_warnings = [
                warning for warning in w if issubclass(warning.category, ResourceWarning)
            ]
            assert len(resource_warnings) > 0
            # At least one should be about AsyncPinecone or AsyncHTTPClient
            warning_messages = [str(warning.message) for warning in resource_warnings]
            assert any(
                "AsyncPinecone" in msg or "AsyncHTTPClient" in msg for msg in warning_messages
            )

    @pytest.mark.asyncio()
    async def test_no_warning_after_manual_close(self):
        """Test that no warning is raised after manual close."""
        pc = AsyncPinecone(api_key="test-api-key")
        await pc.close()

        # Should not warn after proper close
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            del pc

            # Filter for ResourceWarning specifically
            resource_warnings = [
                warning for warning in w if issubclass(warning.category, ResourceWarning)
            ]
            assert len(resource_warnings) == 0

    @pytest.mark.asyncio()
    async def test_no_warning_after_context_manager(self):
        """Test that no warning is raised after context manager usage."""
        async with AsyncPinecone(api_key="test-api-key") as pc:
            pass

        # Should not warn after context manager closes
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            del pc

            # Filter for ResourceWarning specifically
            resource_warnings = [
                warning for warning in w if issubclass(warning.category, ResourceWarning)
            ]
            assert len(resource_warnings) == 0


class TestAsyncHTTPClientResourceWarnings:
    """Tests for AsyncHTTPClient resource leak warnings."""

    @respx.mock
    @pytest.mark.asyncio()
    async def test_del_warns_if_http_client_not_closed(self):
        """Test that __del__ warns if HTTP client wasn't closed after use."""
        respx.get("https://api.pinecone.io/test").mock(
            return_value=httpx.Response(200, json={"result": "ok"})
        )

        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            client = AsyncHTTPClient(api_key="test-api-key")
            # Use the client to initialize httpx.AsyncClient
            await client.get("https://api.pinecone.io/test")
            # Delete without closing
            del client

            # Should have warning about unclosed AsyncHTTPClient
            resource_warnings = [
                warning for warning in w if issubclass(warning.category, ResourceWarning)
            ]
            assert len(resource_warnings) > 0
            warning_messages = [str(warning.message) for warning in resource_warnings]
            assert any("AsyncHTTPClient" in msg for msg in warning_messages)

    @pytest.mark.asyncio()
    async def test_no_warning_after_manual_close_http_client(self):
        """Test that no warning is raised after manual close of HTTP client."""
        client = AsyncHTTPClient(api_key="test-api-key")
        await client.close()

        # Should not warn after proper close
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            del client

            # Filter for ResourceWarning specifically
            resource_warnings = [
                warning for warning in w if issubclass(warning.category, ResourceWarning)
            ]
            assert len(resource_warnings) == 0

    @pytest.mark.asyncio()
    async def test_no_warning_after_context_manager_http_client(self):
        """Test that no warning is raised after context manager usage of HTTP client."""
        async with AsyncHTTPClient(api_key="test-api-key") as client:
            pass

        # Should not warn after context manager closes
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            del client

            # Filter for ResourceWarning specifically
            resource_warnings = [
                warning for warning in w if issubclass(warning.category, ResourceWarning)
            ]
            assert len(resource_warnings) == 0


class TestAsyncPineconeIndexFactory:
    """Tests for the AsyncPinecone.index() factory method."""

    def test_index_factory_with_host(self):
        pc = AsyncPinecone(api_key="test-key")
        index = pc.index(host="my-index-abc123.svc.pinecone.io")

        assert isinstance(index, AsyncIndex)
        assert index._host == "my-index-abc123.svc.pinecone.io"

    def test_index_factory_forwards_api_key(self):
        pc = AsyncPinecone(api_key="test-key")
        index = pc.index(host="my-index.svc.pinecone.io")

        assert index._api_key == "test-key"

    def test_index_factory_forwards_timeout(self):
        pc = AsyncPinecone(api_key="test-key", timeout=120)
        index = pc.index(host="my-index.svc.pinecone.io")

        assert index._timeout == 120

    def test_index_factory_requires_host_or_name(self):
        pc = AsyncPinecone(api_key="test-key")

        with pytest.raises(ValueError, match="Either 'host' or 'name' must be provided"):
            pc.index()

    def test_index_factory_name_without_host_raises(self):
        """AsyncPinecone.index(name=...) without host is not supported."""
        pc = AsyncPinecone(api_key="test-key")

        with pytest.raises(ValueError, match="Name-based host resolution requires an async"):
            pc.index(name="my-index")

    def test_index_factory_returns_fresh_instances(self):
        pc = AsyncPinecone(api_key="test-key")
        idx1 = pc.index(host="my-index.svc.pinecone.io")
        idx2 = pc.index(host="my-index.svc.pinecone.io")

        assert idx1 is not idx2
