"""Access-control and flood-guard middleware tests using real aiogram event types."""

from __future__ import annotations

from datetime import UTC, datetime
from typing import Any

import pytest
from aiogram.types import CallbackQuery, Chat, Message, User

from app.middlewares.access import DENIED_TEXT, PUBLIC_CALLBACKS, AccessMiddleware
from app.middlewares.rate_limit import WAIT_TEXT, RateLimitMiddleware


def _user(user_id: int = 4242) -> User:
    return User(id=user_id, is_bot=False, first_name="Admin")


def _message(text: str | None, user_id: int = 4242) -> Message:
    return Message.model_construct(
        message_id=1,
        date=datetime.now(UTC),
        chat=Chat(id=user_id, type="private"),
        from_user=_user(user_id),
        text=text,
    )


def _callback(data: str, user_id: int = 4242) -> CallbackQuery:
    return CallbackQuery.model_construct(
        id="callback-1",
        from_user=_user(user_id),
        chat_instance="instance",
        data=data,
        message=None,
    )


@pytest.fixture
def answers(monkeypatch: pytest.MonkeyPatch) -> list[tuple[str, dict[str, Any]]]:
    calls: list[tuple[str, dict[str, Any]]] = []

    async def message_answer(self: Message, text: str, **kwargs: Any) -> Message:
        calls.append((text, kwargs))
        return self

    async def callback_answer(self: CallbackQuery, text: str = "", **kwargs: Any) -> bool:
        calls.append((text, kwargs))
        return True

    monkeypatch.setattr(Message, "answer", message_answer)
    monkeypatch.setattr(CallbackQuery, "answer", callback_answer)
    return calls


async def _handler(event: object, data: dict[str, Any]) -> str:
    return "handled"


@pytest.mark.asyncio
async def test_admin_message_passes_regardless_of_command(answers) -> None:
    middleware = AccessMiddleware([4242])
    assert await middleware(_handler, _message("/add https://example.com", 4242), {}) == "handled"
    assert await middleware(_handler, _message("hello", 4242), {}) == "handled"
    assert answers == []


@pytest.mark.asyncio
@pytest.mark.parametrize("command", ["/start", "/help", "/id", "/START", "/start@MyBot", "/help@bot"])
async def test_public_commands_are_available_to_everyone(answers, command: str) -> None:
    middleware = AccessMiddleware([4242])
    assert await middleware(_handler, _message(command, 999), {}) == "handled"
    assert answers == []


@pytest.mark.asyncio
async def test_non_admin_command_is_denied(answers) -> None:
    middleware = AccessMiddleware([4242])
    assert await middleware(_handler, _message("/add https://example.com", 999), {}) is None
    assert answers == [(DENIED_TEXT, {"parse_mode": None})]


@pytest.mark.asyncio
async def test_non_admin_plain_text_is_ignored_silently(answers) -> None:
    middleware = AccessMiddleware([4242])
    assert await middleware(_handler, _message("hello", 999), {}) is None
    assert answers == []


@pytest.mark.asyncio
async def test_public_callback_is_available_to_everyone(answers) -> None:
    middleware = AccessMiddleware([4242])
    for data in PUBLIC_CALLBACKS:
        assert await middleware(_handler, _callback(data, 999), {}) == "handled"
    assert answers == []


@pytest.mark.asyncio
async def test_private_callback_is_denied_with_alert(answers) -> None:
    middleware = AccessMiddleware([4242])
    assert await middleware(_handler, _callback("job:cancel:abc", 999), {}) is None
    assert answers == [(DENIED_TEXT, {"show_alert": True})]


@pytest.mark.asyncio
async def test_unknown_event_without_user_is_ignored(answers) -> None:
    middleware = AccessMiddleware([4242])

    class OtherEvent:
        pass

    assert await middleware(_handler, OtherEvent(), {}) is None
    assert answers == []


@pytest.mark.asyncio
async def test_rate_limit_allows_first_and_blocks_rapid_second(
    answers, monkeypatch: pytest.MonkeyPatch
) -> None:
    clock = [100.0]
    monkeypatch.setattr("app.middlewares.rate_limit.time.monotonic", lambda: clock[0])
    middleware = RateLimitMiddleware(interval_seconds=1.0)

    assert await middleware(_handler, _message("/status", 7), {}) == "handled"
    clock[0] = 100.4
    assert await middleware(_handler, _message("/status", 7), {}) is None
    assert answers == [(WAIT_TEXT, {"parse_mode": None})]

    clock[0] = 101.1
    assert await middleware(_handler, _message("/status", 7), {}) == "handled"
    assert len(answers) == 1


@pytest.mark.asyncio
async def test_rate_limit_is_per_user(answers, monkeypatch: pytest.MonkeyPatch) -> None:
    clock = [200.0]
    monkeypatch.setattr("app.middlewares.rate_limit.time.monotonic", lambda: clock[0])
    middleware = RateLimitMiddleware(interval_seconds=1.0)

    assert await middleware(_handler, _message("/status", 1), {}) == "handled"
    assert await middleware(_handler, _message("/status", 2), {}) == "handled"
    assert answers == []


@pytest.mark.asyncio
async def test_rate_limit_callback_uses_plain_toast(
    answers, monkeypatch: pytest.MonkeyPatch
) -> None:
    clock = [300.0]
    monkeypatch.setattr("app.middlewares.rate_limit.time.monotonic", lambda: clock[0])
    middleware = RateLimitMiddleware(interval_seconds=1.0)

    assert await middleware(_handler, _callback("menu:home", 7), {}) == "handled"
    assert await middleware(_handler, _callback("menu:home", 7), {}) is None
    assert answers == [(WAIT_TEXT, {})]


@pytest.mark.asyncio
async def test_rate_limit_zero_interval_and_anonymous_event_always_pass(answers) -> None:
    disabled = RateLimitMiddleware(interval_seconds=0)
    assert await disabled(_handler, _message("/status", 1), {}) == "handled"
    assert await disabled(_handler, _message("/status", 1), {}) == "handled"

    enabled = RateLimitMiddleware(interval_seconds=10)
    event = Message.model_construct(
        message_id=1,
        date=datetime.now(UTC),
        chat=Chat(id=1, type="private"),
        from_user=None,
        text="/status",
    )
    assert await enabled(_handler, event, {}) == "handled"
    assert answers == []
