"""Tests for CalendarService (U2)."""

from __future__ import annotations

import asyncio
import uuid
from pathlib import Path

import aiosqlite
import pytest

from agentkit.calendar.db import (
    get_event_tags,
    init_calendar_db,
    insert_reminder_rule,
    list_reminder_rules_for_event,
)
from agentkit.calendar.models import ReminderRule
from agentkit.calendar.service import CalendarService
from agentkit.server.auth.models import init_auth_db


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


@pytest.fixture
def calendar_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:
    path = tmp_path / "test_auth.db"
    asyncio.run(init_auth_db(path))
    return path


@pytest.fixture
def service(calendar_db_path: Path, auth_db_path: Path) -> CalendarService:
    return CalendarService(db_path=calendar_db_path, auth_db_path=auth_db_path)


async def _seed_user(
    auth_db_path: Path,
    user_id: str | None = None,
    username: str = "testuser",
    email: str = "test@example.com",
) -> str:
    """Insert a user into the auth DB and return its id."""
    uid = user_id or uuid.uuid4().hex
    async with aiosqlite.connect(str(auth_db_path)) as db:
        await db.execute(
            "INSERT INTO users (id, username, email, password_hash, role, is_active, "
            "is_terminal_authorized, is_server_terminal_authorized, created_at, updated_at) "
            "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
            (
                uid,
                username,
                email,
                "fake-hash",
                "member",
                1,
                0,
                0,
                "2026-01-01T00:00:00+00:00",
                "2026-01-01T00:00:00+00:00",
            ),
        )
        await db.commit()
    return uid


# ---------------------------------------------------------------------------
# Event creation with type and tags
# ---------------------------------------------------------------------------


async def test_create_event_with_type_and_tags(
    service: CalendarService, calendar_db_path: Path
) -> None:
    """Create event with type_id and 2 tags, verify all linked."""
    user_id = "user-1"

    # Create event type
    et = await service.create_event_type(user_id, "Meeting", color="#FF0000")

    # Create tags
    tag1 = await service.create_tag(user_id, "urgent")
    tag2 = await service.create_tag(user_id, "work")

    # Create event with type and tags
    event = await service.create_event(
        user_id=user_id,
        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,
        tag_ids=[tag1.id, tag2.id],
    )

    # Verify event was created
    fetched = await service.get_event(event.id)
    assert fetched is not None
    assert fetched.title == "Sprint Planning"
    assert fetched.event_type_id == et.id

    # Verify tags are linked
    tags = await get_event_tags(event.id, calendar_db_path)
    tag_names = {t.name for t in tags}
    assert tag_names == {"urgent", "work"}


async def test_create_event_broadcasts_via_notify_callback(
    calendar_db_path: Path, auth_db_path: Path
) -> None:
    """create_event must invoke notify_callback with calendar_event_created payload.

    Regression guard: without the broadcast, the frontend calendar view does
    not refresh after an agent creates an event via the calendar tool, even
    though the event was successfully persisted.
    """
    received: list[tuple[str, dict[str, object]]] = []

    async def _capture(user_id: str, message: dict[str, object]) -> None:
        received.append((user_id, message))

    svc = CalendarService(
        db_path=calendar_db_path,
        auth_db_path=auth_db_path,
        notify_callback=_capture,
    )
    event = await svc.create_event(
        user_id="user-broadcast",
        title="Refresh Test",
        start_time="2026-07-02T09:00:00+00:00",
        end_time="2026-07-02T10:00:00+00:00",
    )

    assert len(received) == 1, "notify_callback must fire exactly once"
    uid, msg = received[0]
    assert uid == "user-broadcast"
    assert msg["type"] == "calendar_event_created"
    assert msg["data"]["event"]["id"] == event.id  # type: ignore[index]


async def test_create_event_without_callback_does_not_raise(
    service: CalendarService,
) -> None:
    """Default CalendarService (no callback) must still create events."""
    event = await service.create_event(
        user_id="user-1",
        title="No Callback",
        start_time="2026-07-03T09:00:00+00:00",
        end_time="2026-07-03T10:00:00+00:00",
    )
    assert event.title == "No Callback"


# ---------------------------------------------------------------------------
# Date range filtering
# ---------------------------------------------------------------------------


async def test_list_events_filters_by_date_range(service: CalendarService) -> None:
    """3 events across days, filter returns correct subset."""
    user_id = "user-1"
    for i, day in enumerate([1, 15, 28]):
        await service.create_event(
            user_id=user_id,
            title=f"Event Day {day}",
            start_time=f"2026-07-{day:02d}T10:00:00+00:00",
            end_time=f"2026-07-{day:02d}T11:00:00+00:00",
        )

    # Range covering only day 15
    events = await service.list_events(
        user_id=user_id,
        start="2026-07-10T00:00:00+00:00",
        end="2026-07-20T00:00:00+00:00",
    )
    assert len(events) == 1
    assert events[0].title == "Event Day 15"


# ---------------------------------------------------------------------------
# Type and tag filter combination (G2)
# ---------------------------------------------------------------------------


async def test_list_events_filters_by_type_and_tag(
    service: CalendarService, calendar_db_path: Path
) -> None:
    """Filter by both type_id and tag_id returns only matching events."""
    user_id = "user-1"

    et_meeting = await service.create_event_type(user_id, "Meeting")
    et_personal = await service.create_event_type(user_id, "Personal")

    tag_work = await service.create_tag(user_id, "work")
    tag_family = await service.create_tag(user_id, "family")

    # Event 1: Meeting + work
    e1 = await service.create_event(
        user_id=user_id,
        title="Standup",
        start_time="2026-07-01T09:00:00+00:00",
        end_time="2026-07-01T09:30:00+00:00",
        event_type_id=et_meeting.id,
        tag_ids=[tag_work.id],
    )

    # Event 2: Personal + family
    await service.create_event(
        user_id=user_id,
        title="Birthday",
        start_time="2026-07-02T18:00:00+00:00",
        end_time="2026-07-02T21:00:00+00:00",
        event_type_id=et_personal.id,
        tag_ids=[tag_family.id],
    )

    # Event 3: Meeting + family (cross combination)
    await service.create_event(
        user_id=user_id,
        title="Team Dinner",
        start_time="2026-07-03T19:00:00+00:00",
        end_time="2026-07-03T21:00:00+00:00",
        event_type_id=et_meeting.id,
        tag_ids=[tag_family.id],
    )

    # Filter by type=Meeting AND tag=work → only Event 1
    events = await service.list_events(
        user_id=user_id,
        event_type_id=et_meeting.id,
        tag_id=tag_work.id,
    )
    assert len(events) == 1
    assert events[0].id == e1.id


# ---------------------------------------------------------------------------
# Tag-only filter (G2)
# ---------------------------------------------------------------------------


async def test_list_events_filters_by_tag_only(
    service: CalendarService, calendar_db_path: Path
) -> None:
    """5 events, 2 with tag X, filter tag_id=X returns only 2."""
    user_id = "user-1"
    tag_x = await service.create_tag(user_id, "X")

    tagged_ids: list[str] = []
    for i in range(5):
        tag_ids = [tag_x.id] if i < 2 else None
        event = await service.create_event(
            user_id=user_id,
            title=f"Event {i}",
            start_time=f"2026-07-{i + 1:02d}T10:00:00+00:00",
            end_time=f"2026-07-{i + 1:02d}T11:00:00+00:00",
            tag_ids=tag_ids,
        )
        if i < 2:
            tagged_ids.append(event.id)

    events = await service.list_events(user_id=user_id, tag_id=tag_x.id)
    assert len(events) == 2
    returned_ids = {e.id for e in events}
    assert returned_ids == set(tagged_ids)


# ---------------------------------------------------------------------------
# Recurring event expansion
# ---------------------------------------------------------------------------


async def test_list_events_expands_recurring(service: CalendarService) -> None:
    """Event with FREQ=DAILY;COUNT=3, list range covering 2 days → 2 occurrences."""
    user_id = "user-1"
    await service.create_event(
        user_id=user_id,
        title="Daily Standup",
        start_time="2026-07-01T10:00:00+00:00",
        end_time="2026-07-01T10:15:00+00:00",
        rrule="FREQ=DAILY;COUNT=3",
    )

    # Range covers Jul 1 and Jul 2 (half-open [Jul 1, Jul 3))
    events = await service.list_events(
        user_id=user_id,
        start="2026-07-01T00:00:00+00:00",
        end="2026-07-03T00:00:00+00:00",
    )
    assert len(events) == 2
    # First occurrence on Jul 1
    assert events[0].start_time.startswith("2026-07-01")
    assert events[0].end_time.startswith("2026-07-01")
    # Second occurrence on Jul 2
    assert events[1].start_time.startswith("2026-07-02")
    assert events[1].end_time.startswith("2026-07-02")
    # Duration preserved (15 minutes)
    assert "T10:00:00" in events[0].start_time
    assert "T10:15:00" in events[0].end_time
    assert "T10:00:00" in events[1].start_time
    assert "T10:15:00" in events[1].end_time


# ---------------------------------------------------------------------------
# Partial update
# ---------------------------------------------------------------------------


async def test_update_event_partial_fields(service: CalendarService) -> None:
    """PATCH only title, other fields unchanged."""
    event = await service.create_event(
        user_id="user-1",
        title="Original Title",
        start_time="2026-07-01T10:00:00+00:00",
        end_time="2026-07-01T11:00:00+00:00",
        description="Original description",
        location="Room A",
    )

    original = await service.get_event(event.id)
    assert original is not None

    updated = await service.update_event(event.id, {"title": "New Title"})
    assert updated is True

    refreshed = await service.get_event(event.id)
    assert refreshed is not None
    assert refreshed.title == "New Title"
    # Other fields unchanged
    assert refreshed.description == "Original description"
    assert refreshed.location == "Room A"
    assert refreshed.start_time == original.start_time
    assert refreshed.end_time == original.end_time
    # last_modified should be updated
    assert refreshed.last_modified != original.last_modified


# ---------------------------------------------------------------------------
# Delete cascade
# ---------------------------------------------------------------------------


async def test_delete_event_cascades_reminders_and_tags(
    service: CalendarService, calendar_db_path: Path
) -> None:
    """Delete event, verify reminder rules and junction rows removed."""
    user_id = "user-1"
    tag = await service.create_tag(user_id, "work")
    event = await service.create_event(
        user_id=user_id,
        title="To Delete",
        start_time="2026-07-01T10:00:00+00:00",
        end_time="2026-07-01T11:00:00+00:00",
        tag_ids=[tag.id],
    )

    # Add a reminder rule directly to the event
    rule = ReminderRule(
        id=uuid.uuid4().hex,
        event_id=event.id,
        offset_minutes=-30,
        channels=["email"],
    )
    await insert_reminder_rule(rule, calendar_db_path)

    # Verify rule and tag exist
    rules_before = await list_reminder_rules_for_event(event.id, calendar_db_path)
    assert len(rules_before) == 1
    tags_before = await get_event_tags(event.id, calendar_db_path)
    assert len(tags_before) == 1

    # Delete the event
    deleted = await service.delete_event(event.id)
    assert deleted is True

    # Verify event is gone
    assert await service.get_event(event.id) is None

    # Verify reminder rules are gone (cascade)
    rules_after = await list_reminder_rules_for_event(event.id, calendar_db_path)
    assert len(rules_after) == 0

    # Verify junction rows are gone
    tags_after = await get_event_tags(event.id, calendar_db_path)
    assert len(tags_after) == 0

    # Tag itself should still exist (only the junction is removed)
    all_tags = await service.list_tags(user_id)
    assert len(all_tags) == 1


# ---------------------------------------------------------------------------
# Invitation flow
# ---------------------------------------------------------------------------


async def test_create_invitation_and_respond(service: CalendarService) -> None:
    """Invite, respond 'accepted', verify status + responded_at set."""
    event = await service.create_event(
        user_id="user-1",
        title="Team Meeting",
        start_time="2026-07-01T10:00:00+00:00",
        end_time="2026-07-01T11:00:00+00:00",
    )

    invitation = await service.create_invitation(
        event_id=event.id,
        inviter_user_id="user-1",
        invitee_email="alice@example.com",
    )
    assert invitation.status == "pending"
    assert invitation.responded_at is None

    updated = await service.respond_to_invitation(invitation.id, "accepted")
    assert updated is True

    invitations = await service.list_invitations("alice@example.com")
    assert len(invitations) == 1
    assert invitations[0].status == "accepted"
    assert invitations[0].responded_at is not None


# ---------------------------------------------------------------------------
# User search (G5)
# ---------------------------------------------------------------------------


async def test_search_users_by_username(service: CalendarService, auth_db_path: Path) -> None:
    """Seed users in auth DB, search returns matches."""
    await _seed_user(auth_db_path, username="alice", email="alice@example.com")
    await _seed_user(auth_db_path, username="bob", email="bob@example.com")
    await _seed_user(auth_db_path, username="charlie", email="charlie@example.com")

    results = await service.search_users("ali")
    assert len(results) == 1
    assert results[0]["username"] == "alice"
    assert results[0]["email"] == "alice@example.com"
    # Must NOT return user_id or password fields
    assert "id" not in results[0]
    assert "password_hash" not in results[0]


async def test_search_users_no_match_returns_empty(
    service: CalendarService, auth_db_path: Path
) -> None:
    """Search 'zzz' returns []."""
    await _seed_user(auth_db_path, username="alice", email="alice@example.com")
    results = await service.search_users("zzz")
    assert results == []


async def test_search_users_returns_max_10(service: CalendarService, auth_db_path: Path) -> None:
    """Seed 15 matching users, verify only 10 returned."""
    for i in range(15):
        await _seed_user(
            auth_db_path,
            username=f"user{i:02d}",
            email=f"user{i:02d}@example.com",
        )

    results = await service.search_users("user")
    assert len(results) == 10


# ---------------------------------------------------------------------------
# Event type color persistence
# ---------------------------------------------------------------------------


async def test_event_type_default_color_persistence(service: CalendarService) -> None:
    """Create type with color, verify roundtrip."""
    et = await service.create_event_type("user-1", "Important", color="#00FF00")
    assert et.color == "#00FF00"

    types = await service.list_event_types("user-1")
    assert len(types) == 1
    assert types[0].color == "#00FF00"
    assert types[0].name == "Important"
