"""
Тесты для хранения snapshot трафика в Redis.
"""

from datetime import UTC, datetime, timedelta
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from app.services.traffic_monitoring_service import (
    TRAFFIC_SNAPSHOT_KEY,
    TRAFFIC_SNAPSHOT_TIME_KEY,
    TrafficMonitoringServiceV2,
)


@pytest.fixture
def service():
    """Создаёт экземпляр сервиса для тестов."""
    return TrafficMonitoringServiceV2()


@pytest.fixture
def mock_cache():
    """Мок для cache сервиса."""
    with patch('app.services.traffic_monitoring_service.cache') as mock:
        mock.set = AsyncMock(return_value=True)
        mock.get = AsyncMock(return_value=None)
        yield mock


@pytest.fixture
def sample_snapshot():
    """Пример snapshot данных."""
    return {
        'uuid-1': 1073741824.0,  # 1 GB
        'uuid-2': 2147483648.0,  # 2 GB
        'uuid-3': 5368709120.0,  # 5 GB
    }


# ============== Тесты сохранения snapshot в Redis ==============


async def test_save_snapshot_to_redis_success(service, mock_cache, sample_snapshot):
    """Тест успешного сохранения snapshot в Redis."""
    mock_cache.set = AsyncMock(return_value=True)

    result = await service._save_snapshot_to_redis(sample_snapshot)

    assert result is True
    assert mock_cache.set.call_count == 2  # snapshot + time

    # Проверяем что сохранён snapshot
    first_call = mock_cache.set.call_args_list[0]
    assert first_call[0][0] == TRAFFIC_SNAPSHOT_KEY
    assert first_call[0][1] == sample_snapshot


async def test_save_snapshot_to_redis_failure(service, mock_cache, sample_snapshot):
    """Тест неудачного сохранения snapshot в Redis."""
    mock_cache.set = AsyncMock(return_value=False)

    result = await service._save_snapshot_to_redis(sample_snapshot)

    assert result is False


async def test_save_snapshot_to_redis_exception(service, mock_cache, sample_snapshot):
    """Тест обработки исключения при сохранении."""
    mock_cache.set = AsyncMock(side_effect=Exception('Redis error'))

    result = await service._save_snapshot_to_redis(sample_snapshot)

    assert result is False


# ============== Тесты загрузки snapshot из Redis ==============


async def test_load_snapshot_from_redis_success(service, mock_cache, sample_snapshot):
    """Тест успешной загрузки snapshot из Redis."""
    mock_cache.get = AsyncMock(return_value=sample_snapshot)

    result = await service._load_snapshot_from_redis()

    assert result == sample_snapshot
    mock_cache.get.assert_called_once_with(TRAFFIC_SNAPSHOT_KEY)


async def test_load_snapshot_from_redis_empty(service, mock_cache):
    """Тест загрузки когда snapshot отсутствует."""
    mock_cache.get = AsyncMock(return_value=None)

    result = await service._load_snapshot_from_redis()

    assert result is None


async def test_load_snapshot_from_redis_invalid_data(service, mock_cache):
    """Тест загрузки невалидных данных."""
    mock_cache.get = AsyncMock(return_value='not a dict')

    result = await service._load_snapshot_from_redis()

    assert result is None


async def test_load_snapshot_from_redis_exception(service, mock_cache):
    """Тест обработки исключения при загрузке."""
    mock_cache.get = AsyncMock(side_effect=Exception('Redis error'))

    result = await service._load_snapshot_from_redis()

    assert result is None


# ============== Тесты времени snapshot ==============


async def test_get_snapshot_time_from_redis_success(service, mock_cache):
    """Тест получения времени snapshot."""
    test_time = datetime(2024, 1, 15, 12, 30, 0, tzinfo=UTC)
    mock_cache.get = AsyncMock(return_value=test_time.isoformat())

    result = await service._get_snapshot_time_from_redis()

    assert result == test_time
    mock_cache.get.assert_called_once_with(TRAFFIC_SNAPSHOT_TIME_KEY)


async def test_get_snapshot_time_from_redis_empty(service, mock_cache):
    """Тест когда время отсутствует."""
    mock_cache.get = AsyncMock(return_value=None)

    result = await service._get_snapshot_time_from_redis()

    assert result is None


# ============== Тесты has_snapshot ==============


async def test_has_snapshot_redis_exists(service, mock_cache, sample_snapshot):
    """Тест has_snapshot когда snapshot есть в Redis."""
    mock_cache.get = AsyncMock(return_value=sample_snapshot)

    result = await service.has_snapshot()

    assert result is True


async def test_has_snapshot_memory_fallback(service, mock_cache):
    """Тест has_snapshot с fallback на память."""
    mock_cache.get = AsyncMock(return_value=None)

    # Устанавливаем данные в память
    service._memory_snapshot = {'uuid-1': 1000.0}
    service._memory_snapshot_time = datetime.now(UTC)

    result = await service.has_snapshot()

    assert result is True


async def test_has_snapshot_none(service, mock_cache):
    """Тест has_snapshot когда snapshot нет нигде."""
    mock_cache.get = AsyncMock(return_value=None)
    service._memory_snapshot = {}
    service._memory_snapshot_time = None

    result = await service.has_snapshot()

    assert result is False


# ============== Тесты get_snapshot_age_minutes ==============


async def test_get_snapshot_age_minutes_from_redis(service, mock_cache):
    """Тест возраста snapshot из Redis."""
    # Snapshot создан 30 минут назад
    past_time = datetime.now(UTC) - timedelta(minutes=30)
    mock_cache.get = AsyncMock(return_value=past_time.isoformat())

    result = await service.get_snapshot_age_minutes()

    assert 29 <= result <= 31  # Допуск на время выполнения


async def test_get_snapshot_age_minutes_memory_fallback(service, mock_cache):
    """Тест возраста snapshot из памяти."""
    mock_cache.get = AsyncMock(return_value=None)
    service._memory_snapshot_time = datetime.now(UTC) - timedelta(minutes=15)

    result = await service.get_snapshot_age_minutes()

    assert 14 <= result <= 16


async def test_get_snapshot_age_minutes_no_snapshot(service, mock_cache):
    """Тест возраста когда snapshot нет."""
    mock_cache.get = AsyncMock(return_value=None)
    service._memory_snapshot_time = None

    result = await service.get_snapshot_age_minutes()

    assert result == float('inf')


# ============== Тесты _save_snapshot (с fallback) ==============


async def test_save_snapshot_redis_success(service, mock_cache, sample_snapshot):
    """Тест сохранения snapshot в Redis успешно."""
    mock_cache.set = AsyncMock(return_value=True)

    # Заполняем память чтобы проверить что она очистится
    service._memory_snapshot = {'old': 123.0}
    service._memory_snapshot_time = datetime.now(UTC)

    result = await service._save_snapshot(sample_snapshot)

    assert result is True
    assert service._memory_snapshot == {}  # Память очищена
    assert service._memory_snapshot_time is None


async def test_save_snapshot_fallback_to_memory(service, mock_cache, sample_snapshot):
    """Тест fallback на память когда Redis недоступен."""
    mock_cache.set = AsyncMock(return_value=False)

    result = await service._save_snapshot(sample_snapshot)

    assert result is True
    assert service._memory_snapshot == sample_snapshot
    assert service._memory_snapshot_time is not None


# ============== Тесты _get_current_snapshot ==============


async def test_get_current_snapshot_from_redis(service, mock_cache, sample_snapshot):
    """Тест получения snapshot из Redis."""
    mock_cache.get = AsyncMock(return_value=sample_snapshot)

    result = await service._get_current_snapshot()

    assert result == sample_snapshot


async def test_get_current_snapshot_fallback_to_memory(service, mock_cache, sample_snapshot):
    """Тест fallback на память."""
    mock_cache.get = AsyncMock(return_value=None)
    service._memory_snapshot = sample_snapshot

    result = await service._get_current_snapshot()

    assert result == sample_snapshot


# ============== Тесты уведомлений ==============


async def test_save_notification_to_redis(service, mock_cache):
    """Тест сохранения времени уведомления."""
    mock_cache.set = AsyncMock(return_value=True)

    result = await service._save_notification_to_redis('uuid-123')

    assert result is True
    mock_cache.set.assert_called_once()
    call_args = mock_cache.set.call_args
    assert 'traffic:notifications:uuid-123' in call_args[0][0]


async def test_get_notification_time_from_redis(service, mock_cache):
    """Тест получения времени уведомления."""
    test_time = datetime(2024, 1, 15, 10, 0, 0, tzinfo=UTC)
    mock_cache.get = AsyncMock(return_value=test_time.isoformat())

    result = await service._get_notification_time_from_redis('uuid-123')

    assert result == test_time


async def test_should_send_notification_no_previous(service, mock_cache):
    """Тест should_send_notification когда уведомлений не было."""
    mock_cache.get = AsyncMock(return_value=None)
    service._memory_notification_cache = {}

    result = await service.should_send_notification('uuid-123')

    assert result is True


async def test_should_send_notification_cooldown_active(service, mock_cache):
    """Тест should_send_notification когда кулдаун активен."""
    # Уведомление было 5 минут назад, кулдаун 60 минут
    recent_time = datetime.now(UTC) - timedelta(minutes=5)
    mock_cache.get = AsyncMock(return_value=recent_time.isoformat())

    result = await service.should_send_notification('uuid-123')

    assert result is False


async def test_should_send_notification_cooldown_expired(service, mock_cache):
    """Тест should_send_notification когда кулдаун истёк."""
    # Уведомление было 120 минут назад, кулдаун 60 минут
    old_time = datetime.now(UTC) - timedelta(minutes=120)
    mock_cache.get = AsyncMock(return_value=old_time.isoformat())

    result = await service.should_send_notification('uuid-123')

    assert result is True


async def test_record_notification_redis(service, mock_cache):
    """Тест record_notification сохраняет в Redis."""
    mock_cache.set = AsyncMock(return_value=True)

    await service.record_notification('uuid-123')

    mock_cache.set.assert_called_once()


async def test_record_notification_fallback_to_memory(service, mock_cache):
    """Тест record_notification с fallback на память."""
    mock_cache.set = AsyncMock(return_value=False)

    await service.record_notification('uuid-123')

    assert 'uuid-123' in service._memory_notification_cache


# ============== Тесты create_initial_snapshot ==============


async def test_create_initial_snapshot_uses_existing_redis(service, mock_cache, sample_snapshot):
    """Тест что create_initial_snapshot использует существующий snapshot из Redis."""
    mock_cache.get = AsyncMock(
        side_effect=[
            sample_snapshot,  # _load_snapshot_from_redis
            (datetime.now(UTC) - timedelta(minutes=10)).isoformat(),  # _get_snapshot_time_from_redis
        ]
    )

    with patch.object(service, 'get_all_users_with_traffic', new_callable=AsyncMock) as mock_get_users:
        result = await service.create_initial_snapshot()

        # Не должен вызывать API - используем существующий snapshot
        mock_get_users.assert_not_called()
        assert result == len(sample_snapshot)


async def test_create_initial_snapshot_creates_new(service, mock_cache):
    """Тест создания нового snapshot когда в Redis пусто."""
    mock_cache.get = AsyncMock(return_value=None)
    mock_cache.set = AsyncMock(return_value=True)

    # Мокаем пользователей из API
    mock_user = MagicMock()
    mock_user.uuid = 'uuid-1'
    mock_user.user_traffic = MagicMock()
    mock_user.user_traffic.used_traffic_bytes = 1073741824  # 1 GB

    with patch.object(service, 'get_all_users_with_traffic', new_callable=AsyncMock) as mock_get_users:
        mock_get_users.return_value = [mock_user]

        result = await service.create_initial_snapshot()

        mock_get_users.assert_called_once()
        assert result == 1


# ============== Тесты cleanup_notification_cache ==============


async def test_cleanup_notification_cache_removes_old(service, mock_cache):
    """Тест очистки старых записей из памяти."""
    old_time = datetime.now(UTC) - timedelta(hours=25)
    recent_time = datetime.now(UTC) - timedelta(hours=1)

    service._memory_notification_cache = {
        'uuid-old': old_time,
        'uuid-recent': recent_time,
    }

    await service.cleanup_notification_cache()

    assert 'uuid-old' not in service._memory_notification_cache
    assert 'uuid-recent' in service._memory_notification_cache
