"""Tests for the Meilisearch backend using mocked SDK."""

from __future__ import annotations

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

import pytest

from search.backends.meilisearch import (
    MeilisearchBackend,
    _build_filter_strings,
    _build_sort,
    _extract_highlights,
)
from search.models import (
    IndexConfig,
    SearchDocument,
    SearchQuery,
)


class FakeIndex:
    """Minimal meilisearch index mock."""

    def __init__(self) -> None:
        self.add_documents = AsyncMock()
        self.delete_document = AsyncMock()
        self.update_searchable_attributes = AsyncMock()
        self.update_filterable_attributes = AsyncMock()
        self.update_sortable_attributes = AsyncMock()
        self.update_ranking_rules = AsyncMock()
        self.search = AsyncMock()


class FakeClient:
    """Minimal meilisearch async client mock."""

    def __init__(self) -> None:
        self._index = FakeIndex()
        self.create_index = AsyncMock()
        self.delete_index_if_exists = AsyncMock()

    def index(self, name: str) -> FakeIndex:
        return self._index


@pytest.fixture()
def client() -> FakeClient:
    return FakeClient()


@pytest.fixture()
def backend(client: FakeClient) -> MeilisearchBackend:
    return MeilisearchBackend(client=client)


class TestMeilisearchCreateIndex:
    async def test_creates_index_and_sets_attributes(
        self, backend: MeilisearchBackend, client: FakeClient
    ):
        config = IndexConfig(
            name="products",
            searchable_fields=["name", "description"],
            filterable_fields=["category"],
            sortable_fields=["price"],
            ranking_rules=["words", "typo"],
        )
        await backend.create_index(config)
        client.create_index.assert_awaited_once()
        idx = client._index
        idx.update_searchable_attributes.assert_awaited_once_with(["name", "description"])
        idx.update_filterable_attributes.assert_awaited_once_with(["category"])
        idx.update_sortable_attributes.assert_awaited_once_with(["price"])
        idx.update_ranking_rules.assert_awaited_once_with(["words", "typo"])

    async def test_create_without_optional_fields(
        self, backend: MeilisearchBackend, client: FakeClient
    ):
        config = IndexConfig(name="minimal")
        await backend.create_index(config)
        idx = client._index
        idx.update_searchable_attributes.assert_not_awaited()


class TestMeilisearchIndex:
    async def test_index_single_document(
        self, backend: MeilisearchBackend, client: FakeClient
    ):
        doc = SearchDocument(id="1", content={"name": "Widget"}, index_name="products")
        await backend.index(doc)
        client._index.add_documents.assert_awaited_once_with(
            [{"id": "1", "name": "Widget"}]
        )


class TestMeilisearchBulkIndex:
    async def test_bulk_index_count(self, backend: MeilisearchBackend):
        docs = [
            SearchDocument(id=str(i), content={"name": f"Item {i}"}, index_name="products")
            for i in range(5)
        ]
        count = await backend.bulk_index(docs)
        assert count == 5

    async def test_bulk_index_empty(self, backend: MeilisearchBackend):
        count = await backend.bulk_index([])
        assert count == 0


class TestMeilisearchDelete:
    async def test_delete_returns_true(
        self, backend: MeilisearchBackend, client: FakeClient
    ):
        result = await backend.delete("products", "1")
        assert result is True

    async def test_delete_returns_false_on_error(
        self, backend: MeilisearchBackend, client: FakeClient
    ):
        client._index.delete_document = AsyncMock(side_effect=Exception("not found"))
        result = await backend.delete("products", "999")
        assert result is False


class TestMeilisearchSearch:
    async def test_basic_search(self, backend: MeilisearchBackend, client: FakeClient):
        search_result = MagicMock()
        search_result.hits = [
            {"id": "1", "name": "Widget", "_formatted": {"name": "<em>Widget</em>"}},
        ]
        search_result.estimated_total_hits = 1
        search_result.facet_distribution = None
        client._index.search = AsyncMock(return_value=search_result)

        response = await backend.search("products", SearchQuery(query="widget"))
        assert response.total == 1
        assert response.results[0].id == "1"
        assert response.results[0].document == {"name": "Widget"}

    async def test_search_with_highlights(
        self, backend: MeilisearchBackend, client: FakeClient
    ):
        search_result = MagicMock()
        search_result.hits = [
            {
                "id": "1",
                "name": "Python Guide",
                "_formatted": {"name": "<em>Python</em> Guide"},
            },
        ]
        search_result.estimated_total_hits = 1
        search_result.facet_distribution = None
        client._index.search = AsyncMock(return_value=search_result)

        response = await backend.search("products", SearchQuery(query="python"))
        assert response.results[0].highlights["name"] == "<em>Python</em> Guide"

    async def test_search_with_facets(
        self, backend: MeilisearchBackend, client: FakeClient
    ):
        search_result = MagicMock()
        search_result.hits = []
        search_result.estimated_total_hits = 0
        search_result.facet_distribution = {"category": {"tech": 5, "food": 3}}
        client._index.search = AsyncMock(return_value=search_result)

        response = await backend.search(
            "products", SearchQuery(query="", facets=["category"])
        )
        assert response.facets["category"]["tech"] == 5


class TestFilterHelpers:
    def test_simple_filter(self):
        result = _build_filter_strings({"status": "active"})
        assert result == ['status = "active"']

    def test_list_filter(self):
        result = _build_filter_strings({"color": ["red", "blue"]})
        assert len(result) == 1
        assert "OR" in result[0]

    def test_empty_filters(self):
        assert _build_filter_strings({}) == []


class TestSortHelper:
    def test_ascending(self):
        assert _build_sort("price") == ["price:asc"]

    def test_descending(self):
        assert _build_sort("-price") == ["price:desc"]

    def test_none(self):
        assert _build_sort(None) == []


class TestHighlightExtraction:
    def test_extracts_highlighted_fields(self):
        formatted = {"name": "<em>Widget</em>", "id": "1", "desc": "plain"}
        original = {"name": "Widget", "id": "1", "desc": "plain"}
        result = _extract_highlights(formatted, original)
        assert "name" in result
        assert "desc" not in result
        assert "id" not in result

    def test_empty_formatted(self):
        assert _extract_highlights({}, {}) == {}
