"""Tests for the in-memory search backend."""

from __future__ import annotations

import pytest

from search.backends.memory import MemoryBackend
from search.models import (
    IndexConfig,
    SearchDocument,
    SearchQuery,
)


@pytest.fixture()
async def backend() -> MemoryBackend:
    b = MemoryBackend()
    await b.create_index(
        IndexConfig(
            name="articles",
            searchable_fields=["title", "body"],
            filterable_fields=["category", "status"],
            sortable_fields=["title"],
        )
    )
    return b


@pytest.fixture()
async def populated_backend(backend: MemoryBackend) -> MemoryBackend:
    docs = [
        SearchDocument(
            id="1",
            content={"title": "Python Guide", "body": "Learn Python programming", "category": "tech"},
            index_name="articles",
        ),
        SearchDocument(
            id="2",
            content={"title": "Rust Basics", "body": "Systems programming with Rust", "category": "tech"},
            index_name="articles",
        ),
        SearchDocument(
            id="3",
            content={"title": "Cooking Tips", "body": "How to make pasta at home", "category": "food"},
            index_name="articles",
        ),
        SearchDocument(
            id="4",
            content={
                "title": "Advanced Python",
                "body": "Async Python patterns and generators",
                "category": "tech",
                "status": "draft",
            },
            index_name="articles",
        ),
    ]
    await backend.bulk_index(docs)
    return backend


class TestIndexManagement:
    async def test_create_and_delete_index(self):
        backend = MemoryBackend()
        config = IndexConfig(name="test_idx", searchable_fields=["name"])
        await backend.create_index(config)
        assert "test_idx" in backend._indexes

        await backend.delete_index("test_idx")
        assert "test_idx" not in backend._indexes

    async def test_delete_nonexistent_index(self):
        backend = MemoryBackend()
        await backend.delete_index("nonexistent")  # should not raise


class TestIndexing:
    async def test_index_single_document(self, backend: MemoryBackend):
        doc = SearchDocument(id="1", content={"title": "Test"}, index_name="articles")
        await backend.index(doc)
        assert "1" in backend._documents["articles"]

    async def test_upsert_overwrites(self, backend: MemoryBackend):
        doc1 = SearchDocument(id="1", content={"title": "V1"}, index_name="articles")
        doc2 = SearchDocument(id="1", content={"title": "V2"}, index_name="articles")
        await backend.index(doc1)
        await backend.index(doc2)
        assert backend._documents["articles"]["1"].content["title"] == "V2"

    async def test_bulk_index(self, backend: MemoryBackend):
        docs = [
            SearchDocument(id=str(i), content={"title": f"Doc {i}"}, index_name="articles")
            for i in range(10)
        ]
        count = await backend.bulk_index(docs)
        assert count == 10
        assert len(backend._documents["articles"]) == 10

    async def test_delete_document(self, backend: MemoryBackend):
        doc = SearchDocument(id="1", content={"title": "Test"}, index_name="articles")
        await backend.index(doc)
        assert await backend.delete("articles", "1") is True
        assert await backend.delete("articles", "1") is False

    async def test_delete_from_nonexistent_index(self, backend: MemoryBackend):
        assert await backend.delete("nonexistent", "1") is False


class TestSearch:
    async def test_basic_search(self, populated_backend: MemoryBackend):
        result = await populated_backend.search("articles", SearchQuery(query="Python"))
        assert result.total == 2
        ids = {r.id for r in result.results}
        assert "1" in ids
        assert "4" in ids

    async def test_empty_query_returns_all(self, populated_backend: MemoryBackend):
        result = await populated_backend.search("articles", SearchQuery(query=""))
        assert result.total == 4

    async def test_no_matches(self, populated_backend: MemoryBackend):
        result = await populated_backend.search("articles", SearchQuery(query="javascript"))
        assert result.total == 0
        assert result.results == []

    async def test_search_respects_searchable_fields(self, populated_backend: MemoryBackend):
        result = await populated_backend.search("articles", SearchQuery(query="tech"))
        # "tech" is in category, not in searchable_fields (title, body)
        assert result.total == 0

    async def test_search_nonexistent_index(self, populated_backend: MemoryBackend):
        result = await populated_backend.search("nonexistent", SearchQuery(query="test"))
        assert result.total == 0

    async def test_query_time_tracked(self, populated_backend: MemoryBackend):
        result = await populated_backend.search("articles", SearchQuery(query="Python"))
        assert result.query_time_ms >= 0


class TestFiltering:
    async def test_filter_by_exact_value(self, populated_backend: MemoryBackend):
        result = await populated_backend.search(
            "articles", SearchQuery(query="", filters={"category": "food"})
        )
        assert result.total == 1
        assert result.results[0].id == "3"

    async def test_filter_by_list(self, populated_backend: MemoryBackend):
        result = await populated_backend.search(
            "articles", SearchQuery(query="", filters={"category": ["tech", "food"]})
        )
        assert result.total == 4

    async def test_combined_query_and_filter(self, populated_backend: MemoryBackend):
        result = await populated_backend.search(
            "articles", SearchQuery(query="Python", filters={"status": "draft"})
        )
        assert result.total == 1
        assert result.results[0].id == "4"


class TestPagination:
    async def test_page_1(self, populated_backend: MemoryBackend):
        result = await populated_backend.search(
            "articles", SearchQuery(query="", per_page=2, page=1)
        )
        assert len(result.results) == 2
        assert result.total == 4
        assert result.page == 1

    async def test_page_2(self, populated_backend: MemoryBackend):
        result = await populated_backend.search(
            "articles", SearchQuery(query="", per_page=2, page=2)
        )
        assert len(result.results) == 2
        assert result.page == 2

    async def test_beyond_last_page(self, populated_backend: MemoryBackend):
        result = await populated_backend.search(
            "articles", SearchQuery(query="", per_page=2, page=10)
        )
        assert len(result.results) == 0
        assert result.total == 4


class TestSorting:
    async def test_sort_ascending(self, populated_backend: MemoryBackend):
        result = await populated_backend.search(
            "articles", SearchQuery(query="", sort_by="title")
        )
        titles = [r.document["title"] for r in result.results]
        assert titles == sorted(titles)

    async def test_sort_descending(self, populated_backend: MemoryBackend):
        result = await populated_backend.search(
            "articles", SearchQuery(query="", sort_by="-title")
        )
        titles = [r.document["title"] for r in result.results]
        assert titles == sorted(titles, reverse=True)
