"""Tests for the PostgreSQL backend using mocked asyncpg."""

from __future__ import annotations

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

import pytest

from search.backends.postgres import PostgresBackend, _table_name
from search.models import (
    IndexConfig,
    SearchDocument,
    SearchQuery,
)


class FakeConnection:
    """Minimal asyncpg connection mock."""

    def __init__(self) -> None:
        self.execute = AsyncMock(return_value="DELETE 1")
        self.executemany = AsyncMock()
        self.fetch = AsyncMock(return_value=[])
        self.fetchrow = AsyncMock(return_value={"cnt": 0})


class FakePool:
    """Minimal asyncpg pool mock that yields a FakeConnection via acquire()."""

    def __init__(self) -> None:
        self._conn = FakeConnection()
        self.close = AsyncMock()

    def acquire(self) -> Any:
        conn = self._conn

        class AcquireCtx:
            async def __aenter__(self_ctx):
                return conn

            async def __aexit__(self_ctx, *args):
                pass

        return AcquireCtx()


@pytest.fixture()
def pool() -> FakePool:
    return FakePool()


@pytest.fixture()
def backend(pool: FakePool) -> PostgresBackend:
    return PostgresBackend(pool=pool)


class TestTableName:
    def test_simple_name(self):
        assert _table_name("articles") == "search_articles"

    def test_name_with_special_chars(self):
        assert _table_name("my-index.v2") == "search_my_index_v2"

    def test_name_with_spaces(self):
        assert _table_name("my index") == "search_my_index"


class TestPostgresCreateIndex:
    async def test_creates_table_and_indexes(self, backend: PostgresBackend, pool: FakePool):
        config = IndexConfig(
            name="articles",
            searchable_fields=["title", "body"],
            filterable_fields=["category"],
        )
        await backend.create_index(config)
        conn = pool._conn
        # Should have called execute multiple times: extension, table, gin idx, content idx, filterable idx
        assert conn.execute.call_count >= 4

    async def test_stores_config(self, backend: PostgresBackend):
        config = IndexConfig(name="test", searchable_fields=["name"])
        await backend.create_index(config)
        assert "test" in backend._configs


class TestPostgresIndex:
    async def test_index_calls_upsert(self, backend: PostgresBackend, pool: FakePool):
        doc = SearchDocument(id="1", content={"title": "Hello"}, index_name="articles")
        await backend.index(doc)
        pool._conn.execute.assert_called()
        call_args = pool._conn.execute.call_args
        assert "INSERT INTO" in call_args[0][0]
        assert "ON CONFLICT" in call_args[0][0]


class TestPostgresBulkIndex:
    async def test_bulk_returns_count(self, backend: PostgresBackend):
        docs = [
            SearchDocument(id=str(i), content={"title": f"Doc {i}"}, index_name="articles")
            for i in range(5)
        ]
        count = await backend.bulk_index(docs)
        assert count == 5

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

    async def test_bulk_groups_by_index(self, backend: PostgresBackend, pool: FakePool):
        docs = [
            SearchDocument(id="1", content={"t": "A"}, index_name="idx1"),
            SearchDocument(id="2", content={"t": "B"}, index_name="idx2"),
        ]
        count = await backend.bulk_index(docs)
        assert count == 2
        assert pool._conn.executemany.call_count == 2


class TestPostgresDelete:
    async def test_delete_existing(self, backend: PostgresBackend, pool: FakePool):
        pool._conn.execute = AsyncMock(return_value="DELETE 1")
        result = await backend.delete("articles", "1")
        assert result is True

    async def test_delete_nonexistent(self, backend: PostgresBackend, pool: FakePool):
        pool._conn.execute = AsyncMock(return_value="DELETE 0")
        result = await backend.delete("articles", "999")
        assert result is False


class TestPostgresSearch:
    async def test_search_with_query(self, backend: PostgresBackend, pool: FakePool):
        pool._conn.fetchrow = AsyncMock(return_value={"cnt": 1})
        pool._conn.fetch = AsyncMock(
            return_value=[
                {"id": "1", "content": '{"title": "Test"}', "score": 0.5},
            ]
        )
        config = IndexConfig(name="articles", searchable_fields=["title"])
        await backend.create_index(config)

        result = await backend.search("articles", SearchQuery(query="test", highlight=False))
        assert result.total == 1
        assert result.results[0].id == "1"
        assert result.results[0].score == 0.5

    async def test_search_empty_query(self, backend: PostgresBackend, pool: FakePool):
        pool._conn.fetchrow = AsyncMock(return_value={"cnt": 0})
        pool._conn.fetch = AsyncMock(return_value=[])

        result = await backend.search("articles", SearchQuery(query=""))
        assert result.total == 0
        assert result.results == []


class TestPostgresClose:
    async def test_close_calls_pool_close(self, backend: PostgresBackend, pool: FakePool):
        await backend.close()
        pool.close.assert_awaited_once()


class TestPostgresDeleteIndex:
    async def test_delete_index(self, backend: PostgresBackend, pool: FakePool):
        backend._configs["test"] = IndexConfig(name="test")
        await backend.delete_index("test")
        assert "test" not in backend._configs
        pool._conn.execute.assert_called()
