from unittest.mock import AsyncMock

import pytest

from ui_router.schema import ThrottleConfig, ThrottleScope
from ui_router.execution.throttle import ThrottleChecker


@pytest.fixture
def cache() -> AsyncMock:
    mock = AsyncMock()
    mock.get = AsyncMock(return_value=None)
    mock.set = AsyncMock()
    return mock


@pytest.fixture
def checker(cache: AsyncMock) -> ThrottleChecker:
    return ThrottleChecker(cache_storage=cache)


@pytest.fixture
def user_config() -> ThrottleConfig:
    return ThrottleConfig(max_calls=3, period_seconds=60, scope=ThrottleScope.USER)


@pytest.fixture
def chat_config() -> ThrottleConfig:
    return ThrottleConfig(max_calls=3, period_seconds=60, scope=ThrottleScope.CHAT)


@pytest.fixture
def global_config() -> ThrottleConfig:
    return ThrottleConfig(max_calls=3, period_seconds=60, scope=ThrottleScope.GLOBAL)


@pytest.mark.asyncio
async def test_first_request_allowed(checker: ThrottleChecker, cache: AsyncMock, user_config: ThrottleConfig):
    result = await checker.check("on_start", user_config, user_id=42, chat_id=100)

    assert result is True
    cache.set.assert_awaited_once_with("throttle:user:on_start:42", "1", ttl=60)


@pytest.mark.asyncio
async def test_under_limit_allowed(checker: ThrottleChecker, cache: AsyncMock, user_config: ThrottleConfig):
    cache.get.return_value = "1"

    result = await checker.check("on_start", user_config, user_id=42, chat_id=100)

    assert result is True
    cache.set.assert_awaited_once_with("throttle:user:on_start:42", "2", ttl=60)


@pytest.mark.asyncio
async def test_at_limit_throttled(checker: ThrottleChecker, cache: AsyncMock, user_config: ThrottleConfig):
    cache.get.return_value = "3"

    result = await checker.check("on_start", user_config, user_id=42, chat_id=100)

    assert result is False
    cache.set.assert_not_awaited()


@pytest.mark.asyncio
async def test_over_limit_throttled(checker: ThrottleChecker, cache: AsyncMock, user_config: ThrottleConfig):
    cache.get.return_value = "5"

    result = await checker.check("on_start", user_config, user_id=42, chat_id=100)

    assert result is False
    cache.set.assert_not_awaited()


def test_build_key_user_scope():
    key = ThrottleChecker._build_key("handler_a", ThrottleScope.USER, user_id=42, chat_id=100)
    assert key == "throttle:user:handler_a:42"


def test_build_key_chat_scope():
    key = ThrottleChecker._build_key("handler_a", ThrottleScope.CHAT, user_id=42, chat_id=100)
    assert key == "throttle:chat:handler_a:100"


def test_build_key_global_scope():
    key = ThrottleChecker._build_key("handler_a", ThrottleScope.GLOBAL, user_id=42, chat_id=100)
    assert key == "throttle:global:handler_a"


def test_build_key_user_scope_none_ids():
    key = ThrottleChecker._build_key("handler_a", ThrottleScope.USER, user_id=None, chat_id=None)
    assert key == "throttle:user:handler_a:None"


def test_build_key_chat_scope_none_ids():
    key = ThrottleChecker._build_key("handler_a", ThrottleScope.CHAT, user_id=None, chat_id=None)
    assert key == "throttle:chat:handler_a:None"


@pytest.mark.asyncio
async def test_ttl_matches_period_seconds(checker: ThrottleChecker, cache: AsyncMock):
    config = ThrottleConfig(max_calls=10, period_seconds=120, scope=ThrottleScope.USER)

    await checker.check("action", config, user_id=1, chat_id=1)

    cache.set.assert_awaited_once()
    _, kwargs = cache.set.call_args
    assert kwargs["ttl"] == 120


@pytest.mark.asyncio
async def test_chat_scope_uses_chat_id(checker: ThrottleChecker, cache: AsyncMock, chat_config: ThrottleConfig):
    result = await checker.check("on_click", chat_config, user_id=42, chat_id=999)

    assert result is True
    cache.set.assert_awaited_once_with("throttle:chat:on_click:999", "1", ttl=60)


@pytest.mark.asyncio
async def test_global_scope_ignores_ids(checker: ThrottleChecker, cache: AsyncMock, global_config: ThrottleConfig):
    result = await checker.check("broadcast", global_config, user_id=42, chat_id=999)

    assert result is True
    cache.set.assert_awaited_once_with("throttle:global:broadcast", "1", ttl=60)


@pytest.mark.asyncio
async def test_multiple_calls_increment_correctly(cache: AsyncMock):
    checker = ThrottleChecker(cache_storage=cache)
    config = ThrottleConfig(max_calls=3, period_seconds=60, scope=ThrottleScope.USER)

    cache.get.return_value = None
    result_1 = await checker.check("action", config, user_id=1, chat_id=1)
    assert result_1 is True

    cache.get.return_value = "1"
    result_2 = await checker.check("action", config, user_id=1, chat_id=1)
    assert result_2 is True

    cache.get.return_value = "2"
    result_3 = await checker.check("action", config, user_id=1, chat_id=1)
    assert result_3 is True

    cache.get.return_value = "3"
    result_4 = await checker.check("action", config, user_id=1, chat_id=1)
    assert result_4 is False

    expected_key = "throttle:user:action:1"
    set_calls = cache.set.call_args_list
    assert len(set_calls) == 3
    assert set_calls[0].args == (expected_key, "1")
    assert set_calls[1].args == (expected_key, "2")
    assert set_calls[2].args == (expected_key, "3")


@pytest.mark.asyncio
async def test_different_users_tracked_separately(cache: AsyncMock):
    checker = ThrottleChecker(cache_storage=cache)
    config = ThrottleConfig(max_calls=1, period_seconds=30, scope=ThrottleScope.USER)

    cache.get.return_value = None
    result_user_1 = await checker.check("action", config, user_id=1, chat_id=1)
    assert result_user_1 is True

    cache.get.return_value = None
    result_user_2 = await checker.check("action", config, user_id=2, chat_id=1)
    assert result_user_2 is True

    get_keys = [call.args[0] for call in cache.get.call_args_list]
    assert "throttle:user:action:1" in get_keys
    assert "throttle:user:action:2" in get_keys


@pytest.mark.asyncio
async def test_max_calls_one_allows_then_blocks(checker: ThrottleChecker, cache: AsyncMock):
    config = ThrottleConfig(max_calls=1, period_seconds=10, scope=ThrottleScope.USER)

    cache.get.return_value = None
    assert await checker.check("action", config, user_id=5, chat_id=1) is True

    cache.get.return_value = "1"
    assert await checker.check("action", config, user_id=5, chat_id=1) is False
