"""Unit tests for Index data plane operations."""

from __future__ import annotations

import pytest
import respx

from httpx import Response

from pinecone.index import Index


pytestmark = pytest.mark.unit

HOST = "my-index-abc123.svc.pinecone.io"
API_KEY = "test-key"
BASE_URL = f"https://{HOST}"


def _make_index() -> Index:
    return Index(host=HOST, api_key=API_KEY)


class TestUpsert:
    """Tests for Index.upsert()."""

    @respx.mock
    def test_upsert_basic(self):
        route = respx.post(f"{BASE_URL}/vectors/upsert").mock(
            return_value=Response(200, json={"upsertedCount": 2})
        )

        index = _make_index()
        result = index.upsert(
            vectors=[
                {"id": "v1", "values": [0.1, 0.2]},
                {"id": "v2", "values": [0.3, 0.4]},
            ]
        )

        assert route.called
        assert result.upserted_count == 2

    @respx.mock
    def test_upsert_with_namespace(self):
        route = respx.post(f"{BASE_URL}/vectors/upsert").mock(
            return_value=Response(200, json={"upsertedCount": 1})
        )

        index = _make_index()
        index.upsert(
            vectors=[{"id": "v1", "values": [0.1, 0.2]}],
            namespace="test-ns",
        )

        assert route.called
        request = route.calls.last.request
        assert b'"namespace":"test-ns"' in request.content

    @respx.mock
    def test_upsert_sends_api_version_header(self):
        route = respx.post(f"{BASE_URL}/vectors/upsert").mock(
            return_value=Response(200, json={"upsertedCount": 1})
        )

        index = _make_index()
        index.upsert(vectors=[{"id": "v1", "values": [0.1]}])

        request = route.calls.last.request
        assert request.headers["X-Pinecone-Api-Version"] == "2025-10"

    @respx.mock
    def test_upsert_with_2_tuple(self):
        """Test upsert accepts 2-tuple format (id, values)."""
        route = respx.post(f"{BASE_URL}/vectors/upsert").mock(
            return_value=Response(200, json={"upsertedCount": 2})
        )

        index = _make_index()
        result = index.upsert(
            vectors=[
                ("v1", [0.1, 0.2]),
                ("v2", [0.3, 0.4]),
            ]
        )

        assert route.called
        assert result.upserted_count == 2
        # Verify the API receives normalized dict format
        request = route.calls.last.request
        assert b'"id":"v1"' in request.content
        assert b'"values":[0.1,0.2]' in request.content

    @respx.mock
    def test_upsert_with_3_tuple(self):
        """Test upsert accepts 3-tuple format (id, values, metadata)."""
        route = respx.post(f"{BASE_URL}/vectors/upsert").mock(
            return_value=Response(200, json={"upsertedCount": 2})
        )

        index = _make_index()
        result = index.upsert(
            vectors=[
                ("v1", [0.1, 0.2], {"key": "val"}),
                ("v2", [0.3, 0.4], {"category": "test"}),
            ]
        )

        assert route.called
        assert result.upserted_count == 2
        # Verify metadata is included in the request
        request = route.calls.last.request
        assert b'"metadata":{"key":"val"}' in request.content
        assert b'"metadata":{"category":"test"}' in request.content

    @respx.mock
    def test_upsert_with_mixed_formats(self):
        """Test upsert accepts mixed dict and tuple formats."""
        route = respx.post(f"{BASE_URL}/vectors/upsert").mock(
            return_value=Response(200, json={"upsertedCount": 3})
        )

        index = _make_index()
        result = index.upsert(
            vectors=[
                ("v1", [0.1, 0.2]),  # 2-tuple
                ("v2", [0.3, 0.4], {"key": "val"}),  # 3-tuple
                {"id": "v3", "values": [0.5, 0.6]},  # dict (existing format)
            ],
        )

        assert route.called
        assert result.upserted_count == 3
        # Verify all formats are normalized correctly
        request = route.calls.last.request
        assert b'"id":"v1"' in request.content
        assert b'"id":"v2"' in request.content
        assert b'"id":"v3"' in request.content


class TestQuery:
    """Tests for Index.query()."""

    @respx.mock
    def test_query_by_vector(self):
        route = respx.post(f"{BASE_URL}/query").mock(
            return_value=Response(
                200,
                json={
                    "matches": [
                        {"id": "v1", "score": 0.95, "metadata": {"key": "val"}},
                        {"id": "v2", "score": 0.80},
                    ],
                    "namespace": "",
                    "usage": {"readUnits": 5},
                },
            )
        )

        index = _make_index()
        result = index.query(vector=[0.1, 0.2], top_k=2)

        assert route.called
        assert len(result.matches) == 2
        assert result.matches[0].id == "v1"
        assert result.matches[0].score == 0.95
        assert result.matches[0].metadata == {"key": "val"}
        assert result.matches[1].id == "v2"
        assert result.usage is not None
        assert result.usage.read_units == 5

    @respx.mock
    def test_query_by_id(self):
        route = respx.post(f"{BASE_URL}/query").mock(
            return_value=Response(200, json={"matches": [], "namespace": ""})
        )

        index = _make_index()
        index.query(id="v1", top_k=5)

        request = route.calls.last.request
        assert b'"id":"v1"' in request.content

    @respx.mock
    def test_query_with_filter(self):
        route = respx.post(f"{BASE_URL}/query").mock(
            return_value=Response(200, json={"matches": [], "namespace": ""})
        )

        index = _make_index()
        index.query(vector=[0.1], top_k=5, filter={"genre": "comedy"})

        request = route.calls.last.request
        assert b'"filter"' in request.content

    @respx.mock
    def test_query_include_values(self):
        respx.post(f"{BASE_URL}/query").mock(
            return_value=Response(
                200,
                json={
                    "matches": [
                        {"id": "v1", "score": 0.9, "values": [0.1, 0.2]},
                    ],
                    "namespace": "",
                },
            )
        )

        index = _make_index()
        result = index.query(vector=[0.1, 0.2], top_k=1, include_values=True)

        assert result.matches[0].values == [0.1, 0.2]

    @respx.mock
    def test_query_with_sparse_values(self):
        respx.post(f"{BASE_URL}/query").mock(
            return_value=Response(
                200,
                json={
                    "matches": [
                        {
                            "id": "v1",
                            "score": 0.9,
                            "sparseValues": {"indices": [0, 2], "values": [0.5, 0.3]},
                        },
                    ],
                    "namespace": "",
                },
            )
        )

        index = _make_index()
        result = index.query(vector=[0.1, 0.2], top_k=1, include_values=True)

        assert result.matches[0].sparse_values is not None
        assert result.matches[0].sparse_values.indices == [0, 2]
        assert result.matches[0].sparse_values.values == [0.5, 0.3]


class TestFetch:
    """Tests for Index.fetch()."""

    @respx.mock
    def test_fetch_basic(self):
        route = respx.get(f"{BASE_URL}/vectors/fetch").mock(
            return_value=Response(
                200,
                json={
                    "vectors": {
                        "v1": {"id": "v1", "values": [0.1, 0.2]},
                        "v2": {"id": "v2", "values": [0.3, 0.4]},
                    },
                    "namespace": "",
                    "usage": {"readUnits": 1},
                },
            )
        )

        index = _make_index()
        result = index.fetch(ids=["v1", "v2"])

        assert route.called
        assert len(result.vectors) == 2
        assert result.vectors["v1"].id == "v1"
        assert result.vectors["v1"].values == [0.1, 0.2]
        assert result.usage is not None
        assert result.usage.read_units == 1

    @respx.mock
    def test_fetch_with_namespace(self):
        route = respx.get(f"{BASE_URL}/vectors/fetch").mock(
            return_value=Response(200, json={"vectors": {}, "namespace": "test-ns"})
        )

        index = _make_index()
        result = index.fetch(ids=["v1"], namespace="test-ns")

        assert route.called
        assert result.namespace == "test-ns"

    @respx.mock
    def test_fetch_with_metadata(self):
        respx.get(f"{BASE_URL}/vectors/fetch").mock(
            return_value=Response(
                200,
                json={
                    "vectors": {
                        "v1": {
                            "id": "v1",
                            "values": [0.1],
                            "metadata": {"genre": "comedy"},
                        },
                    },
                    "namespace": "",
                },
            )
        )

        index = _make_index()
        result = index.fetch(ids=["v1"])

        assert result.vectors["v1"].metadata == {"genre": "comedy"}


class TestDelete:
    """Tests for Index.delete()."""

    @respx.mock
    def test_delete_by_ids(self):
        route = respx.post(f"{BASE_URL}/vectors/delete").mock(return_value=Response(200, json={}))

        index = _make_index()
        result = index.delete(ids=["v1", "v2"])

        assert route.called
        request = route.calls.last.request
        assert b'"ids"' in request.content
        assert result is not None

    @respx.mock
    def test_delete_all(self):
        route = respx.post(f"{BASE_URL}/vectors/delete").mock(return_value=Response(200, json={}))

        index = _make_index()
        index.delete(delete_all=True, namespace="test-ns")

        request = route.calls.last.request
        assert b'"deleteAll":true' in request.content
        assert b'"namespace":"test-ns"' in request.content

    @respx.mock
    def test_delete_by_filter(self):
        route = respx.post(f"{BASE_URL}/vectors/delete").mock(return_value=Response(200, json={}))

        index = _make_index()
        index.delete(filter={"genre": "comedy"})

        request = route.calls.last.request
        assert b'"filter"' in request.content


class TestUpdate:
    """Tests for Index.update()."""

    @respx.mock
    def test_update_metadata(self):
        route = respx.post(f"{BASE_URL}/vectors/update").mock(
            return_value=Response(200, json={"matchedRecords": 1})
        )

        index = _make_index()
        result = index.update(id="v1", set_metadata={"category": "updated"})

        assert route.called
        assert result.matched_records == 1
        request = route.calls.last.request
        assert b'"setMetadata"' in request.content

    @respx.mock
    def test_update_values(self):
        route = respx.post(f"{BASE_URL}/vectors/update").mock(
            return_value=Response(200, json={"matchedRecords": 1})
        )

        index = _make_index()
        result = index.update(id="v1", values=[0.5, 0.6])

        assert route.called
        assert result.matched_records == 1
        request = route.calls.last.request
        assert b'"values"' in request.content

    @respx.mock
    def test_update_with_namespace(self):
        route = respx.post(f"{BASE_URL}/vectors/update").mock(
            return_value=Response(200, json={"matchedRecords": 1})
        )

        index = _make_index()
        index.update(id="v1", values=[0.5], namespace="test-ns")

        request = route.calls.last.request
        assert b'"namespace":"test-ns"' in request.content


class TestList:
    """Tests for Index.list()."""

    @respx.mock
    def test_list_basic(self):
        route = respx.get(f"{BASE_URL}/vectors/list").mock(
            return_value=Response(
                200,
                json={
                    "vectors": [{"id": "v1"}, {"id": "v2"}],
                    "namespace": "test-ns",
                    "usage": {"readUnits": 1},
                },
            )
        )

        index = _make_index()
        result = index.list(namespace="test-ns")

        assert route.called
        assert len(result.vectors) == 2
        assert result.vectors[0].id == "v1"
        assert result.vectors[1].id == "v2"
        assert result.namespace == "test-ns"

    @respx.mock
    def test_list_with_prefix(self):
        route = respx.get(f"{BASE_URL}/vectors/list").mock(
            return_value=Response(200, json={"vectors": [{"id": "doc#1"}]})
        )

        index = _make_index()
        index.list(prefix="doc#")

        request = route.calls.last.request
        assert "prefix=doc%23" in str(request.url)

    @respx.mock
    def test_list_with_pagination(self):
        respx.get(f"{BASE_URL}/vectors/list").mock(
            return_value=Response(
                200,
                json={
                    "vectors": [{"id": "v3"}],
                    "pagination": {"next": "token-abc"},
                },
            )
        )

        index = _make_index()
        result = index.list(limit=1)

        assert result.pagination_token == "token-abc"


class TestFetchByMetadata:
    """Tests for Index.fetch_by_metadata()."""

    @respx.mock
    def test_fetch_by_metadata_basic(self):
        route = respx.post(f"{BASE_URL}/vectors/fetch_by_metadata").mock(
            return_value=Response(
                200,
                json={
                    "vectors": {
                        "v1": {
                            "id": "v1",
                            "values": [0.1, 0.2],
                            "metadata": {"genre": "comedy"},
                        },
                    },
                    "namespace": "",
                    "usage": {"readUnits": 2},
                },
            )
        )

        index = _make_index()
        result = index.fetch_by_metadata(filter={"genre": "comedy"})

        assert route.called
        assert len(result.vectors) == 1
        assert result.vectors["v1"].metadata == {"genre": "comedy"}
        assert result.usage is not None
        assert result.usage.read_units == 2

    @respx.mock
    def test_fetch_by_metadata_with_pagination(self):
        respx.post(f"{BASE_URL}/vectors/fetch_by_metadata").mock(
            return_value=Response(
                200,
                json={
                    "vectors": {"v1": {"id": "v1", "values": [0.1]}},
                    "namespace": "",
                    "pagination": {"next": "token-xyz"},
                },
            )
        )

        index = _make_index()
        result = index.fetch_by_metadata(filter={"k": "v"}, limit=1)

        assert result.pagination_token == "token-xyz"

    @respx.mock
    def test_fetch_by_metadata_with_namespace(self):
        route = respx.post(f"{BASE_URL}/vectors/fetch_by_metadata").mock(
            return_value=Response(
                200,
                json={"vectors": {}, "namespace": "test-ns"},
            )
        )

        index = _make_index()
        result = index.fetch_by_metadata(namespace="test-ns")

        assert route.called
        assert result.namespace == "test-ns"


class TestDescribeIndexStats:
    """Tests for Index.describe_index_stats()."""

    @respx.mock
    def test_describe_index_stats(self):
        route = respx.post(f"{BASE_URL}/describe_index_stats").mock(
            return_value=Response(
                200,
                json={
                    "namespaces": {
                        "": {"vectorCount": 100},
                        "test-ns": {"vectorCount": 50},
                    },
                    "dimension": 384,
                    "indexFullness": 0.05,
                    "totalVectorCount": 150,
                    "metric": "cosine",
                    "vectorType": "dense",
                },
            )
        )

        index = _make_index()
        result = index.describe_index_stats()

        assert route.called
        assert result.total_vector_count == 150
        assert result.dimension == 384
        assert result.index_fullness == 0.05
        assert result.metric == "cosine"
        assert result.vector_type == "dense"
        assert len(result.namespaces) == 2
        assert result.namespaces[""].vector_count == 100
        assert result.namespaces["test-ns"].vector_count == 50

    @respx.mock
    def test_describe_index_stats_with_filter(self):
        route = respx.post(f"{BASE_URL}/describe_index_stats").mock(
            return_value=Response(
                200,
                json={
                    "namespaces": {},
                    "totalVectorCount": 0,
                },
            )
        )

        index = _make_index()
        index.describe_index_stats(filter={"genre": "comedy"})

        request = route.calls.last.request
        assert b'"filter"' in request.content

    @respx.mock
    def test_describe_index_stats_repr(self):
        respx.post(f"{BASE_URL}/describe_index_stats").mock(
            return_value=Response(
                200,
                json={
                    "namespaces": {"": {"vectorCount": 10}},
                    "dimension": 128,
                    "totalVectorCount": 10,
                },
            )
        )

        index = _make_index()
        result = index.describe_index_stats()

        assert "total_vector_count=10" in repr(result)
        assert "dimension=128" in repr(result)


class TestModelRepr:
    """Tests for model __repr__ methods."""

    @respx.mock
    def test_query_response_repr(self):
        respx.post(f"{BASE_URL}/query").mock(
            return_value=Response(
                200,
                json={"matches": [{"id": "v1", "score": 0.9}], "namespace": "ns"},
            )
        )

        index = _make_index()
        result = index.query(vector=[0.1], top_k=1)

        assert "matches=1" in repr(result)
        assert "namespace='ns'" in repr(result)

    @respx.mock
    def test_fetch_response_repr(self):
        respx.get(f"{BASE_URL}/vectors/fetch").mock(
            return_value=Response(
                200,
                json={"vectors": {"v1": {"id": "v1", "values": [0.1]}}, "namespace": ""},
            )
        )

        index = _make_index()
        result = index.fetch(ids=["v1"])

        assert "vectors=1" in repr(result)

    def test_vector_repr_short(self):
        from pinecone.models.index._common import Vector

        v = Vector(id="test", values=[0.1, 0.2, 0.3])
        assert repr(v) == "Vector(id='test', values=[0.1, 0.2, 0.3])"

    def test_vector_repr_long(self):
        from pinecone.models.index._common import Vector

        v = Vector(id="test", values=[0.1, 0.2, 0.3, 0.4])
        assert repr(v) == "Vector(id='test', values=[0.1, 0.2, ...])"

    def test_scored_vector_repr(self):
        from pinecone.models.index._common import ScoredVector

        sv = ScoredVector(id="test", score=0.95)
        assert repr(sv) == "ScoredVector(id='test', score=0.95)"
