# Copyright (c) Microsoft. All rights reserved.

from typing import Any
from unittest.mock import AsyncMock, Mock, patch

from pinecone import FetchResponse, IndexModel, Metric, QueryResponse, ServerlessSpec, Vector
from pinecone.core.openapi.db_data.models import (
    Hit,
    ScoredVector,
    SearchRecordsResponse,
    SearchRecordsResponseResult,
    SearchUsage,
)
from pinecone.db_data.index_asyncio import _IndexAsyncio
from pytest import fixture, mark, raises

from semantic_kernel.connectors.pinecone import PineconeCollection, PineconeStore
from semantic_kernel.exceptions.vector_store_exceptions import VectorStoreInitializationException

BASE_PATH_ASYNCIO = "pinecone.PineconeAsyncio"
BASE_PATH_INDEX_CLIENT_ASYNCIO = "pinecone.db_data.index_asyncio._IndexAsyncio"


@fixture
def embed(request) -> dict[str, Any] | None:
    if hasattr(request, "param"):
        return request.param
    return None


@fixture
def mock_index_model(embed: dict[str, Any] | None):
    """Mock IndexModel for testing."""
    mock_index_model = Mock(spec=IndexModel)
    mock_index_model.name = "test"
    mock_index_model.embed = embed
    mock_index_model.host = "test_host"
    return mock_index_model


@fixture(autouse=True)
def mock_list_collection_names(mock_index_model):
    with patch(f"{BASE_PATH_ASYNCIO}.list_indexes") as mock_list_indexes:
        mock_list_indexes.return_value = [mock_index_model]
        yield mock_list_indexes


@fixture(autouse=True)
def mock_create_index(mock_index_model):
    with patch(f"{BASE_PATH_ASYNCIO}.create_index") as mock_create_index:
        mock_create_index.return_value = mock_index_model
        yield mock_create_index


@fixture(autouse=True)
def mock_create_index_for_model(mock_index_model):
    with patch(f"{BASE_PATH_ASYNCIO}.create_index_for_model") as mock_create_index_for_model:
        mock_create_index_for_model.return_value = mock_index_model
        yield mock_create_index_for_model


@fixture(autouse=True)
def mock_describe_index(mock_index_model):
    with patch(f"{BASE_PATH_ASYNCIO}.describe_index") as mock_describe_index:
        mock_describe_index.return_value = mock_index_model
        yield mock_describe_index


@fixture(autouse=True)
def mock_has_index():
    with patch(f"{BASE_PATH_ASYNCIO}.has_index") as mock_has_index:
        mock_create_index.return_value = True
        yield mock_has_index


@fixture(autouse=True)
def mock_index_asyncio():
    mock_index_asyncio = AsyncMock(spec=_IndexAsyncio)
    mock_index_asyncio.close.return_value = None
    with patch(f"{BASE_PATH_ASYNCIO}.IndexAsyncio") as mock_index:
        mock_index.return_value = mock_index_asyncio
        yield mock_index


@fixture(autouse=True)
def mock_delete_index():
    with patch(f"{BASE_PATH_ASYNCIO}.delete_index") as mock_delete:
        yield mock_delete


@fixture
async def store(pinecone_unit_test_env) -> PineconeStore:
    """Fixture to create a Pinecone store."""
    async with PineconeStore() as store:
        yield store


@fixture
async def collection(pinecone_unit_test_env, definition) -> PineconeCollection:
    """Fixture to create a Pinecone store."""
    async with PineconeCollection(
        collection_name="test_collection",
        record_type=dict,
        definition=definition,
    ) as collection:
        yield collection


async def test_create_store(pinecone_unit_test_env):
    """Test the creation of a Pinecone store."""
    # Create a Pinecone store
    store = PineconeStore()
    assert store is not None
    assert store.client is not None


@mark.parametrize("exclude_list", [["PINECONE_API_KEY"]], indirect=True)
async def test_create_store_fail(pinecone_unit_test_env):
    """Test the creation of a Pinecone store."""
    with raises(VectorStoreInitializationException):
        PineconeStore(env_file_path="test.env")


def test_create_store_grpc(pinecone_unit_test_env):
    """Test the creation of a Pinecone store."""

    # Create a Pinecone store
    store = PineconeStore(use_grpc=True)
    assert store is not None
    assert store.client is not None


@mark.parametrize("exclude_list", [["PINECONE_API_KEY"]], indirect=True)
async def test_ensure_collection_exists_fail(pinecone_unit_test_env, definition):
    with raises(VectorStoreInitializationException):
        PineconeCollection(
            collection_name="test_collection",
            record_type=dict,
            definition=definition,
            env_file_path="test.env",
        )


async def test_get_collection(store: PineconeStore, definition):
    """Test the creation of a Pinecone collection."""
    # Create a collection
    collection = store.get_collection(collection_name="test_collection", record_type=dict, definition=definition)
    assert collection is not None
    assert collection.collection_name == "test_collection"


async def test_list_collection_names(store: PineconeStore):
    """Test the listing of Pinecone collections."""
    # List collections
    collections = await store.list_collection_names()
    assert collections is not None
    assert len(collections) == 1
    assert collections[0] == "test"


@mark.parametrize("embed", [None, {"model": "test-model"}])
async def test_load_index_client(collection, mock_index_asyncio):
    # Test loading the index client
    await collection._load_index_client()
    assert collection.index is not None
    assert collection.index_client is not None
    assert isinstance(collection.index_client, _IndexAsyncio)
    assert collection.embed_settings == collection.index.embed


async def test_ensure_collection_exists(collection, mock_create_index):
    await collection.ensure_collection_exists()
    assert collection.index is not None
    assert collection.index_client is not None
    mock_create_index.assert_awaited_once_with(
        name=collection.collection_name,
        spec=ServerlessSpec(cloud="aws", region="us-east-1"),
        dimension=5,
        metric=Metric.COSINE,
        vector_type="dense",
    )


@mark.parametrize("embed", [{"model": "test-model"}])
async def test_ensure_collection_exists_integrated(collection, mock_create_index_for_model):
    await collection.ensure_collection_exists(embed={"model": "test-model"})
    assert collection.index is not None
    assert collection.index_client is not None
    mock_create_index_for_model.assert_awaited_once_with(
        name=collection.collection_name,
        cloud="aws",
        region="us-east-1",
        embed={"model": "test-model", "metric": Metric.COSINE, "field_map": {"text": "vector"}},
    )


async def test_ensure_collection_deleted(collection):
    # Test deleting the collection
    await collection.ensure_collection_deleted()
    assert collection.index is None
    assert collection.index_client is None


async def test_upsert(collection):
    record = {
        "id": "test_id",
        "vector": [0.1, 0.2, 0.3, 0.4, 0.5],
        "content": "test_content",
    }
    pinecone_vector = Vector(values=record["vector"], id=record["id"], metadata={"content": record["content"]})
    await collection._load_index_client()
    with patch.object(collection.index_client, "upsert", new_callable=AsyncMock) as mock_upsert:
        await collection.upsert(record)
        mock_upsert.assert_awaited_once_with(
            [pinecone_vector],
            namespace=collection.namespace,
        )


@mark.parametrize("embed", [{"model": "test-model"}])
async def test_upsert_embed(collection):
    record = {
        "id": "test_id",
        "content": "test_content",
        "vector": [0.1, 0.2, 0.3, 0.4, 0.5],
    }
    await collection._load_index_client()
    with patch.object(collection.index_client, "upsert_records", new_callable=AsyncMock) as mock_upsert:
        await collection.upsert(record)
        mock_upsert.assert_awaited_once_with(
            records=[{"_id": record["id"], "content": record["content"]}],
            namespace=collection.namespace,
        )


async def test_get(collection):
    record = {
        "id": "test_id",
        "vector": [0.1, 0.2, 0.3, 0.4, 0.5],
        "content": "test_content",
    }
    fetch_response = FetchResponse(
        namespace="",
        vectors={
            record["id"]: Vector(values=record["vector"], id=record["id"], metadata={"content": record["content"]})
        },
        usage={},
    )
    await collection._load_index_client()
    with patch.object(collection.index_client, "fetch", new_callable=AsyncMock) as mock_fetch:
        mock_fetch.return_value = fetch_response
        get_record = await collection.get(record["id"])
        mock_fetch.assert_awaited_once_with(
            ids=[record["id"]],
            namespace=collection.namespace,
        )
        assert record["id"] == get_record["id"]
        assert record["content"] == get_record["content"]


async def test_delete(collection):
    await collection._load_index_client()
    with patch.object(collection.index_client, "delete", new_callable=AsyncMock) as mock_delete:
        await collection.delete("test_id")
        mock_delete.assert_awaited_once_with(
            ids=["test_id"],
            namespace=collection.namespace,
        )


async def test_search(collection):
    record = {
        "id": "test_id",
        "vector": [0.1, 0.2, 0.3, 0.4, 0.5],
        "content": "test_content",
    }
    query_response = QueryResponse._from_openapi_data(
        namespace="",
        matches=[
            ScoredVector(**{
                "values": record["vector"],
                "id": record["id"],
                "metadata": {"content": record["content"]},
                "score": 0.1,
            })
        ],
    )
    await collection._load_index_client()
    with patch.object(collection.index_client, "query", new_callable=AsyncMock) as mock_query:
        mock_query.return_value = query_response
        query_response = await collection.search(
            vector=[0.1, 0.2, 0.3, 0.4, 0.5],
            top=1,
            include_vectors=True,
            filter=lambda x: x.content == "test_content",
        )
        mock_query.assert_awaited_once_with(
            vector=[0.1, 0.2, 0.3, 0.4, 0.5],
            top_k=1,
            include_metadata=True,
            include_values=True,
            namespace=collection.namespace,
            filter={"content": "test_content"},
        )
        assert query_response.total_count == 1
        async for result in query_response.results:
            assert result.record == record
            assert result.score == 0.1


@mark.parametrize("embed", [{"model": "test-model"}])
async def test_search_embed(collection):
    record = {"id": "test_id", "content": "test_content", "vector": None}
    query_response = SearchRecordsResponse._from_openapi_data(
        result=SearchRecordsResponseResult._from_openapi_data(**{
            "hits": [
                Hit(**{
                    "_id": record["id"],
                    "fields": {"id": record["id"], "content": record["content"]},
                    "_score": 0.1,
                })
            ]
        }),
        usage=SearchUsage(read_units=0),
    )
    await collection._load_index_client()
    with patch.object(collection.index_client, "search_records", new_callable=AsyncMock) as mock_query:
        mock_query.return_value = query_response
        query_response = await collection.search(values="test", top=1, include_vectors=True)
        mock_query.assert_awaited_once_with(
            query={"inputs": {"text": "test"}, "top_k": 1},
            namespace=collection.namespace,
        )
        assert query_response.total_count == 1
        async for result in query_response.results:
            assert result.record == record
            assert result.score == 0.1
