package handlers

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

func TestInMemoryTablosListExcludesSoftDeletedRows(t *testing.T) {
	repo := NewInMemoryAuthRepository()
	user, err := repo.GetAuthUserByEmail(context.Background(), "demo@xtablo.com")
	if err != nil {
		t.Fatalf("expected demo user, got error %v", err)
	}

	deletedTablo, err := repo.CreateTablo(context.Background(), CreateTabloInput{
		OwnerID: user.ID,
		Name:    "Visible",
		Status:  TabloStatusTodo,
	})
	if err != nil {
		t.Fatalf("create deleted tablo: %v", err)
	}

	keptTablo, err := repo.CreateTablo(context.Background(), CreateTabloInput{
		OwnerID: user.ID,
		Name:    "Kept",
		Status:  TabloStatusTodo,
	})
	if err != nil {
		t.Fatalf("create kept tablo: %v", err)
	}

	if err := repo.SoftDeleteTablo(context.Background(), deletedTablo.ID, user.ID); err != nil {
		t.Fatalf("soft delete tablo: %v", err)
	}

	tablos, err := repo.ListTablos(context.Background(), ListTablosInput{
		OwnerID: user.ID,
	})
	if err != nil {
		t.Fatalf("list tablos: %v", err)
	}

	if len(tablos) != 1 {
		t.Fatalf("expected 1 visible tablo, got %d", len(tablos))
	}

	if tablos[0].ID != keptTablo.ID {
		t.Fatalf("expected kept tablo %s, got %s", keptTablo.ID, tablos[0].ID)
	}
}

func TestInMemoryTablosListFiltersByStatus(t *testing.T) {
	repo := NewInMemoryAuthRepository()
	user, err := repo.GetAuthUserByEmail(context.Background(), "demo@xtablo.com")
	if err != nil {
		t.Fatalf("expected demo user, got error %v", err)
	}

	_, err = repo.CreateTablo(context.Background(), CreateTabloInput{
		OwnerID: user.ID,
		Name:    "Hello Product",
		Status:  TabloStatusTodo,
	})
	if err != nil {
		t.Fatalf("create todo tablo: %v", err)
	}

	expectedTablo, err := repo.CreateTablo(context.Background(), CreateTabloInput{
		OwnerID: user.ID,
		Name:    "Hello Delivery",
		Status:  TabloStatusInProgress,
	})
	if err != nil {
		t.Fatalf("create in progress tablo: %v", err)
	}

	tablos, err := repo.ListTablos(context.Background(), ListTablosInput{
		OwnerID: user.ID,
		Status:  &[]TabloStatus{TabloStatusInProgress}[0],
	})
	if err != nil {
		t.Fatalf("list status-filtered tablos: %v", err)
	}

	if len(tablos) != 1 {
		t.Fatalf("expected 1 status-filtered tablo, got %d", len(tablos))
	}

	if tablos[0].ID != expectedTablo.ID {
		t.Fatalf("expected tablo %s, got %s", expectedTablo.ID, tablos[0].ID)
	}
}

func TestInMemoryTablosSoftDeleteRejectsDifferentOwner(t *testing.T) {
	repo := NewInMemoryAuthRepository()
	owner, err := repo.GetAuthUserByEmail(context.Background(), "demo@xtablo.com")
	if err != nil {
		t.Fatalf("expected demo user, got error %v", err)
	}

	otherUserID, err := repo.CreateAuthUser(context.Background(), CreateAuthUserInput{
		Email:             "other@xtablo.com",
		EncryptedPassword: "hash",
		DisplayName:       "other",
	})
	if err != nil {
		t.Fatalf("create other user: %v", err)
	}

	tablo, err := repo.CreateTablo(context.Background(), CreateTabloInput{
		OwnerID: owner.ID,
		Name:    "Owned",
		Status:  TabloStatusTodo,
	})
	if err != nil {
		t.Fatalf("create owned tablo: %v", err)
	}

	if err := repo.SoftDeleteTablo(context.Background(), tablo.ID, otherUserID); err == nil {
		t.Fatal("expected deleting another user's tablo to fail")
	}
}

func TestInMemoryTablosCreatePersistsColor(t *testing.T) {
	repo := NewInMemoryAuthRepository()
	user, err := repo.GetAuthUserByEmail(context.Background(), "demo@xtablo.com")
	if err != nil {
		t.Fatalf("expected demo user, got error %v", err)
	}

	created, err := repo.CreateTablo(context.Background(), CreateTabloInput{
		OwnerID: user.ID,
		Name:    "Roadmap",
		Color:   "#3B82F6",
		Status:  TabloStatusTodo,
	})
	if err != nil {
		t.Fatalf("create colored tablo: %v", err)
	}

	if created.Color != "#3B82F6" {
		t.Fatalf("expected color to persist, got %q", created.Color)
	}
}

func TestGetTablosPageDefaultsToGridAndAllStatus(t *testing.T) {
	handler := newTestAuthHandler(t)
	sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")

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

	handler.GetTablosPage().ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("expected status 200, got %d", rec.Code)
	}

	body := rec.Body.String()
	for _, want := range []string{
		"Mes Projets",
		"Nouveau projet",
		"Vue en grille",
		"Rechercher...",
		"Tous",
	} {
		if !strings.Contains(body, want) {
			t.Fatalf("expected body to contain %q", want)
		}
	}
}

func TestGetTablosPageIgnoresSearchQueryParamAndLeavesFilteringToClient(t *testing.T) {
	repo := NewInMemoryAuthRepository()
	handler := NewAuthHandler(repo)
	sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.AddCookie(sessionCookie)
	userID, ok := handler.currentUserID(req.Context(), req)
	if !ok {
		t.Fatal("expected user session")
	}

	_, err := repo.CreateTablo(context.Background(), CreateTabloInput{
		OwnerID: userID,
		Name:    "Alpha Draft",
		Status:  TabloStatusInProgress,
	})
	if err != nil {
		t.Fatalf("create first in-progress tablo: %v", err)
	}

	_, err = repo.CreateTablo(context.Background(), CreateTabloInput{
		OwnerID: userID,
		Name:    "Beta Delivery",
		Status:  TabloStatusInProgress,
	})
	if err != nil {
		t.Fatalf("create filtered tablo: %v", err)
	}

	pageReq := httptest.NewRequest(http.MethodGet, "/tablos?q=delivery&status=in_progress", nil)
	pageReq.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()

	handler.GetTablosPage().ServeHTTP(rec, pageReq)

	if rec.Code != http.StatusOK {
		t.Fatalf("expected status 200, got %d", rec.Code)
	}

	body := rec.Body.String()
	if !strings.Contains(body, "Alpha Draft") {
		t.Fatalf("expected non-matching tablo to stay visible for client filtering, got %q", body)
	}
	if !strings.Contains(body, "Beta Delivery") {
		t.Fatalf("expected matching tablo to be visible, got %q", body)
	}
}

func TestGetTablosPageRendersClientSideProjectFilterHooks(t *testing.T) {
	repo := NewInMemoryAuthRepository()
	handler := NewAuthHandler(repo)
	sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.AddCookie(sessionCookie)
	userID, ok := handler.currentUserID(req.Context(), req)
	if !ok {
		t.Fatal("expected user session")
	}

	_, err := repo.CreateTablo(context.Background(), CreateTabloInput{
		OwnerID: userID,
		Name:    "Searchable Project",
		Status:  TabloStatusTodo,
	})
	if err != nil {
		t.Fatalf("create searchable tablo: %v", err)
	}

	pageReq := httptest.NewRequest(http.MethodGet, "/tablos", nil)
	pageReq.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()

	handler.GetTablosPage().ServeHTTP(rec, pageReq)

	body := rec.Body.String()
	for _, want := range []string{
		`data-project-filter-root`,
		`data-project-filter-input`,
		`data-project-filter-item`,
		`window.xtabloProjectFilterInitialized`,
	} {
		if !strings.Contains(body, want) {
			t.Fatalf("expected client-side filter markup %q, got %q", want, body)
		}
	}
	if strings.Contains(body, `name="q"`) {
		t.Fatalf("expected search query field to be removed from markup, got %q", body)
	}
}

func TestGetTablosPageUsesSharedToolbarButtonAndStatusBadge(t *testing.T) {
	repo := NewInMemoryAuthRepository()
	handler := NewAuthHandler(repo)
	sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.AddCookie(sessionCookie)
	userID, ok := handler.currentUserID(req.Context(), req)
	if !ok {
		t.Fatal("expected user session")
	}

	_, err := repo.CreateTablo(context.Background(), CreateTabloInput{
		OwnerID: userID,
		Name:    "Shared UI",
		Status:  TabloStatusInProgress,
	})
	if err != nil {
		t.Fatalf("create tablo: %v", err)
	}

	pageReq := httptest.NewRequest(http.MethodGet, "/tablos", nil)
	pageReq.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()

	handler.GetTablosPage().ServeHTTP(rec, pageReq)

	if rec.Code != http.StatusOK {
		t.Fatalf("expected status 200, got %d", rec.Code)
	}

	body := rec.Body.String()
	for _, want := range []string{
		`ui-button ui-button-solid ui-button-default ui-button-md`,
		`ui-badge ui-badge-warning`,
	} {
		if !strings.Contains(body, want) {
			t.Fatalf("expected shared primitive markup %q, got %q", want, body)
		}
	}
}

func TestGetTablosPageModalUsesSharedFormPrimitives(t *testing.T) {
	handler := newTestAuthHandler(t)
	sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")

	req := httptest.NewRequest(http.MethodGet, "/tablos?modal=create", nil)
	req.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()

	handler.GetTablosPage().ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("expected status 200, got %d", rec.Code)
	}

	body := rec.Body.String()
	for _, want := range []string{
		`ui-modal-panel`,
		`ui-form-field`,
		`class="ui-input"`,
	} {
		if !strings.Contains(body, want) {
			t.Fatalf("expected modal primitive markup %q, got %q", want, body)
		}
	}
}

func TestGetTablosPageListViewRendersTableLayout(t *testing.T) {
	repo := NewInMemoryAuthRepository()
	handler := NewAuthHandler(repo)
	sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.AddCookie(sessionCookie)
	userID, ok := handler.currentUserID(req.Context(), req)
	if !ok {
		t.Fatal("expected user session")
	}

	_, err := repo.CreateTablo(context.Background(), CreateTabloInput{
		OwnerID: userID,
		Name:    "Table View",
		Status:  TabloStatusInProgress,
	})
	if err != nil {
		t.Fatalf("create tablo: %v", err)
	}

	pageReq := httptest.NewRequest(http.MethodGet, "/tablos?view=list", nil)
	pageReq.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()

	handler.GetTablosPage().ServeHTTP(rec, pageReq)

	if rec.Code != http.StatusOK {
		t.Fatalf("expected status 200, got %d", rec.Code)
	}

	body := rec.Body.String()
	for _, want := range []string{
		`class="ui-table-shell"`,
		`<table class="ui-table">`,
		"Progression",
		"Table View",
	} {
		if !strings.Contains(body, want) {
			t.Fatalf("expected list view to contain %q, got %q", want, body)
		}
	}
}

func TestGetTablosPageEmptyStateUsesSharedPrimitive(t *testing.T) {
	handler := newTestAuthHandler(t)
	sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")

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

	handler.GetTablosPage().ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("expected status 200, got %d", rec.Code)
	}

	body := rec.Body.String()
	for _, want := range []string{
		`ui-empty-state`,
		`Aucun projet trouvé`,
		`Créez votre premier projet`,
	} {
		if !strings.Contains(body, want) {
			t.Fatalf("expected empty state primitive markup %q, got %q", want, body)
		}
	}
}

func TestGetTablosPageListViewUsesDirectTableIconMarkup(t *testing.T) {
	repo := NewInMemoryAuthRepository()
	handler := NewAuthHandler(repo)
	sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.AddCookie(sessionCookie)
	userID, ok := handler.currentUserID(req.Context(), req)
	if !ok {
		t.Fatal("expected user session")
	}

	_, err := repo.CreateTablo(context.Background(), CreateTabloInput{
		OwnerID: userID,
		Name:    "Markup Check",
		Status:  TabloStatusInProgress,
	})
	if err != nil {
		t.Fatalf("create tablo: %v", err)
	}

	pageReq := httptest.NewRequest(http.MethodGet, "/tablos?view=list", nil)
	pageReq.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()

	handler.GetTablosPage().ServeHTTP(rec, pageReq)

	if rec.Code != http.StatusOK {
		t.Fatalf("expected status 200, got %d", rec.Code)
	}

	body := rec.Body.String()
	for _, want := range []string{
		`class="flex items-center gap-1.5 [&>svg]:w-4 [&>svg]:h-4 [&>svg]:shrink-0"><svg viewBox="0 0 24 24"`,
		`class="borderless-icon-button ui-icon-button-ghost ui-icon-button-danger" aria-label="Supprimer le projet"`,
		`class="lucide lucide-trash2 w-4 h-4"`,
	} {
		if !strings.Contains(body, want) {
			t.Fatalf("expected list view markup to contain %q, got %q", want, body)
		}
	}
}

func TestGetTablosPageGridUsesProjectDateRowMarkup(t *testing.T) {
	repo := NewInMemoryAuthRepository()
	handler := NewAuthHandler(repo)
	sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.AddCookie(sessionCookie)
	userID, ok := handler.currentUserID(req.Context(), req)
	if !ok {
		t.Fatal("expected user session")
	}

	_, err := repo.CreateTablo(context.Background(), CreateTabloInput{
		OwnerID: userID,
		Name:    "Calendar Check",
		Status:  TabloStatusTodo,
	})
	if err != nil {
		t.Fatalf("create tablo: %v", err)
	}

	pageReq := httptest.NewRequest(http.MethodGet, "/tablos", nil)
	pageReq.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()

	handler.GetTablosPage().ServeHTTP(rec, pageReq)

	if rec.Code != http.StatusOK {
		t.Fatalf("expected status 200, got %d", rec.Code)
	}

	if !strings.Contains(rec.Body.String(), `class="project-date-row"><svg viewBox="0 0 24 24"`) {
		t.Fatalf("expected grid card calendar icon to render inside project-date-row markup, got %q", rec.Body.String())
	}
}

func TestFormatCardDateUsesFrenchMonthNames(t *testing.T) {
	got := formatCardDate(time.Date(2026, time.May, 10, 9, 0, 0, 0, time.UTC))
	if got != "10 mai 2026" {
		t.Fatalf("expected French card date label, got %q", got)
	}
}

func TestTabloIconPresentationUsesClosestPaletteColor(t *testing.T) {
	for _, tt := range []struct {
		name  string
		color string
		icon  string
	}{
		{name: "blue maps to bolt", color: "#3B82F6", icon: "bolt"},
		{name: "green maps to leaf", color: "#22C55E", icon: "leaf"},
		{name: "purple maps to gem", color: "#A855F7", icon: "gem"},
		{name: "red maps to flame", color: "#EF4444", icon: "flame"},
		{name: "yellow maps to star", color: "#EAB308", icon: "star"},
		{name: "indigo maps to compass", color: "#6366F1", icon: "compass"},
		{name: "pink maps to heart", color: "#EC4899", icon: "heart"},
		{name: "teal maps to waves", color: "#14B8A6", icon: "waves"},
		{name: "orange maps to sun", color: "#F97316", icon: "sun"},
		{name: "cyan maps to sparkles", color: "#06B6D4", icon: "sparkles"},
		{name: "nearby blue still maps to bolt", color: "#4F86F7", icon: "bolt"},
	} {
		t.Run(tt.name, func(t *testing.T) {
			icon, _, _, _ := tabloIconPresentation(tt.color)
			if icon != tt.icon {
				t.Fatalf("expected icon %q for color %q, got %q", tt.icon, tt.color, icon)
			}
		})
	}
}

func TestGetTablosPageGridUsesProjectCardMarkup(t *testing.T) {
	repo := NewInMemoryAuthRepository()
	handler := NewAuthHandler(repo)
	sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.AddCookie(sessionCookie)
	userID, ok := handler.currentUserID(req.Context(), req)
	if !ok {
		t.Fatal("expected user session")
	}

	_, err := repo.CreateTablo(context.Background(), CreateTabloInput{
		OwnerID: userID,
		Name:    "Hello",
		Status:  TabloStatusInProgress,
	})
	if err != nil {
		t.Fatalf("create tablo: %v", err)
	}

	pageReq := httptest.NewRequest(http.MethodGet, "/tablos", nil)
	pageReq.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()

	handler.GetTablosPage().ServeHTTP(rec, pageReq)

	if rec.Code != http.StatusOK {
		t.Fatalf("expected status 200, got %d", rec.Code)
	}

	body := rec.Body.String()
	for _, want := range []string{
		`<article class="project-card" style="--project-color:`,
		`class="project-card-top"`,
		`class="borderless-icon-button ui-icon-button-ghost ui-icon-button-neutral" aria-label="Modifier le projet"`,
		`class="borderless-icon-button ui-icon-button-ghost ui-icon-button-danger" aria-label="Supprimer le projet"`,
		`class="project-card-title-row"`,
		`class="project-avatar"`,
		`class="project-date-row"`,
		`class="project-progress-track"`,
	} {
		if !strings.Contains(body, want) {
			t.Fatalf("expected grid card markup to contain %q, got %q", want, body)
		}
	}
}

func TestPostTablosCreatesTodoTablo(t *testing.T) {
	handler := newTestAuthHandler(t)
	sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")

	form := url.Values{}
	form.Set("name", "Roadmap")
	form.Set("color", "#3B82F6")
	form.Set("view", "grid")
	form.Set("status", "all")

	req := httptest.NewRequest(http.MethodPost, "/tablos", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()

	handler.PostTablos().ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("expected status 200, got %d", rec.Code)
	}

	body := rec.Body.String()
	if !strings.Contains(body, "Roadmap") {
		t.Fatalf("expected created tablo in response, got %q", body)
	}
	if !strings.Contains(body, "À faire") {
		t.Fatalf("expected todo status label in response, got %q", body)
	}
}

func TestPostTablosWithEmptyNameReturns422(t *testing.T) {
	handler := newTestAuthHandler(t)
	sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")

	form := url.Values{}
	req := httptest.NewRequest(http.MethodPost, "/tablos", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()

	handler.PostTablos().ServeHTTP(rec, req)

	if rec.Code != http.StatusUnprocessableEntity {
		t.Fatalf("expected status 422, got %d", rec.Code)
	}

	if !strings.Contains(rec.Body.String(), "Le nom du projet est requis") {
		t.Fatalf("expected validation error, got %q", rec.Body.String())
	}
}

func TestPostTablosWithMissingColorReturns422(t *testing.T) {
	handler := newTestAuthHandler(t)
	sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")

	form := url.Values{}
	form.Set("name", "Roadmap")

	req := httptest.NewRequest(http.MethodPost, "/tablos", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()

	handler.PostTablos().ServeHTTP(rec, req)

	if rec.Code != http.StatusUnprocessableEntity {
		t.Fatalf("expected status 422, got %d", rec.Code)
	}

	if !strings.Contains(rec.Body.String(), "La couleur du projet doit être un code hexadécimal au format #RRGGBB") {
		t.Fatalf("expected color validation error, got %q", rec.Body.String())
	}
}

func TestPostTablosWithInvalidColorReturns422(t *testing.T) {
	handler := newTestAuthHandler(t)
	sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")

	form := url.Values{}
	form.Set("name", "Roadmap")
	form.Set("color", "#0af")

	req := httptest.NewRequest(http.MethodPost, "/tablos", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()

	handler.PostTablos().ServeHTTP(rec, req)

	if rec.Code != http.StatusUnprocessableEntity {
		t.Fatalf("expected status 422, got %d", rec.Code)
	}

	if !strings.Contains(rec.Body.String(), "La couleur du projet doit être un code hexadécimal au format #RRGGBB") {
		t.Fatalf("expected invalid color validation error, got %q", rec.Body.String())
	}
}

func TestGetTablosEditModalPrefillsNameAndColorPicker(t *testing.T) {
	repo := NewInMemoryAuthRepository()
	handler := NewAuthHandler(repo)
	sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.AddCookie(sessionCookie)
	userID, ok := handler.currentUserID(req.Context(), req)
	if !ok {
		t.Fatal("expected user session")
	}

	tablo, err := repo.CreateTablo(context.Background(), CreateTabloInput{
		OwnerID: userID,
		Name:    "Palette",
		Color:   "#3B82F6",
		Status:  TabloStatusTodo,
	})
	if err != nil {
		t.Fatalf("create tablo: %v", err)
	}

	editReq := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String()+"/edit?view=grid&status=all", nil)
	editReq.SetPathValue("tabloID", tablo.ID.String())
	editReq.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()

	handler.GetEditTabloModal().ServeHTTP(rec, editReq)

	if rec.Code != http.StatusOK {
		t.Fatalf("expected status 200, got %d", rec.Code)
	}

	body := rec.Body.String()
	for _, want := range []string{
		"Palette",
		`value="#3B82F6"`,
		`type="color"`,
	} {
		if !strings.Contains(body, want) {
			t.Fatalf("expected edit modal to contain %q, got %q", want, body)
		}
	}
}

func TestPostTablosUpdateRejectsDifferentOwner(t *testing.T) {
	repo := NewInMemoryAuthRepository()
	handler := NewAuthHandler(repo)

	ownerCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
	ownerReq := httptest.NewRequest(http.MethodGet, "/", nil)
	ownerReq.AddCookie(ownerCookie)
	ownerID, ok := handler.currentUserID(ownerReq.Context(), ownerReq)
	if !ok {
		t.Fatal("expected owner session")
	}

	tablo, err := repo.CreateTablo(context.Background(), CreateTabloInput{
		OwnerID: ownerID,
		Name:    "Owned",
		Color:   "#3B82F6",
		Status:  TabloStatusTodo,
	})
	if err != nil {
		t.Fatalf("create tablo: %v", err)
	}

	passwordHash, err := hashPassword("xtablo-demo")
	if err != nil {
		t.Fatalf("hash password: %v", err)
	}

	_, err = repo.CreateAuthUser(context.Background(), CreateAuthUserInput{
		Email:             "other@xtablo.com",
		EncryptedPassword: passwordHash,
		DisplayName:       "other",
	})
	if err != nil {
		t.Fatalf("create user: %v", err)
	}

	otherCookie := loginTestUser(t, handler, "other@xtablo.com", "xtablo-demo")
	form := url.Values{}
	form.Set("name", "Updated")
	form.Set("color", "#EF4444")

	updateReq := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String(), strings.NewReader(form.Encode()))
	updateReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	updateReq.SetPathValue("tabloID", tablo.ID.String())
	updateReq.AddCookie(otherCookie)
	rec := httptest.NewRecorder()

	handler.PostTabloUpdate().ServeHTTP(rec, updateReq)

	if rec.Code != http.StatusNotFound {
		t.Fatalf("expected status 404, got %d", rec.Code)
	}
}

func TestDeleteTabloSoftDeletesOwnedRow(t *testing.T) {
	repo := NewInMemoryAuthRepository()
	handler := NewAuthHandler(repo)
	sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.AddCookie(sessionCookie)
	userID, ok := handler.currentUserID(req.Context(), req)
	if !ok {
		t.Fatal("expected user session")
	}

	tablo, err := repo.CreateTablo(context.Background(), CreateTabloInput{
		OwnerID: userID,
		Name:    "Disposable",
		Status:  TabloStatusTodo,
	})
	if err != nil {
		t.Fatalf("create tablo: %v", err)
	}

	deleteReq := httptest.NewRequest(http.MethodDelete, "/tablos/"+tablo.ID.String(), nil)
	deleteReq.SetPathValue("tabloID", tablo.ID.String())
	deleteReq.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()

	handler.DeleteTablo().ServeHTTP(rec, deleteReq)

	if rec.Code != http.StatusOK {
		t.Fatalf("expected status 200, got %d", rec.Code)
	}

	list, err := repo.ListTablos(context.Background(), ListTablosInput{OwnerID: userID})
	if err != nil {
		t.Fatalf("list tablos: %v", err)
	}
	if len(list) != 0 {
		t.Fatalf("expected deleted tablo to be hidden, got %d rows", len(list))
	}
}

func TestDeleteTabloRejectsDifferentOwner(t *testing.T) {
	repo := NewInMemoryAuthRepository()
	handler := NewAuthHandler(repo)

	ownerCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
	ownerReq := httptest.NewRequest(http.MethodGet, "/", nil)
	ownerReq.AddCookie(ownerCookie)
	ownerID, ok := handler.currentUserID(ownerReq.Context(), ownerReq)
	if !ok {
		t.Fatal("expected owner session")
	}

	tablo, err := repo.CreateTablo(context.Background(), CreateTabloInput{
		OwnerID: ownerID,
		Name:    "Owned",
		Status:  TabloStatusTodo,
	})
	if err != nil {
		t.Fatalf("create tablo: %v", err)
	}

	passwordHash, err := hashPassword("xtablo-demo")
	if err != nil {
		t.Fatalf("hash password: %v", err)
	}

	_, err = repo.CreateAuthUser(context.Background(), CreateAuthUserInput{
		Email:             "other@xtablo.com",
		EncryptedPassword: passwordHash,
		DisplayName:       "other",
	})
	if err != nil {
		t.Fatalf("create user: %v", err)
	}

	otherCookie := loginTestUser(t, handler, "other@xtablo.com", "xtablo-demo")

	req := httptest.NewRequest(http.MethodDelete, "/tablos/"+tablo.ID.String(), nil)
	req.SetPathValue("tabloID", tablo.ID.String())
	req.AddCookie(otherCookie)
	rec := httptest.NewRecorder()

	handler.DeleteTablo().ServeHTTP(rec, req)

	if rec.Code != http.StatusNotFound {
		t.Fatalf("expected status 404, got %d", rec.Code)
	}
}

func loginTestUser(t *testing.T, handler *AuthHandler, email string, password string) *http.Cookie {
	t.Helper()

	form := url.Values{}
	form.Set("email", email)
	form.Set("password", password)

	req := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	rec := httptest.NewRecorder()

	handler.PostLogin().ServeHTTP(rec, req)

	for _, cookie := range rec.Result().Cookies() {
		if cookie.Name == "xtablo_session" {
			return cookie
		}
	}

	t.Fatal("expected session cookie to be set")
	return nil
}
