# tests/unit/services/memory/cache/test_cache_manager.py
"""Tests for CacheManager."""

from unittest.mock import MagicMock, patch
from uuid import uuid4

import pytest

from app.services.memory.cache.cache_manager import (
    CacheManager,
    CacheStats,
    get_cache_manager,
    reset_cache_manager,
)
from app.services.memory.cache.embedding_cache import EmbeddingCache
from app.services.memory.cache.hot_cache import HotMemoryCache

pytestmark = pytest.mark.asyncio(loop_scope="function")


@pytest.fixture(autouse=True)
def reset_singleton() -> None:
    """Reset singleton before each test."""
    reset_cache_manager()


class TestCacheStats:
    """Tests for CacheStats."""

    def test_to_dict(self) -> None:
        """Should convert to dictionary."""
        from datetime import UTC, datetime

        stats = CacheStats(
            hot_cache={"hits": 10},
            embedding_cache={"hits": 20},
            overall_hit_rate=0.75,
            last_cleanup=datetime.now(UTC),
            cleanup_count=5,
        )

        result = stats.to_dict()

        assert result["hot_cache"] == {"hits": 10}
        assert result["overall_hit_rate"] == 0.75
        assert result["cleanup_count"] == 5
        assert result["last_cleanup"] is not None


class TestCacheManager:
    """Tests for CacheManager."""

    @pytest.fixture
    def manager(self) -> CacheManager:
        """Create a cache manager."""
        return CacheManager()

    def test_is_enabled(self, manager: CacheManager) -> None:
        """Should check if caching is enabled."""
        # Default is enabled from settings
        assert manager.is_enabled is True

    def test_has_hot_cache(self, manager: CacheManager) -> None:
        """Should have hot memory cache."""
        assert manager.hot_cache is not None
        assert isinstance(manager.hot_cache, HotMemoryCache)

    def test_has_embedding_cache(self, manager: CacheManager) -> None:
        """Should have embedding cache."""
        assert manager.embedding_cache is not None
        assert isinstance(manager.embedding_cache, EmbeddingCache)

    def test_cache_memory(self, manager: CacheManager) -> None:
        """Should cache memory in hot cache."""
        memory_id = uuid4()
        memory = {"task": "test", "data": "value"}

        manager.cache_memory("episodic", memory_id, memory)
        result = manager.get_memory("episodic", memory_id)

        assert result == memory

    def test_cache_memory_with_scope(self, manager: CacheManager) -> None:
        """Should cache memory with scope."""
        memory_id = uuid4()
        memory = {"task": "test"}

        manager.cache_memory("semantic", memory_id, memory, scope="proj-123")
        result = manager.get_memory("semantic", memory_id, scope="proj-123")

        assert result == memory

    async def test_cache_embedding(self, manager: CacheManager) -> None:
        """Should cache embedding."""
        content = "test content"
        embedding = [0.1, 0.2, 0.3]

        content_hash = await manager.cache_embedding(content, embedding)
        result = await manager.get_embedding(content)

        assert result == embedding
        assert len(content_hash) == 32

    async def test_invalidate_memory(self, manager: CacheManager) -> None:
        """Should invalidate memory from hot cache."""
        memory_id = uuid4()
        manager.cache_memory("episodic", memory_id, {"data": "test"})

        count = await manager.invalidate_memory("episodic", memory_id)

        assert count >= 1
        assert manager.get_memory("episodic", memory_id) is None

    async def test_invalidate_by_type(self, manager: CacheManager) -> None:
        """Should invalidate all entries of a type."""
        manager.cache_memory("episodic", uuid4(), {"data": "1"})
        manager.cache_memory("episodic", uuid4(), {"data": "2"})
        manager.cache_memory("semantic", uuid4(), {"data": "3"})

        count = await manager.invalidate_by_type("episodic")

        assert count >= 2

    async def test_invalidate_by_scope(self, manager: CacheManager) -> None:
        """Should invalidate all entries in a scope."""
        manager.cache_memory("episodic", uuid4(), {"data": "1"}, scope="proj-1")
        manager.cache_memory("semantic", uuid4(), {"data": "2"}, scope="proj-1")
        manager.cache_memory("episodic", uuid4(), {"data": "3"}, scope="proj-2")

        count = await manager.invalidate_by_scope("proj-1")

        assert count >= 2

    async def test_invalidate_embedding(self, manager: CacheManager) -> None:
        """Should invalidate cached embedding."""
        content = "test content"
        await manager.cache_embedding(content, [0.1, 0.2])

        result = await manager.invalidate_embedding(content)

        assert result is True
        assert await manager.get_embedding(content) is None

    async def test_clear_all(self, manager: CacheManager) -> None:
        """Should clear all caches."""
        manager.cache_memory("episodic", uuid4(), {"data": "test"})
        await manager.cache_embedding("content", [0.1])

        count = await manager.clear_all()

        assert count >= 2

    async def test_cleanup_expired(self, manager: CacheManager) -> None:
        """Should clean up expired entries."""
        count = await manager.cleanup_expired()

        # May be 0 if no expired entries
        assert count >= 0
        assert manager._cleanup_count == 1
        assert manager._last_cleanup is not None

    def test_get_stats(self, manager: CacheManager) -> None:
        """Should return aggregated statistics."""
        manager.cache_memory("episodic", uuid4(), {"data": "test"})

        stats = manager.get_stats()

        assert "hot_cache" in stats.to_dict()
        assert "embedding_cache" in stats.to_dict()
        assert "overall_hit_rate" in stats.to_dict()

    def test_get_hot_memories(self, manager: CacheManager) -> None:
        """Should return most accessed memories."""
        id1 = uuid4()
        id2 = uuid4()

        manager.cache_memory("episodic", id1, {"data": "1"})
        manager.cache_memory("episodic", id2, {"data": "2"})

        # Access first multiple times
        for _ in range(5):
            manager.get_memory("episodic", id1)

        hot = manager.get_hot_memories(limit=2)

        assert len(hot) == 2

    def test_reset_stats(self, manager: CacheManager) -> None:
        """Should reset all statistics."""
        manager.cache_memory("episodic", uuid4(), {"data": "test"})
        manager.get_memory("episodic", uuid4())  # Miss

        manager.reset_stats()

        stats = manager.get_stats()
        assert stats.hot_cache.get("hits", 0) == 0

    async def test_warmup(self, manager: CacheManager) -> None:
        """Should warm up cache with memories."""
        memories = [
            ("episodic", uuid4(), {"data": "1"}),
            ("episodic", uuid4(), {"data": "2"}),
            ("semantic", uuid4(), {"data": "3"}),
        ]

        count = await manager.warmup(memories)

        assert count == 3


class TestCacheManagerWithRetrieval:
    """Tests for CacheManager with retrieval cache."""

    @pytest.fixture
    def mock_retrieval_cache(self) -> MagicMock:
        """Create mock retrieval cache."""
        cache = MagicMock()
        cache.invalidate_by_memory = MagicMock(return_value=1)
        cache.clear = MagicMock(return_value=5)
        cache.get_stats = MagicMock(return_value={"entries": 10})
        return cache

    @pytest.fixture
    def manager_with_retrieval(
        self,
        mock_retrieval_cache: MagicMock,
    ) -> CacheManager:
        """Create manager with retrieval cache."""
        manager = CacheManager()
        manager.set_retrieval_cache(mock_retrieval_cache)
        return manager

    async def test_invalidate_clears_retrieval(
        self,
        manager_with_retrieval: CacheManager,
        mock_retrieval_cache: MagicMock,
    ) -> None:
        """Should invalidate retrieval cache entries."""
        memory_id = uuid4()

        await manager_with_retrieval.invalidate_memory("episodic", memory_id)

        mock_retrieval_cache.invalidate_by_memory.assert_called_once_with(memory_id)

    def test_stats_includes_retrieval(
        self,
        manager_with_retrieval: CacheManager,
    ) -> None:
        """Should include retrieval cache stats."""
        stats = manager_with_retrieval.get_stats()

        assert "retrieval_cache" in stats.to_dict()


class TestCacheManagerDisabled:
    """Tests for CacheManager when disabled."""

    @pytest.fixture
    def disabled_manager(self) -> CacheManager:
        """Create a disabled cache manager."""
        with patch(
            "app.services.memory.cache.cache_manager.get_memory_settings"
        ) as mock_settings:
            settings = MagicMock()
            settings.cache_enabled = False
            settings.cache_max_items = 1000
            settings.cache_ttl_seconds = 300
            mock_settings.return_value = settings

            return CacheManager()

    def test_get_memory_returns_none(self, disabled_manager: CacheManager) -> None:
        """Should return None when disabled."""
        disabled_manager.cache_memory("episodic", uuid4(), {"data": "test"})
        result = disabled_manager.get_memory("episodic", uuid4())

        assert result is None

    async def test_get_embedding_returns_none(
        self,
        disabled_manager: CacheManager,
    ) -> None:
        """Should return None for embeddings when disabled."""
        result = await disabled_manager.get_embedding("content")

        assert result is None

    async def test_warmup_returns_zero(self, disabled_manager: CacheManager) -> None:
        """Should return 0 from warmup when disabled."""
        count = await disabled_manager.warmup([("episodic", uuid4(), {})])

        assert count == 0


class TestGetCacheManager:
    """Tests for get_cache_manager factory."""

    def test_returns_singleton(self) -> None:
        """Should return same instance."""
        manager1 = get_cache_manager()
        manager2 = get_cache_manager()

        assert manager1 is manager2

    def test_reset_creates_new(self) -> None:
        """Should create new instance after reset."""
        manager1 = get_cache_manager()
        reset_cache_manager()
        manager2 = get_cache_manager()

        assert manager1 is not manager2

    def test_reset_parameter(self) -> None:
        """Should create new instance with reset=True."""
        manager1 = get_cache_manager()
        manager2 = get_cache_manager(reset=True)

        assert manager1 is not manager2


class TestResetCacheManager:
    """Tests for reset_cache_manager."""

    def test_resets_singleton(self) -> None:
        """Should reset the singleton."""
        get_cache_manager()
        reset_cache_manager()

        # Next call should create new instance
        manager = get_cache_manager()
        assert manager is not None
