"""Tests for SearchCache._normalize_query and _evict_lru_memory.

_normalize_query handles query normalization for consistent caching.
_evict_lru_memory manages memory cache size via LRU eviction.
Both have zero prior test coverage.
"""

import pytest

from local_deep_research.utilities.search_cache import SearchCache


@pytest.fixture
def cache(tmp_path):
    """Create a SearchCache with a temp database."""
    return SearchCache(
        cache_dir=str(tmp_path / "search_cache"),
        default_ttl=3600,
        max_memory_items=5,
    )


# ── _normalize_query ──


class TestNormalizeQuery:
    """Tests for _normalize_query method."""

    def test_converts_to_lowercase(self, cache):
        assert cache._normalize_query("Hello World") == "hello world"

    def test_strips_whitespace(self, cache):
        assert cache._normalize_query("  hello  ") == "hello"

    def test_collapses_internal_whitespace(self, cache):
        assert (
            cache._normalize_query("hello   world   test") == "hello world test"
        )

    def test_removes_double_quotes(self, cache):
        assert cache._normalize_query('"hello world"') == "hello world"

    def test_removes_single_quotes(self, cache):
        assert cache._normalize_query("it's a test") == "its a test"

    def test_preserves_other_punctuation(self, cache):
        assert cache._normalize_query("hello, world!") == "hello, world!"

    def test_empty_string(self, cache):
        assert cache._normalize_query("") == ""

    def test_tabs_and_newlines_collapsed(self, cache):
        assert (
            cache._normalize_query("hello\tworld\ntest") == "hello world test"
        )

    def test_unicode_preserved(self, cache):
        result = cache._normalize_query("Héllo Wörld")
        assert result == "héllo wörld"

    def test_mixed_quotes_all_removed(self, cache):
        result = cache._normalize_query("\"it's\" a 'test'")
        assert '"' not in result
        assert "'" not in result


# ── _evict_lru_memory ──


class TestEvictLruMemory:
    """Tests for _evict_lru_memory method."""

    def test_no_eviction_when_under_limit(self, cache):
        cache._memory_cache = {"a": 1, "b": 2}
        cache._access_times = {"a": 100, "b": 200}
        cache.max_memory_items = 5

        cache._evict_lru_memory()

        assert len(cache._memory_cache) == 2

    def test_eviction_when_over_limit(self, cache):
        cache.max_memory_items = 3
        # Add 5 items
        for i in range(5):
            key = f"key_{i}"
            cache._memory_cache[key] = {"data": i}
            cache._access_times[key] = i * 100  # Older items have lower times

        cache._evict_lru_memory()

        # Should have evicted to get below limit
        assert len(cache._memory_cache) <= cache.max_memory_items

    def test_evicts_oldest_items_first(self, cache):
        # _evict_lru_memory removes (count - max + 100) items for efficiency.
        # With max=200 and count=250: removes 250-200+100=150 oldest items.
        cache.max_memory_items = 200
        for i in range(250):
            cache._memory_cache[f"key_{i}"] = {"data": i}
            cache._access_times[f"key_{i}"] = i

        cache._evict_lru_memory()

        remaining = set(cache._memory_cache.keys())
        # 100 items should survive (250 - 150 removed)
        assert len(remaining) == 100
        # Remaining items should be the newest (highest access times)
        for key in remaining:
            idx = int(key.split("_")[1])
            assert idx >= 150  # Only items 150-249 should survive

    def test_eviction_removes_from_both_caches(self, cache):
        cache.max_memory_items = 1
        cache._memory_cache = {"a": 1, "b": 2, "c": 3}
        cache._access_times = {"a": 100, "b": 200, "c": 300}

        cache._evict_lru_memory()

        # Evicted items should be removed from both dicts
        for key in list(cache._memory_cache.keys()):
            assert key in cache._access_times

    def test_no_eviction_at_exact_limit(self, cache):
        cache.max_memory_items = 3
        cache._memory_cache = {"a": 1, "b": 2, "c": 3}
        cache._access_times = {"a": 100, "b": 200, "c": 300}

        cache._evict_lru_memory()

        assert len(cache._memory_cache) == 3

    def test_empty_cache_no_error(self, cache):
        cache._memory_cache = {}
        cache._access_times = {}
        cache.max_memory_items = 5

        cache._evict_lru_memory()  # Should not raise

        assert len(cache._memory_cache) == 0


# ── _get_query_hash ──


class TestGetQueryHash:
    """Tests for _get_query_hash determinism and partitioning."""

    def test_same_query_same_hash(self, cache):
        h1 = cache._get_query_hash("hello world")
        h2 = cache._get_query_hash("hello world")
        assert h1 == h2

    def test_case_insensitive_hashing(self, cache):
        h1 = cache._get_query_hash("Hello World")
        h2 = cache._get_query_hash("hello world")
        assert h1 == h2

    def test_different_engines_different_hashes(self, cache):
        h1 = cache._get_query_hash("test", "google")
        h2 = cache._get_query_hash("test", "bing")
        assert h1 != h2

    def test_whitespace_normalized_before_hashing(self, cache):
        h1 = cache._get_query_hash("hello  world")
        h2 = cache._get_query_hash("hello world")
        assert h1 == h2

    def test_quotes_stripped_before_hashing(self, cache):
        h1 = cache._get_query_hash('"hello world"')
        h2 = cache._get_query_hash("hello world")
        assert h1 == h2
