"""
Unit tests for Redis Service.

These tests use mocking to test the RedisService without requiring
a real Redis connection.
"""

import pytest
from unittest.mock import Mock, patch, MagicMock
import json

from redis.exceptions import RedisError, ConnectionError as RedisConnectionError

from app.services.redis_service import (
    RedisService,
    RedisServiceError,
    RedisConnectionFailed
)


class TestRedisServiceInit:
    """Test RedisService initialization."""

    @patch('app.services.redis_service.redis.ConnectionPool.from_url')
    @patch('app.services.redis_service.redis.Redis')
    def test_init_success(self, mock_redis_class, mock_pool_from_url):
        """Test successful initialization."""
        # Setup mocks
        mock_pool = MagicMock()
        mock_pool_from_url.return_value = mock_pool

        mock_client = MagicMock()
        mock_client.ping.return_value = True
        mock_redis_class.return_value = mock_client

        # Create service
        service = RedisService(redis_url='redis://localhost:6379/0')

        # Verify
        mock_pool_from_url.assert_called_once()
        mock_redis_class.assert_called_once_with(connection_pool=mock_pool)
        mock_client.ping.assert_called_once()
        assert service.client == mock_client

    @patch('app.services.redis_service.redis.ConnectionPool.from_url')
    def test_init_connection_failed(self, mock_pool_from_url):
        """Test initialization fails when Redis is unavailable."""
        mock_pool_from_url.side_effect = RedisConnectionError("Connection refused")

        with pytest.raises(RedisConnectionFailed) as exc_info:
            RedisService(redis_url='redis://localhost:6379/0')

        assert "Could not connect to Redis" in str(exc_info.value)

    def test_init_missing_url(self):
        """Test initialization fails with missing URL."""
        # Clear environment variable and pass empty string
        with patch.dict('os.environ', {'REDIS_URL': ''}, clear=True):
            with pytest.raises(ValueError) as exc_info:
                RedisService(redis_url='')

            assert "Redis URL not configured" in str(exc_info.value)


class TestRedisServiceOperations:
    """Test Redis operations (get, set, delete, exists)."""

    @pytest.fixture
    def redis_service(self):
        """Create a RedisService with mocked client."""
        with patch('app.services.redis_service.redis.ConnectionPool.from_url') as mock_pool_from_url:
            with patch('app.services.redis_service.redis.Redis') as mock_redis_class:
                mock_pool = MagicMock()
                mock_pool_from_url.return_value = mock_pool

                mock_client = MagicMock()
                mock_client.ping.return_value = True
                mock_redis_class.return_value = mock_client

                service = RedisService(redis_url='redis://localhost:6379/0')
                yield service

    def test_get_existing_key(self, redis_service):
        """Test getting an existing key."""
        redis_service.client.get.return_value = "test_value"

        result = redis_service.get("test_key")

        redis_service.client.get.assert_called_once_with("test_key")
        assert result == "test_value"

    def test_get_nonexistent_key(self, redis_service):
        """Test getting a non-existent key returns None."""
        redis_service.client.get.return_value = None

        result = redis_service.get("nonexistent_key")

        assert result is None

    def test_get_error(self, redis_service):
        """Test get raises RedisServiceError on failure."""
        redis_service.client.get.side_effect = RedisError("Connection lost")

        with pytest.raises(RedisServiceError) as exc_info:
            redis_service.get("test_key")

        assert "Failed to get key" in str(exc_info.value)

    def test_set_basic(self, redis_service):
        """Test basic set operation."""
        redis_service.client.set.return_value = True

        result = redis_service.set("test_key", "test_value")

        redis_service.client.set.assert_called_once_with(
            "test_key", "test_value", ex=None, nx=False, xx=False
        )
        assert result is True

    def test_set_with_ttl(self, redis_service):
        """Test set with TTL."""
        redis_service.client.set.return_value = True

        result = redis_service.set("test_key", "test_value", ttl=3600)

        redis_service.client.set.assert_called_once_with(
            "test_key", "test_value", ex=3600, nx=False, xx=False
        )
        assert result is True

    def test_set_nx_success(self, redis_service):
        """Test set with NX (only if not exists) - success."""
        redis_service.client.set.return_value = True

        result = redis_service.set("test_key", "test_value", nx=True)

        redis_service.client.set.assert_called_once_with(
            "test_key", "test_value", ex=None, nx=True, xx=False
        )
        assert result is True

    def test_set_nx_failure(self, redis_service):
        """Test set with NX fails when key exists."""
        redis_service.client.set.return_value = None  # NX returns None if key exists

        result = redis_service.set("test_key", "test_value", nx=True)

        assert result is False

    def test_set_error(self, redis_service):
        """Test set raises RedisServiceError on failure."""
        redis_service.client.set.side_effect = RedisError("Connection lost")

        with pytest.raises(RedisServiceError) as exc_info:
            redis_service.set("test_key", "test_value")

        assert "Failed to set key" in str(exc_info.value)

    def test_delete_single_key(self, redis_service):
        """Test deleting a single key."""
        redis_service.client.delete.return_value = 1

        result = redis_service.delete("test_key")

        redis_service.client.delete.assert_called_once_with("test_key")
        assert result == 1

    def test_delete_multiple_keys(self, redis_service):
        """Test deleting multiple keys."""
        redis_service.client.delete.return_value = 3

        result = redis_service.delete("key1", "key2", "key3")

        redis_service.client.delete.assert_called_once_with("key1", "key2", "key3")
        assert result == 3

    def test_delete_no_keys(self, redis_service):
        """Test delete with no keys returns 0."""
        result = redis_service.delete()

        redis_service.client.delete.assert_not_called()
        assert result == 0

    def test_delete_error(self, redis_service):
        """Test delete raises RedisServiceError on failure."""
        redis_service.client.delete.side_effect = RedisError("Connection lost")

        with pytest.raises(RedisServiceError) as exc_info:
            redis_service.delete("test_key")

        assert "Failed to delete keys" in str(exc_info.value)

    def test_exists_single_key(self, redis_service):
        """Test checking existence of a single key."""
        redis_service.client.exists.return_value = 1

        result = redis_service.exists("test_key")

        redis_service.client.exists.assert_called_once_with("test_key")
        assert result == 1

    def test_exists_multiple_keys(self, redis_service):
        """Test checking existence of multiple keys."""
        redis_service.client.exists.return_value = 2

        result = redis_service.exists("key1", "key2", "key3")

        redis_service.client.exists.assert_called_once_with("key1", "key2", "key3")
        assert result == 2

    def test_exists_no_keys(self, redis_service):
        """Test exists with no keys returns 0."""
        result = redis_service.exists()

        redis_service.client.exists.assert_not_called()
        assert result == 0

    def test_exists_error(self, redis_service):
        """Test exists raises RedisServiceError on failure."""
        redis_service.client.exists.side_effect = RedisError("Connection lost")

        with pytest.raises(RedisServiceError) as exc_info:
            redis_service.exists("test_key")

        assert "Failed to check existence" in str(exc_info.value)


class TestRedisServiceJSON:
    """Test JSON serialization methods."""

    @pytest.fixture
    def redis_service(self):
        """Create a RedisService with mocked client."""
        with patch('app.services.redis_service.redis.ConnectionPool.from_url') as mock_pool_from_url:
            with patch('app.services.redis_service.redis.Redis') as mock_redis_class:
                mock_pool = MagicMock()
                mock_pool_from_url.return_value = mock_pool

                mock_client = MagicMock()
                mock_client.ping.return_value = True
                mock_redis_class.return_value = mock_client

                service = RedisService(redis_url='redis://localhost:6379/0')
                yield service

    def test_get_json_success(self, redis_service):
        """Test getting and deserializing JSON."""
        test_data = {"name": "test", "value": 123, "nested": {"key": "value"}}
        redis_service.client.get.return_value = json.dumps(test_data)

        result = redis_service.get_json("test_key")

        assert result == test_data

    def test_get_json_none(self, redis_service):
        """Test get_json returns None for non-existent key."""
        redis_service.client.get.return_value = None

        result = redis_service.get_json("nonexistent_key")

        assert result is None

    def test_get_json_invalid(self, redis_service):
        """Test get_json raises error for invalid JSON."""
        redis_service.client.get.return_value = "not valid json {"

        with pytest.raises(RedisServiceError) as exc_info:
            redis_service.get_json("test_key")

        assert "Failed to decode JSON" in str(exc_info.value)

    def test_set_json_success(self, redis_service):
        """Test serializing and setting JSON."""
        redis_service.client.set.return_value = True
        test_data = {"name": "test", "value": 123}

        result = redis_service.set_json("test_key", test_data, ttl=3600)

        # Verify the value was serialized
        call_args = redis_service.client.set.call_args
        stored_value = call_args[0][1]
        assert json.loads(stored_value) == test_data
        assert result is True

    def test_set_json_non_serializable(self, redis_service):
        """Test set_json raises error for non-serializable data."""
        non_serializable = {"func": lambda x: x}

        with pytest.raises(RedisServiceError) as exc_info:
            redis_service.set_json("test_key", non_serializable)

        assert "Failed to serialize value" in str(exc_info.value)


class TestRedisServiceTTL:
    """Test TTL-related operations."""

    @pytest.fixture
    def redis_service(self):
        """Create a RedisService with mocked client."""
        with patch('app.services.redis_service.redis.ConnectionPool.from_url') as mock_pool_from_url:
            with patch('app.services.redis_service.redis.Redis') as mock_redis_class:
                mock_pool = MagicMock()
                mock_pool_from_url.return_value = mock_pool

                mock_client = MagicMock()
                mock_client.ping.return_value = True
                mock_redis_class.return_value = mock_client

                service = RedisService(redis_url='redis://localhost:6379/0')
                yield service

    def test_expire_success(self, redis_service):
        """Test setting expiration on existing key."""
        redis_service.client.expire.return_value = True

        result = redis_service.expire("test_key", 3600)

        redis_service.client.expire.assert_called_once_with("test_key", 3600)
        assert result is True

    def test_expire_nonexistent_key(self, redis_service):
        """Test expire returns False for non-existent key."""
        redis_service.client.expire.return_value = False

        result = redis_service.expire("nonexistent_key", 3600)

        assert result is False

    def test_ttl_existing_key(self, redis_service):
        """Test getting TTL of existing key."""
        redis_service.client.ttl.return_value = 3500

        result = redis_service.ttl("test_key")

        redis_service.client.ttl.assert_called_once_with("test_key")
        assert result == 3500

    def test_ttl_no_expiry(self, redis_service):
        """Test TTL returns -1 for key without expiry."""
        redis_service.client.ttl.return_value = -1

        result = redis_service.ttl("test_key")

        assert result == -1

    def test_ttl_nonexistent_key(self, redis_service):
        """Test TTL returns -2 for non-existent key."""
        redis_service.client.ttl.return_value = -2

        result = redis_service.ttl("test_key")

        assert result == -2


class TestRedisServiceIncrement:
    """Test increment/decrement operations."""

    @pytest.fixture
    def redis_service(self):
        """Create a RedisService with mocked client."""
        with patch('app.services.redis_service.redis.ConnectionPool.from_url') as mock_pool_from_url:
            with patch('app.services.redis_service.redis.Redis') as mock_redis_class:
                mock_pool = MagicMock()
                mock_pool_from_url.return_value = mock_pool

                mock_client = MagicMock()
                mock_client.ping.return_value = True
                mock_redis_class.return_value = mock_client

                service = RedisService(redis_url='redis://localhost:6379/0')
                yield service

    def test_incr_default(self, redis_service):
        """Test incrementing by default amount (1)."""
        redis_service.client.incrby.return_value = 5

        result = redis_service.incr("counter")

        redis_service.client.incrby.assert_called_once_with("counter", 1)
        assert result == 5

    def test_incr_custom_amount(self, redis_service):
        """Test incrementing by custom amount."""
        redis_service.client.incrby.return_value = 15

        result = redis_service.incr("counter", 10)

        redis_service.client.incrby.assert_called_once_with("counter", 10)
        assert result == 15

    def test_decr_default(self, redis_service):
        """Test decrementing by default amount (1)."""
        redis_service.client.decrby.return_value = 4

        result = redis_service.decr("counter")

        redis_service.client.decrby.assert_called_once_with("counter", 1)
        assert result == 4

    def test_decr_custom_amount(self, redis_service):
        """Test decrementing by custom amount."""
        redis_service.client.decrby.return_value = 0

        result = redis_service.decr("counter", 5)

        redis_service.client.decrby.assert_called_once_with("counter", 5)
        assert result == 0


class TestRedisServiceHealth:
    """Test health check and info operations."""

    @pytest.fixture
    def redis_service(self):
        """Create a RedisService with mocked client."""
        with patch('app.services.redis_service.redis.ConnectionPool.from_url') as mock_pool_from_url:
            with patch('app.services.redis_service.redis.Redis') as mock_redis_class:
                mock_pool = MagicMock()
                mock_pool_from_url.return_value = mock_pool

                mock_client = MagicMock()
                mock_client.ping.return_value = True
                mock_redis_class.return_value = mock_client

                service = RedisService(redis_url='redis://localhost:6379/0')
                yield service

    def test_health_check_success(self, redis_service):
        """Test health check when Redis is healthy."""
        redis_service.client.ping.return_value = True

        result = redis_service.health_check()

        assert result is True

    def test_health_check_failure(self, redis_service):
        """Test health check when Redis is unhealthy."""
        redis_service.client.ping.side_effect = RedisError("Connection lost")

        result = redis_service.health_check()

        assert result is False

    def test_info_success(self, redis_service):
        """Test getting Redis info."""
        mock_info = {
            'redis_version': '7.0.0',
            'used_memory': 1000000,
            'connected_clients': 5
        }
        redis_service.client.info.return_value = mock_info

        result = redis_service.info()

        assert result == mock_info

    def test_info_error(self, redis_service):
        """Test info raises error on failure."""
        redis_service.client.info.side_effect = RedisError("Connection lost")

        with pytest.raises(RedisServiceError) as exc_info:
            redis_service.info()

        assert "Failed to get Redis info" in str(exc_info.value)


class TestRedisServiceUtility:
    """Test utility methods."""

    @pytest.fixture
    def redis_service(self):
        """Create a RedisService with mocked client."""
        with patch('app.services.redis_service.redis.ConnectionPool.from_url') as mock_pool_from_url:
            with patch('app.services.redis_service.redis.Redis') as mock_redis_class:
                mock_pool = MagicMock()
                mock_pool_from_url.return_value = mock_pool

                mock_client = MagicMock()
                mock_client.ping.return_value = True
                mock_redis_class.return_value = mock_client

                service = RedisService(redis_url='redis://localhost:6379/0')
                yield service

    def test_flush_db(self, redis_service):
        """Test flushing database."""
        redis_service.client.flushdb.return_value = True

        result = redis_service.flush_db()

        redis_service.client.flushdb.assert_called_once()
        assert result is True

    def test_close(self, redis_service):
        """Test closing connection pool."""
        redis_service.close()

        redis_service.pool.disconnect.assert_called_once()

    def test_context_manager(self, redis_service):
        """Test using service as context manager."""
        with redis_service as service:
            assert service is not None

        redis_service.pool.disconnect.assert_called_once()

    def test_sanitize_url_with_password(self, redis_service):
        """Test URL sanitization masks password."""
        url = "redis://user:secretpassword@localhost:6379/0"

        result = redis_service._sanitize_url(url)

        assert "secretpassword" not in result
        assert "***" in result
        assert "localhost:6379/0" in result

    def test_sanitize_url_without_password(self, redis_service):
        """Test URL sanitization with no password."""
        url = "redis://localhost:6379/0"

        result = redis_service._sanitize_url(url)

        assert result == url


class TestRedisServiceIntegration:
    """Integration-style tests that verify the flow of operations."""

    @pytest.fixture
    def redis_service(self):
        """Create a RedisService with mocked client."""
        with patch('app.services.redis_service.redis.ConnectionPool.from_url') as mock_pool_from_url:
            with patch('app.services.redis_service.redis.Redis') as mock_redis_class:
                mock_pool = MagicMock()
                mock_pool_from_url.return_value = mock_pool

                mock_client = MagicMock()
                mock_client.ping.return_value = True
                mock_redis_class.return_value = mock_client

                service = RedisService(redis_url='redis://localhost:6379/0')
                yield service

    def test_set_then_get(self, redis_service):
        """Test setting and then getting a value."""
        # Set
        redis_service.client.set.return_value = True
        redis_service.set("test_key", "test_value")

        # Get
        redis_service.client.get.return_value = "test_value"
        result = redis_service.get("test_key")

        assert result == "test_value"

    def test_json_roundtrip(self, redis_service):
        """Test JSON serialization roundtrip."""
        test_data = {
            "user_id": "user_123",
            "tokens_used": 450,
            "model": "claude-3-5-haiku"
        }

        # Set JSON
        redis_service.client.set.return_value = True
        redis_service.set_json("job_result", test_data, ttl=3600)

        # Get JSON
        redis_service.client.get.return_value = json.dumps(test_data)
        result = redis_service.get_json("job_result")

        assert result == test_data
