"""
Test suite for connector caching system
"""

import pytest
import time
import tempfile
import shutil
from pathlib import Path
from connectors.cache import (
    CacheEntry, MemoryCache, FileCache, MultiLevelCache,
    cache_key, cached, get_global_cache
)


class TestCacheEntry:
    """Test CacheEntry class"""
    
    def test_cache_entry_creation(self):
        """Test basic cache entry creation"""
        entry = CacheEntry(
            value="test_value",
            created_at=time.time(),
            accessed_at=time.time(),
            access_count=1,
            ttl=60.0
        )
        
        assert entry.value == "test_value"
        assert entry.access_count == 1
        assert entry.ttl == 60.0
        assert not entry.is_expired
    
    def test_cache_entry_expiration(self):
        """Test cache entry expiration"""
        past_time = time.time() - 120  # 2 minutes ago
        
        entry = CacheEntry(
            value="test_value",
            created_at=past_time,
            accessed_at=past_time,
            access_count=1,
            ttl=60.0  # 1 minute TTL
        )
        
        assert entry.is_expired
    
    def test_cache_entry_no_expiration(self):
        """Test cache entry without TTL"""
        entry = CacheEntry(
            value="test_value",
            created_at=time.time(),
            accessed_at=time.time(),
            access_count=1,
            ttl=None
        )
        
        assert not entry.is_expired
    
    def test_cache_entry_touch(self):
        """Test cache entry touch method"""
        entry = CacheEntry(
            value="test_value",
            created_at=time.time(),
            accessed_at=time.time(),
            access_count=1,
            ttl=60.0
        )
        
        original_count = entry.access_count
        original_access_time = entry.accessed_at
        
        time.sleep(0.01)  # Small delay
        entry.touch()
        
        assert entry.access_count == original_count + 1
        assert entry.accessed_at > original_access_time


class TestMemoryCache:
    """Test MemoryCache class"""
    
    def setup_method(self):
        """Setup test cache"""
        self.cache = MemoryCache(max_size=3, default_ttl=60.0)
    
    def test_cache_set_and_get(self):
        """Test basic cache set and get operations"""
        self.cache.set("key1", "value1")
        assert self.cache.get("key1") == "value1"
        assert self.cache.size() == 1
    
    def test_cache_get_nonexistent(self):
        """Test getting non-existent key"""
        assert self.cache.get("nonexistent") is None
    
    def test_cache_ttl_expiration(self):
        """Test TTL expiration"""
        self.cache.set("key1", "value1", ttl=0.1)  # 0.1 second TTL
        assert self.cache.get("key1") == "value1"
        
        time.sleep(0.2)  # Wait for expiration
        assert self.cache.get("key1") is None
    
    def test_cache_lru_eviction(self):
        """Test LRU eviction when cache is full"""
        # Fill cache to capacity
        self.cache.set("key1", "value1")
        self.cache.set("key2", "value2")
        self.cache.set("key3", "value3")
        
        # Access key1 to make it most recently used
        self.cache.get("key1")
        
        # Add new key, should evict key2 (least recently used)
        self.cache.set("key4", "value4")
        
        assert self.cache.get("key1") == "value1"  # Should still exist
        assert self.cache.get("key2") is None  # Should be evicted
        assert self.cache.get("key3") == "value3"  # Should still exist
        assert self.cache.get("key4") == "value4"  # Should exist
        assert self.cache.size() == 3
    
    def test_cache_delete(self):
        """Test cache deletion"""
        self.cache.set("key1", "value1")
        assert self.cache.delete("key1") is True
        assert self.cache.get("key1") is None
        assert self.cache.delete("key1") is False  # Already deleted
    
    def test_cache_clear(self):
        """Test cache clearing"""
        self.cache.set("key1", "value1")
        self.cache.set("key2", "value2")
        
        self.cache.clear()
        
        assert self.cache.size() == 0
        assert self.cache.get("key1") is None
        assert self.cache.get("key2") is None
    
    def test_cache_stats(self):
        """Test cache statistics"""
        self.cache.set("key1", "value1")
        self.cache.set("key2", "value2")
        
        stats = self.cache.stats()
        
        assert stats['size'] == 2
        assert stats['max_size'] == 3
        assert 'expired_count' in stats
        assert 'hit_rate' in stats
        assert 'key1' in stats['keys']
        assert 'key2' in stats['keys']


class TestFileCache:
    """Test FileCache class"""
    
    def setup_method(self):
        """Setup test cache with temporary directory"""
        self.temp_dir = tempfile.mkdtemp()
        self.cache = FileCache(self.temp_dir, max_size=3, default_ttl=60.0)
    
    def teardown_method(self):
        """Cleanup temporary directory"""
        shutil.rmtree(self.temp_dir)
    
    def test_file_cache_set_and_get(self):
        """Test basic file cache operations"""
        self.cache.set("key1", "value1")
        assert self.cache.get("key1") == "value1"
        assert self.cache.size() == 1
    
    def test_file_cache_persistence(self):
        """Test cache persistence across instances"""
        # Set value in first instance
        self.cache.set("key1", "value1")
        
        # Create new instance with same directory
        new_cache = FileCache(self.temp_dir)
        assert new_cache.get("key1") == "value1"
    
    def test_file_cache_ttl_expiration(self):
        """Test TTL expiration in file cache"""
        self.cache.set("key1", "value1", ttl=0.1)  # 0.1 second TTL
        assert self.cache.get("key1") == "value1"
        
        time.sleep(0.2)  # Wait for expiration
        assert self.cache.get("key1") is None
    
    def test_file_cache_complex_objects(self):
        """Test caching complex objects"""
        test_obj = {"key": "value", "number": 42, "list": [1, 2, 3]}
        self.cache.set("complex", test_obj)
        
        result = self.cache.get("complex")
        assert result == test_obj
        assert result is not test_obj  # Should be a copy
    
    def test_file_cache_delete(self):
        """Test file cache deletion"""
        self.cache.set("key1", "value1")
        assert self.cache.delete("key1") is True
        assert self.cache.get("key1") is None
        assert self.cache.delete("key1") is False
    
    def test_file_cache_clear(self):
        """Test file cache clearing"""
        self.cache.set("key1", "value1")
        self.cache.set("key2", "value2")
        
        self.cache.clear()
        
        assert self.cache.size() == 0
        assert self.cache.get("key1") is None
        assert self.cache.get("key2") is None


class TestMultiLevelCache:
    """Test MultiLevelCache class"""
    
    def setup_method(self):
        """Setup test multi-level cache"""
        self.temp_dir = tempfile.mkdtemp()
        self.cache = MultiLevelCache(
            memory_size=2,
            file_cache_dir=self.temp_dir,
            file_cache_size=5
        )
    
    def teardown_method(self):
        """Cleanup temporary directory"""
        shutil.rmtree(self.temp_dir)
    
    def test_multi_level_cache_memory_first(self):
        """Test that memory cache is checked first"""
        self.cache.set("key1", "value1")
        
        # Value should be in memory
        assert self.cache.memory_cache.get("key1") == "value1"
        assert self.cache.get("key1") == "value1"
    
    def test_multi_level_cache_file_fallback(self):
        """Test file cache fallback when memory is empty"""
        # Set value directly in file cache
        self.cache.file_cache.set("key1", "value1")
        
        # Should get from file cache and promote to memory
        assert self.cache.get("key1") == "value1"
        assert self.cache.memory_cache.get("key1") == "value1"
    
    def test_multi_level_cache_set_both(self):
        """Test setting value in both cache levels"""
        self.cache.set("key1", "value1")
        
        assert self.cache.memory_cache.get("key1") == "value1"
        assert self.cache.file_cache.get("key1") == "value1"
    
    def test_multi_level_cache_delete_both(self):
        """Test deletion from both cache levels"""
        self.cache.set("key1", "value1")
        assert self.cache.delete("key1") is True
        
        assert self.cache.memory_cache.get("key1") is None
        assert self.cache.file_cache.get("key1") is None
    
    def test_multi_level_cache_stats(self):
        """Test multi-level cache statistics"""
        self.cache.set("key1", "value1")
        
        stats = self.cache.stats()
        
        assert 'memory_cache' in stats
        assert 'file_cache' in stats
        assert stats['memory_cache']['size'] == 1
        assert stats['file_cache']['size'] == 1


class TestCacheUtilities:
    """Test cache utility functions"""
    
    def test_cache_key_generation(self):
        """Test cache key generation"""
        key1 = cache_key("arg1", "arg2", key1="value1", key2="value2")
        key2 = cache_key("arg1", "arg2", key2="value2", key1="value1")  # Same args, different order
        
        assert key1 == key2  # Should be same due to sorted kwargs
        assert len(key1) == 32  # MD5 hash length
    
    def test_cache_key_with_complex_objects(self):
        """Test cache key generation with complex objects"""
        key1 = cache_key({"key": "value"})
        key2 = cache_key({"key": "value"})
        
        assert key1 == key2
    
    def test_cached_decorator(self):
        """Test cached decorator"""
        call_count = 0
        
        @cached(get_global_cache(), ttl=60.0)
        def expensive_function(x, y):
            nonlocal call_count
            call_count += 1
            return x + y
        
        # First call should execute function
        result1 = expensive_function(1, 2)
        assert result1 == 3
        assert call_count == 1
        
        # Second call should use cache
        result2 = expensive_function(1, 2)
        assert result2 == 3
        assert call_count == 1  # Should not increment
        
        # Different arguments should execute function
        result3 = expensive_function(2, 3)
        assert result3 == 5
        assert call_count == 2
    
    def test_cached_decorator_with_custom_key(self):
        """Test cached decorator with custom key function"""
        call_count = 0
        
        def custom_key(x, y):
            return f"sum_{x}_{y}"
        
        @cached(get_global_cache(), key_func=custom_key)
        def another_function(x, y):
            nonlocal call_count
            call_count += 1
            return x * y
        
        # Test caching with custom key
        result1 = another_function(3, 4)
        assert result1 == 12
        assert call_count == 1
        
        result2 = another_function(3, 4)
        assert result2 == 12
        assert call_count == 1  # Should use cache


class TestGlobalCache:
    """Test global cache functionality"""
    
    def test_get_global_cache(self):
        """Test getting global cache instance"""
        cache1 = get_global_cache()
        cache2 = get_global_cache()
        
        assert cache1 is cache2  # Should be same instance
        assert isinstance(cache1, MultiLevelCache)
