"""Tests for Redis client wrapper."""

import json
import pytest
from datetime import datetime
from unittest.mock import AsyncMock, MagicMock, patch
from bson import ObjectId
from redis.exceptions import RedisError, ConnectionError as RedisConnectionError

from src.geaicontext.clients.redis_client import (
    RedisClient,
    MongoJSONEncoder,
    get_redis_client,
)


class TestMongoJSONEncoder:
    """Test cases for MongoJSONEncoder class."""

    def test_encode_datetime(self):
        """Test encoding datetime objects."""
        dt = datetime(2024, 1, 15, 10, 30, 0)
        encoder = MongoJSONEncoder()
        result = encoder.default(dt)
        assert result == "2024-01-15T10:30:00"

    def test_encode_objectid(self):
        """Test encoding ObjectId objects."""
        oid = ObjectId("507f1f77bcf86cd799439011")
        encoder = MongoJSONEncoder()
        result = encoder.default(oid)
        assert result == "507f1f77bcf86cd799439011"

    def test_encode_regular_type_raises(self):
        """Test that regular types raise TypeError."""
        encoder = MongoJSONEncoder()
        with pytest.raises(TypeError):
            encoder.default(set([1, 2, 3]))

    def test_json_dumps_with_encoder(self):
        """Test json.dumps with MongoJSONEncoder."""
        data = {
            "created_at": datetime(2024, 1, 15, 10, 30, 0),
            "id": ObjectId("507f1f77bcf86cd799439011"),
            "name": "test"
        }
        result = json.dumps(data, cls=MongoJSONEncoder)
        parsed = json.loads(result)
        assert parsed["created_at"] == "2024-01-15T10:30:00"
        assert parsed["id"] == "507f1f77bcf86cd799439011"
        assert parsed["name"] == "test"


class TestRedisClient:
    """Test cases for RedisClient class."""

    @pytest.fixture
    def redis_client(self):
        """Create a fresh RedisClient instance."""
        return RedisClient()

    @pytest.mark.asyncio
    async def test_init(self, redis_client):
        """Test RedisClient initialization."""
        assert redis_client._client is None
        assert redis_client._connected is False

    @pytest.mark.asyncio
    async def test_connect_success(self, redis_client, mock_redis_client):
        """Test successful Redis connection."""
        with patch("src.geaicontext.clients.redis_client.Redis.from_url", return_value=mock_redis_client):
            await redis_client.connect()
            
            assert redis_client._connected is True
            mock_redis_client.ping.assert_called_once()

    @pytest.mark.asyncio
    async def test_connect_connection_error(self, redis_client):
        """Test Redis connection failure."""
        mock_client = MagicMock()
        mock_client.ping = AsyncMock(side_effect=RedisConnectionError("Connection refused"))
        
        with patch("src.geaicontext.clients.redis_client.Redis.from_url", return_value=mock_client):
            with pytest.raises(RedisConnectionError):
                await redis_client.connect()
            
            assert redis_client._connected is False

    @pytest.mark.asyncio
    async def test_connect_unexpected_error(self, redis_client):
        """Test Redis connection with unexpected error."""
        mock_client = MagicMock()
        mock_client.ping = AsyncMock(side_effect=Exception("Unexpected error"))
        
        with patch("src.geaicontext.clients.redis_client.Redis.from_url", return_value=mock_client):
            with pytest.raises(Exception):
                await redis_client.connect()

    @pytest.mark.asyncio
    async def test_disconnect(self, redis_client, mock_redis_client):
        """Test Redis disconnection."""
        with patch("src.geaicontext.clients.redis_client.Redis.from_url", return_value=mock_redis_client):
            await redis_client.connect()
            await redis_client.disconnect()
            
            assert redis_client._connected is False
            mock_redis_client.aclose.assert_called_once()

    @pytest.mark.asyncio
    async def test_disconnect_when_not_connected(self, redis_client):
        """Test disconnect when not connected."""
        await redis_client.disconnect()
        assert redis_client._connected is False

    @pytest.mark.asyncio
    async def test_health_check_healthy(self, redis_client, mock_redis_client):
        """Test health check when connected."""
        with patch("src.geaicontext.clients.redis_client.Redis.from_url", return_value=mock_redis_client):
            await redis_client.connect()
            result = await redis_client.health_check()
            
            assert result is True

    @pytest.mark.asyncio
    async def test_health_check_not_connected(self, redis_client):
        """Test health check when not connected."""
        result = await redis_client.health_check()
        assert result is False

    @pytest.mark.asyncio
    async def test_health_check_ping_fails(self, redis_client, mock_redis_client):
        """Test health check when ping fails."""
        with patch("src.geaicontext.clients.redis_client.Redis.from_url", return_value=mock_redis_client):
            await redis_client.connect()
            # Make ping fail after connection
            mock_redis_client.ping = AsyncMock(side_effect=RedisError("Ping failed"))
            
            result = await redis_client.health_check()
            assert result is False

    def test_get_key(self, redis_client):
        """Test key generation."""
        key = redis_client._get_key("CUST-123")
        assert key == "context:CUST-123"

    @pytest.mark.asyncio
    async def test_get_context_found(self, redis_client, mock_redis_client, sample_context, sample_customer_id):
        """Test retrieving existing context from cache."""
        mock_redis_client.get = AsyncMock(return_value=json.dumps(sample_context))
        
        with patch("src.geaicontext.clients.redis_client.Redis.from_url", return_value=mock_redis_client):
            await redis_client.connect()
            result = await redis_client.get_context(sample_customer_id)
            
            assert result is not None
            assert result["name"] == "John Doe"
            mock_redis_client.get.assert_called_once_with(f"context:{sample_customer_id}")

    @pytest.mark.asyncio
    async def test_get_context_not_found(self, redis_client, mock_redis_client, sample_customer_id):
        """Test retrieving non-existent context from cache."""
        mock_redis_client.get = AsyncMock(return_value=None)
        
        with patch("src.geaicontext.clients.redis_client.Redis.from_url", return_value=mock_redis_client):
            await redis_client.connect()
            result = await redis_client.get_context(sample_customer_id)
            
            assert result is None

    @pytest.mark.asyncio
    async def test_get_context_invalid_json(self, redis_client, mock_redis_client, sample_customer_id):
        """Test get_context with corrupted JSON data."""
        mock_redis_client.get = AsyncMock(return_value="invalid json {{{")
        mock_redis_client.delete = AsyncMock(return_value=1)
        
        with patch("src.geaicontext.clients.redis_client.Redis.from_url", return_value=mock_redis_client):
            await redis_client.connect()
            result = await redis_client.get_context(sample_customer_id)
            
            assert result is None
            # Should delete corrupted data
            mock_redis_client.delete.assert_called_once()

    @pytest.mark.asyncio
    async def test_get_context_not_connected(self, redis_client, sample_customer_id):
        """Test get_context when not connected."""
        with pytest.raises(RedisError, match="Redis client not connected"):
            await redis_client.get_context(sample_customer_id)

    @pytest.mark.asyncio
    async def test_get_context_redis_error(self, redis_client, mock_redis_client, sample_customer_id):
        """Test get_context with Redis error."""
        mock_redis_client.get = AsyncMock(side_effect=RedisError("Get error"))
        
        with patch("src.geaicontext.clients.redis_client.Redis.from_url", return_value=mock_redis_client):
            await redis_client.connect()
            
            with pytest.raises(RedisError):
                await redis_client.get_context(sample_customer_id)

    @pytest.mark.asyncio
    async def test_set_context_success(self, redis_client, mock_redis_client, sample_context, sample_customer_id):
        """Test storing context in cache."""
        with patch("src.geaicontext.clients.redis_client.Redis.from_url", return_value=mock_redis_client):
            await redis_client.connect()
            result = await redis_client.set_context(sample_customer_id, sample_context)
            
            assert result is True
            mock_redis_client.set.assert_called_once()

    @pytest.mark.asyncio
    async def test_set_context_with_ttl(self, redis_client, mock_redis_client, sample_context, sample_customer_id):
        """Test storing context with TTL."""
        with patch("src.geaicontext.clients.redis_client.Redis.from_url", return_value=mock_redis_client):
            await redis_client.connect()
            result = await redis_client.set_context(sample_customer_id, sample_context, ttl=3600)
            
            assert result is True
            mock_redis_client.setex.assert_called_once()

    @pytest.mark.asyncio
    async def test_set_context_not_connected(self, redis_client, sample_context, sample_customer_id):
        """Test set_context when not connected."""
        with pytest.raises(RedisError, match="Redis client not connected"):
            await redis_client.set_context(sample_customer_id, sample_context)

    @pytest.mark.asyncio
    async def test_set_context_serialization_error(self, redis_client, mock_redis_client, sample_customer_id):
        """Test set_context with non-serializable data."""
        # Create data that can't be serialized even with MongoJSONEncoder
        non_serializable = {"func": lambda x: x}
        
        with patch("src.geaicontext.clients.redis_client.Redis.from_url", return_value=mock_redis_client):
            await redis_client.connect()
            result = await redis_client.set_context(sample_customer_id, non_serializable)
            
            assert result is False

    @pytest.mark.asyncio
    async def test_set_context_redis_error(self, redis_client, mock_redis_client, sample_context, sample_customer_id):
        """Test set_context with Redis error."""
        mock_redis_client.set = AsyncMock(side_effect=RedisError("Set error"))
        
        with patch("src.geaicontext.clients.redis_client.Redis.from_url", return_value=mock_redis_client):
            await redis_client.connect()
            
            with pytest.raises(RedisError):
                await redis_client.set_context(sample_customer_id, sample_context)

    @pytest.mark.asyncio
    async def test_delete_context_success(self, redis_client, mock_redis_client, sample_customer_id):
        """Test deleting existing context from cache."""
        mock_redis_client.delete = AsyncMock(return_value=1)
        
        with patch("src.geaicontext.clients.redis_client.Redis.from_url", return_value=mock_redis_client):
            await redis_client.connect()
            result = await redis_client.delete_context(sample_customer_id)
            
            assert result is True

    @pytest.mark.asyncio
    async def test_delete_context_not_found(self, redis_client, mock_redis_client, sample_customer_id):
        """Test deleting non-existent context from cache."""
        mock_redis_client.delete = AsyncMock(return_value=0)
        
        with patch("src.geaicontext.clients.redis_client.Redis.from_url", return_value=mock_redis_client):
            await redis_client.connect()
            result = await redis_client.delete_context(sample_customer_id)
            
            assert result is False

    @pytest.mark.asyncio
    async def test_delete_context_not_connected(self, redis_client, sample_customer_id):
        """Test delete_context when not connected."""
        with pytest.raises(RedisError, match="Redis client not connected"):
            await redis_client.delete_context(sample_customer_id)

    @pytest.mark.asyncio
    async def test_delete_context_redis_error(self, redis_client, mock_redis_client, sample_customer_id):
        """Test delete_context with Redis error."""
        mock_redis_client.delete = AsyncMock(side_effect=RedisError("Delete error"))
        
        with patch("src.geaicontext.clients.redis_client.Redis.from_url", return_value=mock_redis_client):
            await redis_client.connect()
            
            with pytest.raises(RedisError):
                await redis_client.delete_context(sample_customer_id)


class TestGetRedisClient:
    """Test cases for get_redis_client function."""

    @pytest.mark.asyncio
    async def test_get_redis_client_creates_new(self, reset_global_clients):
        """Test that get_redis_client creates a new client."""
        client = await get_redis_client()
        assert client is not None
        assert isinstance(client, RedisClient)

    @pytest.mark.asyncio
    async def test_get_redis_client_returns_same_instance(self, reset_global_clients):
        """Test that get_redis_client returns the same instance."""
        client1 = await get_redis_client()
        client2 = await get_redis_client()
        assert client1 is client2
