from unittest.mock import AsyncMock, MagicMock
from decimal import Decimal
from enum import IntEnum
from uuid import UUID, uuid4

import pytest
from aiogram.types import InlineQuery

from ui_router.aiogram.filters.inline_data import InlineData, InlineQueryFilter, MAX_CALLBACK_LENGTH


class TestInlineDataSubclass:
    def test_subclass_without_prefix_raises_value_error(self):
        with pytest.raises(ValueError, match="prefix required"):

            class Bad(InlineData):
                pass

    def test_separator_in_prefix_raises_value_error(self):
        with pytest.raises(ValueError, match="Separator symbol"):

            class Bad(InlineData, prefix="pre:fix"):
                pass


class TestInlineDataPack:
    def test_pack_simple_str_int_fields(self):
        class MyData(InlineData, prefix="cmd"):
            name: str
            age: int

        data = MyData(name="alice", age=30)
        assert data.pack() == "cmd:alice:30"

    def test_pack_enum_field(self):
        class Color(IntEnum):
            RED = 1
            BLUE = 2

        class MyData(InlineData, prefix="clr"):
            color: Color

        data = MyData(color=Color.RED)
        assert data.pack() == "clr:1"

    def test_pack_none_optional_field_produces_empty_string(self):
        class MyData(InlineData, prefix="opt"):
            value: str | None = None

        data = MyData(value=None)
        assert data.pack() == "opt:"

    def test_pack_decimal_and_uuid(self):
        class MyData(InlineData, prefix="du"):
            price: Decimal
            uid: UUID

        uid = uuid4()
        data = MyData(price=Decimal("9.99"), uid=uid)
        assert data.pack() == f"du:9.99:{uid}"

    def test_pack_exceeding_max_length_raises_value_error(self):
        class MyData(InlineData, prefix="p"):
            value: str

        long_value = "x" * MAX_CALLBACK_LENGTH
        data = MyData(value=long_value)
        with pytest.raises(ValueError, match="too long"):
            data.pack()

    def test_pack_separator_in_value_raises_value_error(self):
        class MyData(InlineData, prefix="sep"):
            value: str

        data = MyData(value="has:colon")
        with pytest.raises(ValueError, match="Separator symbol"):
            data.pack()


class TestInlineDataUnpack:
    def test_unpack_correct_data(self):
        class MyData(InlineData, prefix="cmd"):
            name: str
            age: int

        result = MyData.unpack("cmd:alice:30")
        assert result.name == "alice"
        assert result.age == 30

    def test_unpack_wrong_prefix_raises_value_error(self):
        class MyData(InlineData, prefix="cmd"):
            name: str

        with pytest.raises(ValueError, match="Bad prefix"):
            MyData.unpack("wrong:alice")

    def test_unpack_wrong_field_count_raises_type_error(self):
        class MyData(InlineData, prefix="cmd"):
            name: str
            age: int

        with pytest.raises(TypeError, match="takes 2 arguments but 1 were given"):
            MyData.unpack("cmd:alice")

    def test_unpack_optional_empty_string_becomes_none(self):
        class MyData(InlineData, prefix="opt"):
            value: str | None = None

        result = MyData.unpack("opt:")
        assert result.value is None

    def test_custom_separator(self):
        class MyData(InlineData, prefix="cmd", sep="|"):
            name: str
            age: int

        data = MyData(name="alice", age=25)
        assert data.pack() == "cmd|alice|25"

        result = MyData.unpack("cmd|alice|25")
        assert result.name == "alice"
        assert result.age == 25


class TestInlineQueryFilter:
    async def test_non_inline_query_event_returns_false(self):
        class MyData(InlineData, prefix="test"):
            value: str

        flt = InlineQueryFilter(inline_data=MyData, rule=None)
        event = MagicMock()
        assert await flt(event) is False

    async def test_empty_query_returns_false(self):
        class MyData(InlineData, prefix="test"):
            value: str

        flt = InlineQueryFilter(inline_data=MyData, rule=None)
        query = MagicMock(spec=InlineQuery)
        query.query = ""
        assert await flt(query) is False

    async def test_valid_query_unpacks_and_returns_dict(self):
        class MyData(InlineData, prefix="test"):
            value: str

        flt = InlineQueryFilter(inline_data=MyData, rule=None)
        query = MagicMock(spec=InlineQuery)
        query.query = "test:hello"

        result = await flt(query)
        assert isinstance(result, dict)
        assert "inline_data" in result
        assert result["inline_data"].value == "hello"

    async def test_invalid_query_wrong_prefix_returns_false(self):
        class MyData(InlineData, prefix="test"):
            value: str

        flt = InlineQueryFilter(inline_data=MyData, rule=None)
        query = MagicMock(spec=InlineQuery)
        query.query = "wrong:hello"

        assert await flt(query) is False

    async def test_magic_filter_rule_matching_returns_dict(self):
        class MyData(InlineData, prefix="test"):
            value: str

        rule = MagicMock()
        rule.resolve.return_value = True
        flt = InlineQueryFilter(inline_data=MyData, rule=rule)
        query = MagicMock(spec=InlineQuery)
        query.query = "test:world"

        result = await flt(query)
        assert isinstance(result, dict)
        assert result["inline_data"].value == "world"
        rule.resolve.assert_called_once()

    async def test_magic_filter_rule_not_matching_returns_false(self):
        class MyData(InlineData, prefix="test"):
            value: str

        rule = MagicMock()
        rule.resolve.return_value = False
        flt = InlineQueryFilter(inline_data=MyData, rule=rule)
        query = MagicMock(spec=InlineQuery)
        query.query = "test:world"

        assert await flt(query) is False
