from datetime import datetime, timedelta

import pytest
from django.core.exceptions import ValidationError
from django.db import connection
from django.test.utils import CaptureQueriesContext
from django.urls import reverse
from django.utils import timezone

from events.admin import EventDateColumn, OrganizersColumn
from events.models import (
    EventCategory,
    EventOccurrence,
    EventOrganizer,
    EventOrganizerLink,
    EventPage,
)
from events.views import EventOrganizerCreationForm
from tests.conftest import (
    AssociationPageFactory,
    CustomImageFactory,
    EventPageFactory,
)


def test_eventpage_clean_unsets_specific_pricing_when_free():
    page = EventPage(
        title="Free event",
        slug="free-event",
        free=True,
        price_regular="100",
        price_student="50",
        price_member="25",
    )

    page.clean()

    assert page.price_regular == ""
    assert page.price_student == ""
    assert page.price_member == ""


def test_eventpage_clean_keeps_specific_pricing_when_not_free():
    page = EventPage(
        title="Paid event",
        slug="paid-event",
        free=False,
        price_regular="100",
        price_student="50",
        price_member="25",
    )

    page.clean()

    assert page.price_regular == "100"
    assert page.price_student == "50"
    assert page.price_member == "25"


def test_eventpage_clean_dedupes_organizers_by_name(event_index):
    org_a = EventOrganizer.objects.create(name="DNS", slug="dns-a")
    org_b = EventOrganizer.objects.create(name="DNS", slug="dns-b")

    event = EventPageFactory(parent=event_index)
    EventOrganizerLink.objects.create(event=event, organizer=org_a)
    EventOrganizerLink.objects.create(event=event, organizer=org_b)

    event = EventPage.objects.get(pk=event.pk)
    assert event.organizer_links.count() == 2

    event.clean()

    assert event.organizer_links.count() == 1


def test_eventpage_clean_dedupes_three_duplicates_and_keeps_distinct(event_index):
    dup_1 = EventOrganizer.objects.create(name="DNS", slug="dns-1")
    dup_2 = EventOrganizer.objects.create(name="DNS", slug="dns-2")
    dup_3 = EventOrganizer.objects.create(name="DNS", slug="dns-3")
    distinct = EventOrganizer.objects.create(name="Studentersamfundet", slug="ss")

    event = EventPageFactory(parent=event_index)
    for organizer in (dup_1, dup_2, dup_3, distinct):
        EventOrganizerLink.objects.create(event=event, organizer=organizer)

    event = EventPage.objects.get(pk=event.pk)
    assert event.organizer_links.count() == 4

    event.clean()

    names = sorted(link.organizer.name for link in event.organizer_links.all())
    assert names == ["DNS", "Studentersamfundet"]


def test_eventoccurrence_clean_rejects_both_venue_and_venue_custom(event_index, venue):
    event = EventPageFactory(parent=event_index)
    occurrence = EventOccurrence(
        event=event,
        start=timezone.now(),
        venue=venue,
        venue_custom="Frederikkeplassen",
    )

    with pytest.raises(ValidationError) as exc:
        occurrence.clean()
    assert "venue_custom" in exc.value.message_dict


def test_eventoccurrence_clean_promotes_matching_custom_text_to_venue(event_index, venue):
    event = EventPageFactory(parent=event_index)
    occurrence = EventOccurrence(
        event=event,
        start=timezone.now(),
        venue_custom=f"  {venue.title}  ",
    )

    occurrence.clean()

    assert occurrence.venue_id == venue.pk
    assert occurrence.venue_custom == ""


def test_event_organizer_creation_form_auto_slugifies_name(db):
    form = EventOrganizerCreationForm(data={"name": "Forening for ÆØÅ", "external_url": ""})

    assert form.is_valid(), form.errors
    organizer = form.save()

    assert organizer.pk is not None
    assert organizer.name == "Forening for ÆØÅ"
    assert organizer.slug == "forening-for-aeoa"


def test_event_organizer_creation_form_keeps_explicit_slug(db):
    organizer = EventOrganizer(name="Forening", slug="custom-slug")
    form = EventOrganizerCreationForm(
        data={"name": "Forening", "external_url": ""}, instance=organizer
    )

    assert form.is_valid(), form.errors
    organizer = form.save()

    assert organizer.slug == "custom-slug"


def test_eventoccurrence_clean_keeps_custom_text_when_no_venue_matches(event_index):
    event = EventPageFactory(parent=event_index)
    occurrence = EventOccurrence(
        event=event,
        start=timezone.now(),
        venue_custom="  Frederikkeplassen  ",
    )

    occurrence.clean()

    assert occurrence.venue is None
    assert occurrence.venue_custom == "Frederikkeplassen"


def test_eventoccurrence_clean_requires_venue_or_venue_custom(event_index):
    event = EventPageFactory(parent=event_index)
    occurrence = EventOccurrence(event=event, start=timezone.now())

    with pytest.raises(ValidationError) as exc:
        occurrence.clean()
    assert "venue" in exc.value.message_dict


def test_eventpage_manager_future_filters_past_and_annotates(event_index):
    now = timezone.now()

    past = EventPageFactory(parent=event_index, title="Past")
    EventOccurrence.objects.create(event=past, start=now - timedelta(days=7), venue_custom="Old")

    future = EventPageFactory(parent=event_index, title="Future")
    EventOccurrence.objects.create(event=future, start=now + timedelta(days=7), venue_custom="New")

    results = list(EventPage.objects.live().future().order_by("next_occurrence"))

    assert [p.pk for p in results] == [future.pk]
    assert results[0].next_occurrence is not None


def test_future_includes_occurrence_late_today(event_index):
    today_start = timezone.localtime(timezone.now()).replace(
        hour=0, minute=0, second=0, microsecond=0
    )
    late_today = today_start + timedelta(hours=23, minutes=59)

    event = EventPageFactory(parent=event_index, title="Late today")
    EventOccurrence.objects.create(event=event, start=late_today, venue_custom="X")

    assert event.pk in EventPage.objects.future().values_list("pk", flat=True)


def test_future_excludes_occurrence_just_before_today(event_index):
    today_start = timezone.localtime(timezone.now()).replace(
        hour=0, minute=0, second=0, microsecond=0
    )
    just_before_today = today_start - timedelta(seconds=1)

    event = EventPageFactory(parent=event_index, title="Just past")
    EventOccurrence.objects.create(event=event, start=just_before_today, venue_custom="X")

    assert event.pk not in EventPage.objects.future().values_list("pk", flat=True)


def test_future_next_occurrence_picks_earliest_future_ignoring_past(event_index):
    now = timezone.now()
    soonest_future = now + timedelta(days=3)

    event = EventPageFactory(parent=event_index, title="With history")
    EventOccurrence.objects.create(event=event, start=now - timedelta(days=30), venue_custom="X")
    EventOccurrence.objects.create(event=event, start=soonest_future, venue_custom="X")
    EventOccurrence.objects.create(event=event, start=now + timedelta(days=10), venue_custom="X")

    annotated = EventPage.objects.future().filter(pk=event.pk).first()
    assert annotated is not None
    assert abs((annotated.next_occurrence - soonest_future).total_seconds()) < 1


def test_graphql_event_index_future_events_query(event_index, graphql_post):
    upcoming = EventPageFactory(parent=event_index, title="Upcoming gig")
    EventOccurrence.objects.create(
        event=upcoming,
        start=timezone.now() + timedelta(days=3),
        venue_custom="Storsalen",
    )

    response, body = graphql_post(
        """
        query {
          eventIndex {
            futureEvents { title }
          }
        }
        """
    )

    assert response.status_code == 200
    assert "errors" not in body, body
    titles = [e["title"] for e in body["data"]["eventIndex"]["futureEvents"]]
    assert "Upcoming gig" in titles


def test_future_events_does_not_have_n_plus_one_queries(
    event_index, venue, association_index, graphql_post
):
    """Regression test: query count for futureEvents stays bounded as events grow."""
    konsert = EventCategory.objects.create(name="Konsert", slug="konsert")
    association = AssociationPageFactory(parent=association_index, title="DNS")
    org = EventOrganizer.objects.create(name="Forening", slug="forening", association=association)
    image = CustomImageFactory(title="Cover")
    now = timezone.now()

    for i in range(5):
        event = EventPageFactory(
            parent=event_index,
            title=f"Event {i}",
            body=[("paragraph", "<p>x</p>")],
            featured_image=image,
        )
        event.categories.add(konsert)
        EventOrganizerLink.objects.create(event=event, organizer=org)
        EventOccurrence.objects.create(
            event=event,
            start=now + timedelta(days=i + 1),
            venue=venue,
        )

    home_query = """
        query {
          eventIndex {
            futureEvents {
              id
              title
              subtitle
              body { blockType }
              featuredImage { url }
              occurrences { start end venueCustom venue { title } }
              categories { name slug }
              organizers { name slug association { title } }
            }
          }
        }
    """

    with CaptureQueriesContext(connection) as ctx:
        response, body = graphql_post(home_query)

    assert response.status_code == 200
    assert "errors" not in body, body
    assert len(body["data"]["eventIndex"]["futureEvents"]) == 5

    # Bump only alongside an intentional resolver change.
    max_queries = 6
    assert len(ctx) <= max_queries, (
        f"futureEvents took {len(ctx)} queries for 5 events — likely N+1. "
        f"Captured queries:\n"
        + "\n".join(f"  {i + 1}. {q['sql'][:120]}" for i, q in enumerate(ctx.captured_queries))
    )


def test_future_events_does_not_load_wp_import_fields(event_index, graphql_post):
    """wp_* columns must stay deferred and lazy-load on explicit access."""
    event = EventPageFactory(parent=event_index, wp_raw_content="marker")
    EventOccurrence.objects.create(
        event=event, start=timezone.now() + timedelta(days=1), venue_custom="X"
    )

    with CaptureQueriesContext(connection) as ctx:
        response, body = graphql_post("{ eventIndex { futureEvents { id } } }")

    assert response.status_code == 200 and "errors" not in body, body
    sql = "\n".join(q["sql"] for q in ctx.captured_queries)
    assert "wp_raw_content" not in sql, f"wp_* must be deferred. SQL:\n{sql}"
    assert EventPage.objects.get(pk=event.pk).wp_raw_content == "marker"


def test_graphql_event_index_future_events_ordered_by_next_occurrence(event_index, graphql_post):
    now = timezone.now()

    later = EventPageFactory(parent=event_index, title="Later gig")
    EventOccurrence.objects.create(event=later, start=now + timedelta(days=10), venue_custom="X")

    sooner = EventPageFactory(parent=event_index, title="Sooner gig")
    EventOccurrence.objects.create(event=sooner, start=now + timedelta(days=3), venue_custom="X")

    response, body = graphql_post(
        """
        query {
          eventIndex {
            futureEvents { title }
          }
        }
        """
    )

    assert response.status_code == 200
    assert "errors" not in body, body
    titles = [e["title"] for e in body["data"]["eventIndex"]["futureEvents"]]
    assert titles.index("Sooner gig") < titles.index("Later gig")


def test_event_date_column_no_occurrences(event_index):
    event = EventPageFactory(parent=event_index)
    column = EventDateColumn("event_date")

    assert column.get_value(event) == "—"


def test_event_date_column_single_occurrence(event_index):
    event = EventPageFactory(parent=event_index)
    start = timezone.make_aware(datetime(2025, 7, 22, 19, 30))
    EventOccurrence.objects.create(event=event, start=start, venue_custom="X")
    column = EventDateColumn("event_date")

    assert column.get_value(event) == "2025-07-22 kl 19:30"


def test_event_date_column_multiple_occurrences_shows_count(event_index):
    event = EventPageFactory(parent=event_index)
    now = timezone.now()
    EventOccurrence.objects.create(event=event, start=now, venue_custom="X")
    EventOccurrence.objects.create(event=event, start=now + timedelta(days=1), venue_custom="X")
    EventOccurrence.objects.create(event=event, start=now + timedelta(days=2), venue_custom="X")
    column = EventDateColumn("event_date")

    assert column.get_value(event) == "3 forekomster"


def test_organizers_column_no_organizers(event_index):
    event = EventPageFactory(parent=event_index)
    column = OrganizersColumn("organizers")

    assert column.get_value(event) == "—"


def test_organizers_column_single_organizer_shows_name(event_index):
    org = EventOrganizer.objects.create(name="Forening A", slug="forening-a")
    event = EventPageFactory(parent=event_index)
    EventOrganizerLink.objects.create(event=event, organizer=org)
    column = OrganizersColumn("organizers")

    assert column.get_value(event) == "Forening A"


def test_organizers_column_multiple_organizers_truncates_with_count(event_index):
    org_a = EventOrganizer.objects.create(name="Forening A", slug="forening-a")
    org_b = EventOrganizer.objects.create(name="Forening B", slug="forening-b")
    org_c = EventOrganizer.objects.create(name="Forening C", slug="forening-c")
    event = EventPageFactory(parent=event_index)
    EventOrganizerLink.objects.create(event=event, organizer=org_a, sort_order=0)
    EventOrganizerLink.objects.create(event=event, organizer=org_b, sort_order=1)
    EventOrganizerLink.objects.create(event=event, organizer=org_c, sort_order=2)
    column = OrganizersColumn("organizers")

    assert column.get_value(event) == "Forening A (+2)"


@pytest.fixture
def comprehensive_event(event_index, venue, association_index):
    """A fully-populated paid EventPage exercising every field exposed via GraphQL."""
    image = CustomImageFactory(
        title="Cover",
        alt="Et fotografi av en gris med solbriller",
        attribution="Foto: Test",
    )

    konsert = EventCategory.objects.create(
        name="Konsert", slug="konsert", show_in_filters=True, pig="pigHeadLogo"
    )
    klubb = EventCategory.objects.create(name="Klubb", slug="klubb")

    association = AssociationPageFactory(
        parent=association_index,
        title="Internal",
        association_type="forening",
    )
    internal_org = EventOrganizer.objects.create(
        name="Internal", slug="internal", association=association
    )
    external_org = EventOrganizer.objects.create(
        name="External",
        slug="external",
        external_url="https://external.example.com",
    )

    event = EventPageFactory(
        parent=event_index,
        title="Et arrangement",
        slug="et-arrangement",
        subtitle="En undertekst",
        lead="<p>Ingress.</p>",
        body=[("paragraph", "<p>Body content.</p>")],
        pig="automatic",
        free=False,
        price_regular="150",
        price_student="100",
        price_member="75",
        ticket_url="https://example.com/tickets",
        facebook_url="https://facebook.com/example",
        featured_image=image,
    )
    event.categories.add(konsert, klubb)
    EventOrganizerLink.objects.create(event=event, organizer=internal_org)
    EventOrganizerLink.objects.create(event=event, organizer=external_org)

    now = timezone.now()
    EventOccurrence.objects.create(
        event=event,
        start=now + timedelta(days=5),
        end=now + timedelta(days=5, hours=3),
        venue=venue,
    )
    EventOccurrence.objects.create(
        event=event,
        start=now + timedelta(days=12),
        end=now + timedelta(days=12, hours=2),
        venue_custom="Frederikkeplassen",
    )

    event.save()
    return event


def test_graphql_event_index_returns_all_fields_for_comprehensive_event(
    comprehensive_event, graphql_post
):
    response, body = graphql_post(
        """
        query {
          eventIndex {
            futureEvents {
              title
              slug
              subtitle
              lead
              body {
                blockType
                field
                ... on RichTextBlock {
                  value
                }
              }
              pig
              free
              priceRegular
              priceStudent
              priceMember
              ticketUrl
              facebookUrl
              featuredImage {
                alt
                attribution
              }
              categories {
                name
                slug
                showInFilters
                pig
              }
              organizers {
                name
                slug
                externalUrl
                association {
                  title
                }
              }
              occurrences {
                start
                end
                venueCustom
                venue {
                  title
                }
              }
            }
          }
        }
        """
    )

    assert response.status_code == 200
    assert "errors" not in body, body

    events = body["data"]["eventIndex"]["futureEvents"]
    event = next(e for e in events if e["title"] == "Et arrangement")

    assert event["slug"] == "et-arrangement"
    assert event["subtitle"] == "En undertekst"
    assert "Ingress." in event["lead"]
    assert event["pig"] == "automatic"
    assert event["free"] is False
    assert event["priceRegular"] == "150"
    assert event["priceStudent"] == "100"
    assert event["priceMember"] == "75"
    assert event["ticketUrl"] == "https://example.com/tickets"
    assert event["facebookUrl"] == "https://facebook.com/example"

    assert event["featuredImage"]["alt"] == "Et fotografi av en gris med solbriller"
    assert event["featuredImage"]["attribution"] == "Foto: Test"

    assert event["body"][0]["blockType"] == "RichTextBlock"
    assert "Body content." in event["body"][0]["value"]

    categories_by_name = {c["name"]: c for c in event["categories"]}
    assert set(categories_by_name) == {"Konsert", "Klubb"}
    assert categories_by_name["Konsert"]["slug"] == "konsert"
    assert categories_by_name["Konsert"]["showInFilters"] is True
    assert categories_by_name["Konsert"]["pig"] == "pigHeadLogo"
    assert categories_by_name["Klubb"]["showInFilters"] is False

    organizers_by_name = {o["name"]: o for o in event["organizers"]}
    assert set(organizers_by_name) == {"Internal", "External"}
    assert organizers_by_name["Internal"]["association"]["title"] == "Internal"
    assert organizers_by_name["Internal"]["externalUrl"] == ""
    assert organizers_by_name["External"]["association"] is None
    assert organizers_by_name["External"]["externalUrl"] == "https://external.example.com"

    assert len(event["occurrences"]) == 2
    venue_occ = next(o for o in event["occurrences"] if o["venue"] is not None)
    custom_occ = next(o for o in event["occurrences"] if o["venueCustom"])
    assert venue_occ["venueCustom"] == ""
    assert venue_occ["venue"]["title"]
    assert custom_occ["venue"] is None
    assert custom_occ["venueCustom"] == "Frederikkeplassen"

    venue_occ_db = comprehensive_event.occurrences.exclude(venue=None).get()
    custom_occ_db = comprehensive_event.occurrences.exclude(venue_custom="").get()
    assert datetime.fromisoformat(venue_occ["start"]) == venue_occ_db.start
    assert datetime.fromisoformat(venue_occ["end"]) == venue_occ_db.end
    assert datetime.fromisoformat(custom_occ["start"]) == custom_occ_db.start
    assert datetime.fromisoformat(custom_occ["end"]) == custom_occ_db.end


@pytest.fixture
def searchable_organizers(db):
    return [
        EventOrganizer.objects.create(name="Arrangementsutvalget", slug="arrangementsutvalget"),
        EventOrganizer.objects.create(name="Cinema Neuf", slug="kino"),
    ]


@pytest.mark.parametrize(
    "url_name",
    ["event_organizer_chooser:choose", "wagtailsnippets_events_eventorganizer:list"],
)
def test_organizer_search_matches_substrings_of_name_and_slug(
    admin_client, searchable_organizers, url_name
):
    url = reverse(url_name)

    # mid-word match inside a compound name
    content = admin_client.get(url, {"q": "utvalget"}).content.decode()
    assert "Arrangementsutvalget" in content
    assert "Cinema Neuf" not in content

    # slug-only match
    content = admin_client.get(url, {"q": "kino"}).content.decode()
    assert "Cinema Neuf" in content
    assert "Arrangementsutvalget" not in content
