package handlers

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

	"github.com/google/uuid"
	tablomodel "xtablo-backend/internal/tablos"
	taskmodel "xtablo-backend/internal/tasks"
	"xtablo-backend/internal/web/views"
)

func TestGetTasksPageRendersEtapesAndSansEtapeSections(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 authenticated user")
	}

	tablo := mustCreateOwnedTablo(t, repo, userID)
	etape := mustCreateEtape(t, repo, userID, tablo.ID, "Production")
	_ = mustCreateTask(t, repo, userID, tablo.ID, &etape.ID, "Cut footage")
	_ = mustCreateTask(t, repo, userID, tablo.ID, nil, "Inbox task")

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

	handler.GetTasksPage().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{"Mes Tâches", "Production", "Sans étape", "Inbox task", "Cut footage"} {
		if !strings.Contains(body, want) {
			t.Fatalf("expected body to contain %q, got %q", want, body)
		}
	}
}

func TestGetTasksPageDefaultsToKanbanView(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 authenticated user")
	}

	tablo := mustCreateOwnedTablo(t, repo, userID)
	_ = mustCreateTask(t, repo, userID, tablo.ID, nil, "Shell task")

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

	handler.GetTasksPage().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{
		`data-current-view="kanban"`,
		`Nouvelle tâche`,
		`Tableau`,
		`Liste`,
		`Roadmap`,
		`Calendrier`,
		`Bientôt`,
		`grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4`,
		`draggable="true"`,
		`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"`,
		`/tasks/`,
		`Supprimer la tâche Shell task`,
	} {
		if !strings.Contains(body, want) {
			t.Fatalf("expected body to contain %q, got %q", want, body)
		}
	}
}

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

	req := httptest.NewRequest(http.MethodGet, "/tasks?view=nope&roadmap_mode=nope&status=bad", nil)
	req.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()

	handler.GetTasksPage().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, `data-current-view="kanban"`) {
		t.Fatalf("expected normalized kanban view, got %q", body)
	}
	if strings.Contains(body, `value="bad" selected`) {
		t.Fatalf("expected invalid status filter to be dropped, got %q", body)
	}
}

func TestTasksListGroupsByStatusAcrossTablos(t *testing.T) {
	now := time.Date(2026, 5, 10, 12, 0, 0, 0, time.UTC)
	tabloA := tablomodel.Record{ID: uuid.New(), Name: "Alpha", Color: "#111111"}
	tabloB := tablomodel.Record{ID: uuid.New(), Name: "Beta", Color: "#222222"}

	records := []taskmodel.Record{
		{ID: uuid.New(), TabloID: tabloA.ID, Title: "Write copy", Status: taskmodel.StatusTodo, CreatedAt: now},
		{ID: uuid.New(), TabloID: tabloB.ID, Title: "Ship docs", Status: taskmodel.StatusTodo, CreatedAt: now.Add(time.Minute)},
		{ID: uuid.New(), TabloID: tabloB.ID, Title: "Review launch", Status: taskmodel.StatusInReview, CreatedAt: now.Add(2 * time.Minute)},
	}

	vm := views.NewTasksPageViewModel(
		[]tablomodel.Record{tabloA, tabloB},
		records,
		nil,
		taskmodel.TaskPageState{View: taskmodel.TaskViewList, RoadmapMode: taskmodel.TaskRoadmapModeWeek},
		now,
	)

	if len(vm.List.Groups) != 4 {
		t.Fatalf("expected four status groups, got %d", len(vm.List.Groups))
	}
	if got := len(vm.List.Groups[0].Tasks); got != 2 {
		t.Fatalf("expected two todo tasks across tablos, got %d", got)
	}
}

func TestTasksRoadmapCreatesPerTabloSansEtapeLanes(t *testing.T) {
	now := time.Date(2026, 5, 10, 12, 0, 0, 0, time.UTC)
	tabloA := tablomodel.Record{ID: uuid.New(), Name: "Alpha"}
	tabloB := tablomodel.Record{ID: uuid.New(), Name: "Beta"}
	due := now.AddDate(0, 0, 3)

	records := []taskmodel.Record{
		{ID: uuid.New(), TabloID: tabloA.ID, Title: "A task", Status: taskmodel.StatusTodo, DueDate: &due, CreatedAt: now},
		{ID: uuid.New(), TabloID: tabloB.ID, Title: "B task", Status: taskmodel.StatusTodo, DueDate: &due, CreatedAt: now},
	}

	vm := views.NewTasksPageViewModel(
		[]tablomodel.Record{tabloA, tabloB},
		records,
		nil,
		taskmodel.TaskPageState{View: taskmodel.TaskViewRoadmap, RoadmapMode: taskmodel.TaskRoadmapModeWeek},
		now,
	)

	if len(vm.Roadmap.Lanes) != 2 {
		t.Fatalf("expected one Sans étape lane per tablo, got %d", len(vm.Roadmap.Lanes))
	}
}

func TestPostTasksCreatesEtape(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 authenticated user")
	}
	tablo := mustCreateOwnedTablo(t, repo, userID)

	form := url.Values{}
	form.Set("tablo_id", tablo.ID.String())
	form.Set("title", "Launch")
	form.Set("status", string(taskmodel.StatusTodo))
	form.Set("is_etape", "true")

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

	handler.PostTasks().ServeHTTP(rec, postReq)

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

	tasks, err := repo.ListTasksByOwner(context.Background(), userID)
	if err != nil {
		t.Fatalf("list tasks: %v", err)
	}

	var created *TaskRecord
	for i := range tasks {
		if tasks[i].Title == "Launch" {
			created = &tasks[i]
			break
		}
	}
	if created == nil {
		t.Fatalf("expected created etape to be persisted")
	}
	if !created.IsEtape {
		t.Fatalf("expected created record to be an etape, got %#v", created)
	}
	if created.ParentTaskID != nil {
		t.Fatalf("expected created etape to have no parent, got %#v", created.ParentTaskID)
	}
}

func TestPostTasksRejectsNonEtapeParent(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 authenticated user")
	}
	tablo := mustCreateOwnedTablo(t, repo, userID)
	parentTask := mustCreateTask(t, repo, userID, tablo.ID, nil, "Regular task")

	form := url.Values{}
	form.Set("tablo_id", tablo.ID.String())
	form.Set("title", "Should fail")
	form.Set("status", string(taskmodel.StatusTodo))
	form.Set("parent_task_id", parentTask.ID.String())

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

	handler.PostTasks().ServeHTTP(rec, postReq)

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

func TestGetEditTaskModalRendersCurrentValues(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 authenticated user")
	}

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

	tablo := mustCreateOwnedTablo(t, repo, userID)
	task := mustCreateTaskWithAssignee(t, repo, userID, tablo.ID, nil, "Editable", &assigneeID)

	editReq := httptest.NewRequest(http.MethodGet, "/tasks/"+task.ID.String()+"/edit", nil)
	editReq.SetPathValue("taskID", task.ID.String())
	editReq.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()

	handler.GetEditTaskModal().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{"Editable", string(task.Status), assigneeID.String()} {
		if !strings.Contains(body, want) {
			t.Fatalf("expected edit modal to contain %q, got %q", want, body)
		}
	}
}

func TestPatchTaskUpdatesEditableFields(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 authenticated user")
	}

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

	tablo := mustCreateOwnedTablo(t, repo, userID)
	etape := mustCreateEtape(t, repo, userID, tablo.ID, "Editing")
	task := mustCreateTask(t, repo, userID, tablo.ID, nil, "Old title")

	form := url.Values{}
	form.Set("tablo_id", tablo.ID.String())
	form.Set("title", "New title")
	form.Set("description", "New description")
	form.Set("status", string(taskmodel.StatusInReview))
	form.Set("due_date", "2026-05-20")
	form.Set("assignee_id", assigneeID.String())
	form.Set("parent_task_id", etape.ID.String())

	patchReq := httptest.NewRequest(http.MethodPatch, "/tasks/"+task.ID.String(), strings.NewReader(form.Encode()))
	patchReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	patchReq.SetPathValue("taskID", task.ID.String())
	patchReq.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()

	handler.PatchTask().ServeHTTP(rec, patchReq)

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

	updated, err := repo.GetTaskByID(context.Background(), task.ID, userID)
	if err != nil {
		t.Fatalf("get updated task: %v", err)
	}
	if updated.Title != "New title" || updated.Description != "New description" {
		t.Fatalf("expected title/description to update, got %#v", updated)
	}
	if updated.Status != taskmodel.StatusInReview {
		t.Fatalf("expected status to update, got %q", updated.Status)
	}
	if updated.AssigneeID == nil || *updated.AssigneeID != assigneeID {
		t.Fatalf("expected assignee to update, got %#v", updated.AssigneeID)
	}
	if updated.ParentTaskID == nil || *updated.ParentTaskID != etape.ID {
		t.Fatalf("expected parent etape to update, got %#v", updated.ParentTaskID)
	}
	if updated.DueDate == nil || updated.DueDate.Format("2006-01-02") != "2026-05-20" {
		t.Fatalf("expected due date to update, got %#v", updated.DueDate)
	}
}

func TestPatchTaskRejectsParentEtapeFromAnotherTablo(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 authenticated user")
	}

	tabloA := mustCreateOwnedTablo(t, repo, userID)
	tabloB := mustCreateOwnedTablo(t, repo, userID)
	task := mustCreateTask(t, repo, userID, tabloA.ID, nil, "Alpha task")
	foreignEtape := mustCreateEtape(t, repo, userID, tabloB.ID, "Beta stage")

	form := url.Values{}
	form.Set("tablo_id", tabloA.ID.String())
	form.Set("title", task.Title)
	form.Set("status", string(taskmodel.StatusTodo))
	form.Set("parent_task_id", foreignEtape.ID.String())

	patchReq := httptest.NewRequest(http.MethodPatch, "/tasks/"+task.ID.String(), strings.NewReader(form.Encode()))
	patchReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	patchReq.SetPathValue("taskID", task.ID.String())
	patchReq.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()

	handler.PatchTask().ServeHTTP(rec, patchReq)

	if rec.Code != http.StatusUnprocessableEntity {
		t.Fatalf("expected 422 for cross-tablo parent, got %d", rec.Code)
	}
}

func TestGetEditTaskModalShowsOnlyEtapesFromSelectedTablo(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 authenticated user")
	}

	tabloA := mustCreateOwnedTablo(t, repo, userID)
	tabloB := mustCreateOwnedTablo(t, repo, userID)
	task := mustCreateTask(t, repo, userID, tabloA.ID, nil, "Editable")
	etapeA := mustCreateEtape(t, repo, userID, tabloA.ID, "Stage A")
	_ = mustCreateEtape(t, repo, userID, tabloB.ID, "Stage B")

	editReq := httptest.NewRequest(http.MethodGet, "/tasks/"+task.ID.String()+"/edit", nil)
	editReq.SetPathValue("taskID", task.ID.String())
	editReq.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()

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

	body := rec.Body.String()
	if !strings.Contains(body, etapeA.ID.String()) {
		t.Fatalf("expected same-tablo etape in edit form, got %q", body)
	}
	if strings.Contains(body, "Stage B") {
		t.Fatalf("did not expect foreign tablo etape in edit form, got %q", body)
	}
}

func TestDeleteTaskSoftDeletesRegularTask(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 authenticated user")
	}

	tablo := mustCreateOwnedTablo(t, repo, userID)
	task := mustCreateTask(t, repo, userID, tablo.ID, nil, "Delete me")

	deleteReq := httptest.NewRequest(http.MethodDelete, "/tasks/"+task.ID.String(), nil)
	deleteReq.SetPathValue("taskID", task.ID.String())
	deleteReq.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()

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

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

	if _, err := repo.GetTaskByID(context.Background(), task.ID, userID); err == nil {
		t.Fatal("expected deleted task to become unavailable")
	}
}

func TestGetTasksPageRendersListViewAcrossTablos(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 authenticated user")
	}

	tabloA := mustCreateOwnedTablo(t, repo, userID)
	tabloB := mustCreateOwnedTablo(t, repo, userID)
	_ = mustCreateTask(t, repo, userID, tabloA.ID, nil, "Alpha task")
	_ = mustCreateTask(t, repo, userID, tabloB.ID, nil, "Beta task")

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

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

	body := rec.Body.String()
	for _, want := range []string{
		`data-current-view="list"`,
		`Alpha task`,
		`Beta task`,
		`Liste`,
		`À faire`,
		`data-task-list`,
		`data-status-group="todo"`,
		`Tablo`,
		`Assignée`,
	} {
		if !strings.Contains(body, want) {
			t.Fatalf("expected body to contain %q, got %q", want, body)
		}
	}
}

func TestGetTasksPageRendersRoadmapWeekMode(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 authenticated user")
	}

	tablo := mustCreateOwnedTablo(t, repo, userID)
	etape := mustCreateEtape(t, repo, userID, tablo.ID, "Préparation")
	task := mustCreateTask(t, repo, userID, tablo.ID, &etape.ID, "Planifier")
	due := time.Date(2026, 5, 12, 0, 0, 0, 0, time.UTC)
	_, err := repo.UpdateTask(context.Background(), UpdateTaskInput{
		ID:           task.ID,
		OwnerID:      userID,
		TabloID:      tablo.ID,
		Title:        task.Title,
		Description:  task.Description,
		Status:       task.Status,
		DueDate:      &due,
		ParentTaskID: &etape.ID,
	})
	if err != nil {
		t.Fatalf("update task due date: %v", err)
	}

	pageReq := httptest.NewRequest(http.MethodGet, "/tasks?view=roadmap&roadmap_mode=week", nil)
	pageReq.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()

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

	body := rec.Body.String()
	for _, want := range []string{
		`data-current-view="roadmap"`,
		`Roadmap`,
		`Préparation`,
		`Planifier`,
		`Semaine`,
		`Mois`,
		`data-task-roadmap`,
		`data-roadmap-lane=`,
		`data-roadmap-bucket=`,
	} {
		if !strings.Contains(body, want) {
			t.Fatalf("expected body to contain %q, got %q", want, body)
		}
	}
}

func TestPatchTaskRendersCurrentTaskQueryState(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 authenticated user")
	}

	tablo := mustCreateOwnedTablo(t, repo, userID)
	task := mustCreateTask(t, repo, userID, tablo.ID, nil, "Stateful")

	form := url.Values{}
	form.Set("tablo_id", tablo.ID.String())
	form.Set("title", "Stateful")
	form.Set("status", string(taskmodel.StatusDone))

	patchReq := httptest.NewRequest(http.MethodPatch, "/tasks/"+task.ID.String()+"?view=list&status=done", strings.NewReader(form.Encode()))
	patchReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	patchReq.SetPathValue("taskID", task.ID.String())
	patchReq.AddCookie(sessionCookie)
	rec := httptest.NewRecorder()

	handler.PatchTask().ServeHTTP(rec, patchReq)

	body := rec.Body.String()
	if !strings.Contains(body, `data-current-view="list"`) {
		t.Fatalf("expected response to preserve current list view, got %q", body)
	}
}

func TestInMemoryTasksListExcludesSoftDeletedRows(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)
	}

	tablo := mustCreateOwnedTablo(t, repo, user.ID)
	etape := mustCreateEtape(t, repo, user.ID, tablo.ID, "Etape 1")
	task := mustCreateTask(t, repo, user.ID, tablo.ID, &etape.ID, "Task 1")

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

	records, err := repo.ListTasksByTablo(context.Background(), ListTasksByTabloInput{
		OwnerID: user.ID,
		TabloID: tablo.ID,
	})
	if err != nil {
		t.Fatalf("list tasks: %v", err)
	}

	if len(records) != 1 || records[0].ID != etape.ID {
		t.Fatalf("expected only etape to remain visible, got %#v", records)
	}
}

func TestInMemoryDeleteEtapeClearsChildParentID(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)
	}

	tablo := mustCreateOwnedTablo(t, repo, user.ID)
	etape := mustCreateEtape(t, repo, user.ID, tablo.ID, "Launch")
	child := mustCreateTask(t, repo, user.ID, tablo.ID, &etape.ID, "Ship copy")

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

	updated, err := repo.GetTaskByID(context.Background(), child.ID, user.ID)
	if err != nil {
		t.Fatalf("get child task: %v", err)
	}
	if updated.ParentTaskID != nil {
		t.Fatalf("expected child task to move to Sans etape, got parent %v", *updated.ParentTaskID)
	}
}

func TestInMemoryEtapeCannotHaveParent(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)
	}

	tablo := mustCreateOwnedTablo(t, repo, user.ID)
	parent := mustCreateEtape(t, repo, user.ID, tablo.ID, "Parent")

	_, err = repo.CreateTask(context.Background(), CreateTaskInput{
		OwnerID:      user.ID,
		TabloID:      tablo.ID,
		Title:        "Invalid child etape",
		IsEtape:      true,
		Status:       taskmodel.StatusTodo,
		ParentTaskID: &parent.ID,
	})
	if err == nil {
		t.Fatal("expected etape with parent to fail")
	}
}

func TestInMemoryTaskParentMustBeEtape(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)
	}

	tablo := mustCreateOwnedTablo(t, repo, user.ID)
	taskParent := mustCreateTask(t, repo, user.ID, tablo.ID, nil, "Not an etape")

	_, err = repo.CreateTask(context.Background(), CreateTaskInput{
		OwnerID:      user.ID,
		TabloID:      tablo.ID,
		Title:        "Invalid child task",
		Status:       taskmodel.StatusTodo,
		ParentTaskID: &taskParent.ID,
	})
	if err == nil {
		t.Fatal("expected task with non-etape parent to fail")
	}
}

func TestInMemoryTaskAssigneePersistsAndCanBeCleared(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)
	}

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

	tablo := mustCreateOwnedTablo(t, repo, user.ID)
	task := mustCreateTaskWithAssignee(t, repo, user.ID, tablo.ID, nil, "Assigned", &assigneeID)

	if task.AssigneeID == nil || *task.AssigneeID != assigneeID {
		t.Fatalf("expected assignee to persist, got %#v", task.AssigneeID)
	}

	updated, err := repo.UpdateTask(context.Background(), UpdateTaskInput{
		ID:           task.ID,
		OwnerID:      user.ID,
		Title:        task.Title,
		Description:  task.Description,
		Status:       task.Status,
		AssigneeID:   nil,
		DueDate:      task.DueDate,
		ParentTaskID: task.ParentTaskID,
	})
	if err != nil {
		t.Fatalf("clear assignee: %v", err)
	}

	if updated.AssigneeID != nil {
		t.Fatalf("expected assignee to clear, got %#v", updated.AssigneeID)
	}
}

func mustCreateOwnedTablo(t *testing.T, repo *InMemoryAuthRepository, ownerID uuid.UUID) TabloRecord {
	t.Helper()

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

func mustCreateEtape(t *testing.T, repo *InMemoryAuthRepository, ownerID uuid.UUID, tabloID uuid.UUID, title string) TaskRecord {
	t.Helper()

	record, err := repo.CreateTask(context.Background(), CreateTaskInput{
		OwnerID: ownerID,
		TabloID: tabloID,
		Title:   title,
		IsEtape: true,
		Status:  taskmodel.StatusTodo,
	})
	if err != nil {
		t.Fatalf("create etape: %v", err)
	}
	return record
}

func mustCreateTask(t *testing.T, repo *InMemoryAuthRepository, ownerID uuid.UUID, tabloID uuid.UUID, parentTaskID *uuid.UUID, title string) TaskRecord {
	t.Helper()

	return mustCreateTaskWithAssignee(t, repo, ownerID, tabloID, parentTaskID, title, nil)
}

func mustCreateTaskWithAssignee(t *testing.T, repo *InMemoryAuthRepository, ownerID uuid.UUID, tabloID uuid.UUID, parentTaskID *uuid.UUID, title string, assigneeID *uuid.UUID) TaskRecord {
	t.Helper()

	record, err := repo.CreateTask(context.Background(), CreateTaskInput{
		OwnerID:      ownerID,
		TabloID:      tabloID,
		Title:        title,
		Status:       taskmodel.StatusTodo,
		ParentTaskID: parentTaskID,
		AssigneeID:   assigneeID,
	})
	if err != nil {
		t.Fatalf("create task: %v", err)
	}
	return record
}
