import json
from unittest.mock import AsyncMock, MagicMock, patch

import pytest


@pytest.mark.asyncio
async def test_get_redis_connection_fails():
    """_get_redis returns None when Redis.from_url raises ConnectionError."""
    from app.core.cache import CacheService

    cache = CacheService()
    with patch("redis.asyncio.Redis.from_url", side_effect=ConnectionError("refused")):
        r = await cache._get_redis()

    assert r is None
    assert cache._redis is None


@pytest.mark.asyncio
async def test_get_cache_miss():
    """get returns None when key does not exist in Redis."""
    from app.core.cache import CacheService

    cache = CacheService()
    mock_redis = AsyncMock()
    mock_redis.get.return_value = None
    cache._redis = mock_redis

    result = await cache.get("missing_key")

    assert result is None
    mock_redis.get.assert_awaited_once_with("missing_key")


@pytest.mark.asyncio
async def test_get_cache_hit():
    """get returns parsed dict when key exists in Redis."""
    from app.core.cache import CacheService

    cache = CacheService()
    mock_redis = AsyncMock()
    mock_redis.get.return_value = '{"key": "value"}'
    cache._redis = mock_redis

    result = await cache.get("test_key")

    assert result == {"key": "value"}
    mock_redis.get.assert_awaited_once_with("test_key")


@pytest.mark.asyncio
async def test_get_no_redis():
    """get returns None when _get_redis returns None."""
    from app.core.cache import CacheService

    cache = CacheService()
    cache._redis = None

    with patch.object(cache, "_get_redis", return_value=None):
        result = await cache.get("any_key")

    assert result is None


@pytest.mark.asyncio
async def test_set_success():
    """set returns True when Redis.set succeeds."""
    from app.core.cache import CacheService

    cache = CacheService()
    mock_redis = AsyncMock()
    mock_redis.set.return_value = True
    cache._redis = mock_redis

    result = await cache.set("k", {"a": 1})

    assert result is True
    mock_redis.set.assert_awaited_once_with("k", json.dumps({"a": 1}, default=str), ex=300)


@pytest.mark.asyncio
async def test_set_no_redis():
    """set stores in-memory fallback when _get_redis returns None."""
    from app.core.cache import CacheService

    cache = CacheService()
    with patch.object(cache, "_get_redis", return_value=None):
        result = await cache.set("k", {"a": 1})

    assert result is True  # falls back to in-memory store


@pytest.mark.asyncio
async def test_set_exception():
    """set returns False when Redis.set raises an exception."""
    from app.core.cache import CacheService

    cache = CacheService()
    mock_redis = AsyncMock()
    mock_redis.set.side_effect = RuntimeError("timeout")
    cache._redis = mock_redis

    result = await cache.set("k", {"a": 1})

    assert result is False


@pytest.mark.asyncio
async def test_delete():
    """delete calls Redis.delete with the given key."""
    from app.core.cache import CacheService

    cache = CacheService()
    mock_redis = AsyncMock()
    cache._redis = mock_redis

    await cache.delete("some_key")

    mock_redis.delete.assert_awaited_once_with("some_key")


@pytest.mark.asyncio
async def test_pop_found():
    """pop returns parsed dict and deletes the key when found in Redis."""
    from app.core.cache import CacheService

    cache = CacheService()
    mock_redis = AsyncMock()
    mock_redis.get.return_value = '{"name": "test"}'
    cache._redis = mock_redis

    result = await cache.pop("pop_key")

    assert result == {"name": "test"}
    mock_redis.get.assert_awaited_once_with("pop_key")
    mock_redis.delete.assert_awaited_once_with("pop_key")


@pytest.mark.asyncio
async def test_pop_not_found():
    """pop returns None when key does not exist in Redis."""
    from app.core.cache import CacheService

    cache = CacheService()
    mock_redis = AsyncMock()
    mock_redis.get.return_value = None
    cache._redis = mock_redis

    result = await cache.pop("missing_pop")

    assert result is None
    mock_redis.get.assert_awaited_once_with("missing_pop")
    mock_redis.delete.assert_not_called()


@pytest.mark.asyncio
async def test_pop_no_redis():
    """pop returns None when _get_redis returns None."""
    from app.core.cache import CacheService

    cache = CacheService()
    with patch.object(cache, "_get_redis", return_value=None):
        result = await cache.pop("any_key")

    assert result is None


@pytest.mark.asyncio
async def test_invalidate_user():
    """invalidate_user deletes the user profile key."""
    from app.core.cache import CacheService

    cache = CacheService()
    mock_redis = AsyncMock()
    cache._redis = mock_redis

    await cache.invalidate_user("user_123")

    mock_redis.delete.assert_awaited_once_with("user:user_123:profile")


@pytest.mark.asyncio
async def test_invalidate_tenant():
    """invalidate_tenant deletes both plan and settings keys for the tenant."""
    from app.core.cache import CacheService

    cache = CacheService()
    mock_redis = AsyncMock()
    cache._redis = mock_redis

    await cache.invalidate_tenant("tenant_abc")

    assert mock_redis.delete.await_count == 2
    mock_redis.delete.assert_any_await("tenant:tenant_abc:plan")
    mock_redis.delete.assert_any_await("tenant:tenant_abc:settings")
