"""Tests for search cache persistence and caching edge cases.

These tests exercise the SearchCache's persistence across instances,
the get_or_fetch lifecycle with falsy results, and JSON serialization
roundtrips through SQLite.
"""

import pytest

from local_deep_research.utilities.search_cache import SearchCache


@pytest.fixture
def cache_dir(tmp_path):
    """Provide a temporary cache directory."""
    return str(tmp_path / "test_cache")


@pytest.fixture
def cache(cache_dir):
    """Provide a SearchCache instance with a temporary directory."""
    c = SearchCache(cache_dir=cache_dir)
    yield c
    c.dispose()


class TestPersistenceAcrossInstances:
    """Tests for data surviving across cache instance lifecycles."""

    def test_persistence_across_cache_instances(self, cache_dir):
        """Data put with instance 1 survives after dispose and re-creation.
        This is the fundamental purpose of the persistent cache."""
        cache1 = SearchCache(cache_dir=cache_dir)
        results = [{"title": "Persistent Result", "url": "https://example.com"}]
        cache1.put("persistent query", results, "test_engine")
        cache1.dispose()

        # Create a new instance with the same directory
        cache2 = SearchCache(cache_dir=cache_dir)
        retrieved = cache2.get("persistent query", "test_engine")
        cache2.dispose()

        assert retrieved is not None
        assert len(retrieved) == 1
        assert retrieved[0]["title"] == "Persistent Result"


class TestGetOrFetchWithFalsyResults:
    """Tests for get_or_fetch behavior with empty/None results."""

    def test_empty_list_result_not_cached_via_get_or_fetch(self, cache):
        """fetch_func returns [] (falsy) — it's returned to caller but NOT
        cached, causing re-fetch on next call. This documents the behavior
        where put() rejects empty results."""
        call_count = 0

        def fetch_empty():
            nonlocal call_count
            call_count += 1
            return []

        # First call: fetches and returns []
        result1 = cache.get_or_fetch("empty query", fetch_empty, "test_engine")
        assert result1 == []
        assert call_count == 1

        # Clear memory cache to force DB lookup
        cache._memory_cache.clear()

        # Second call: fetches AGAIN because [] wasn't cached
        result2 = cache.get_or_fetch("empty query", fetch_empty, "test_engine")
        assert result2 == []
        assert (
            call_count == 2
        )  # Called again because empty results aren't cached

    def test_fetch_returning_none_path(self, cache):
        """fetch_func returning None (not exception) goes through the full
        stampede protection path and returns None."""

        def fetch_none():
            return None

        result = cache.get_or_fetch("none query", fetch_none, "test_engine")
        assert result is None

    def test_fetch_exception_returns_none(self, cache):
        """fetch_func raising an exception returns None (not the exception)."""

        def fetch_error():
            raise RuntimeError("network error")

        result = cache.get_or_fetch("error query", fetch_error, "test_engine")
        assert result is None


class TestMemoryCacheMasksDbAccess:
    """Tests documenting memory cache interaction with DB stats."""

    def test_memory_cache_masks_db_access_count(self, cache):
        """Memory cache satisfies reads, so DB access_count never increments
        beyond the initial put. This documents a stats limitation."""
        results = [{"title": "Test", "url": "https://example.com"}]
        cache.put("stats query", results, "test_engine")

        # Multiple reads from memory cache
        for _ in range(5):
            cache.get("stats query", "test_engine")

        # Check DB access_count - should be 1 (from initial put)
        # because all reads were served from memory cache
        with cache.Session() as session:
            from local_deep_research.database.models import (
                SearchCache as SearchCacheModel,
            )

            query_hash = cache._get_query_hash("stats query", "test_engine")
            entry = (
                session.query(SearchCacheModel)
                .filter_by(query_hash=query_hash)
                .first()
            )
            assert entry is not None
            # access_count stays at 1 because memory cache serves all reads
            assert entry.access_count == 1


class TestJsonSerializationRoundtrip:
    """Tests for complex data surviving SQLite JSON serialization."""

    def test_complex_nested_results_json_roundtrip(self, cache_dir):
        """Deeply nested structures with None, unicode, and special chars
        survive the SQLite JSON serialization roundtrip."""
        complex_results = [
            {
                "title": "Unicode Test: \u00e4\u00f6\u00fc \u00df \u2603 \u2764",
                "url": "https://example.com/path?q=test&lang=en",
                "metadata": {
                    "nested": {
                        "deep": {
                            "value": 42,
                            "list": [1, None, "three", True, False],
                        }
                    },
                    "empty_dict": {},
                    "empty_list": [],
                    "null_value": None,
                },
                "snippet": 'Text with "quotes" and\nnewlines\tand\ttabs',
            }
        ]

        cache = SearchCache(cache_dir=cache_dir)
        cache.put("complex query", complex_results, "test_engine")
        cache.dispose()

        # Retrieve from a fresh instance (forces DB read)
        cache2 = SearchCache(cache_dir=cache_dir)
        retrieved = cache2.get("complex query", "test_engine")
        cache2.dispose()

        assert retrieved is not None
        assert len(retrieved) == 1
        result = retrieved[0]
        assert "\u00e4\u00f6\u00fc" in result["title"]
        assert result["metadata"]["nested"]["deep"]["value"] == 42
        assert result["metadata"]["nested"]["deep"]["list"] == [
            1,
            None,
            "three",
            True,
            False,
        ]
        assert result["metadata"]["null_value"] is None
        assert "\n" in result["snippet"]
