package web

// handlers_tablos_test.go — Wave 0 integration test scaffold for TABLO-01..06.
//
// All tests are expected to FAIL at runtime until Plans 02 and 03 implement the
// actual handlers and routes. This file is the RED baseline; Plans 02/03 turn it green.
//
// Pattern: mirrors handlers_auth_test.go exactly — setupTestDB, preInsertUser,
// getCSRFToken, and newTestRouter are all reused from that file (same package).

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

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

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

// newTabloTestRouter builds a router with TablosDeps wired for tablo handler tests.
func newTabloTestRouter(q *sqlc.Queries, store *auth.Store) http.Handler {
	authDeps := AuthDeps{Queries: q, Store: store, Secure: false}
	tabloDeps := TablosDeps{Queries: q}
	router, err := NewRouter(stubPinger{}, os.DirFS("./static"), authDeps, tabloDeps, TasksDeps{Queries: q}, EtapesDeps{Queries: q}, EventsDeps{Queries: q}, DiscussionDeps{Queries: q}, PlanningDeps{Queries: q}, FilesDeps{Queries: q}, testCSRFKey, "dev", "localhost")
	if err != nil {
		panic("newTabloTestRouter: " + err.Error())
	}
	return router
}

// loginUser signs up a user and returns the session cookie set after signup.
// Reuses the signup flow (same as handlers_auth_test.go integration tests).
func loginUser(t *testing.T, router http.Handler, email, password string) []*http.Cookie {
	t.Helper()
	csrfToken, csrfCookies := getCSRFToken(t, router, "/signup", nil)
	form := url.Values{
		"email":    {email},
		"password": {password},
		"_csrf":    {csrfToken},
	}
	req := httptest.NewRequest(http.MethodPost, "/signup", 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.StatusSeeOther {
		t.Fatalf("loginUser: signup status = %d; want 303", rec.Code)
	}

	// Collect all cookies from the response (session + gorilla_csrf).
	var allCookies []*http.Cookie
	allCookies = append(allCookies, csrfCookies...)
	allCookies = append(allCookies, rec.Result().Cookies()...)
	return allCookies
}

// ---- TestTabloList ----

// TestTabloList verifies that an authenticated GET / returns the user's tablos
// newest-first, and that tablos owned by other users are NOT present (TABLO-01).
func TestTabloList(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

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

	// Create two users.
	userA := preInsertUser(t, ctx, q, "lista@example.com", "correct-horse-12")
	userB := preInsertUser(t, ctx, q, "listb@example.com", "correct-horse-12")

	// Insert tablos for userA.
	tabloA1, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      userA.ID,
		Title:       "TabloA First",
		Description: pgtype.Text{Valid: false},
		Color:       pgtype.Text{Valid: false},
	})
	if err != nil {
		t.Fatalf("InsertTablo tabloA1: %v", err)
	}
	tabloA2, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      userA.ID,
		Title:       "TabloA Second",
		Description: pgtype.Text{Valid: false},
		Color:       pgtype.Text{Valid: false},
	})
	if err != nil {
		t.Fatalf("InsertTablo tabloA2: %v", err)
	}

	// Insert a tablo for userB — must NOT appear in userA's list.
	_, err = q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      userB.ID,
		Title:       "TabloB Private",
		Description: pgtype.Text{Valid: false},
		Color:       pgtype.Text{Valid: false},
	})
	if err != nil {
		t.Fatalf("InsertTablo tabloB: %v", err)
	}

	// Log in as userA.
	cookieVal, _, storeErr := store.Create(ctx, userA.ID)
	if storeErr != nil {
		t.Fatalf("store.Create: %v", storeErr)
	}
	sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal}

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("GET / status = %d; want 200", rec.Code)
	}

	body := rec.Body.String()

	// Both of userA's tablos must appear.
	if !strings.Contains(body, tabloA1.Title) {
		t.Errorf("body missing tabloA1 title %q", tabloA1.Title)
	}
	if !strings.Contains(body, tabloA2.Title) {
		t.Errorf("body missing tabloA2 title %q", tabloA2.Title)
	}

	// userB's tablo must NOT appear.
	if strings.Contains(body, "TabloB Private") {
		t.Errorf("body must NOT contain userB's tablo title")
	}

	// Newest-first ordering (D-03): tabloA2 was inserted after tabloA1.
	// Check that tabloA2 appears before tabloA1 in the response body.
	idx1 := strings.Index(body, tabloA1.Title)
	idx2 := strings.Index(body, tabloA2.Title)
	if idx1 != -1 && idx2 != -1 && idx2 > idx1 {
		t.Errorf("tablos not in newest-first order: tabloA2 index=%d, tabloA1 index=%d", idx2, idx1)
	}
}

// ---- TestTabloList_Empty ----

// TestTabloList_Empty verifies the empty state copy when the user has no tablos (TABLO-01).
func TestTabloList_Empty(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

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

	user := preInsertUser(t, ctx, q, "empty@example.com", "correct-horse-12")
	cookieVal, _, err := store.Create(ctx, user.ID)
	if err != nil {
		t.Fatalf("store.Create: %v", err)
	}
	sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal}

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("GET / status = %d; want 200", rec.Code)
	}

	body := rec.Body.String()
	if !strings.Contains(body, "No tablos yet") {
		t.Errorf("empty state missing 'No tablos yet'; body: %.300s", body)
	}
	if !strings.Contains(body, "Create your first tablo to get started.") {
		t.Errorf("empty state missing CTA text; body: %.300s", body)
	}
}

// ---- TestTabloCreate ----

// TestTabloCreate verifies HTMX-driven create (TABLO-02, TABLO-06).
// Sub-asserts cover:
//   - HTMX POST returns 200 with card fragment + HX-Retarget + HX-Reswap + OOB form clear
//   - Non-HTMX POST returns 303 to /
//   - DB row exists with user_id = caller
func TestTabloCreate(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

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

	user := preInsertUser(t, ctx, q, "create@example.com", "correct-horse-12")

	t.Run("HTMX create", func(t *testing.T) {
		cookieVal, _, err := store.Create(ctx, user.ID)
		if err != nil {
			t.Fatalf("store.Create: %v", err)
		}
		sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal}
		csrfToken, csrfCookies := getCSRFToken(t, router, "/", []*http.Cookie{sessionCookie})

		form := url.Values{
			"title":       {"My New Tablo"},
			"description": {"A description"},
			"color":       {"#6366f1"},
			"_csrf":       {csrfToken},
		}
		req := httptest.NewRequest(http.MethodPost, "/tablos", 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("HTMX POST /tablos status = %d; want 200", rec.Code)
		}

		body := rec.Body.String()
		if !strings.Contains(body, "My New Tablo") {
			t.Errorf("response body missing new tablo title; body: %.300s", body)
		}

		// HX-Retarget must be #tablos-list (dual-target swap per RESEARCH Pattern 4).
		if hxRetarget := rec.Header().Get("HX-Retarget"); hxRetarget != "#tablos-list" {
			t.Errorf("HX-Retarget = %q; want #tablos-list", hxRetarget)
		}

		// HX-Reswap must be afterbegin.
		if hxReswap := rec.Header().Get("HX-Reswap"); hxReswap != "afterbegin" {
			t.Errorf("HX-Reswap = %q; want afterbegin", hxReswap)
		}

		// OOB element to clear the form slot must be present.
		if !strings.Contains(body, `id="create-form-slot"`) {
			t.Errorf("response body missing OOB form-clear element (id=create-form-slot); body: %.500s", body)
		}
		if !strings.Contains(body, `hx-swap-oob="true"`) {
			t.Errorf("response body missing hx-swap-oob attribute; body: %.500s", body)
		}

		// DB row must exist with user_id = caller (TABLO-02).
		tablos, err := q.ListTablosByUser(ctx, user.ID)
		if err != nil {
			t.Fatalf("ListTablosByUser: %v", err)
		}
		if len(tablos) == 0 {
			t.Fatal("no tablo row in DB after create")
		}
		if tablos[0].Title != "My New Tablo" {
			t.Errorf("DB tablo title = %q; want My New Tablo", tablos[0].Title)
		}
		if tablos[0].UserID != user.ID {
			t.Errorf("DB tablo user_id = %v; want %v", tablos[0].UserID, user.ID)
		}
	})

	t.Run("non-HTMX create", func(t *testing.T) {
		// Delete any existing tablos to keep state clean.
		tablos, _ := q.ListTablosByUser(ctx, user.ID)
		for _, tbl := range tablos {
			_ = q.DeleteTablo(ctx, sqlc.DeleteTabloParams{ID: tbl.ID, UserID: tbl.UserID})
		}

		cookieVal, _, err := store.Create(ctx, user.ID)
		if err != nil {
			t.Fatalf("store.Create: %v", err)
		}
		sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal}
		csrfToken, csrfCookies := getCSRFToken(t, router, "/", []*http.Cookie{sessionCookie})

		form := url.Values{
			"title": {"Non-HTMX Tablo"},
			"_csrf": {csrfToken},
		}
		req := httptest.NewRequest(http.MethodPost, "/tablos", strings.NewReader(form.Encode()))
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		// No HX-Request header — non-HTMX path.
		for _, c := range csrfCookies {
			req.AddCookie(c)
		}
		rec := httptest.NewRecorder()
		router.ServeHTTP(rec, req)

		if rec.Code != http.StatusSeeOther {
			t.Fatalf("non-HTMX POST /tablos status = %d; want 303", rec.Code)
		}
		if loc := rec.Header().Get("Location"); loc != "/" {
			t.Errorf("Location = %q; want /", loc)
		}
	})
}

// ---- TestTabloCreate_Validation ----

// TestTabloCreate_Validation verifies that POST /tablos with empty title returns
// 422 with an error message (TABLO-02 validation, T-03-03).
func TestTabloCreate_Validation(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

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

	user := preInsertUser(t, ctx, q, "createval@example.com", "correct-horse-12")
	cookieVal, _, err := store.Create(ctx, user.ID)
	if err != nil {
		t.Fatalf("store.Create: %v", err)
	}
	sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal}
	csrfToken, csrfCookies := getCSRFToken(t, router, "/", []*http.Cookie{sessionCookie})

	// Empty title.
	form := url.Values{
		"title": {""},
		"_csrf": {csrfToken},
	}
	req := httptest.NewRequest(http.MethodPost, "/tablos", 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("status = %d; want 422", rec.Code)
	}
	if !strings.Contains(rec.Body.String(), "Title is required.") {
		t.Errorf("body missing 'Title is required.'; got: %.300s", rec.Body.String())
	}

	// No DB row should have been inserted.
	tablos, _ := q.ListTablosByUser(ctx, user.ID)
	if len(tablos) != 0 {
		t.Errorf("tablo count = %d; want 0 (no insert on validation error)", len(tablos))
	}
}

// ---- TestTabloDetail_Owner ----

// TestTabloDetail_Owner verifies that the owner of a tablo can view its detail page (TABLO-03).
func TestTabloDetail_Owner(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

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

	user := preInsertUser(t, ctx, q, "owner@example.com", "correct-horse-12")

	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      user.ID,
		Title:       "Owners Detail Tablo",
		Description: pgtype.Text{Valid: false},
		Color:       pgtype.Text{Valid: false},
	})
	if err != nil {
		t.Fatalf("InsertTablo: %v", err)
	}

	cookieVal, _, storeErr := store.Create(ctx, user.ID)
	if storeErr != nil {
		t.Fatalf("store.Create: %v", storeErr)
	}
	sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal}

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

	if rec.Code != http.StatusOK {
		t.Fatalf("GET /tablos/{id} as owner: status = %d; want 200", rec.Code)
	}
	if !strings.Contains(rec.Body.String(), "Owners Detail Tablo") {
		t.Errorf("body missing tablo title; got: %.300s", rec.Body.String())
	}
}

// ---- TestTabloDetail_NonOwner ----

// TestTabloDetail_NonOwner verifies that a different authenticated user gets 404
// (not 403 — no information leakage per D-04) when accessing another user's tablo (TABLO-03).
func TestTabloDetail_NonOwner(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

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

	// User A creates a tablo.
	userA := preInsertUser(t, ctx, q, "nonowner_a@example.com", "correct-horse-12")
	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      userA.ID,
		Title:       "UserA's Secret Tablo",
		Description: pgtype.Text{Valid: false},
		Color:       pgtype.Text{Valid: false},
	})
	if err != nil {
		t.Fatalf("InsertTablo: %v", err)
	}

	// User B tries to access it.
	userB := preInsertUser(t, ctx, q, "nonowner_b@example.com", "correct-horse-12")
	cookieVal, _, storeErr := store.Create(ctx, userB.ID)
	if storeErr != nil {
		t.Fatalf("store.Create for userB: %v", storeErr)
	}
	sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal}

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

	// D-04: 404 (not 403) — no information leakage.
	if rec.Code != http.StatusNotFound {
		t.Fatalf("GET /tablos/{id} as non-owner: status = %d; want 404", rec.Code)
	}
}

// ---- TestTabloDetail_InvalidID ----

// TestTabloDetail_InvalidID verifies that a non-UUID path param returns 404 (TABLO-03).
func TestTabloDetail_InvalidID(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

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

	user := preInsertUser(t, ctx, q, "invalidid@example.com", "correct-horse-12")
	cookieVal, _, err := store.Create(ctx, user.ID)
	if err != nil {
		t.Fatalf("store.Create: %v", err)
	}
	sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal}

	req := httptest.NewRequest(http.MethodGet, "/tablos/not-a-uuid", nil)
	req.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusNotFound {
		t.Fatalf("GET /tablos/not-a-uuid: status = %d; want 404", rec.Code)
	}
}

// ---- TestTabloUpdate ----

// TestTabloUpdate verifies that POST /tablos/{id} updates title and description (TABLO-04).
// Checks: 200 response with updated title in body; DB row updated; updated_at refreshed.
func TestTabloUpdate(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

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

	user := preInsertUser(t, ctx, q, "update@example.com", "correct-horse-12")

	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      user.ID,
		Title:       "Original Title",
		Description: pgtype.Text{Valid: true, String: "Original description"},
		Color:       pgtype.Text{Valid: false},
	})
	if err != nil {
		t.Fatalf("InsertTablo: %v", err)
	}

	cookieVal, _, storeErr := store.Create(ctx, user.ID)
	if storeErr != nil {
		t.Fatalf("store.Create: %v", storeErr)
	}
	sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal}
	csrfToken, csrfCookies := getCSRFToken(t, router, "/tablos/"+tablo.ID.String(), []*http.Cookie{sessionCookie})

	form := url.Values{
		"title":       {"Updated Title"},
		"description": {"Updated description"},
		"_csrf":       {csrfToken},
	}
	req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String(), 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} status = %d; want 200", rec.Code)
	}
	if !strings.Contains(rec.Body.String(), "Updated Title") {
		t.Errorf("body missing updated title; got: %.300s", rec.Body.String())
	}

	// Verify DB state.
	updated, err := q.GetTabloByID(ctx, sqlc.GetTabloByIDParams{ID: tablo.ID, UserID: user.ID})
	if err != nil {
		t.Fatalf("GetTabloByID after update: %v", err)
	}
	if updated.Title != "Updated Title" {
		t.Errorf("DB title = %q; want 'Updated Title'", updated.Title)
	}
	if !updated.Description.Valid || updated.Description.String != "Updated description" {
		t.Errorf("DB description = %v/%q; want valid 'Updated description'", updated.Description.Valid, updated.Description.String)
	}
	// updated_at must be >= original (Pitfall 7).
	if updated.UpdatedAt.Time.Before(tablo.UpdatedAt.Time) || updated.UpdatedAt.Time.Equal(tablo.UpdatedAt.Time) {
		// In tests the DB is local; allow same time but not earlier.
		// We do a lenient check: updated_at must be >= original.
		if updated.UpdatedAt.Time.Before(tablo.UpdatedAt.Time) {
			t.Errorf("updated_at did not advance: was %v, got %v", tablo.UpdatedAt.Time, updated.UpdatedAt.Time)
		}
	}
}

// ---- TestTabloDeleteConfirm ----

// TestTabloDeleteConfirm verifies that GET /tablos/{id}/delete-confirm returns a
// confirmation fragment with "Delete tablo?" and "Yes, delete" (TABLO-05, D-07).
func TestTabloDeleteConfirm(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

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

	user := preInsertUser(t, ctx, q, "delconfirm@example.com", "correct-horse-12")

	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      user.ID,
		Title:       "To Be Deleted",
		Description: pgtype.Text{Valid: false},
		Color:       pgtype.Text{Valid: false},
	})
	if err != nil {
		t.Fatalf("InsertTablo: %v", err)
	}

	cookieVal, _, storeErr := store.Create(ctx, user.ID)
	if storeErr != nil {
		t.Fatalf("store.Create: %v", storeErr)
	}
	sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal}

	req := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String()+"/delete-confirm", 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}/delete-confirm: status = %d; want 200", rec.Code)
	}
	body := rec.Body.String()
	if !strings.Contains(body, "Delete tablo?") {
		t.Errorf("confirm fragment missing 'Delete tablo?'; body: %.300s", body)
	}
	if !strings.Contains(body, "Yes, delete") {
		t.Errorf("confirm fragment missing 'Yes, delete'; body: %.300s", body)
	}
}

// ---- TestTabloDelete ----

// TestTabloDelete verifies that POST /tablos/{id}/delete hard-deletes the row (TABLO-05).
// Sub-asserts:
//   - HTMX: 200 + HX-Redirect:/ (or HX-Retarget #tablos-list for in-list deletes)
//   - Non-HTMX: 303 to /
//   - DB row deleted
func TestTabloDelete(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

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

	user := preInsertUser(t, ctx, q, "delete@example.com", "correct-horse-12")

	t.Run("HTMX delete", func(t *testing.T) {
		tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
			UserID:      user.ID,
			Title:       "Tablo to Delete HTMX",
			Description: pgtype.Text{Valid: false},
			Color:       pgtype.Text{Valid: false},
		})
		if err != nil {
			t.Fatalf("InsertTablo: %v", err)
		}

		cookieVal, _, storeErr := store.Create(ctx, user.ID)
		if storeErr != nil {
			t.Fatalf("store.Create: %v", storeErr)
		}
		sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal}
		csrfToken, csrfCookies := getCSRFToken(t, router, "/", []*http.Cookie{sessionCookie})

		form := url.Values{"_csrf": {csrfToken}}
		req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/delete", 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("HTMX DELETE status = %d; want 200", rec.Code)
		}

		// Must have HX-Redirect header pointing back to /.
		hxRedirect := rec.Header().Get("HX-Redirect")
		if hxRedirect != "/" {
			t.Errorf("HX-Redirect = %q; want /", hxRedirect)
		}

		// DB row must be gone.
		_, err = q.GetTabloByID(ctx, sqlc.GetTabloByIDParams{ID: tablo.ID, UserID: user.ID})
		if err == nil {
			t.Error("tablo row still exists in DB after delete")
		}
	})

	t.Run("non-HTMX delete", func(t *testing.T) {
		tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
			UserID:      user.ID,
			Title:       "Tablo to Delete Plain",
			Description: pgtype.Text{Valid: false},
			Color:       pgtype.Text{Valid: false},
		})
		if err != nil {
			t.Fatalf("InsertTablo: %v", err)
		}

		cookieVal, _, storeErr := store.Create(ctx, user.ID)
		if storeErr != nil {
			t.Fatalf("store.Create: %v", storeErr)
		}
		sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal}
		csrfToken, csrfCookies := getCSRFToken(t, router, "/", []*http.Cookie{sessionCookie})

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

		if rec.Code != http.StatusSeeOther {
			t.Fatalf("non-HTMX DELETE status = %d; want 303", rec.Code)
		}
		if loc := rec.Header().Get("Location"); loc != "/" {
			t.Errorf("Location = %q; want /", loc)
		}

		// DB row must be gone.
		_, err = q.GetTabloByID(ctx, sqlc.GetTabloByIDParams{ID: tablo.ID, UserID: user.ID})
		if err == nil {
			t.Error("tablo row still exists in DB after non-HTMX delete")
		}
	})
}
