"""
Coverage tests for search_cache.py targeting exception paths and edge cases
not covered by existing test files.

Focuses on:
- _init_db exception handling
- _cleanup_expired success and exception paths
- get/put/invalidate/clear_all/get_stats DB exception paths
- get_or_fetch stale event cleanup
- _dispose_global_cache cleanup and noop-when-None
"""

import threading
from unittest.mock import MagicMock, patch


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_cache(tmpdir):
    """Create a SearchCache backed by a temp directory."""
    from local_deep_research.utilities.search_cache import SearchCache

    return SearchCache(cache_dir=str(tmpdir))


# ---------------------------------------------------------------------------
# _init_db exception path
# ---------------------------------------------------------------------------


class TestInitDbException:
    """_init_db should log and swallow exceptions so the constructor doesn't crash."""

    def test_init_db_exception_is_logged_and_swallowed(self, tmp_path):
        """When create_engine or metadata.create_all raises, _init_db catches it."""
        from local_deep_research.utilities.search_cache import SearchCache

        with patch(
            "local_deep_research.utilities.search_cache.create_engine",
            side_effect=RuntimeError("disk full"),
        ):
            # Should not raise – the exception is caught internally
            cache = SearchCache(cache_dir=str(tmp_path / "bad_db"))
            # engine / Session may not be set, but the object exists
            assert cache.max_memory_items == 1000


# ---------------------------------------------------------------------------
# _cleanup_expired
# ---------------------------------------------------------------------------


class TestCleanupExpired:
    """Tests for the _cleanup_expired helper."""

    def test_cleanup_expired_deletes_old_entries(self, tmp_path):
        """Expired entries are removed from the database."""
        cache = _make_cache(tmp_path)
        # Insert an entry that is already expired (ttl=-10)
        cache.put("old query", [{"title": "stale"}], ttl=-10)
        # The entry won't be returned by get (expired), but row exists in DB.
        cache._cleanup_expired()
        # After cleanup the DB row should be gone; get still returns None.
        result = cache.get("old query")
        assert result is None

    def test_cleanup_expired_logs_deleted_count(self, tmp_path):
        """When entries are deleted, a debug log is emitted."""
        cache = _make_cache(tmp_path)
        cache.put("q1", [{"a": 1}], ttl=-5)
        with patch(
            "local_deep_research.utilities.search_cache.logger"
        ) as mock_logger:
            cache._cleanup_expired()
            # At least one debug call about cleaned entries
            assert mock_logger.debug.called or mock_logger.exception.called

    def test_cleanup_expired_exception_is_caught(self, tmp_path):
        """If the DB query fails, the exception is logged, not raised."""
        cache = _make_cache(tmp_path)
        # Replace Session with one that raises
        original_session = cache.Session
        cache.Session = MagicMock(side_effect=RuntimeError("connection lost"))
        # Should not raise
        cache._cleanup_expired()
        # Restore
        cache.Session = original_session


# ---------------------------------------------------------------------------
# get – DB exception path
# ---------------------------------------------------------------------------


class TestGetDbException:
    """When the database query inside get() fails, it should return None."""

    def test_get_returns_none_on_db_error(self, tmp_path):
        cache = _make_cache(tmp_path)
        # Ensure nothing in memory cache so it falls through to DB
        cache._memory_cache.clear()

        mock_session = MagicMock()
        mock_session.__enter__ = MagicMock(
            side_effect=RuntimeError("db locked")
        )
        mock_session.__exit__ = MagicMock(return_value=False)
        cache.Session = MagicMock(return_value=mock_session)

        result = cache.get("any query")
        assert result is None


# ---------------------------------------------------------------------------
# put – DB exception path
# ---------------------------------------------------------------------------


class TestPutDbException:
    """When the database write inside put() fails, it should return False."""

    def test_put_returns_false_on_db_error(self, tmp_path):
        cache = _make_cache(tmp_path)

        mock_session = MagicMock()
        mock_session.__enter__ = MagicMock(
            side_effect=RuntimeError("read-only")
        )
        mock_session.__exit__ = MagicMock(return_value=False)
        cache.Session = MagicMock(return_value=mock_session)

        result = cache.put("query", [{"data": 1}])
        assert result is False


# ---------------------------------------------------------------------------
# invalidate – DB exception path
# ---------------------------------------------------------------------------


class TestInvalidateDbException:
    """When the database delete inside invalidate() fails, it returns False."""

    def test_invalidate_returns_false_on_db_error(self, tmp_path):
        cache = _make_cache(tmp_path)

        mock_session = MagicMock()
        mock_session.__enter__ = MagicMock(side_effect=RuntimeError("corrupt"))
        mock_session.__exit__ = MagicMock(return_value=False)
        cache.Session = MagicMock(return_value=mock_session)

        result = cache.invalidate("query")
        assert result is False

    def test_invalidate_clears_memory_even_on_db_error(self, tmp_path):
        """Memory cache entries are removed before attempting DB delete."""
        cache = _make_cache(tmp_path)
        # Populate memory cache
        cache.put("query", [{"v": 1}])
        query_hash = cache._get_query_hash("query")
        assert query_hash in cache._memory_cache

        # Now break the DB
        mock_session = MagicMock()
        mock_session.__enter__ = MagicMock(side_effect=RuntimeError("corrupt"))
        mock_session.__exit__ = MagicMock(return_value=False)
        cache.Session = MagicMock(return_value=mock_session)

        cache.invalidate("query")
        # Memory should be cleared even though DB failed
        assert query_hash not in cache._memory_cache


# ---------------------------------------------------------------------------
# clear_all – DB exception path
# ---------------------------------------------------------------------------


class TestClearAllDbException:
    """When the DB delete inside clear_all() fails, it returns False."""

    def test_clear_all_returns_false_on_db_error(self, tmp_path):
        cache = _make_cache(tmp_path)

        mock_session = MagicMock()
        mock_session.__enter__ = MagicMock(side_effect=RuntimeError("locked"))
        mock_session.__exit__ = MagicMock(return_value=False)
        cache.Session = MagicMock(return_value=mock_session)

        result = cache.clear_all()
        assert result is False


# ---------------------------------------------------------------------------
# get_stats – DB exception path
# ---------------------------------------------------------------------------


class TestGetStatsDbException:
    """When the DB query inside get_stats() fails, an error dict is returned."""

    def test_get_stats_returns_error_dict_on_db_failure(self, tmp_path):
        cache = _make_cache(tmp_path)

        mock_session = MagicMock()
        mock_session.__enter__ = MagicMock(side_effect=RuntimeError("gone"))
        mock_session.__exit__ = MagicMock(return_value=False)
        cache.Session = MagicMock(return_value=mock_session)

        stats = cache.get_stats()
        assert "error" in stats
        assert stats["error"] == "Cache stats unavailable"


# ---------------------------------------------------------------------------
# get_or_fetch – stale event cleanup branch
# ---------------------------------------------------------------------------


class TestGetOrFetchStaleEventCleanup:
    """
    When get_or_fetch encounters a stale (already-set) event for a query_hash,
    it should clean up the old event/lock/result and create fresh ones, then
    proceed to fetch.
    """

    def test_stale_event_triggers_fresh_fetch(self, tmp_path):
        cache = _make_cache(tmp_path)
        query = "stale test query"
        query_hash = cache._get_query_hash(query)

        # Pre-populate a stale (already-set) event
        stale_event = threading.Event()
        stale_event.set()  # marks it as completed / stale
        cache._fetch_events[query_hash] = stale_event
        cache._fetch_locks[query_hash] = threading.Lock()
        cache._fetch_results[query_hash] = [{"old": True}]

        fetch_called = threading.Event()

        def mock_fetch():
            fetch_called.set()
            return [{"fresh": True}]

        result = cache.get_or_fetch(query, mock_fetch)

        # The stale event should have been replaced and fetch should have run
        assert fetch_called.is_set()
        assert result == [{"fresh": True}]


# ---------------------------------------------------------------------------
# _dispose_global_cache
# ---------------------------------------------------------------------------


class TestDisposeGlobalCache:
    """Tests for the module-level _dispose_global_cache function."""

    def test_dispose_global_cache_calls_dispose_and_resets(self):
        """When _global_cache is set, dispose() is called and it is set to None."""
        import local_deep_research.utilities.search_cache as sc

        mock_cache = MagicMock()
        original = sc._global_cache
        try:
            sc._global_cache = mock_cache
            sc._dispose_global_cache()
            mock_cache.dispose.assert_called_once()
            assert sc._global_cache is None
        finally:
            sc._global_cache = original

    def test_dispose_global_cache_noop_when_none(self):
        """When _global_cache is already None, nothing happens."""
        import local_deep_research.utilities.search_cache as sc

        original = sc._global_cache
        try:
            sc._global_cache = None
            # Should not raise
            sc._dispose_global_cache()
            assert sc._global_cache is None
        finally:
            sc._global_cache = original


# ---------------------------------------------------------------------------
# clear_all – memory is cleared even when DB fails
# ---------------------------------------------------------------------------


class TestClearAllMemoryCleared:
    """Verify memory caches are cleared before the DB call in clear_all."""

    def test_clear_all_clears_memory_even_on_db_error(self, tmp_path):
        cache = _make_cache(tmp_path)
        cache.put("q", [{"v": 1}])
        assert len(cache._memory_cache) > 0

        # Break the DB
        mock_session = MagicMock()
        mock_session.__enter__ = MagicMock(side_effect=RuntimeError("boom"))
        mock_session.__exit__ = MagicMock(return_value=False)
        cache.Session = MagicMock(return_value=mock_session)

        cache.clear_all()
        # Memory caches should still be cleared (they are cleared before DB call)
        assert len(cache._memory_cache) == 0
        assert len(cache._access_times) == 0
