package web

import (
	"context"
	"errors"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"strings"
	"testing"
	"time"

	"backend/internal/auth"
	"backend/internal/db/sqlc"
	"backend/templates"

	"github.com/google/uuid"
	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgtype"
)

func newEventTestRouter(q *sqlc.Queries, store *auth.Store) http.Handler {
	authDeps := AuthDeps{Queries: q, Store: store, Secure: false}
	tabloDeps := TablosDeps{Queries: q}
	taskDeps := TasksDeps{Queries: q}
	etapeDeps := EtapesDeps{Queries: q}
	eventDeps := EventsDeps{Queries: q}
	router, err := NewRouter(stubPinger{}, os.DirFS("./static"), authDeps, tabloDeps, taskDeps, etapeDeps, eventDeps, DiscussionDeps{Queries: q}, PlanningDeps{Queries: q}, FilesDeps{Queries: q}, testCSRFKey, "dev", "localhost")
	if err != nil {
		panic("newEventTestRouter: " + err.Error())
	}
	return router
}

func insertEventTestTablo(t *testing.T, ctx context.Context, q *sqlc.Queries, user sqlc.User, title string) sqlc.Tablo {
	t.Helper()
	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      user.ID,
		Title:       title,
		Description: pgtype.Text{Valid: false},
		Color:       pgtype.Text{Valid: false},
	})
	if err != nil {
		t.Fatalf("InsertTablo: %v", err)
	}
	return tablo
}

func sessionCookieForUser(t *testing.T, ctx context.Context, store *auth.Store, user sqlc.User) *http.Cookie {
	t.Helper()
	cookieVal, _, err := store.Create(ctx, user.ID)
	if err != nil {
		t.Fatalf("store.Create: %v", err)
	}
	return &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal}
}

func eventTestDate(t *testing.T, raw string) pgtype.Date {
	t.Helper()
	parsed, err := time.Parse("2006-01-02", raw)
	if err != nil {
		t.Fatalf("eventTestDate(%q): %v", raw, err)
	}
	return pgtype.Date{Time: parsed, Valid: true}
}

func eventTestTime(t *testing.T, raw string) pgtype.Time {
	t.Helper()
	parsed, err := time.Parse("15:04", raw)
	if err != nil {
		t.Fatalf("eventTestTime(%q): %v", raw, err)
	}
	micros := int64(parsed.Hour())*int64(time.Hour/time.Microsecond) + int64(parsed.Minute())*int64(time.Minute/time.Microsecond)
	return pgtype.Time{Microseconds: micros, Valid: true}
}

func insertEventTestEvent(t *testing.T, ctx context.Context, q *sqlc.Queries, tabloID uuid.UUID, title, date, start, end, location, description string) sqlc.Event {
	t.Helper()
	var endTime pgtype.Time
	if end != "" {
		endTime = eventTestTime(t, end)
	}
	event, err := q.CreateEvent(ctx, sqlc.CreateEventParams{
		TabloID:     tabloID,
		Title:       title,
		EventDate:   eventTestDate(t, date),
		StartTime:   eventTestTime(t, start),
		EndTime:     endTime,
		Description: pgtype.Text{String: description, Valid: description != ""},
		Location:    pgtype.Text{String: location, Valid: location != ""},
	})
	if err != nil {
		t.Fatalf("CreateEvent: %v", err)
	}
	return event
}

func TestEventsTabRendersMonthGrid(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	q := sqlc.New(pool)
	store := auth.NewStore(q)
	router := newEventTestRouter(q, store)

	user := preInsertUser(t, ctx, q, "eventsgrid@example.com", "correct-horse-12")
	tablo := insertEventTestTablo(t, ctx, q, user, "Events Grid Tablo")
	sessionCookie := sessionCookieForUser(t, ctx, store, user)

	req := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String()+"/events?month=2026-05", nil)
	req.Header.Set("HX-Request", "true")
	req.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("GET /tablos/{id}/events status = %d; want 200", rec.Code)
	}
	body := rec.Body.String()
	for _, want := range []string{"Events", "May 2026", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "New event"} {
		if !strings.Contains(body, want) {
			t.Errorf("events calendar missing %q; body: %.800s", want, body)
		}
	}
}

func TestEventCreateRendersTitleInCalendar(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	q := sqlc.New(pool)
	store := auth.NewStore(q)
	router := newEventTestRouter(q, store)

	user := preInsertUser(t, ctx, q, "eventcreate@example.com", "correct-horse-12")
	tablo := insertEventTestTablo(t, ctx, q, user, "Event Create Tablo")
	sessionCookie := sessionCookieForUser(t, ctx, store, user)
	csrfToken, csrfCookies := getCSRFToken(t, router, "/tablos/"+tablo.ID.String()+"/events?month=2026-05", []*http.Cookie{sessionCookie})

	form := url.Values{
		"title":       {"Kickoff"},
		"event_date":  {"2026-05-20"},
		"start_time":  {"09:30"},
		"end_time":    {""},
		"location":    {"Office"},
		"description": {"Planning"},
		"_csrf":       {csrfToken},
	}
	req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/events?month=2026-05", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Set("HX-Request", "true")
	for _, c := range csrfCookies {
		req.AddCookie(c)
	}
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("POST /tablos/{id}/events status = %d; want 200; body: %.500s", rec.Code, rec.Body.String())
	}
	if !strings.Contains(rec.Body.String(), "Kickoff") {
		t.Fatalf("created event title missing from refreshed calendar; body: %.800s", rec.Body.String())
	}
}

func TestEventCreateRejectsEndTimeBeforeOrEqualStart(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	q := sqlc.New(pool)
	store := auth.NewStore(q)
	router := newEventTestRouter(q, store)

	user := preInsertUser(t, ctx, q, "eventinvalid@example.com", "correct-horse-12")
	tablo := insertEventTestTablo(t, ctx, q, user, "Event Invalid Tablo")
	sessionCookie := sessionCookieForUser(t, ctx, store, user)
	csrfToken, csrfCookies := getCSRFToken(t, router, "/tablos/"+tablo.ID.String()+"/events?month=2026-05", []*http.Cookie{sessionCookie})

	form := url.Values{
		"title":      {"Invalid"},
		"event_date": {"2026-05-20"},
		"start_time": {"10:00"},
		"end_time":   {"10:00"},
		"_csrf":      {csrfToken},
	}
	req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/events?month=2026-05", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Set("HX-Request", "true")
	for _, c := range csrfCookies {
		req.AddCookie(c)
	}
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusUnprocessableEntity {
		t.Fatalf("invalid event create status = %d; want 422", rec.Code)
	}
	if !strings.Contains(rec.Body.String(), "End time must be after the start time.") {
		t.Fatalf("validation copy missing; body: %.800s", rec.Body.String())
	}
}

func TestEventsTabOwnershipReturns404(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	q := sqlc.New(pool)
	store := auth.NewStore(q)
	router := newEventTestRouter(q, store)

	owner := preInsertUser(t, ctx, q, "eventsowner@example.com", "correct-horse-12")
	nonOwner := preInsertUser(t, ctx, q, "eventsnonowner@example.com", "correct-horse-12")
	tablo := insertEventTestTablo(t, ctx, q, owner, "Private Events Tablo")
	nonOwnerCookie := sessionCookieForUser(t, ctx, store, nonOwner)

	req := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String()+"/events?month=2026-05", nil)
	req.AddCookie(nonOwnerCookie)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusNotFound {
		t.Fatalf("non-owner GET /tablos/{id}/events status = %d; want 404", rec.Code)
	}
}

func TestEventEditRendersInlineForm(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	q := sqlc.New(pool)
	store := auth.NewStore(q)
	router := newEventTestRouter(q, store)

	user := preInsertUser(t, ctx, q, "eventedit@example.com", "correct-horse-12")
	tablo := insertEventTestTablo(t, ctx, q, user, "Event Edit Tablo")
	event := insertEventTestEvent(t, ctx, q, tablo.ID, "Planning Review", "2026-05-21", "09:30", "10:30", "Studio", "Discuss schedule")
	sessionCookie := sessionCookieForUser(t, ctx, store, user)

	req := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String()+"/events/"+event.ID.String()+"/edit?month=2026-05", nil)
	req.Header.Set("HX-Request", "true")
	req.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("GET event edit status = %d; want 200; body: %.500s", rec.Code, rec.Body.String())
	}
	body := rec.Body.String()
	for _, want := range []string{"Save event changes", "Planning Review", "2026-05-21", "09:30", "Studio", "Discuss schedule"} {
		if !strings.Contains(body, want) {
			t.Errorf("edit form missing %q; body: %.800s", want, body)
		}
	}
}

func TestEventUpdateChangesCalendarPlacement(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	q := sqlc.New(pool)
	store := auth.NewStore(q)
	router := newEventTestRouter(q, store)

	user := preInsertUser(t, ctx, q, "eventupdate@example.com", "correct-horse-12")
	tablo := insertEventTestTablo(t, ctx, q, user, "Event Update Tablo")
	event := insertEventTestEvent(t, ctx, q, tablo.ID, "Original Review", "2026-05-21", "09:30", "", "Studio", "Discuss schedule")
	sessionCookie := sessionCookieForUser(t, ctx, store, user)
	csrfToken, csrfCookies := getCSRFToken(t, router, "/tablos/"+tablo.ID.String()+"/events?month=2026-05", []*http.Cookie{sessionCookie})

	form := url.Values{
		"title":       {"Moved Review"},
		"event_date":  {"2026-05-24"},
		"start_time":  {"14:15"},
		"end_time":    {"15:00"},
		"location":    {"HQ"},
		"description": {"Updated details"},
		"_csrf":       {csrfToken},
	}
	req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/events/"+event.ID.String()+"?month=2026-05", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Set("HX-Request", "true")
	for _, c := range csrfCookies {
		req.AddCookie(c)
	}
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("POST event update status = %d; want 200; body: %.500s", rec.Code, rec.Body.String())
	}
	body := rec.Body.String()
	if !strings.Contains(body, "Moved Review") {
		t.Fatalf("updated event title missing from refreshed calendar; body: %.800s", body)
	}
	if strings.Contains(body, "Original Review") {
		t.Fatalf("old event title still present after update; body: %.800s", body)
	}
}

func TestEventUpdateRejectsInvalidEndTime(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	q := sqlc.New(pool)
	store := auth.NewStore(q)
	router := newEventTestRouter(q, store)

	user := preInsertUser(t, ctx, q, "eventupdateinvalid@example.com", "correct-horse-12")
	tablo := insertEventTestTablo(t, ctx, q, user, "Event Update Invalid Tablo")
	event := insertEventTestEvent(t, ctx, q, tablo.ID, "Invalid Update", "2026-05-21", "09:30", "", "", "")
	sessionCookie := sessionCookieForUser(t, ctx, store, user)
	csrfToken, csrfCookies := getCSRFToken(t, router, "/tablos/"+tablo.ID.String()+"/events?month=2026-05", []*http.Cookie{sessionCookie})

	form := url.Values{
		"title":      {"Invalid Update"},
		"event_date": {"2026-05-21"},
		"start_time": {"10:00"},
		"end_time":   {"10:00"},
		"_csrf":      {csrfToken},
	}
	req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/events/"+event.ID.String()+"?month=2026-05", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Set("HX-Request", "true")
	for _, c := range csrfCookies {
		req.AddCookie(c)
	}
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusUnprocessableEntity {
		t.Fatalf("invalid event update status = %d; want 422", rec.Code)
	}
	if !strings.Contains(rec.Body.String(), "End time must be after the start time.") {
		t.Fatalf("validation copy missing; body: %.800s", rec.Body.String())
	}
}

func TestEventDeleteRemovesFromCalendarAndDatabase(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	q := sqlc.New(pool)
	store := auth.NewStore(q)
	router := newEventTestRouter(q, store)

	user := preInsertUser(t, ctx, q, "eventdelete@example.com", "correct-horse-12")
	tablo := insertEventTestTablo(t, ctx, q, user, "Event Delete Tablo")
	event := insertEventTestEvent(t, ctx, q, tablo.ID, "Delete Me", "2026-05-21", "09:30", "", "", "")
	sessionCookie := sessionCookieForUser(t, ctx, store, user)
	csrfToken, csrfCookies := getCSRFToken(t, router, "/tablos/"+tablo.ID.String()+"/events?month=2026-05", []*http.Cookie{sessionCookie})

	confirmReq := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String()+"/events/"+event.ID.String()+"/delete-confirm?month=2026-05", nil)
	confirmReq.Header.Set("HX-Request", "true")
	confirmReq.AddCookie(sessionCookie)
	confirmRec := httptest.NewRecorder()
	router.ServeHTTP(confirmRec, confirmReq)
	if confirmRec.Code != http.StatusOK {
		t.Fatalf("GET delete confirm status = %d; want 200; body: %.500s", confirmRec.Code, confirmRec.Body.String())
	}
	if !strings.Contains(confirmRec.Body.String(), "Delete event?") {
		t.Fatalf("delete confirmation copy missing; body: %.800s", confirmRec.Body.String())
	}

	form := url.Values{"_csrf": {csrfToken}}
	req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/events/"+event.ID.String()+"/delete?month=2026-05", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Set("HX-Request", "true")
	for _, c := range csrfCookies {
		req.AddCookie(c)
	}
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("POST event delete status = %d; want 200; body: %.500s", rec.Code, rec.Body.String())
	}
	if strings.Contains(rec.Body.String(), "Delete Me") {
		t.Fatalf("deleted event title still present in refreshed calendar; body: %.800s", rec.Body.String())
	}
	_, err := q.GetEventByID(ctx, sqlc.GetEventByIDParams{ID: event.ID, TabloID: tablo.ID})
	if !errors.Is(err, pgx.ErrNoRows) {
		t.Fatalf("GetEventByID after delete err = %v; want pgx.ErrNoRows", err)
	}
}

func TestEventMutationOwnershipReturns404(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	q := sqlc.New(pool)
	store := auth.NewStore(q)
	router := newEventTestRouter(q, store)

	owner := preInsertUser(t, ctx, q, "eventmutationowner@example.com", "correct-horse-12")
	nonOwner := preInsertUser(t, ctx, q, "eventmutationnonowner@example.com", "correct-horse-12")
	tablo := insertEventTestTablo(t, ctx, q, owner, "Private Mutation Tablo")
	event := insertEventTestEvent(t, ctx, q, tablo.ID, "Private Event", "2026-05-21", "09:30", "", "", "")
	nonOwnerCookie := sessionCookieForUser(t, ctx, store, nonOwner)
	csrfToken, csrfCookies := getCSRFToken(t, router, "/tablos/new", []*http.Cookie{nonOwnerCookie})

	getPaths := []string{
		"/tablos/" + tablo.ID.String() + "/events/" + event.ID.String() + "/edit?month=2026-05",
		"/tablos/" + tablo.ID.String() + "/events/" + event.ID.String() + "/delete-confirm?month=2026-05",
	}
	for _, path := range getPaths {
		req := httptest.NewRequest(http.MethodGet, path, nil)
		req.AddCookie(nonOwnerCookie)
		rec := httptest.NewRecorder()
		router.ServeHTTP(rec, req)
		if rec.Code != http.StatusNotFound {
			t.Fatalf("non-owner GET %s status = %d; want 404", path, rec.Code)
		}
	}

	postBodies := map[string]url.Values{
		"/tablos/" + tablo.ID.String() + "/events/" + event.ID.String() + "?month=2026-05": {
			"title":      {"Private Event"},
			"event_date": {"2026-05-21"},
			"start_time": {"09:30"},
			"_csrf":      {csrfToken},
		},
		"/tablos/" + tablo.ID.String() + "/events/" + event.ID.String() + "/delete?month=2026-05": {
			"_csrf": {csrfToken},
		},
	}
	for path, form := range postBodies {
		req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(form.Encode()))
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		for _, c := range csrfCookies {
			req.AddCookie(c)
		}
		rec := httptest.NewRecorder()
		router.ServeHTTP(rec, req)
		if rec.Code != http.StatusNotFound {
			t.Fatalf("non-owner POST %s status = %d; want 404", path, rec.Code)
		}
	}
}

func TestEventsMonthNavigationPushesMonthParam(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	q := sqlc.New(pool)
	store := auth.NewStore(q)
	router := newEventTestRouter(q, store)

	user := preInsertUser(t, ctx, q, "eventnav@example.com", "correct-horse-12")
	tablo := insertEventTestTablo(t, ctx, q, user, "Event Navigation Tablo")
	sessionCookie := sessionCookieForUser(t, ctx, store, user)

	req := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String()+"/events?month=2026-05", nil)
	req.Header.Set("HX-Request", "true")
	req.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("GET events month status = %d; want 200", rec.Code)
	}
	body := rec.Body.String()
	for _, want := range []string{"month=2026-04", "month=2026-06", "Previous month: April 2026", "Next month: June 2026"} {
		if !strings.Contains(body, want) {
			t.Errorf("month navigation missing %q; body: %.800s", want, body)
		}
	}
}

func TestEventNewFromDayPrefillsDate(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	q := sqlc.New(pool)
	store := auth.NewStore(q)
	router := newEventTestRouter(q, store)

	user := preInsertUser(t, ctx, q, "eventprefill@example.com", "correct-horse-12")
	tablo := insertEventTestTablo(t, ctx, q, user, "Event Prefill Tablo")
	sessionCookie := sessionCookieForUser(t, ctx, store, user)

	req := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String()+"/events/new?date=2026-05-20&month=2026-05", nil)
	req.Header.Set("HX-Request", "true")
	req.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("GET event new status = %d; want 200", rec.Code)
	}
	if !strings.Contains(rec.Body.String(), `value="2026-05-20"`) {
		t.Fatalf("date input was not prefilled from day query; body: %.800s", rec.Body.String())
	}
}

func TestEventCreateSubmittedDateOverridesQueryDefault(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	q := sqlc.New(pool)
	store := auth.NewStore(q)
	router := newEventTestRouter(q, store)

	user := preInsertUser(t, ctx, q, "eventsubmitteddate@example.com", "correct-horse-12")
	tablo := insertEventTestTablo(t, ctx, q, user, "Submitted Date Tablo")
	sessionCookie := sessionCookieForUser(t, ctx, store, user)
	csrfToken, csrfCookies := getCSRFToken(t, router, "/tablos/"+tablo.ID.String()+"/events?month=2026-05", []*http.Cookie{sessionCookie})

	form := url.Values{
		"title":      {"Submitted Date"},
		"event_date": {"2026-05-21"},
		"start_time": {"09:30"},
		"_csrf":      {csrfToken},
	}
	req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/events?date=2026-05-20&month=2026-05", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Set("HX-Request", "true")
	for _, c := range csrfCookies {
		req.AddCookie(c)
	}
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("POST event create status = %d; want 200; body: %.500s", rec.Code, rec.Body.String())
	}
	events, err := q.ListEventsByTabloRange(ctx, sqlc.ListEventsByTabloRangeParams{
		TabloID:     tablo.ID,
		EventDate:   eventTestDate(t, "2026-05-01"),
		EventDate_2: eventTestDate(t, "2026-05-31"),
	})
	if err != nil {
		t.Fatalf("ListEventsByTabloRange: %v", err)
	}
	if len(events) != 1 {
		t.Fatalf("events len = %d; want 1", len(events))
	}
	if got := templates.FormatEventDate(events[0].EventDate); got != "2026-05-21" {
		t.Fatalf("stored event date = %s; want 2026-05-21", got)
	}
}

func TestEventsCalendarShowsMoreIndicator(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	q := sqlc.New(pool)
	store := auth.NewStore(q)
	router := newEventTestRouter(q, store)

	user := preInsertUser(t, ctx, q, "eventoverflow@example.com", "correct-horse-12")
	tablo := insertEventTestTablo(t, ctx, q, user, "Overflow Tablo")
	sessionCookie := sessionCookieForUser(t, ctx, store, user)
	for i, title := range []string{"Overflow 1", "Overflow 2", "Overflow 3", "Overflow 4"} {
		insertEventTestEvent(t, ctx, q, tablo.ID, title, "2026-05-20", "09:0"+string(rune('0'+i)), "", "", "")
	}

	req := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String()+"/events?month=2026-05", nil)
	req.Header.Set("HX-Request", "true")
	req.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("GET events overflow status = %d; want 200", rec.Code)
	}
	body := rec.Body.String()
	for _, want := range []string{"Overflow 1", "Overflow 2", "Overflow 3", "+1 more"} {
		if !strings.Contains(body, want) {
			t.Errorf("overflow calendar missing %q; body: %.800s", want, body)
		}
	}
	if strings.Contains(body, "Overflow 4") {
		t.Fatalf("fourth event title should be hidden behind overflow indicator; body: %.800s", body)
	}
}

func TestListUserEventsRangeReturnsOnlyOwnedTablos(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	q := sqlc.New(pool)

	owner := preInsertUser(t, ctx, q, "eventrangeowner@example.com", "correct-horse-12")
	other := preInsertUser(t, ctx, q, "eventrangeother@example.com", "correct-horse-12")
	ownedTablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      owner.ID,
		Title:       "Owned Calendar",
		Description: pgtype.Text{Valid: false},
		Color:       pgtype.Text{String: "#123abc", Valid: true},
	})
	if err != nil {
		t.Fatalf("InsertTablo owned: %v", err)
	}
	foreignTablo := insertEventTestTablo(t, ctx, q, other, "Foreign Calendar")
	insertEventTestEvent(t, ctx, q, ownedTablo.ID, "Owned Range Event", "2026-05-20", "09:30", "", "", "")
	insertEventTestEvent(t, ctx, q, foreignTablo.ID, "Foreign Range Event", "2026-05-20", "09:30", "", "", "")

	rows, err := q.ListUserEventsRange(ctx, sqlc.ListUserEventsRangeParams{
		UserID:    owner.ID,
		StartDate: eventTestDate(t, "2026-05-01"),
		EndDate:   eventTestDate(t, "2026-05-31"),
	})
	if err != nil {
		t.Fatalf("ListUserEventsRange: %v", err)
	}
	if len(rows) != 1 {
		t.Fatalf("rows len = %d; want 1", len(rows))
	}
	row := rows[0]
	if row.Title != "Owned Range Event" {
		t.Fatalf("row title = %q; want Owned Range Event", row.Title)
	}
	if row.TabloTitle != "Owned Calendar" {
		t.Fatalf("row tablo title = %q; want Owned Calendar", row.TabloTitle)
	}
	if !row.TabloColor.Valid || row.TabloColor.String != "#123abc" {
		t.Fatalf("row tablo color = %#v; want #123abc", row.TabloColor)
	}
}

func TestTabloDetailEventsTabFullPageFallback(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	q := sqlc.New(pool)
	store := auth.NewStore(q)
	router := newEventTestRouter(q, store)

	user := preInsertUser(t, ctx, q, "eventfullpage@example.com", "correct-horse-12")
	tablo := insertEventTestTablo(t, ctx, q, user, "Events Full Page Tablo")
	sessionCookie := sessionCookieForUser(t, ctx, store, user)

	req := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String()+"/events?month=2026-05", nil)
	req.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("GET full events page status = %d; want 200; body: %.500s", rec.Code, rec.Body.String())
	}
	body := rec.Body.String()
	for _, want := range []string{"Back to tablos", "Overview", "Tasks", "Files", "Events", "May 2026", `id="tab-content"`} {
		if !strings.Contains(body, want) {
			t.Errorf("full events page missing %q; body: %.800s", want, body)
		}
	}
}

func TestEventContentIsEscaped(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	q := sqlc.New(pool)
	store := auth.NewStore(q)
	router := newEventTestRouter(q, store)

	user := preInsertUser(t, ctx, q, "eventescaped@example.com", "correct-horse-12")
	tablo := insertEventTestTablo(t, ctx, q, user, "Escaped Event Tablo")
	insertEventTestEvent(t, ctx, q, tablo.ID, "<script>alert(1)</script>", "2026-05-20", "09:30", "", "", "")
	sessionCookie := sessionCookieForUser(t, ctx, store, user)

	req := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String()+"/events?month=2026-05", nil)
	req.Header.Set("HX-Request", "true")
	req.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("GET escaped event status = %d; want 200; body: %.500s", rec.Code, rec.Body.String())
	}
	body := rec.Body.String()
	if strings.Contains(body, "<script>alert(1)</script>") {
		t.Fatalf("event title rendered as raw script; body: %.800s", body)
	}
	if !strings.Contains(body, "&lt;script&gt;alert(1)&lt;/script&gt;") {
		t.Fatalf("escaped event title missing; body: %.800s", body)
	}
}

func TestEventWithoutEndTimeDisplaysOnlyStartTime(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	q := sqlc.New(pool)
	store := auth.NewStore(q)
	router := newEventTestRouter(q, store)

	user := preInsertUser(t, ctx, q, "eventnoend@example.com", "correct-horse-12")
	tablo := insertEventTestTablo(t, ctx, q, user, "No End Event Tablo")
	event := insertEventTestEvent(t, ctx, q, tablo.ID, "Open End", "2026-05-20", "09:30", "", "", "")
	sessionCookie := sessionCookieForUser(t, ctx, store, user)

	req := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String()+"/events/"+event.ID.String()+"/edit?month=2026-05", nil)
	req.Header.Set("HX-Request", "true")
	req.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("GET event edit status = %d; want 200; body: %.500s", rec.Code, rec.Body.String())
	}
	body := rec.Body.String()
	if !strings.Contains(body, `name="start_time" value="09:30"`) {
		t.Fatalf("start time missing for event without end time; body: %.800s", body)
	}
	if strings.Contains(body, "09:30-") || strings.Contains(body, "09:30 –") {
		t.Fatalf("event without end time rendered an inferred range; body: %.800s", body)
	}
}

func TestEventsOrderedByStartTimeThenTitle(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

	ctx := context.Background()
	q := sqlc.New(pool)
	store := auth.NewStore(q)
	router := newEventTestRouter(q, store)

	user := preInsertUser(t, ctx, q, "eventorder@example.com", "correct-horse-12")
	tablo := insertEventTestTablo(t, ctx, q, user, "Ordered Events Tablo")
	insertEventTestEvent(t, ctx, q, tablo.ID, "Beta", "2026-05-20", "10:00", "", "", "")
	insertEventTestEvent(t, ctx, q, tablo.ID, "Gamma", "2026-05-20", "09:00", "", "", "")
	insertEventTestEvent(t, ctx, q, tablo.ID, "Alpha", "2026-05-20", "09:00", "", "", "")
	sessionCookie := sessionCookieForUser(t, ctx, store, user)

	req := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String()+"/events?month=2026-05", nil)
	req.Header.Set("HX-Request", "true")
	req.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("GET ordered events status = %d; want 200; body: %.500s", rec.Code, rec.Body.String())
	}
	body := rec.Body.String()
	alpha := strings.Index(body, "Alpha")
	gamma := strings.Index(body, "Gamma")
	beta := strings.Index(body, "Beta")
	if alpha == -1 || gamma == -1 || beta == -1 {
		t.Fatalf("ordered event titles missing; Alpha=%d Gamma=%d Beta=%d body: %.800s", alpha, gamma, beta, body)
	}
	if !(alpha < gamma && gamma < beta) {
		t.Fatalf("events not ordered by start time then title; Alpha=%d Gamma=%d Beta=%d body: %.800s", alpha, gamma, beta, body)
	}
}
