# tests/unit/services/memory/cache/test_hot_cache.py
"""Tests for HotMemoryCache."""

import time
from uuid import uuid4

import pytest

from app.services.memory.cache.hot_cache import (
    CacheEntry,
    CacheKey,
    HotCacheStats,
    HotMemoryCache,
    create_hot_cache,
)


class TestCacheKey:
    """Tests for CacheKey."""

    def test_creates_key(self) -> None:
        """Should create key with required fields."""
        key = CacheKey(memory_type="episodic", memory_id="123")

        assert key.memory_type == "episodic"
        assert key.memory_id == "123"
        assert key.scope is None

    def test_creates_key_with_scope(self) -> None:
        """Should create key with scope."""
        key = CacheKey(memory_type="semantic", memory_id="456", scope="proj-123")

        assert key.scope == "proj-123"

    def test_hash_and_equality(self) -> None:
        """Keys with same values should be equal and have same hash."""
        key1 = CacheKey(memory_type="episodic", memory_id="123", scope="proj-1")
        key2 = CacheKey(memory_type="episodic", memory_id="123", scope="proj-1")

        assert key1 == key2
        assert hash(key1) == hash(key2)

    def test_str_representation(self) -> None:
        """Should produce readable string."""
        key = CacheKey(memory_type="episodic", memory_id="123", scope="proj-1")

        assert str(key) == "episodic:proj-1:123"

    def test_str_without_scope(self) -> None:
        """Should produce string without scope."""
        key = CacheKey(memory_type="episodic", memory_id="123")

        assert str(key) == "episodic:123"


class TestCacheEntry:
    """Tests for CacheEntry."""

    def test_creates_entry(self) -> None:
        """Should create entry with value."""
        entry = CacheEntry(
            value={"data": "test"},
            created_at=pytest.importorskip("datetime").datetime.now(
                pytest.importorskip("datetime").UTC
            ),
            last_accessed_at=pytest.importorskip("datetime").datetime.now(
                pytest.importorskip("datetime").UTC
            ),
        )

        assert entry.value == {"data": "test"}
        assert entry.access_count == 1
        assert entry.ttl_seconds == 300.0

    def test_is_expired(self) -> None:
        """Should detect expired entries."""
        from datetime import UTC, datetime, timedelta

        old_time = datetime.now(UTC) - timedelta(seconds=400)
        entry = CacheEntry(
            value="test",
            created_at=old_time,
            last_accessed_at=old_time,
            ttl_seconds=300.0,
        )

        assert entry.is_expired() is True

    def test_not_expired(self) -> None:
        """Should detect non-expired entries."""
        from datetime import UTC, datetime

        entry = CacheEntry(
            value="test",
            created_at=datetime.now(UTC),
            last_accessed_at=datetime.now(UTC),
            ttl_seconds=300.0,
        )

        assert entry.is_expired() is False

    def test_touch_updates_access(self) -> None:
        """Touch should update access time and count."""
        from datetime import UTC, datetime, timedelta

        old_time = datetime.now(UTC) - timedelta(seconds=10)
        entry = CacheEntry(
            value="test",
            created_at=old_time,
            last_accessed_at=old_time,
            access_count=5,
        )

        entry.touch()

        assert entry.access_count == 6
        assert entry.last_accessed_at > old_time


class TestHotCacheStats:
    """Tests for HotCacheStats."""

    def test_hit_rate_calculation(self) -> None:
        """Should calculate hit rate correctly."""
        stats = HotCacheStats(hits=80, misses=20)

        assert stats.hit_rate == 0.8

    def test_hit_rate_zero_requests(self) -> None:
        """Should return 0 for no requests."""
        stats = HotCacheStats()

        assert stats.hit_rate == 0.0

    def test_to_dict(self) -> None:
        """Should convert to dictionary."""
        stats = HotCacheStats(hits=10, misses=5, evictions=2)

        result = stats.to_dict()

        assert result["hits"] == 10
        assert result["misses"] == 5
        assert result["evictions"] == 2
        assert "hit_rate" in result


class TestHotMemoryCache:
    """Tests for HotMemoryCache."""

    @pytest.fixture
    def cache(self) -> HotMemoryCache[dict]:
        """Create a hot memory cache."""
        return HotMemoryCache[dict](max_size=100, default_ttl_seconds=300.0)

    def test_put_and_get(self, cache: HotMemoryCache[dict]) -> None:
        """Should store and retrieve values."""
        key = CacheKey(memory_type="episodic", memory_id="123")
        value = {"data": "test"}

        cache.put(key, value)
        result = cache.get(key)

        assert result == value

    def test_get_missing_key(self, cache: HotMemoryCache[dict]) -> None:
        """Should return None for missing keys."""
        key = CacheKey(memory_type="episodic", memory_id="nonexistent")

        result = cache.get(key)

        assert result is None

    def test_put_by_id(self, cache: HotMemoryCache[dict]) -> None:
        """Should store by type and ID."""
        memory_id = uuid4()
        value = {"data": "test"}

        cache.put_by_id("episodic", memory_id, value)
        result = cache.get_by_id("episodic", memory_id)

        assert result == value

    def test_put_by_id_with_scope(self, cache: HotMemoryCache[dict]) -> None:
        """Should store with scope."""
        memory_id = uuid4()
        value = {"data": "test"}

        cache.put_by_id("semantic", memory_id, value, scope="proj-123")
        result = cache.get_by_id("semantic", memory_id, scope="proj-123")

        assert result == value

    def test_lru_eviction(self) -> None:
        """Should evict LRU entries when at capacity."""
        cache = HotMemoryCache[str](max_size=3)

        # Fill cache
        cache.put_by_id("test", "1", "first")
        cache.put_by_id("test", "2", "second")
        cache.put_by_id("test", "3", "third")

        # Access first to make it recent
        cache.get_by_id("test", "1")

        # Add fourth, should evict second (LRU)
        cache.put_by_id("test", "4", "fourth")

        assert cache.get_by_id("test", "1") is not None  # Accessed, kept
        assert cache.get_by_id("test", "2") is None  # Evicted (LRU)
        assert cache.get_by_id("test", "3") is not None
        assert cache.get_by_id("test", "4") is not None

    def test_ttl_expiration(self) -> None:
        """Should expire entries after TTL."""
        cache = HotMemoryCache[str](max_size=100, default_ttl_seconds=0.05)

        cache.put_by_id("test", "1", "value")

        # Wait for expiration
        time.sleep(0.06)

        result = cache.get_by_id("test", "1")

        assert result is None

    def test_invalidate(self, cache: HotMemoryCache[dict]) -> None:
        """Should invalidate specific entry."""
        key = CacheKey(memory_type="episodic", memory_id="123")
        cache.put(key, {"data": "test"})

        result = cache.invalidate(key)

        assert result is True
        assert cache.get(key) is None

    def test_invalidate_by_id(self, cache: HotMemoryCache[dict]) -> None:
        """Should invalidate by ID."""
        memory_id = uuid4()
        cache.put_by_id("episodic", memory_id, {"data": "test"})

        result = cache.invalidate_by_id("episodic", memory_id)

        assert result is True
        assert cache.get_by_id("episodic", memory_id) is None

    def test_invalidate_by_type(self, cache: HotMemoryCache[dict]) -> None:
        """Should invalidate all entries of a type."""
        cache.put_by_id("episodic", "1", {"data": "1"})
        cache.put_by_id("episodic", "2", {"data": "2"})
        cache.put_by_id("semantic", "3", {"data": "3"})

        count = cache.invalidate_by_type("episodic")

        assert count == 2
        assert cache.get_by_id("episodic", "1") is None
        assert cache.get_by_id("episodic", "2") is None
        assert cache.get_by_id("semantic", "3") is not None

    def test_invalidate_by_scope(self, cache: HotMemoryCache[dict]) -> None:
        """Should invalidate all entries in a scope."""
        cache.put_by_id("episodic", "1", {"data": "1"}, scope="proj-1")
        cache.put_by_id("semantic", "2", {"data": "2"}, scope="proj-1")
        cache.put_by_id("episodic", "3", {"data": "3"}, scope="proj-2")

        count = cache.invalidate_by_scope("proj-1")

        assert count == 2
        assert cache.get_by_id("episodic", "3", scope="proj-2") is not None

    def test_invalidate_pattern(self, cache: HotMemoryCache[dict]) -> None:
        """Should invalidate entries matching pattern."""
        cache.put_by_id("episodic", "123", {"data": "1"})
        cache.put_by_id("episodic", "124", {"data": "2"})
        cache.put_by_id("semantic", "125", {"data": "3"})

        count = cache.invalidate_pattern("episodic:*")

        assert count == 2

    def test_clear(self, cache: HotMemoryCache[dict]) -> None:
        """Should clear all entries."""
        cache.put_by_id("episodic", "1", {"data": "1"})
        cache.put_by_id("semantic", "2", {"data": "2"})

        count = cache.clear()

        assert count == 2
        assert cache.size == 0

    def test_cleanup_expired(self) -> None:
        """Should remove expired entries."""
        cache = HotMemoryCache[str](max_size=100, default_ttl_seconds=0.05)

        cache.put_by_id("test", "1", "value1")
        cache.put_by_id("test", "2", "value2", ttl_seconds=10)

        time.sleep(0.06)

        count = cache.cleanup_expired()

        assert count == 1  # Only the first one expired
        assert cache.size == 1

    def test_get_hot_memories(self, cache: HotMemoryCache[dict]) -> None:
        """Should return most accessed memories."""
        cache.put_by_id("episodic", "1", {"data": "1"})
        cache.put_by_id("episodic", "2", {"data": "2"})

        # Access first one multiple times
        for _ in range(5):
            cache.get_by_id("episodic", "1")

        hot = cache.get_hot_memories(limit=2)

        assert len(hot) == 2
        assert hot[0][1] >= hot[1][1]  # Sorted by access count

    def test_get_stats(self, cache: HotMemoryCache[dict]) -> None:
        """Should return accurate statistics."""
        cache.put_by_id("episodic", "1", {"data": "1"})
        cache.get_by_id("episodic", "1")  # Hit
        cache.get_by_id("episodic", "2")  # Miss

        stats = cache.get_stats()

        assert stats.hits == 1
        assert stats.misses == 1
        assert stats.current_size == 1

    def test_reset_stats(self, cache: HotMemoryCache[dict]) -> None:
        """Should reset statistics."""
        cache.put_by_id("episodic", "1", {"data": "1"})
        cache.get_by_id("episodic", "1")

        cache.reset_stats()
        stats = cache.get_stats()

        assert stats.hits == 0
        assert stats.misses == 0


class TestCreateHotCache:
    """Tests for factory function."""

    def test_creates_cache(self) -> None:
        """Should create cache with defaults."""
        cache = create_hot_cache()

        assert cache.max_size == 10000

    def test_creates_cache_with_options(self) -> None:
        """Should create cache with custom options."""
        cache = create_hot_cache(max_size=500, default_ttl_seconds=60.0)

        assert cache.max_size == 500
