"""
Unit tests for EntityResolver pg_trgm auto-detection (PR #626/#649).

These tests verify:
1. When entity_lookup="trigram" and pg_trgm IS available, the trigram path is used.
2. When entity_lookup="trigram" and pg_trgm is NOT available, the resolver falls back
   to entity_lookup="full" and uses the full-scan path.
3. The pg_trgm check is only performed once (_pg_trgm_checked flag prevents re-checking).
4. When entity_lookup="full" from the start, the trgm check is never performed.
"""

from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from hindsight_api.engine.entity_resolver import EntityResolver


def _make_conn(pg_trgm_available: bool) -> MagicMock:
    """Create a minimal mock asyncpg connection for the pg_trgm availability check."""
    conn = MagicMock()
    conn.fetchval = AsyncMock(return_value=pg_trgm_available)
    conn.fetch = AsyncMock(return_value=[])
    conn.executemany = AsyncMock()
    conn.fetchrow = AsyncMock(return_value=None)
    return conn


def _make_resolver(entity_lookup: str = "trigram") -> EntityResolver:
    """Return an EntityResolver with a None pool (not needed for unit tests)."""
    return EntityResolver(pool=None, entity_lookup=entity_lookup)  # type: ignore[arg-type]


class TestPgTrgmAutoDetection:
    """Unit tests for pg_trgm detection logic inside _resolve_entities_batch_impl."""

    @pytest.mark.asyncio
    async def test_falls_back_to_full_when_pg_trgm_unavailable(self):
        """When pg_trgm is absent the resolver switches to 'full' and calls the full-scan path."""
        resolver = _make_resolver(entity_lookup="trigram")
        conn = _make_conn(pg_trgm_available=False)

        with (
            patch.object(resolver, "_resolve_entities_batch_full", new=AsyncMock(return_value=[])) as mock_full,
            patch.object(resolver, "_resolve_entities_batch_trigram", new=AsyncMock(return_value=[])) as mock_trgm,
        ):
            await resolver._resolve_entities_batch_impl(
                conn=conn,
                bank_id="test-bank",
                entities_data=[],
                context="",
                unit_event_date=None,
            )

        # Trigram path must NOT be called
        mock_trgm.assert_not_called()
        # Full-scan path must be called as the fallback
        mock_full.assert_called_once()
        # Strategy is permanently downgraded
        assert resolver.entity_lookup == "full"
        assert resolver._pg_trgm_checked is True

    @pytest.mark.asyncio
    async def test_uses_trigram_when_pg_trgm_available(self):
        """When pg_trgm is present the trigram path is used."""
        resolver = _make_resolver(entity_lookup="trigram")
        conn = _make_conn(pg_trgm_available=True)

        with (
            patch.object(resolver, "_resolve_entities_batch_full", new=AsyncMock(return_value=[])) as mock_full,
            patch.object(resolver, "_resolve_entities_batch_trigram", new=AsyncMock(return_value=[])) as mock_trgm,
        ):
            await resolver._resolve_entities_batch_impl(
                conn=conn,
                bank_id="test-bank",
                entities_data=[],
                context="",
                unit_event_date=None,
            )

        mock_trgm.assert_called_once()
        mock_full.assert_not_called()
        assert resolver.entity_lookup == "trigram"
        assert resolver._pg_trgm_checked is True

    @pytest.mark.asyncio
    async def test_pg_trgm_check_performed_only_once(self):
        """The fetchval check is only issued on the first call; subsequent calls skip it."""
        resolver = _make_resolver(entity_lookup="trigram")
        conn = _make_conn(pg_trgm_available=True)

        with patch.object(resolver, "_resolve_entities_batch_trigram", new=AsyncMock(return_value=[])):
            # First call — check is issued
            await resolver._resolve_entities_batch_impl(
                conn=conn,
                bank_id="test-bank",
                entities_data=[],
                context="",
                unit_event_date=None,
            )
            # Second call — check must NOT be issued again
            await resolver._resolve_entities_batch_impl(
                conn=conn,
                bank_id="test-bank",
                entities_data=[],
                context="",
                unit_event_date=None,
            )

        # fetchval (the pg_trgm availability query) should be called exactly once
        assert conn.fetchval.call_count == 1

    @pytest.mark.asyncio
    async def test_full_strategy_skips_pg_trgm_check(self):
        """When entity_lookup='full' from the start, no pg_trgm check is ever issued."""
        resolver = _make_resolver(entity_lookup="full")
        conn = _make_conn(pg_trgm_available=False)

        with patch.object(resolver, "_resolve_entities_batch_full", new=AsyncMock(return_value=[])):
            await resolver._resolve_entities_batch_impl(
                conn=conn,
                bank_id="test-bank",
                entities_data=[],
                context="",
                unit_event_date=None,
            )

        # fetchval should never be called when entity_lookup is already "full"
        conn.fetchval.assert_not_called()

    @pytest.mark.asyncio
    async def test_fallback_is_sticky_across_calls(self):
        """After falling back to 'full', subsequent calls also use the full path."""
        resolver = _make_resolver(entity_lookup="trigram")
        conn = _make_conn(pg_trgm_available=False)

        with (
            patch.object(resolver, "_resolve_entities_batch_full", new=AsyncMock(return_value=[])) as mock_full,
            patch.object(resolver, "_resolve_entities_batch_trigram", new=AsyncMock(return_value=[])) as mock_trgm,
        ):
            # First call triggers the fallback
            await resolver._resolve_entities_batch_impl(
                conn=conn,
                bank_id="b",
                entities_data=[],
                context="",
                unit_event_date=None,
            )
            # Second call — _pg_trgm_checked is True so no re-check; entity_lookup=="full"
            await resolver._resolve_entities_batch_impl(
                conn=conn,
                bank_id="b",
                entities_data=[],
                context="",
                unit_event_date=None,
            )

        # Trigram path is never called
        mock_trgm.assert_not_called()
        # Full-scan path is called both times
        assert mock_full.call_count == 2
        # pg_trgm check was issued exactly once
        assert conn.fetchval.call_count == 1
