"""Tests for ReminderScheduler (U5)."""

from __future__ import annotations

import asyncio
import uuid
from datetime import datetime, timedelta, timezone
from pathlib import Path
from unittest.mock import AsyncMock

import pytest

from agentkit.calendar.db import (
    get_pending_deliveries,
    init_calendar_db,
    insert_event,
    insert_reminder_rule,
    list_reminder_rules_for_event,
)
from agentkit.calendar.models import CalendarEvent, ReminderRule, _now_iso
from agentkit.calendar.reminders import ReminderDispatcher
from agentkit.calendar.scheduler import ReminderScheduler


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
def db_path(tmp_path: Path) -> Path:
    path = tmp_path / "test_calendar.db"
    asyncio.run(init_calendar_db(path))
    return path


@pytest.fixture
def auth_db_path(tmp_path: Path) -> Path:
    from agentkit.server.auth.models import init_auth_db

    path = tmp_path / "test_auth.db"
    asyncio.run(init_auth_db(path))
    return path


def _make_event(
    event_id: str,
    user_id: str,
    start_time: str,
    title: str = "Test Event",
) -> CalendarEvent:
    now = _now_iso()
    return CalendarEvent(
        id=event_id,
        user_id=user_id,
        title=title,
        start_time=start_time,
        end_time=start_time,
        last_modified=now,
        created_at=now,
    )


def _mock_dispatcher(return_value: bool = True) -> ReminderDispatcher:
    """Create a dispatcher with a mocked dispatch method."""
    dispatcher = ReminderDispatcher()
    dispatcher.dispatch = AsyncMock(return_value=return_value)  # type: ignore
    return dispatcher


# ---------------------------------------------------------------------------
# Scheduler scan logic
# ---------------------------------------------------------------------------


async def test_scheduler_finds_event_within_reminder_window(db_path: Path) -> None:
    """Event 10 min away, rule offset -15min → reminder_time 5 min ago → found."""
    now = datetime.now(timezone.utc)
    event_start = (now + timedelta(minutes=10)).isoformat()
    event = _make_event("evt-1", "user-1", event_start, "Meeting")
    await insert_event(event, db_path)

    rule = ReminderRule(id="rule-1", event_id="evt-1", offset_minutes=-15, channels=["client"])
    await insert_reminder_rule(rule, db_path)

    dispatcher = _mock_dispatcher(return_value=True)
    scheduler = ReminderScheduler(db_path=db_path, dispatcher=dispatcher)

    count = await scheduler.scan_once()

    assert count == 1
    assert dispatcher.dispatch.call_count == 1  # type: ignore


async def test_scheduler_skips_event_outside_window(db_path: Path) -> None:
    """Event 2 hours away, rule offset -15min → reminder_time 1hr45min away → not found."""
    now = datetime.now(timezone.utc)
    event_start = (now + timedelta(hours=2)).isoformat()
    event = _make_event("evt-1", "user-1", event_start, "Meeting")
    await insert_event(event, db_path)

    rule = ReminderRule(id="rule-1", event_id="evt-1", offset_minutes=-15, channels=["client"])
    await insert_reminder_rule(rule, db_path)

    dispatcher = _mock_dispatcher(return_value=True)
    scheduler = ReminderScheduler(db_path=db_path, dispatcher=dispatcher)

    count = await scheduler.scan_once()

    assert count == 0
    assert dispatcher.dispatch.call_count == 0  # type: ignore


async def test_idempotent_delivery_no_duplicate(db_path: Path) -> None:
    """Scheduler runs twice, only one delivery record created."""
    now = datetime.now(timezone.utc)
    event_start = (now + timedelta(minutes=10)).isoformat()
    event = _make_event("evt-1", "user-1", event_start, "Meeting")
    await insert_event(event, db_path)

    rule = ReminderRule(id="rule-1", event_id="evt-1", offset_minutes=-15, channels=["client"])
    await insert_reminder_rule(rule, db_path)

    dispatcher = _mock_dispatcher(return_value=True)
    scheduler = ReminderScheduler(db_path=db_path, dispatcher=dispatcher)

    count1 = await scheduler.scan_once()
    assert count1 == 1

    count2 = await scheduler.scan_once()
    assert count2 == 0

    deliveries = await get_pending_deliveries("evt-1", "rule-1", db_path)
    assert len(deliveries) == 1


async def test_failed_delivery_retries_up_to_3_times(db_path: Path) -> None:
    """Mock channel to fail, verify 3 attempts and delivery status=failed."""
    now = datetime.now(timezone.utc)
    event_start = (now + timedelta(minutes=10)).isoformat()
    event = _make_event("evt-1", "user-1", event_start, "Meeting")
    await insert_event(event, db_path)

    rule = ReminderRule(id="rule-1", event_id="evt-1", offset_minutes=-15, channels=["client"])
    await insert_reminder_rule(rule, db_path)

    dispatcher = _mock_dispatcher(return_value=False)
    scheduler = ReminderScheduler(db_path=db_path, dispatcher=dispatcher, retry_base_delay=0)

    await scheduler.scan_once()

    assert dispatcher.dispatch.call_count == 3  # type: ignore

    deliveries = await get_pending_deliveries("evt-1", "rule-1", db_path, status="failed")
    assert len(deliveries) == 1
    assert deliveries[0].attempts == 3
    assert deliveries[0].status == "failed"


async def test_scheduler_dispatches_multiple_channels(db_path: Path) -> None:
    """Rule with 2 channels creates 2 delivery records."""
    now = datetime.now(timezone.utc)
    event_start = (now + timedelta(minutes=10)).isoformat()
    event = _make_event("evt-1", "user-1", event_start, "Meeting")
    await insert_event(event, db_path)

    rule = ReminderRule(
        id="rule-1",
        event_id="evt-1",
        offset_minutes=-15,
        channels=["client", "email"],
    )
    await insert_reminder_rule(rule, db_path)

    dispatcher = _mock_dispatcher(return_value=True)
    scheduler = ReminderScheduler(db_path=db_path, dispatcher=dispatcher)

    count = await scheduler.scan_once()
    assert count == 2
    assert dispatcher.dispatch.call_count == 2  # type: ignore


# ---------------------------------------------------------------------------
# Start/stop lifecycle
# ---------------------------------------------------------------------------


async def test_scheduler_start_stop_lifecycle(db_path: Path) -> None:
    """start() creates task, stop() cancels it."""
    scheduler = ReminderScheduler(db_path=db_path, interval_seconds=1)

    assert scheduler._task is None

    await scheduler.start()
    assert scheduler._task is not None
    assert not scheduler._task.done()

    await scheduler.stop()
    assert scheduler._task is None


async def test_scheduler_start_idempotent(db_path: Path) -> None:
    """Calling start() twice does not create a second task."""
    scheduler = ReminderScheduler(db_path=db_path, interval_seconds=1)

    await scheduler.start()
    task1 = scheduler._task
    await scheduler.start()
    assert scheduler._task is task1

    await scheduler.stop()


# ---------------------------------------------------------------------------
# Default reminders inherited from event type
# ---------------------------------------------------------------------------


async def test_default_reminders_inherited_from_event_type(
    db_path: Path, auth_db_path: Path
) -> None:
    """Create event with type that has default rules, verify rules cloned to event."""
    from agentkit.calendar.service import CalendarService

    service = CalendarService(db_path=db_path, auth_db_path=auth_db_path)

    # Create event type
    et = await service.create_event_type("user-1", "Meeting")

    # Add a default reminder rule at the type level
    type_rule = ReminderRule(
        id=uuid.uuid4().hex,
        event_type_id=et.id,
        offset_minutes=-30,
        channels=["email"],
    )
    await insert_reminder_rule(type_rule, db_path)

    # Create an event with this type
    event = await service.create_event(
        user_id="user-1",
        title="Sprint Planning",
        start_time="2026-07-01T10:00:00+00:00",
        end_time="2026-07-01T11:00:00+00:00",
        event_type_id=et.id,
    )

    # Verify the type-level rule was cloned to the event
    event_rules = await list_reminder_rules_for_event(event.id, db_path)
    assert len(event_rules) == 1
    assert event_rules[0].event_id == event.id
    assert event_rules[0].event_type_id is None
    assert event_rules[0].offset_minutes == -30
    assert event_rules[0].channels == ["email"]
    # Cloned rule has a new ID
    assert event_rules[0].id != type_rule.id
