from __future__ import annotations

from datetime import timedelta

import pytest
from django.test import Client
from django.urls import reverse
from django.utils import timezone

from apps.alumni.engagement.models import (
    AlumniEvent,
    EventRegistration,
    EventVisibility,
)

LIST_URL = reverse("alumni_engagement:event_list")
FEED_URL = reverse("alumni_engagement:events_ics_feed")


def _entries(resp, pk):
    """Yield (cell, entry) for every calendar cell showing event ``pk``."""
    for week in resp.context["calendar_weeks"]:
        for cell in week:
            for entry in cell["events"]:
                if entry["event"].pk == pk:
                    yield cell, entry


def _make_event(title, *, when, visibility):
    return AlumniEvent.objects.create(
        title=title,
        start_at=when,
        end_at=when + timedelta(hours=2),
        visibility=visibility,
    )


@pytest.fixture
def this_month_events(db):
    now = timezone.now().replace(hour=10, minute=0, second=0, microsecond=0)
    # Keep both events comfortably inside the current month.
    anchor = now.replace(day=15)
    public = _make_event(
        "Public Demo Day", when=anchor, visibility=EventVisibility.PUBLIC
    )
    alumni_only = _make_event(
        "Alumni Roundtable",
        when=anchor + timedelta(days=1),
        visibility=EventVisibility.ALUMNI_ONLY,
    )
    return public, alumni_only


@pytest.mark.django_db
def test_calendar_view_renders_month_grid(client, this_month_events):
    resp = client.get(LIST_URL, {"view": "calendar"})
    assert resp.status_code == 200
    assert resp.context["view_mode"] == "calendar"
    # Monday-first 7-column weekday header.
    assert resp.context["calendar_weekdays"][0] == "Mon"
    # Header shows the current month name (e.g. "July 2026").
    assert timezone.localdate().strftime("%B").encode() in resp.content


@pytest.mark.django_db
def test_calendar_anonymous_sees_only_public(client, this_month_events):
    public, alumni_only = this_month_events
    resp = client.get(LIST_URL, {"view": "calendar"})
    assert public.title.encode() in resp.content
    assert alumni_only.title.encode() not in resp.content


@pytest.mark.django_db
def test_calendar_authenticated_alumni_sees_alumni_only(
    client, this_month_events, alumni_user
):
    public, alumni_only = this_month_events
    client.force_login(alumni_user)
    resp = client.get(LIST_URL, {"view": "calendar"})
    assert public.title.encode() in resp.content
    assert alumni_only.title.encode() in resp.content


@pytest.mark.django_db
def test_calendar_invalid_month_falls_back_to_current(client, this_month_events):
    resp = client.get(LIST_URL, {"view": "calendar", "year": "2026", "month": "13"})
    assert resp.status_code == 200
    assert resp.context["calendar_month"].month == timezone.localdate().month


@pytest.mark.django_db
def test_list_view_is_default(client, this_month_events):
    resp = client.get(LIST_URL)
    assert resp.status_code == 200
    assert resp.context["view_mode"] == "list"


@pytest.mark.django_db
def test_calendar_multiday_event_spans_every_day(client):
    start = timezone.now().replace(hour=9, minute=0, second=0, microsecond=0, day=10)
    ev = AlumniEvent.objects.create(
        title="Spanning Forum",
        start_at=start,
        end_at=start + timedelta(days=2),
        visibility=EventVisibility.PUBLIC,
    )
    resp = client.get(
        LIST_URL, {"view": "calendar", "year": start.year, "month": start.month}
    )
    by_day = {cell["date"].day: entry for cell, entry in _entries(resp, ev.pk)}
    assert sorted(by_day) == [10, 11, 12]
    # Only the first day is the "start"; the rest are continuations.
    assert by_day[10]["is_start"] is True
    assert by_day[11]["is_start"] is False and by_day[12]["is_start"] is False


@pytest.mark.django_db
def test_calendar_overflow_caps_and_shows_more(client):
    day = timezone.now().replace(hour=9, minute=0, second=0, microsecond=0, day=20)
    for i in range(5):
        AlumniEvent.objects.create(
            title=f"Busy {i}",
            start_at=day + timedelta(minutes=i),
            end_at=day + timedelta(hours=1),
            visibility=EventVisibility.PUBLIC,
        )
    resp = client.get(
        LIST_URL, {"view": "calendar", "year": day.year, "month": day.month}
    )
    cell = next(
        c
        for week in resp.context["calendar_weeks"]
        for c in week
        if c["date"].day == 20 and c["in_month"]
    )
    assert len(cell["events"]) == 3  # capped at CALENDAR_MAX_PER_DAY
    assert cell["overflow"] == 2
    assert b"+2 more" in resp.content


@pytest.mark.django_db
def test_calendar_marks_rsvp(client, alumni_user):
    day = timezone.now().replace(hour=9, minute=0, second=0, microsecond=0, day=14)
    ev = AlumniEvent.objects.create(
        title="RSVP Event",
        start_at=day,
        end_at=day + timedelta(hours=2),
        visibility=EventVisibility.ALUMNI_ONLY,
    )
    EventRegistration.objects.create(user=alumni_user, event=ev)
    client.force_login(alumni_user)
    resp = client.get(
        LIST_URL, {"view": "calendar", "year": day.year, "month": day.month}
    )
    _, entry = next(_entries(resp, ev.pk))
    assert entry["is_registered"] is True


@pytest.mark.django_db
def test_calendar_flags_past_events(client):
    past = timezone.now() - timedelta(days=400)
    ev = AlumniEvent.objects.create(
        title="Old Event",
        start_at=past,
        end_at=past + timedelta(hours=2),
        visibility=EventVisibility.PUBLIC,
    )
    resp = client.get(
        LIST_URL, {"view": "calendar", "year": past.year, "month": past.month}
    )
    _, entry = next(_entries(resp, ev.pk))
    assert entry["is_past"] is True


@pytest.mark.django_db
def test_day_filter_lists_only_that_day(client):
    d = timezone.now().replace(hour=9, minute=0, second=0, microsecond=0, day=12)
    on = AlumniEvent.objects.create(
        title="On The Day",
        start_at=d,
        end_at=d + timedelta(hours=1),
        visibility=EventVisibility.PUBLIC,
    )
    off = AlumniEvent.objects.create(
        title="Other Day",
        start_at=d + timedelta(days=3),
        end_at=d + timedelta(days=3, hours=1),
        visibility=EventVisibility.PUBLIC,
    )
    resp = client.get(LIST_URL, {"view": "list", "day": d.strftime("%Y-%m-%d")})
    assert resp.status_code == 200
    assert resp.context["filter_day"] == d.date()
    assert on.title.encode() in resp.content
    assert off.title.encode() not in resp.content


@pytest.mark.django_db
def test_ics_feed_is_public_upcoming_only(client):
    now = timezone.now()
    future = now + timedelta(days=10)
    AlumniEvent.objects.create(
        title="Feed Public",
        start_at=future,
        end_at=future + timedelta(hours=2),
        visibility=EventVisibility.PUBLIC,
    )
    AlumniEvent.objects.create(
        title="Feed AlumniOnly",
        start_at=future,
        end_at=future + timedelta(hours=2),
        visibility=EventVisibility.ALUMNI_ONLY,
    )
    past = now - timedelta(days=5)
    AlumniEvent.objects.create(
        title="Feed Past",
        start_at=past,
        end_at=past + timedelta(hours=1),
        visibility=EventVisibility.PUBLIC,
    )
    resp = client.get(FEED_URL)
    assert resp.status_code == 200
    assert resp["Content-Type"].startswith("text/calendar")
    body = resp.content.decode()
    assert "BEGIN:VCALENDAR" in body
    assert "Feed Public" in body
    assert "Feed AlumniOnly" not in body  # alumni-only never leaks to the public feed
    assert "Feed Past" not in body  # past events excluded


# --- Regression: event create/update must not 500 on the m2m ``groups`` field ---
# Previously EventCreateView/EventUpdateView.form_valid() called form.save_m2m()
# after form.save() (commit=True), raising AttributeError *after* the row was
# already persisted — a 500 the user saw despite the event being created.

CREATE_URL = reverse("alumni_engagement:event_create")


def _event_post_data(**overrides):
    now = timezone.now().replace(microsecond=0)
    data = {
        "title": "Reg Test Event",
        "description": "Body",
        "start_at": (now + timedelta(days=7)).strftime("%Y-%m-%dT%H:%M"),
        "end_at": (now + timedelta(days=7, hours=2)).strftime("%Y-%m-%dT%H:%M"),
        "location": "Kampala",
        "virtual_url": "",
        "capacity": 50,
        "visibility": EventVisibility.PUBLIC,
    }
    data.update(overrides)
    return data


@pytest.mark.django_db
def test_event_create_no_group_does_not_500(client, alumni_officer_user):
    client.force_login(alumni_officer_user)
    resp = client.post(CREATE_URL, _event_post_data(), follow=False)
    assert resp.status_code == 302  # redirect to detail, not a 500
    ev = AlumniEvent.objects.get(title="Reg Test Event")
    assert ev.organiser_id == alumni_officer_user.pk


@pytest.mark.django_db
def test_event_create_with_group_attaches_m2m(client, alumni_officer_user):
    from apps.alumni.engagement.models import NetworkGroup

    group = NetworkGroup.objects.create(title="Reg Test Group")
    client.force_login(alumni_officer_user)
    resp = client.post(
        CREATE_URL, _event_post_data(title="Reg Group Event", groups=[group.pk])
    )
    assert resp.status_code == 302
    ev = AlumniEvent.objects.get(title="Reg Group Event")
    assert list(ev.groups.values_list("pk", flat=True)) == [group.pk]


# --- Events approval workflow (alumni propose → officer approves) ---

PENDING_URL = reverse("alumni_engagement:event_approval_queue")


@pytest.mark.django_db
def test_alumni_created_event_is_pending_and_hidden(client, alumni_user):
    from apps.alumni.engagement.models import EventStatus
    from apps.alumni.engagement.views import visible_events_for

    client.force_login(alumni_user)
    resp = client.post(CREATE_URL, _event_post_data(title="Alumni Proposed"))
    assert resp.status_code == 302
    ev = AlumniEvent.objects.get(title="Alumni Proposed")
    assert ev.status == EventStatus.PENDING
    # Organiser sees it; an unrelated user does not.
    from django.contrib.auth import get_user_model

    other = get_user_model().objects.create_user(
        email="other.alum@example.com", password="pw"
    )
    assert visible_events_for(alumni_user).filter(pk=ev.pk).exists()
    assert not visible_events_for(other).filter(pk=ev.pk).exists()


@pytest.mark.django_db
def test_staff_created_event_is_auto_approved(client, alumni_officer_user):
    from apps.alumni.engagement.models import EventStatus

    client.force_login(alumni_officer_user)
    resp = client.post(CREATE_URL, _event_post_data(title="Officer Event"))
    assert resp.status_code == 302
    ev = AlumniEvent.objects.get(title="Officer Event")
    assert ev.status == EventStatus.APPROVED
    assert ev.reviewed_by_id == alumni_officer_user.pk


@pytest.mark.django_db
def test_officer_approves_pending_event(client, alumni_user, alumni_officer_user):
    from apps.alumni.engagement.models import EventStatus
    from apps.alumni.engagement.views import visible_events_for

    client.force_login(alumni_user)
    client.post(CREATE_URL, _event_post_data(title="Needs Approval"))
    ev = AlumniEvent.objects.get(title="Needs Approval")

    staff = Client()
    staff.force_login(alumni_officer_user)
    resp = staff.post(
        reverse("alumni_engagement:event_approve", args=[ev.slug])
    )
    assert resp.status_code == 302
    ev.refresh_from_db()
    assert ev.status == EventStatus.APPROVED
    assert visible_events_for(alumni_user).filter(pk=ev.pk).exists()


@pytest.mark.django_db
def test_officer_rejects_with_notes(client, alumni_user, alumni_officer_user):
    from apps.alumni.engagement.models import EventStatus

    client.force_login(alumni_user)
    client.post(CREATE_URL, _event_post_data(title="To Reject"))
    ev = AlumniEvent.objects.get(title="To Reject")

    staff = Client()
    staff.force_login(alumni_officer_user)
    resp = staff.post(
        reverse("alumni_engagement:event_reject", args=[ev.slug]),
        {"review_notes": "Off-topic for alumni."},
    )
    assert resp.status_code == 302
    ev.refresh_from_db()
    assert ev.status == EventStatus.REJECTED
    assert ev.review_notes == "Off-topic for alumni."


@pytest.mark.django_db
def test_approval_queue_denied_to_non_staff(client, alumni_user):
    client.force_login(alumni_user)
    resp = client.get(PENDING_URL)
    assert resp.status_code in (403, 302)
