from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from ui_router.events import EventData
from ui_router.exceptions import EventSchedulingError
from ui_router.integrations.taskiq_scheduler import TaskiqEventScheduler


@pytest.fixture
def captured_events():
    return []


@pytest.fixture
def event_callback(captured_events):
    async def callback(event: EventData):
        captured_events.append(event)

    return callback


@pytest.fixture
def mock_schedule():
    schedule = AsyncMock()
    schedule.unschedule = AsyncMock()
    return schedule


@pytest.fixture
def mock_task(mock_schedule):
    task = MagicMock()
    task.schedule_by_time = AsyncMock(return_value=mock_schedule)
    task.schedule_by_cron = AsyncMock(return_value=mock_schedule)
    task.schedule_by_interval = AsyncMock(return_value=mock_schedule)
    return task


@pytest.fixture
def mock_broker(mock_task):
    broker = MagicMock()
    broker.register_task = MagicMock(return_value=mock_task)
    return broker


@pytest.fixture
def mock_source():
    return MagicMock()


@pytest.fixture
def scheduler(event_callback, mock_broker, mock_source):
    return TaskiqEventScheduler(
        event_callback=event_callback,
        broker=mock_broker,
        schedule_source=mock_source,
    )


@pytest.fixture
def scheduler_no_source(event_callback, mock_broker):
    return TaskiqEventScheduler(
        event_callback=event_callback,
        broker=mock_broker,
        schedule_source=None,
    )


class TestScheduleOnce:
    async def test_calls_schedule_by_time(self, scheduler, mock_task, mock_source):
        task_id = await scheduler.schedule_once("ev", "10m", {"k": "v"}, bot_id=1, user_id=42)
        assert isinstance(task_id, str)
        mock_task.schedule_by_time.assert_called_once()
        call_args = mock_task.schedule_by_time.call_args
        assert call_args.args[0] is mock_source
        assert call_args.kwargs["event_name"] == "ev"
        assert call_args.kwargs["event_data"] == {"k": "v"}
        assert call_args.kwargs["bot_id"] == 1
        assert call_args.kwargs["user_id"] == 42

    async def test_invalid_delay(self, scheduler):
        with pytest.raises(EventSchedulingError):
            await scheduler.schedule_once("ev", "bad", {}, bot_id=1)

    async def test_without_source_raises(self, scheduler_no_source):
        with pytest.raises(EventSchedulingError, match="schedule_source is required"):
            await scheduler_no_source.schedule_once("ev", "10m", {}, bot_id=1)


class TestScheduleCron:
    async def test_calls_schedule_by_cron(self, scheduler, mock_task, mock_source):
        task_id = await scheduler.schedule_cron("ev", "* * * * *", {}, bot_id=1)
        assert isinstance(task_id, str)
        mock_task.schedule_by_cron.assert_called_once()
        call_args = mock_task.schedule_by_cron.call_args
        assert call_args.args[0] is mock_source
        assert call_args.args[1] == "* * * * *"

    async def test_without_source_raises(self, scheduler_no_source):
        with pytest.raises(EventSchedulingError, match="schedule_source is required"):
            await scheduler_no_source.schedule_cron("ev", "* * * * *", {}, bot_id=1)


class TestScheduleInterval:
    async def test_calls_schedule_by_interval(self, scheduler, mock_task, mock_source):
        task_id = await scheduler.schedule_interval("ev", 60, {}, bot_id=1)
        assert isinstance(task_id, str)
        mock_task.schedule_by_interval.assert_called_once()
        call_args = mock_task.schedule_by_interval.call_args
        assert call_args.args[0] is mock_source
        assert call_args.args[1] == 60

    async def test_without_source_raises(self, scheduler_no_source):
        with pytest.raises(EventSchedulingError, match="schedule_source is required"):
            await scheduler_no_source.schedule_interval("ev", 60, {}, bot_id=1)


class TestCancelScheduled:
    async def test_cancel_calls_unschedule(self, scheduler, mock_schedule):
        task_id = await scheduler.schedule_once("ev", "5s", {}, bot_id=1)
        result = await scheduler.cancel_scheduled(task_id)
        assert result is True
        mock_schedule.unschedule.assert_called_once()

    async def test_cancel_nonexistent(self, scheduler):
        result = await scheduler.cancel_scheduled("nonexistent_id")
        assert result is False

    async def test_cancel_removes_from_dict(self, scheduler):
        task_id = await scheduler.schedule_once("ev", "5s", {}, bot_id=1)
        assert task_id in scheduler._schedules
        await scheduler.cancel_scheduled(task_id)
        assert task_id not in scheduler._schedules


class TestFireEvent:
    async def test_fire_event(self, scheduler, captured_events):
        await scheduler._fire_event("test", {"x": 1}, bot_id=1, user_id=10, chat_id=20)
        assert len(captured_events) == 1
        event = captured_events[0]
        assert event.event_name == "test"
        assert event.data == {"x": 1}
        assert event.bot_id == 1
        assert event.user_id == 10
        assert event.chat_id == 20


class TestBrokerRegistration:
    def test_registers_task_on_init(self, event_callback, mock_broker):
        TaskiqEventScheduler(event_callback=event_callback, broker=mock_broker)
        mock_broker.register_task.assert_called_once()
        call_kwargs = mock_broker.register_task.call_args.kwargs
        assert call_kwargs["task_name"] == "ui_router_fire_event"
