# File: tests/unit/infrastructure/json/mcp/test_cache_manager.py
# Purpose: Unit tests for CacheManager class
# Context: Validates cache lifecycle, eviction policies, and unified interface

"""Comprehensive unit tests for CacheManager cache coordination system.

Tests cover:
- FileIOCache operations (get, invalidate, clear)
- JSON cache operations (get, set, clear)
- Path cache operations (get, set, clear)
- Cache eviction (LRU policy)
- Cache cleanup (periodic and emergency)
- Cache statistics
"""

from pathlib import Path
from unittest.mock import Mock

import pytest

from noveler.infrastructure.json.mcp.servers.cache_manager import CacheManager
from noveler.infrastructure.logging.unified_logger import get_logger


@pytest.fixture
def mock_logger():
    """Create a mock logger for testing."""
    return Mock()


@pytest.fixture
def cache_manager(mock_logger):
    """Create a CacheManager instance for testing."""
    return CacheManager(
        logger=mock_logger,
        file_cache_max_size=3,
        file_cache_ttl_seconds=60,
        json_cache_max_size=2,
        json_cache_ttl_seconds=30,
    )


class TestCacheManagerInitialization:
    """Test CacheManager initialization and configuration."""

    def test_cache_manager_initializes_with_logger(self, mock_logger):
        """Test that CacheManager initializes with a logger."""
        manager = CacheManager(logger=mock_logger)
        assert manager._logger is mock_logger

    def test_cache_manager_initializes_with_custom_sizes(self, mock_logger):
        """Test initialization with custom cache sizes."""
        manager = CacheManager(
            logger=mock_logger,
            file_cache_max_size=512,
            json_cache_max_size=100,
        )
        assert manager._file_cache_max_size == 512
        assert manager._json_cache_max_size == 100

    def test_cache_manager_initializes_with_custom_ttl(self, mock_logger):
        """Test initialization with custom TTL values."""
        manager = CacheManager(
            logger=mock_logger,
            file_cache_ttl_seconds=1200,
            json_cache_ttl_seconds=600,
        )
        assert manager._file_cache_ttl == 1200
        assert manager._json_cache_ttl == 600


class TestFileIOCache:
    """Test FileIOCache interface operations."""

    def test_file_cache_get_calls_loader_on_miss(self, cache_manager, mock_logger):
        """Test that file_cache_get calls loader on cache miss."""
        mock_loader = Mock(return_value={"data": "test"})
        path = Path("/tmp/test.txt")

        result = cache_manager.file_cache_get(path, mock_loader)

        mock_loader.assert_called_once_with(path)
        assert result == {"data": "test"}

    def test_file_cache_get_returns_cached_on_hit(self, cache_manager):
        """Test that file_cache_get returns cached data on hit."""
        path = Path("/tmp/test.txt")
        mock_loader = Mock(return_value={"data": "test"})

        # First call: cache miss
        cache_manager.file_cache_get(path, mock_loader)
        assert mock_loader.call_count == 1

        # Second call: cache hit
        result = cache_manager.file_cache_get(path, mock_loader)
        assert mock_loader.call_count == 1  # No additional call
        assert result == {"data": "test"}

    def test_file_cache_invalidate_removes_entry(self, cache_manager):
        """Test that file_cache_invalidate removes the cached entry."""
        path = Path("/tmp/test.txt")
        mock_loader = Mock(return_value={"data": "test"})

        # Add to cache
        cache_manager.file_cache_get(path, mock_loader)
        assert len(cache_manager._file_cache) == 1

        # Invalidate
        cache_manager.file_cache_invalidate(path)
        assert len(cache_manager._file_cache) == 0

    def test_file_cache_clear_removes_all(self, cache_manager):
        """Test that file_cache_clear removes all entries."""
        mock_loader = Mock(return_value={"data": "test"})
        paths = [Path(f"/tmp/test{i}.txt") for i in range(3)]

        # Add multiple entries
        for path in paths:
            cache_manager.file_cache_get(path, mock_loader)
        assert len(cache_manager._file_cache) == 3

        # Clear
        cache_manager.file_cache_clear()
        assert len(cache_manager._file_cache) == 0
        assert len(cache_manager._file_cache_timestamps) == 0


class TestJSONCache:
    """Test JSON conversion cache operations."""

    def test_json_cache_set_stores_value(self, cache_manager):
        """Test that json_cache_set stores a value."""
        cache_manager.json_cache_set("key1", {"result": "data"})

        assert "key1" in cache_manager._json_conversion_cache
        assert cache_manager._json_conversion_cache["key1"] == {"result": "data"}

    def test_json_cache_get_returns_cached_value(self, cache_manager):
        """Test that json_cache_get returns cached value."""
        cache_manager.json_cache_set("key1", {"result": "data"})
        result = cache_manager.json_cache_get("key1")

        assert result == {"result": "data"}

    def test_json_cache_get_calls_compute_func_on_miss(self, cache_manager):
        """Test that json_cache_get calls compute_func on cache miss."""
        compute_func = Mock(return_value={"computed": "data"})

        result = cache_manager.json_cache_get("key1", compute_func)

        compute_func.assert_called_once()
        assert result == {"computed": "data"}
        assert cache_manager._json_conversion_cache["key1"] == {"computed": "data"}

    def test_json_cache_clear_removes_all(self, cache_manager):
        """Test that json_cache_clear removes all JSON cache entries."""
        cache_manager.json_cache_set("key1", {"data": "1"})
        cache_manager.json_cache_set("key2", {"data": "2"})
        assert len(cache_manager._json_conversion_cache) == 2

        cache_manager.json_cache_clear()
        assert len(cache_manager._json_conversion_cache) == 0


class TestPathCache:
    """Test path resolution cache operations."""

    def test_path_cache_set_stores_path(self, cache_manager):
        """Test that path_cache_set stores a path."""
        path = Path("/tmp/resolved.txt")
        cache_manager.path_cache_set("key1", path)

        assert cache_manager._path_resolution_cache["key1"] == path

    def test_path_cache_get_returns_cached_path(self, cache_manager):
        """Test that path_cache_get returns the cached path."""
        path = Path("/tmp/resolved.txt")
        cache_manager.path_cache_set("key1", path)

        result = cache_manager.path_cache_get("key1")
        assert result == path

    def test_path_cache_get_returns_none_on_miss(self, cache_manager):
        """Test that path_cache_get returns None on cache miss."""
        result = cache_manager.path_cache_get("nonexistent")
        assert result is None

    def test_path_cache_clear_removes_all(self, cache_manager):
        """Test that path_cache_clear removes all paths."""
        cache_manager.path_cache_set("key1", Path("/tmp/1.txt"))
        cache_manager.path_cache_set("key2", Path("/tmp/2.txt"))
        assert len(cache_manager._path_resolution_cache) == 2

        cache_manager.path_cache_clear()
        assert len(cache_manager._path_resolution_cache) == 0


class TestCacheEviction:
    """Test LRU eviction policies."""

    def test_file_cache_evicts_lru_on_overflow(self, cache_manager):
        """Test that FileIOCache evicts LRU entry when max size exceeded."""
        mock_loader = Mock(return_value={"data": "test"})
        paths = [Path(f"/tmp/test{i}.txt") for i in range(5)]

        # Add 5 entries to a cache with max_size=3
        for path in paths:
            cache_manager.file_cache_get(path, mock_loader)

        # Should have only 3 entries (2 evicted)
        assert len(cache_manager._file_cache) == 3
        assert cache_manager._file_cache_evictions == 2

    def test_json_cache_evicts_lru_on_overflow(self, cache_manager):
        """Test that JSON cache evicts LRU entry when max size exceeded."""
        # Add 3 entries to a cache with max_size=2
        cache_manager.json_cache_set("key1", {"data": "1"})
        cache_manager.json_cache_set("key2", {"data": "2"})
        cache_manager.json_cache_set("key3", {"data": "3"})

        # Should have only 2 entries (1 evicted)
        assert len(cache_manager._json_conversion_cache) == 2
        assert cache_manager._json_cache_evictions == 1


class TestCacheCleanup:
    """Test cache cleanup and emergency operations."""

    def test_cleanup_expired_removes_expired_entries(self, cache_manager, monkeypatch):
        """Test that cleanup_expired removes expired entries."""
        import time

        mock_loader = Mock(return_value={"data": "test"})
        path = Path("/tmp/test.txt")

        # Add entry
        cache_manager.file_cache_get(path, mock_loader)
        assert len(cache_manager._file_cache) == 1

        # Simulate time passage
        current_time = time.time()
        cache_manager._file_cache_timestamps[str(path)] = current_time - 100  # 100s old

        # Cleanup with TTL=60
        cache_manager.cleanup_expired()

        assert len(cache_manager._file_cache) == 0

    def test_emergency_cleanup_clears_all_caches(self, cache_manager):
        """Test that emergency_cleanup clears all caches."""
        mock_loader = Mock(return_value={"data": "test"})

        # Populate all caches
        cache_manager.file_cache_get(Path("/tmp/test.txt"), mock_loader)
        cache_manager.json_cache_set("key1", {"data": "1"})
        cache_manager.path_cache_set("key1", Path("/tmp/resolved.txt"))

        assert len(cache_manager._file_cache) == 1
        assert len(cache_manager._json_conversion_cache) == 1
        assert len(cache_manager._path_resolution_cache) == 1

        # Emergency cleanup
        cache_manager.emergency_cleanup()

        assert len(cache_manager._file_cache) == 0
        assert len(cache_manager._json_conversion_cache) == 0
        assert len(cache_manager._path_resolution_cache) == 0


class TestCacheStatistics:
    """Test cache statistics and monitoring."""

    def test_get_statistics_returns_correct_counts(self, cache_manager):
        """Test that get_statistics returns accurate cache counts."""
        mock_loader = Mock(return_value={"data": "test"})

        # Populate caches
        cache_manager.file_cache_get(Path("/tmp/test.txt"), mock_loader)
        cache_manager.json_cache_set("key1", {"data": "1"})
        cache_manager.path_cache_set("key1", Path("/tmp/resolved.txt"))

        stats = cache_manager.get_statistics()

        assert stats.file_cache_size == 1
        assert stats.json_cache_size == 1
        assert stats.path_cache_size == 1
        assert stats.total_entries == 3

    def test_get_statistics_includes_eviction_counts(self, cache_manager):
        """Test that statistics include eviction counts."""
        mock_loader = Mock(return_value={"data": "test"})

        # Cause evictions
        for i in range(5):
            cache_manager.file_cache_get(Path(f"/tmp/test{i}.txt"), mock_loader)

        stats = cache_manager.get_statistics()
        assert stats.file_cache_evictions > 0


class TestCacheManagerIntegration:
    """Integration tests for CacheManager with real logger."""

    def test_cache_manager_with_real_logger(self):
        """Test CacheManager initialization with real logger."""
        logger = get_logger(__name__)
        manager = CacheManager(logger=logger)

        assert manager._logger is not None
        assert len(manager._file_cache) == 0

    def test_cache_manager_end_to_end_workflow(self):
        """Test complete cache workflow: add, access, cleanup."""
        logger = get_logger(__name__)
        manager = CacheManager(logger=logger, file_cache_ttl_seconds=60)

        # FileIOCache workflow
        path = Path("/tmp/test.txt")
        loader = Mock(return_value={"content": "file data"})
        data1 = manager.file_cache_get(path, loader)
        data2 = manager.file_cache_get(path, loader)
        assert data1 == data2
        assert loader.call_count == 1

        # JSON cache workflow
        manager.json_cache_set("result", {"status": "success"})
        result = manager.json_cache_get("result")
        assert result == {"status": "success"}

        # Path cache workflow
        manager.path_cache_set("project", Path("/home/project"))
        cached_path = manager.path_cache_get("project")
        assert cached_path == Path("/home/project")

        # Statistics
        stats = manager.get_statistics()
        assert stats.file_cache_size == 1
        assert stats.json_cache_size == 1
        assert stats.path_cache_size == 1
