"""
Tests for caching utilities (Redis and in-memory fallback).
"""

import pytest

pytestmark = [pytest.mark.unit, pytest.mark.utils]

from unittest.mock import Mock, patch, MagicMock
from app.utils.cache import get_cache, InMemoryCache, RedisCache


class TestInMemoryCache:
    """Tests for in-memory cache implementation"""

    def test_get_set_delete(self):
        """Test basic cache operations"""
        cache = InMemoryCache()

        # Test set and get
        cache.set("test_key", "test_value", ttl=3600)
        assert cache.get("test_key") == "test_value"

        # Test delete
        cache.delete("test_key")
        assert cache.get("test_key") is None

    def test_expiration(self):
        """Test that expired entries are not returned"""
        cache = InMemoryCache(default_ttl=1)

        cache.set("expired_key", "value", ttl=0.1)  # Very short TTL
        assert cache.get("expired_key") == "value"

        import time

        time.sleep(0.2)
        assert cache.get("expired_key") is None

    def test_exists(self):
        """Test exists method"""
        cache = InMemoryCache()

        assert cache.exists("nonexistent") is False
        cache.set("existing", "value")
        assert cache.exists("existing") is True

    def test_clear(self):
        """Test clearing all cache"""
        cache = InMemoryCache()

        cache.set("key1", "value1")
        cache.set("key2", "value2")
        cache.clear()

        assert cache.get("key1") is None
        assert cache.get("key2") is None


class TestRedisCache:
    """Tests for Redis cache implementation"""

    @patch("app.utils.cache.redis")
    def test_redis_connection_success(self, mock_redis):
        """Test successful Redis connection"""
        mock_client = MagicMock()
        mock_client.ping.return_value = True
        mock_redis.Redis.return_value = mock_client

        cache = RedisCache("redis://localhost:6379/0")

        assert cache._connected is True
        mock_client.ping.assert_called_once()

    @patch("app.utils.cache.redis")
    def test_redis_connection_failure(self, mock_redis):
        """Test Redis connection failure falls back to in-memory"""
        mock_redis.Redis.side_effect = Exception("Connection failed")

        cache = RedisCache("redis://localhost:6379/0")

        assert cache._connected is False
        assert hasattr(cache, "_fallback")

    @patch("app.utils.cache.redis")
    def test_redis_get_set(self, mock_redis):
        """Test Redis get and set operations"""
        import pickle

        mock_client = MagicMock()
        mock_client.ping.return_value = True
        mock_redis.Redis.return_value = mock_client

        cache = RedisCache("redis://localhost:6379/0")

        # Test set
        cache.set("test_key", "test_value", ttl=3600)
        mock_client.setex.assert_called_once()
        args, kwargs = mock_client.setex.call_args
        assert args[0] == "test_key"
        assert args[1] == 3600
        assert pickle.loads(args[2]) == "test_value"

        # Test get
        mock_client.get.return_value = pickle.dumps("test_value")
        result = cache.get("test_key")
        assert result == "test_value"
        mock_client.get.assert_called_with("test_key")

    @patch("app.utils.cache.redis")
    def test_redis_fallback_on_error(self, mock_redis):
        """Test that Redis errors fall back to in-memory cache"""
        mock_client = MagicMock()
        mock_client.ping.return_value = True
        mock_client.get.side_effect = Exception("Redis error")
        mock_redis.Redis.return_value = mock_client

        cache = RedisCache("redis://localhost:6379/0")

        # Should not raise, but return None
        result = cache.get("test_key")
        assert result is None


class TestCacheIntegration:
    """Integration tests for cache utilities"""

    @patch("app.utils.cache.current_app")
    def test_get_cache_with_redis_enabled(self, mock_app):
        """Test get_cache when Redis is enabled"""
        mock_config = {"REDIS_ENABLED": True, "REDIS_URL": "redis://localhost:6379/0", "REDIS_DEFAULT_TTL": 3600}
        mock_app.config = mock_config

        with patch("app.utils.cache.RedisCache") as mock_redis_cache:
            mock_instance = MagicMock()
            mock_instance._connected = True
            mock_redis_cache.return_value = mock_instance

            cache = get_cache()
            assert cache is not None

    @patch("app.utils.cache.current_app")
    def test_get_cache_fallback_to_memory(self, mock_app):
        """Test get_cache falls back to in-memory when Redis unavailable"""
        mock_config = {"REDIS_ENABLED": False, "REDIS_DEFAULT_TTL": 3600}
        mock_app.config = mock_config

        # Reset global cache
        import app.utils.cache

        app.utils.cache._cache = None

        cache = get_cache()
        assert isinstance(cache, InMemoryCache)

    def test_cache_decorator(self):
        """Test the @cached decorator"""
        from app.utils.cache import cached

        call_count = [0]

        @cached(ttl=60, key_prefix="test")
        def expensive_function(x, y):
            call_count[0] += 1
            return x + y

        # First call should execute function
        result1 = expensive_function(1, 2)
        assert result1 == 3
        assert call_count[0] == 1

        # Second call should use cache
        result2 = expensive_function(1, 2)
        assert result2 == 3
        assert call_count[0] == 1  # Should not increment

        # Different args should execute again
        result3 = expensive_function(2, 3)
        assert result3 == 5
        assert call_count[0] == 2
