package web

// handlers_tasks_test.go — Wave 0 RED test scaffold for TASK-01..07 + IDOR test.
//
// All test functions call t.Skip("handlers_tasks not yet implemented") so they
// compile but do NOT fail before Plan 02 implements the handlers and routes.
// This file is the RED baseline; Plan 02 turns it green.
//
// Pattern: mirrors handlers_tablos_test.go exactly — setupTestDB, loginUser,
// preInsertUser, getCSRFToken, and testCSRFKey are reused from the existing
// test files in the 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"
)

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

// ---- TestTasksKanbanRenders (TASK-01) ----

// TestTasksKanbanRenders verifies that GET /tablos/{id} by the owner renders
// all four kanban column headers: To do, In progress, In review, Done (TASK-01).
func TestTasksKanbanRenders(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

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

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

	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      user.ID,
		Title:       "Kanban Test 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()+"/tasks", nil)
	req.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

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

	body := rec.Body.String()
	// Column labels as defined in TaskColumnLabels (tasks_forms.go).
	for _, col := range []string{"To do", "In progress", "In review", "Done"} {
		if !strings.Contains(body, col) {
			t.Errorf("kanban board missing column header %q", col)
		}
	}
	if !strings.Contains(body, "kanban-board") {
		t.Errorf("kanban board missing id=kanban-board")
	}

	// Non-owner should get 404.
	nonOwner := preInsertUser(t, ctx, q, "kanban-other@example.com", "correct-horse-12")
	nonOwnerCookieVal, _, _ := store.Create(ctx, nonOwner.ID)
	nonOwnerCookie := &http.Cookie{Name: auth.SessionCookieName, Value: nonOwnerCookieVal}
	req2 := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String()+"/tasks", nil)
	req2.AddCookie(nonOwnerCookie)
	rec2 := httptest.NewRecorder()
	router.ServeHTTP(rec2, req2)
	if rec2.Code != http.StatusNotFound {
		t.Errorf("non-owner GET /tablos/{id}/tasks: status = %d; want 404", rec2.Code)
	}
}

// ---- TestTaskCreate (TASK-02) ----

// TestTaskCreate verifies that POST /tablos/{id}/tasks creates a task and
// returns a 200 response with an HTMX card fragment (TASK-02).
func TestTaskCreate(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

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

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

	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      user.ID,
		Title:       "Task Create 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}
	csrfToken, csrfCookies := getCSRFToken(t, router, "/tablos/"+tablo.ID.String(), []*http.Cookie{sessionCookie})

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

	// DB row must exist.
	tasks, err := q.ListTasksByTablo(ctx, tablo.ID)
	if err != nil {
		t.Fatalf("ListTasksByTablo: %v", err)
	}
	if len(tasks) == 0 {
		t.Fatal("no task row in DB after create")
	}
	if tasks[0].Title != "My Test Task" {
		t.Errorf("DB task title = %q; want 'My Test Task'", tasks[0].Title)
	}
}

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

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

	user := preInsertUser(t, ctx, q, "taskcreatecount@example.com", "correct-horse-12")
	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      user.ID,
		Title:       "Task Count Tablo",
		Description: pgtype.Text{Valid: false},
		Color:       pgtype.Text{Valid: false},
	})
	if err != nil {
		t.Fatalf("InsertTablo: %v", err)
	}
	etape, err := q.InsertEtape(ctx, sqlc.InsertEtapeParams{
		TabloID:     tablo.ID,
		Title:       "Design",
		Description: pgtype.Text{Valid: false},
		Position:    100,
	})
	if err != nil {
		t.Fatalf("InsertEtape: %v", err)
	}

	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, "/tablos/"+tablo.ID.String()+"/tasks?etape="+etape.ID.String(), []*http.Cookie{sessionCookie})

	form := url.Values{
		"title":    {"Refresh Count"},
		"status":   {"todo"},
		"etape_id": {etape.ID.String()},
		"_csrf":    {csrfToken},
	}
	req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/tasks?etape="+etape.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 task create status = %d; want 200", rec.Code)
	}
	body := rec.Body.String()
	if !strings.Contains(body, `id="etape-strip"`) || !strings.Contains(body, `hx-swap-oob="outerHTML"`) {
		t.Fatalf("task create response did not include OOB etape strip refresh; body: %.800s", body)
	}
	if !strings.Contains(body, "Design") || !strings.Contains(body, ">1<") {
		t.Fatalf("task create response missing updated etape count; body: %.800s", body)
	}
}

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

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

	user := preInsertUser(t, ctx, q, "taskcreateselector@example.com", "correct-horse-12")
	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      user.ID,
		Title:       "Task Selected Etape Tablo",
		Description: pgtype.Text{Valid: false},
		Color:       pgtype.Text{Valid: false},
	})
	if err != nil {
		t.Fatalf("InsertTablo: %v", err)
	}
	filterEtape, err := q.InsertEtape(ctx, sqlc.InsertEtapeParams{
		TabloID:     tablo.ID,
		Title:       "Filtered",
		Description: pgtype.Text{Valid: false},
		Position:    100,
	})
	if err != nil {
		t.Fatalf("InsertEtape filter: %v", err)
	}
	selectedEtape, err := q.InsertEtape(ctx, sqlc.InsertEtapeParams{
		TabloID:     tablo.ID,
		Title:       "Selected",
		Description: pgtype.Text{Valid: false},
		Position:    200,
	})
	if err != nil {
		t.Fatalf("InsertEtape selected: %v", err)
	}

	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, "/tablos/"+tablo.ID.String()+"/tasks?etape="+filterEtape.ID.String(), []*http.Cookie{sessionCookie})

	form := url.Values{
		"title":  {"Selected Wins"},
		"status": {"todo"},
		// Mirrors browser order from the rendered form before this regression:
		// hidden active-filter etape_id first, visible selector value second.
		"etape_id": {filterEtape.ID.String(), selectedEtape.ID.String()},
		"_csrf":    {csrfToken},
	}
	req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/tasks?etape="+filterEtape.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 task create status = %d; want 200", rec.Code)
	}
	tasks, err := q.ListTasksByTablo(ctx, tablo.ID)
	if err != nil {
		t.Fatalf("ListTasksByTablo: %v", err)
	}
	if len(tasks) != 1 {
		t.Fatalf("task count = %d; want 1", len(tasks))
	}
	if !tasks[0].EtapeID.Valid || tasks[0].EtapeID.Bytes != selectedEtape.ID {
		t.Fatalf("task etape_id = %+v; want selected etape %s", tasks[0].EtapeID, selectedEtape.ID)
	}
}

// ---- TestTaskCreateValidation (TASK-02) ----

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

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

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

	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      user.ID,
		Title:       "Task Validation 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}
	csrfToken, csrfCookies := getCSRFToken(t, router, "/tablos/"+tablo.ID.String(), []*http.Cookie{sessionCookie})

	form := url.Values{
		"title":  {""},
		"status": {"todo"},
		"_csrf":  {csrfToken},
	}
	req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/tasks", 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.
	tasks, _ := q.ListTasksByTablo(ctx, tablo.ID)
	if len(tasks) != 0 {
		t.Errorf("task count = %d; want 0 (no insert on validation error)", len(tasks))
	}
}

// ---- TestTaskUpdate (TASK-03) ----

// TestTaskUpdate verifies that POST /tablos/{id}/tasks/{task_id} updates the
// task title and description and returns a card fragment (TASK-03).
func TestTaskUpdate(t *testing.T) {

	pool, cleanup := setupTestDB(t)
	defer cleanup()

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

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

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

	task, err := q.InsertTask(ctx, sqlc.InsertTaskParams{
		TabloID:     tablo.ID,
		Title:       "Original Task Title",
		Description: pgtype.Text{Valid: false},
		Status:      sqlc.TaskStatusTodo,
		Position:    100,
	})
	if err != nil {
		t.Fatalf("InsertTask: %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 Task Title"},
		"description": {"Updated description"},
		"_csrf":       {csrfToken},
	}
	req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/tasks/"+task.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}/tasks/{task_id} status = %d; want 200", rec.Code)
	}
	if !strings.Contains(rec.Body.String(), "Updated Task Title") {
		t.Errorf("response body missing updated title; body: %.300s", rec.Body.String())
	}

	// Verify DB state.
	updated, err := q.GetTaskByID(ctx, sqlc.GetTaskByIDParams{ID: task.ID, TabloID: tablo.ID})
	if err != nil {
		t.Fatalf("GetTaskByID after update: %v", err)
	}
	if updated.Title != "Updated Task Title" {
		t.Errorf("DB task title = %q; want 'Updated Task Title'", updated.Title)
	}
}

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

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

	user := preInsertUser(t, ctx, q, "taskeditetape@example.com", "correct-horse-12")
	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      user.ID,
		Title:       "Task Edit Etape Tablo",
		Description: pgtype.Text{Valid: false},
		Color:       pgtype.Text{Valid: false},
	})
	if err != nil {
		t.Fatalf("InsertTablo: %v", err)
	}
	etape, err := q.InsertEtape(ctx, sqlc.InsertEtapeParams{
		TabloID:     tablo.ID,
		Title:       "Design",
		Description: pgtype.Text{Valid: false},
		Position:    100,
	})
	if err != nil {
		t.Fatalf("InsertEtape: %v", err)
	}
	task, err := q.InsertTask(ctx, sqlc.InsertTaskParams{
		TabloID:     tablo.ID,
		Title:       "Editable",
		Description: pgtype.Text{Valid: false},
		Status:      sqlc.TaskStatusTodo,
		Position:    100,
		EtapeID:     pgtype.UUID{Bytes: etape.ID, Valid: true},
	})
	if err != nil {
		t.Fatalf("InsertTask: %v", err)
	}

	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/"+tablo.ID.String()+"/tasks/"+task.ID.String()+"/edit", 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 task edit status = %d; want 200", rec.Code)
	}
	body := rec.Body.String()
	for _, want := range []string{"Etape", "No etape", "Design"} {
		if !strings.Contains(body, want) {
			t.Errorf("edit form missing %q; body: %.500s", want, body)
		}
	}
}

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

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

	user := preInsertUser(t, ctx, q, "taskupdateetape@example.com", "correct-horse-12")
	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      user.ID,
		Title:       "Task Update Etape Tablo",
		Description: pgtype.Text{Valid: false},
		Color:       pgtype.Text{Valid: false},
	})
	if err != nil {
		t.Fatalf("InsertTablo: %v", err)
	}
	etape, err := q.InsertEtape(ctx, sqlc.InsertEtapeParams{
		TabloID:     tablo.ID,
		Title:       "Design",
		Description: pgtype.Text{Valid: false},
		Position:    100,
	})
	if err != nil {
		t.Fatalf("InsertEtape: %v", err)
	}
	task, err := q.InsertTask(ctx, sqlc.InsertTaskParams{
		TabloID:     tablo.ID,
		Title:       "Assignable",
		Description: pgtype.Text{Valid: false},
		Status:      sqlc.TaskStatusInReview,
		Position:    300,
	})
	if err != nil {
		t.Fatalf("InsertTask: %v", err)
	}

	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, "/tablos/"+tablo.ID.String(), []*http.Cookie{sessionCookie})

	postUpdate := func(etapeID string) {
		t.Helper()
		form := url.Values{
			"title":       {"Assignable"},
			"description": {"Updated"},
			"etape_id":    {etapeID},
			"_csrf":       {csrfToken},
		}
		req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/tasks/"+task.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 task update status = %d; want 200; body: %.500s", rec.Code, rec.Body.String())
		}
	}

	postUpdate(etape.ID.String())
	assigned, err := q.GetTaskByID(ctx, sqlc.GetTaskByIDParams{ID: task.ID, TabloID: tablo.ID})
	if err != nil {
		t.Fatalf("GetTaskByID assigned: %v", err)
	}
	if !assigned.EtapeID.Valid || assigned.EtapeID.Bytes != etape.ID {
		t.Fatalf("assigned etape_id = %+v; want %s", assigned.EtapeID, etape.ID)
	}
	if assigned.Status != sqlc.TaskStatusInReview || assigned.Position != 300 {
		t.Fatalf("status/position changed after assignment: %+v", assigned)
	}

	postUpdate("")
	unassigned, err := q.GetTaskByID(ctx, sqlc.GetTaskByIDParams{ID: task.ID, TabloID: tablo.ID})
	if err != nil {
		t.Fatalf("GetTaskByID unassigned: %v", err)
	}
	if unassigned.EtapeID.Valid {
		t.Fatalf("etape_id valid after unassign; want null")
	}
}

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

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

	user := preInsertUser(t, ctx, q, "taskforeignetape@example.com", "correct-horse-12")
	firstTablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      user.ID,
		Title:       "First",
		Description: pgtype.Text{Valid: false},
		Color:       pgtype.Text{Valid: false},
	})
	if err != nil {
		t.Fatalf("InsertTablo first: %v", err)
	}
	secondTablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      user.ID,
		Title:       "Second",
		Description: pgtype.Text{Valid: false},
		Color:       pgtype.Text{Valid: false},
	})
	if err != nil {
		t.Fatalf("InsertTablo second: %v", err)
	}
	foreignEtape, err := q.InsertEtape(ctx, sqlc.InsertEtapeParams{
		TabloID:     secondTablo.ID,
		Title:       "Foreign",
		Description: pgtype.Text{Valid: false},
		Position:    100,
	})
	if err != nil {
		t.Fatalf("InsertEtape: %v", err)
	}
	task, err := q.InsertTask(ctx, sqlc.InsertTaskParams{
		TabloID:     firstTablo.ID,
		Title:       "Protected",
		Description: pgtype.Text{Valid: false},
		Status:      sqlc.TaskStatusTodo,
		Position:    100,
	})
	if err != nil {
		t.Fatalf("InsertTask: %v", err)
	}

	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, "/tablos/"+firstTablo.ID.String(), []*http.Cookie{sessionCookie})

	form := url.Values{
		"title":       {"Protected"},
		"description": {""},
		"etape_id":    {foreignEtape.ID.String()},
		"_csrf":       {csrfToken},
	}
	req := httptest.NewRequest(http.MethodPost, "/tablos/"+firstTablo.ID.String()+"/tasks/"+task.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("foreign etape assignment status = 200; want failure")
	}
	unchanged, err := q.GetTaskByID(ctx, sqlc.GetTaskByIDParams{ID: task.ID, TabloID: firstTablo.ID})
	if err != nil {
		t.Fatalf("GetTaskByID: %v", err)
	}
	if unchanged.EtapeID.Valid {
		t.Fatalf("foreign assignment updated task etape_id: %+v", unchanged.EtapeID)
	}
}

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

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

	user := preInsertUser(t, ctx, q, "tasksunassignedfilter@example.com", "correct-horse-12")
	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      user.ID,
		Title:       "Unassigned Filter",
		Description: pgtype.Text{Valid: false},
		Color:       pgtype.Text{Valid: false},
	})
	if err != nil {
		t.Fatalf("InsertTablo: %v", err)
	}
	etape, err := q.InsertEtape(ctx, sqlc.InsertEtapeParams{
		TabloID:     tablo.ID,
		Title:       "Design",
		Description: pgtype.Text{Valid: false},
		Position:    100,
	})
	if err != nil {
		t.Fatalf("InsertEtape: %v", err)
	}
	_, err = q.InsertTask(ctx, sqlc.InsertTaskParams{
		TabloID:     tablo.ID,
		Title:       "Assigned Hidden",
		Description: pgtype.Text{Valid: false},
		Status:      sqlc.TaskStatusTodo,
		Position:    100,
		EtapeID:     pgtype.UUID{Bytes: etape.ID, Valid: true},
	})
	if err != nil {
		t.Fatalf("InsertTask assigned: %v", err)
	}
	_, err = q.InsertTask(ctx, sqlc.InsertTaskParams{
		TabloID:     tablo.ID,
		Title:       "Unassigned Visible",
		Description: pgtype.Text{Valid: false},
		Status:      sqlc.TaskStatusDone,
		Position:    100,
	})
	if err != nil {
		t.Fatalf("InsertTask unassigned: %v", err)
	}

	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/"+tablo.ID.String()+"/tasks?etape=unassigned", 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 unassigned filter status = %d; want 200", rec.Code)
	}
	body := rec.Body.String()
	for _, col := range []string{"To do", "In progress", "In review", "Done"} {
		if !strings.Contains(body, col) {
			t.Errorf("unassigned filter missing column %q", col)
		}
	}
	if !strings.Contains(body, "Unassigned Visible") {
		t.Errorf("unassigned filter missing unassigned task; body: %.500s", body)
	}
	if strings.Contains(body, "Assigned Hidden") {
		t.Errorf("unassigned filter includes assigned task; body: %.500s", body)
	}
}

// ---- TestTaskReorderCrossColumn (TASK-04) ----

// TestTaskReorderCrossColumn verifies that POST /tablos/{id}/tasks/reorder
// changes the task's column (status) when dragged across columns (TASK-04).
func TestTaskReorderCrossColumn(t *testing.T) {

	pool, cleanup := setupTestDB(t)
	defer cleanup()

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

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

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

	task, err := q.InsertTask(ctx, sqlc.InsertTaskParams{
		TabloID:     tablo.ID,
		Title:       "Cross Column Task",
		Description: pgtype.Text{Valid: false},
		Status:      sqlc.TaskStatusTodo,
		Position:    100,
	})
	if err != nil {
		t.Fatalf("InsertTask: %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})

	// Move the task from "todo" to "in_progress".
	form := url.Values{
		"task_id": {task.ID.String()},
		"status":  {"in_progress"},
		"_csrf":   {csrfToken},
	}
	req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/tasks/reorder", 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}/tasks/reorder status = %d; want 200", rec.Code)
	}

	// Verify DB: task status should have changed to in_progress.
	updated, err := q.GetTaskByID(ctx, sqlc.GetTaskByIDParams{ID: task.ID, TabloID: tablo.ID})
	if err != nil {
		t.Fatalf("GetTaskByID after reorder: %v", err)
	}
	if updated.Status != sqlc.TaskStatusInProgress {
		t.Errorf("task status = %q; want 'in_progress'", updated.Status)
	}
}

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

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

	user := preInsertUser(t, ctx, q, "reorderpreserveetape@example.com", "correct-horse-12")
	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      user.ID,
		Title:       "Reorder Preserve Etape",
		Description: pgtype.Text{Valid: false},
		Color:       pgtype.Text{Valid: false},
	})
	if err != nil {
		t.Fatalf("InsertTablo: %v", err)
	}
	etape, err := q.InsertEtape(ctx, sqlc.InsertEtapeParams{
		TabloID:     tablo.ID,
		Title:       "Design",
		Description: pgtype.Text{Valid: false},
		Position:    100,
	})
	if err != nil {
		t.Fatalf("InsertEtape: %v", err)
	}
	task, err := q.InsertTask(ctx, sqlc.InsertTaskParams{
		TabloID:     tablo.ID,
		Title:       "Move Me",
		Description: pgtype.Text{Valid: false},
		Status:      sqlc.TaskStatusTodo,
		Position:    100,
		EtapeID:     pgtype.UUID{Bytes: etape.ID, Valid: true},
	})
	if err != nil {
		t.Fatalf("InsertTask: %v", err)
	}

	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, "/tablos/"+tablo.ID.String(), []*http.Cookie{sessionCookie})

	form := url.Values{
		"task_id":  {task.ID.String()},
		"status":   {"done"},
		"position": {"100"},
		"_csrf":    {csrfToken},
	}
	req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/tasks/reorder", 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 reorder status = %d; want 200", rec.Code)
	}
	updated, err := q.GetTaskByID(ctx, sqlc.GetTaskByIDParams{ID: task.ID, TabloID: tablo.ID})
	if err != nil {
		t.Fatalf("GetTaskByID: %v", err)
	}
	if !updated.EtapeID.Valid || updated.EtapeID.Bytes != etape.ID {
		t.Fatalf("etape assignment changed after reorder: %+v", updated.EtapeID)
	}
}

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

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

	user := preInsertUser(t, ctx, q, "filteredreorderhidden@example.com", "correct-horse-12")
	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      user.ID,
		Title:       "Filtered Reorder",
		Description: pgtype.Text{Valid: false},
		Color:       pgtype.Text{Valid: false},
	})
	if err != nil {
		t.Fatalf("InsertTablo: %v", err)
	}
	firstEtape, err := q.InsertEtape(ctx, sqlc.InsertEtapeParams{
		TabloID:     tablo.ID,
		Title:       "Design",
		Description: pgtype.Text{Valid: false},
		Position:    100,
	})
	if err != nil {
		t.Fatalf("InsertEtape first: %v", err)
	}
	secondEtape, err := q.InsertEtape(ctx, sqlc.InsertEtapeParams{
		TabloID:     tablo.ID,
		Title:       "Build",
		Description: pgtype.Text{Valid: false},
		Position:    200,
	})
	if err != nil {
		t.Fatalf("InsertEtape second: %v", err)
	}
	visible, err := q.InsertTask(ctx, sqlc.InsertTaskParams{
		TabloID:     tablo.ID,
		Title:       "Visible",
		Description: pgtype.Text{Valid: false},
		Status:      sqlc.TaskStatusTodo,
		Position:    100,
		EtapeID:     pgtype.UUID{Bytes: firstEtape.ID, Valid: true},
	})
	if err != nil {
		t.Fatalf("InsertTask visible: %v", err)
	}
	hidden, err := q.InsertTask(ctx, sqlc.InsertTaskParams{
		TabloID:     tablo.ID,
		Title:       "Hidden",
		Description: pgtype.Text{Valid: false},
		Status:      sqlc.TaskStatusTodo,
		Position:    200,
		EtapeID:     pgtype.UUID{Bytes: secondEtape.ID, Valid: true},
	})
	if err != nil {
		t.Fatalf("InsertTask hidden: %v", err)
	}

	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, "/tablos/"+tablo.ID.String()+"/tasks?etape="+firstEtape.ID.String(), []*http.Cookie{sessionCookie})

	form := url.Values{
		"task_id":  {visible.ID.String()},
		"status":   {"in_progress"},
		"position": {"100"},
		"_csrf":    {csrfToken},
	}
	req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/tasks/reorder?etape="+firstEtape.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 filtered reorder status = %d; want 200", rec.Code)
	}
	hiddenAfter, err := q.GetTaskByID(ctx, sqlc.GetTaskByIDParams{ID: hidden.ID, TabloID: tablo.ID})
	if err != nil {
		t.Fatalf("GetTaskByID hidden: %v", err)
	}
	if !hiddenAfter.EtapeID.Valid || hiddenAfter.EtapeID.Bytes != secondEtape.ID {
		t.Fatalf("hidden task etape changed: %+v", hiddenAfter.EtapeID)
	}
}

// ---- TestTaskReorderSameColumn (TASK-05) ----

// TestTaskReorderSameColumn verifies that POST /tablos/{id}/tasks/reorder
// changes the position of a task within the same column (TASK-05).
func TestTaskReorderSameColumn(t *testing.T) {

	pool, cleanup := setupTestDB(t)
	defer cleanup()

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

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

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

	task1, err := q.InsertTask(ctx, sqlc.InsertTaskParams{
		TabloID:     tablo.ID,
		Title:       "Task Position 100",
		Description: pgtype.Text{Valid: false},
		Status:      sqlc.TaskStatusTodo,
		Position:    100,
	})
	if err != nil {
		t.Fatalf("InsertTask task1: %v", err)
	}

	task2, err := q.InsertTask(ctx, sqlc.InsertTaskParams{
		TabloID:     tablo.ID,
		Title:       "Task Position 200",
		Description: pgtype.Text{Valid: false},
		Status:      sqlc.TaskStatusTodo,
		Position:    200,
	})
	if err != nil {
		t.Fatalf("InsertTask task2: %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})

	// Move task1 after task2 within the same column.
	form := url.Values{
		"task_id":  {task1.ID.String()},
		"status":   {"todo"},
		"position": {"300"},
		"_csrf":    {csrfToken},
	}
	_ = task2 // referenced for test context
	req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/tasks/reorder", 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}/tasks/reorder status = %d; want 200", rec.Code)
	}

	// Verify: task1 position updated.
	updated, err := q.GetTaskByID(ctx, sqlc.GetTaskByIDParams{ID: task1.ID, TabloID: tablo.ID})
	if err != nil {
		t.Fatalf("GetTaskByID after same-column reorder: %v", err)
	}
	if updated.Position == 100 {
		t.Error("task position did not change after same-column reorder")
	}
}

// ---- TestTaskDelete (TASK-06) ----

// TestTaskDelete verifies that POST /tablos/{id}/tasks/{task_id}/delete removes
// the task and returns an empty div (TASK-06).
func TestTaskDelete(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

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

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

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

	task, err := q.InsertTask(ctx, sqlc.InsertTaskParams{
		TabloID:     tablo.ID,
		Title:       "Task To Delete",
		Description: pgtype.Text{Valid: false},
		Status:      sqlc.TaskStatusTodo,
		Position:    100,
	})
	if err != nil {
		t.Fatalf("InsertTask: %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{"_csrf": {csrfToken}}
	req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/tasks/"+task.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("POST /tablos/{id}/tasks/{task_id}/delete status = %d; want 200", rec.Code)
	}

	// DB row must be gone.
	_, err = q.GetTaskByID(ctx, sqlc.GetTaskByIDParams{ID: task.ID, TabloID: tablo.ID})
	if err == nil {
		t.Error("task row still exists in DB after delete")
	}
}

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

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

	user := preInsertUser(t, ctx, q, "taskdeletecount@example.com", "correct-horse-12")
	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      user.ID,
		Title:       "Task Delete Count Tablo",
		Description: pgtype.Text{Valid: false},
		Color:       pgtype.Text{Valid: false},
	})
	if err != nil {
		t.Fatalf("InsertTablo: %v", err)
	}
	etape, err := q.InsertEtape(ctx, sqlc.InsertEtapeParams{
		TabloID:     tablo.ID,
		Title:       "Design",
		Description: pgtype.Text{Valid: false},
		Position:    100,
	})
	if err != nil {
		t.Fatalf("InsertEtape: %v", err)
	}
	task, err := q.InsertTask(ctx, sqlc.InsertTaskParams{
		TabloID:     tablo.ID,
		Title:       "Delete Count",
		Description: pgtype.Text{Valid: false},
		Status:      sqlc.TaskStatusTodo,
		Position:    100,
		EtapeID:     pgtype.UUID{Bytes: etape.ID, Valid: true},
	})
	if err != nil {
		t.Fatalf("InsertTask: %v", err)
	}

	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, "/tablos/"+tablo.ID.String()+"/tasks?etape="+etape.ID.String(), []*http.Cookie{sessionCookie})

	form := url.Values{"_csrf": {csrfToken}}
	req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/tasks/"+task.ID.String()+"/delete?etape="+etape.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 task delete status = %d; want 200", rec.Code)
	}
	body := rec.Body.String()
	if !strings.Contains(body, `id="etape-strip"`) || !strings.Contains(body, `hx-swap-oob="outerHTML"`) {
		t.Fatalf("task delete response did not include OOB etape strip refresh; body: %.800s", body)
	}
	if !strings.Contains(body, "Design") || !strings.Contains(body, ">0<") {
		t.Fatalf("task delete response missing updated etape count; body: %.800s", body)
	}
}

// ---- TestTaskOrderPersists (TASK-07) ----

// TestTaskOrderPersists verifies that after a reorder, a subsequent GET
// /tablos/{id} renders tasks in the new position order (TASK-07).
func TestTaskOrderPersists(t *testing.T) {

	pool, cleanup := setupTestDB(t)
	defer cleanup()

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

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

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

	task1, err := q.InsertTask(ctx, sqlc.InsertTaskParams{
		TabloID:     tablo.ID,
		Title:       "Task First",
		Description: pgtype.Text{Valid: false},
		Status:      sqlc.TaskStatusTodo,
		Position:    100,
	})
	if err != nil {
		t.Fatalf("InsertTask task1: %v", err)
	}
	task2, err := q.InsertTask(ctx, sqlc.InsertTaskParams{
		TabloID:     tablo.ID,
		Title:       "Task Second",
		Description: pgtype.Text{Valid: false},
		Status:      sqlc.TaskStatusTodo,
		Position:    200,
	})
	if err != nil {
		t.Fatalf("InsertTask task2: %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})

	// Reorder: move task2 to position 50 (before task1 at 100).
	form := url.Values{
		"task_id":  {task2.ID.String()},
		"status":   {"todo"},
		"position": {"50"},
		"_csrf":    {csrfToken},
	}
	reorderReq := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/tasks/reorder", strings.NewReader(form.Encode()))
	reorderReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	reorderReq.Header.Set("HX-Request", "true")
	for _, c := range csrfCookies {
		reorderReq.AddCookie(c)
	}
	reorderRec := httptest.NewRecorder()
	router.ServeHTTP(reorderRec, reorderReq)

	if reorderRec.Code != http.StatusOK {
		t.Fatalf("reorder POST status = %d; want 200", reorderRec.Code)
	}

	// Now fetch the tablo page and verify order.
	getReq := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String(), nil)
	getReq.AddCookie(sessionCookie)
	getRec := httptest.NewRecorder()
	router.ServeHTTP(getRec, getReq)

	if getRec.Code != http.StatusOK {
		t.Fatalf("GET /tablos/{id} status = %d; want 200", getRec.Code)
	}
	body := getRec.Body.String()

	// task2 ("Task Second") should appear before task1 ("Task First") after reorder.
	idx1 := strings.Index(body, task1.Title)
	idx2 := strings.Index(body, task2.Title)
	if idx1 != -1 && idx2 != -1 && idx2 > idx1 {
		t.Errorf("task order not persisted: task2 index=%d, task1 index=%d (task2 should be first)", idx2, idx1)
	}
}

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

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

	user := preInsertUser(t, ctx, q, "orderfilteretape@example.com", "correct-horse-12")
	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      user.ID,
		Title:       "Order Filter",
		Description: pgtype.Text{Valid: false},
		Color:       pgtype.Text{Valid: false},
	})
	if err != nil {
		t.Fatalf("InsertTablo: %v", err)
	}
	etape, err := q.InsertEtape(ctx, sqlc.InsertEtapeParams{
		TabloID:     tablo.ID,
		Title:       "Design",
		Description: pgtype.Text{Valid: false},
		Position:    100,
	})
	if err != nil {
		t.Fatalf("InsertEtape: %v", err)
	}
	first, err := q.InsertTask(ctx, sqlc.InsertTaskParams{
		TabloID:     tablo.ID,
		Title:       "First Filtered",
		Description: pgtype.Text{Valid: false},
		Status:      sqlc.TaskStatusTodo,
		Position:    100,
		EtapeID:     pgtype.UUID{Bytes: etape.ID, Valid: true},
	})
	if err != nil {
		t.Fatalf("InsertTask first: %v", err)
	}
	second, err := q.InsertTask(ctx, sqlc.InsertTaskParams{
		TabloID:     tablo.ID,
		Title:       "Second Filtered",
		Description: pgtype.Text{Valid: false},
		Status:      sqlc.TaskStatusTodo,
		Position:    200,
		EtapeID:     pgtype.UUID{Bytes: etape.ID, Valid: true},
	})
	if err != nil {
		t.Fatalf("InsertTask second: %v", err)
	}

	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, "/tablos/"+tablo.ID.String()+"/tasks?etape="+etape.ID.String(), []*http.Cookie{sessionCookie})

	form := url.Values{
		"task_id":  {second.ID.String()},
		"status":   {"todo"},
		"position": {"50"},
		"_csrf":    {csrfToken},
	}
	req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/tasks/reorder?etape="+etape.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("reorder status = %d; want 200", rec.Code)
	}

	getReq := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String()+"/tasks?etape="+etape.ID.String(), nil)
	getReq.Header.Set("HX-Request", "true")
	getReq.AddCookie(sessionCookie)
	getRec := httptest.NewRecorder()
	router.ServeHTTP(getRec, getReq)
	if getRec.Code != http.StatusOK {
		t.Fatalf("filtered GET status = %d; want 200", getRec.Code)
	}
	body := getRec.Body.String()
	idxFirst := strings.Index(body, first.Title)
	idxSecond := strings.Index(body, second.Title)
	if idxFirst == -1 || idxSecond == -1 {
		t.Fatalf("filtered body missing task titles; body: %.500s", body)
	}
	if idxSecond > idxFirst {
		t.Fatalf("filtered order did not persist: second index=%d first index=%d", idxSecond, idxFirst)
	}
}

// ---- TestTaskOwnership (T-04-IDOR) ----

// TestTaskOwnership verifies that GET and POST task routes accessed by a
// non-owner return 404 (no information leakage, D-04) (T-04-IDOR).
func TestTaskOwnership(t *testing.T) {
	pool, cleanup := setupTestDB(t)
	defer cleanup()

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

	// User A creates a tablo + task.
	userA := preInsertUser(t, ctx, q, "taskiodra@example.com", "correct-horse-12")
	tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
		UserID:      userA.ID,
		Title:       "UserA Secret Tablo",
		Description: pgtype.Text{Valid: false},
		Color:       pgtype.Text{Valid: false},
	})
	if err != nil {
		t.Fatalf("InsertTablo: %v", err)
	}
	task, err := q.InsertTask(ctx, sqlc.InsertTaskParams{
		TabloID:     tablo.ID,
		Title:       "UserA Secret Task",
		Description: pgtype.Text{Valid: false},
		Status:      sqlc.TaskStatusTodo,
		Position:    100,
	})
	if err != nil {
		t.Fatalf("InsertTask: %v", err)
	}

	// User B tries to access userA's task routes.
	userB := preInsertUser(t, ctx, q, "taskiodorb@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}
	csrfToken, csrfCookies := getCSRFToken(t, router, "/", []*http.Cookie{sessionCookie})

	routes := []struct {
		method string
		path   string
	}{
		{http.MethodPost, "/tablos/" + tablo.ID.String() + "/tasks/" + task.ID.String()},
		{http.MethodPost, "/tablos/" + tablo.ID.String() + "/tasks/" + task.ID.String() + "/delete"},
	}

	for _, route := range routes {
		form := url.Values{"_csrf": {csrfToken}}
		req := httptest.NewRequest(route.method, route.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)

		// D-04: 404 (not 403) — no information leakage.
		if rec.Code != http.StatusNotFound {
			t.Errorf("%s %s as non-owner: status = %d; want 404", route.method, route.path, rec.Code)
		}
	}
}
