package taskboard

import (
	"context"
	"fmt"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/testfixture"
)

type taskboardUserFixture struct {
	pool                coredb.DB
	service             *Service
	workspaceID         string
	channelID           string
	ownerUserID         string
	ownerDisplayName    string
	ownerAvatarURL      string
	ownerMemberID       string
	reviewerUserID      string
	reviewerDisplayName string
	reviewerAvatarURL   string
	reviewerMemberID    string
	agentID             string
	agentDisplayName    string
	agentAvatarURL      string
	agentMemberID       string
	machineID           string
}

func TestTaskDetailProjectionIncludesExecutionMetadata(t *testing.T) {
	ctx := context.Background()
	fixture := seedTaskboardUserFixture(t, ctx)

	parentResponse, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: NewUserServiceContext(fixture.workspaceID, fixture.ownerUserID, fixture.ownerDisplayName),
		Board:   BoardRef{ChannelID: fixture.channelID},
		Titles:  []string{"Parent rollout task"},
	})
	require.NoError(t, err)
	require.Len(t, parentResponse.Tasks, 1)

	createdResponse, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: NewUserServiceContext(fixture.workspaceID, fixture.ownerUserID, fixture.ownerDisplayName),
		Board:   BoardRef{ChannelID: fixture.channelID},
		Titles:  []string{"Ship task detail projection"},
	})
	require.NoError(t, err)
	require.Len(t, createdResponse.Tasks, 1)

	taskID := createdResponse.Tasks[0].ID
	assigneeRef := fixture.reviewerMemberID
	description := "Document the rollout execution path and attach the proof."
	kind := "feature"
	priority := "p1"
	blockedReason := "Waiting on review-target plumbing"
	parentTaskRef := parentResponse.Tasks[0].ID
	pinnedMessageRefs := []string{"#all:msg-1", "#all:msg-2"}
	relevantURLs := []string{"https://example.com/spec", "https://example.com/runbook"}
	workspaceRefs := []string{"specs/task/board.md", "web/src/agentteam/workspace/TaskDetailPanel.tsx"}

	_, err = fixture.service.UpdateTask(ctx, UpdateTaskRequest{
		Context: NewUserServiceContext(fixture.workspaceID, fixture.ownerUserID, fixture.ownerDisplayName),
		Task:    TaskRef{TaskID: taskID},
		Patch: TaskUpdatePatch{
			Description:        stringPointer(description),
			Kind:               stringPointer(kind),
			Priority:           stringPointer(priority),
			AcceptanceCriteria: []TaskChecklistItem{{Text: "Task detail shows execution metadata"}, {Text: "Reviewer is visible on the task"}},
			ReviewChecklist:    []TaskChecklistItem{{Text: "Code review completed"}, {Text: "Regression coverage attached"}},
			AssigneeRef:        stringPointer(assigneeRef),
			BlockedReason:      stringPointer(blockedReason),
			ParentTaskRef:      stringPointer(parentTaskRef),
			PinnedMessageRefs:  pinnedMessageRefs,
			RelevantURLs:       relevantURLs,
			WorkspaceRefs:      workspaceRefs,
		},
	})
	require.NoError(t, err)

	fieldOptions, err := fixture.service.ListTaskFieldOptions(ctx, ListTaskFieldOptionsRequest{
		Context: NewUserServiceContext(fixture.workspaceID, fixture.ownerUserID, fixture.ownerDisplayName),
	})
	require.NoError(t, err)
	kindOption := findTaskFieldOption(t, fieldOptions.KindOptions, kind)
	priorityOption := findTaskFieldOption(t, fieldOptions.PriorityOptions, priority)

	detailResponse, err := fixture.service.GetTaskDetail(ctx, GetTaskDetailRequest{
		Context: NewUserServiceContext(fixture.workspaceID, fixture.ownerUserID, fixture.ownerDisplayName),
		Task:    TaskRef{TaskID: taskID},
	})
	require.NoError(t, err)

	require.Equal(t, description, detailResponse.Detail.Task.Description)
	require.Equal(t, kindOption.Label, detailResponse.Detail.Task.Kind)
	require.Equal(t, priorityOption.Label, detailResponse.Detail.Task.Priority)
	require.Equal(t, kindOption.ID, detailResponse.Detail.Task.KindField.OptionID)
	require.Equal(t, kindOption.Key, detailResponse.Detail.Task.KindField.Key)
	require.Equal(t, kindOption.Label, detailResponse.Detail.Task.KindField.Label)
	require.Equal(t, priorityOption.ID, detailResponse.Detail.Task.PriorityField.OptionID)
	require.Equal(t, priorityOption.Key, detailResponse.Detail.Task.PriorityField.Key)
	require.Equal(t, priorityOption.Label, detailResponse.Detail.Task.PriorityField.Label)
	require.Len(t, detailResponse.Detail.AcceptanceCriteria, 2)
	require.Equal(t, "Task detail shows execution metadata", detailResponse.Detail.AcceptanceCriteria[0].Text)
	require.Len(t, detailResponse.Detail.ReviewChecklist, 2)
	require.Equal(t, "Regression coverage attached", detailResponse.Detail.ReviewChecklist[1].Text)
	require.NotNil(t, detailResponse.Detail.Task.AssigneeProfile)
	require.Equal(t, "human", detailResponse.Detail.Task.AssigneeProfile.ActorType)
	require.Equal(t, fixture.reviewerUserID, detailResponse.Detail.Task.AssigneeProfile.PrincipalID)
	require.Equal(t, fixture.reviewerDisplayName, detailResponse.Detail.Task.AssigneeProfile.DisplayName)
	require.Equal(t, blockedReason, detailResponse.Detail.Task.BlockedReason)
	require.Equal(t, parentTaskRef, detailResponse.Detail.Task.ParentTaskRef)
	require.NotNil(t, detailResponse.Detail.ParentTask)
	require.Equal(t, parentResponse.Tasks[0].ID, detailResponse.Detail.ParentTask.ID)
	require.Equal(t, parentResponse.Tasks[0].TaskNumber, detailResponse.Detail.ParentTask.TaskNumber)
	require.Equal(t, parentResponse.Tasks[0].Title, detailResponse.Detail.ParentTask.Title)
	require.Equal(t, pinnedMessageRefs, detailResponse.Detail.PinnedMessageRefs)
	require.Equal(t, relevantURLs, detailResponse.Detail.RelevantURLs)
	require.Equal(t, workspaceRefs, detailResponse.Detail.WorkspaceRefs)
	require.NotNil(t, detailResponse.Detail.Task.LastActivityAt)
}

func TestTaskUpdateParentMembershipSetClearAndReparent(t *testing.T) {
	ctx := context.Background()
	fixture := seedTaskboardUserFixture(t, ctx)
	userContext := taskboardUserContext(fixture)

	created, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: userContext,
		Board:   BoardRef{ChannelID: fixture.channelID},
		Titles:  []string{"First parent", "Second parent", "Membership child"},
	})
	require.NoError(t, err)
	require.Len(t, created.Tasks, 3)
	firstParent := created.Tasks[0]
	secondParent := created.Tasks[1]
	child := created.Tasks[2]

	updated, err := fixture.service.UpdateTask(ctx, UpdateTaskRequest{
		Context: userContext,
		Task:    TaskRef{TaskID: child.ID},
		Patch:   TaskUpdatePatch{ParentTaskRef: stringPointer(firstParent.ID)},
	})
	require.NoError(t, err)
	require.Equal(t, firstParent.ID, updated.Task.ParentTaskRef)

	updated, err = fixture.service.UpdateTask(ctx, UpdateTaskRequest{
		Context: userContext,
		Task:    TaskRef{TaskID: child.ID},
		Patch:   TaskUpdatePatch{ParentTaskRef: stringPointer(secondParent.ID)},
	})
	require.NoError(t, err)
	require.Equal(t, secondParent.ID, updated.Task.ParentTaskRef)

	updated, err = fixture.service.UpdateTask(ctx, UpdateTaskRequest{
		Context: userContext,
		Task:    TaskRef{TaskID: child.ID},
		Patch:   TaskUpdatePatch{ParentTaskRef: stringPointer("")},
	})
	require.NoError(t, err)
	require.Empty(t, updated.Task.ParentTaskRef)
}

func TestTaskDetailResolvesParentTaskSummaryFromLegacyTaskNumberRef(t *testing.T) {
	ctx := context.Background()
	fixture := seedTaskboardUserFixture(t, ctx)

	parentResponse, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: NewUserServiceContext(fixture.workspaceID, fixture.ownerUserID, fixture.ownerDisplayName),
		Board:   BoardRef{ChannelID: fixture.channelID},
		Titles:  []string{"Parent task"},
	})
	require.NoError(t, err)
	require.Len(t, parentResponse.Tasks, 1)

	childResponse, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: NewUserServiceContext(fixture.workspaceID, fixture.ownerUserID, fixture.ownerDisplayName),
		Board:   BoardRef{ChannelID: fixture.channelID},
		Titles:  []string{"Child task"},
	})
	require.NoError(t, err)
	require.Len(t, childResponse.Tasks, 1)

	_, err = fixture.pool.Exec(ctx, `
UPDATE tasks
SET parent_task_ref = $2
WHERE id = $1
`, childResponse.Tasks[0].ID, fmt.Sprintf("#t%d", parentResponse.Tasks[0].TaskNumber))
	require.NoError(t, err)

	detailResponse, err := fixture.service.GetTaskDetail(ctx, GetTaskDetailRequest{
		Context: NewUserServiceContext(fixture.workspaceID, fixture.ownerUserID, fixture.ownerDisplayName),
		Task:    TaskRef{TaskID: childResponse.Tasks[0].ID},
	})
	require.NoError(t, err)
	require.Equal(t, fmt.Sprintf("#t%d", parentResponse.Tasks[0].TaskNumber), detailResponse.Detail.Task.ParentTaskRef)
	require.NotNil(t, detailResponse.Detail.ParentTask)
	require.Equal(t, parentResponse.Tasks[0].ID, detailResponse.Detail.ParentTask.ID)
	require.Equal(t, parentResponse.Tasks[0].TaskNumber, detailResponse.Detail.ParentTask.TaskNumber)
	require.Equal(t, parentResponse.Tasks[0].Title, detailResponse.Detail.ParentTask.Title)
}

func TestTaskDetailForUserCanLoadByChannelAndTaskNumber(t *testing.T) {
	ctx := context.Background()
	fixture := seedTaskboardUserFixture(t, ctx)

	createdResponse, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: NewUserServiceContext(fixture.workspaceID, fixture.ownerUserID, fixture.ownerDisplayName),
		Board:   BoardRef{ChannelID: fixture.channelID},
		Titles:  []string{"Load task detail by number"},
	})
	require.NoError(t, err)
	require.Len(t, createdResponse.Tasks, 1)

	detailResponse, err := fixture.service.GetTaskDetail(ctx, GetTaskDetailRequest{
		Context: NewUserServiceContext(fixture.workspaceID, fixture.ownerUserID, fixture.ownerDisplayName),
		Board:   BoardRef{ChannelID: fixture.channelID},
		Task:    TaskRef{TaskNumber: createdResponse.Tasks[0].TaskNumber},
	})
	require.NoError(t, err)
	require.Equal(t, createdResponse.Tasks[0].ID, detailResponse.Detail.Task.ID)
	require.Equal(t, fixture.channelID, detailResponse.Detail.Task.ChannelID)
	require.Equal(t, createdResponse.Tasks[0].TaskNumber, detailResponse.Detail.Task.TaskNumber)
}

func TestTaskReadStateTracksUnreadTaskScopeMessages(t *testing.T) {
	ctx := context.Background()
	fixture := seedTaskboardUserFixture(t, ctx)

	createdResponse, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: NewUserServiceContext(fixture.workspaceID, fixture.ownerUserID, fixture.ownerDisplayName),
		Board:   BoardRef{ChannelID: fixture.channelID},
		Titles:  []string{"Track task read state"},
	})
	require.NoError(t, err)
	require.Len(t, createdResponse.Tasks, 1)

	task := createdResponse.Tasks[0]
	readResponse, err := fixture.service.MarkTaskRead(ctx, MarkTaskReadRequest{
		Context: NewUserServiceContext(fixture.workspaceID, fixture.ownerUserID, fixture.ownerDisplayName),
		Task:    TaskRef{TaskID: task.ID},
	})
	require.NoError(t, err)
	require.Equal(t, int64(0), readResponse.ReadState.UnreadCount)
	require.Empty(t, readResponse.ReadState.LastReadEventID)

	discussionResponse, err := fixture.service.SendTaskScopeMessage(ctx, SendTaskScopeMessageRequest{
		Context: NewAgentServiceContext(fixture.workspaceID, fixture.agentID, fixture.agentMemberID),
		Board:   BoardRef{Channel: "#all"},
		Task:    TaskRef{TaskNumber: task.TaskNumber},
		Message: TaskScopeMessageInput{
			Content: "Unread discussion update",
		},
	})
	require.NoError(t, err)

	detailResponse, err := fixture.service.GetTaskDetail(ctx, GetTaskDetailRequest{
		Context: NewUserServiceContext(fixture.workspaceID, fixture.ownerUserID, fixture.ownerDisplayName),
		Task:    TaskRef{TaskID: task.ID},
	})
	require.NoError(t, err)
	require.NotNil(t, detailResponse.Detail.Task.ReadState)
	require.Equal(t, int64(0), detailResponse.Detail.Task.ReadState.UnreadCount, "store read state leaves runtime-owned unread count to API overlays")
	require.Equal(t, discussionResponse.Message.ID, detailResponse.Detail.Task.ReadState.LatestEventID)

	readResponse, err = fixture.service.MarkTaskRead(ctx, MarkTaskReadRequest{
		Context: NewUserServiceContext(fixture.workspaceID, fixture.ownerUserID, fixture.ownerDisplayName),
		Task:    TaskRef{TaskID: task.ID},
	})
	require.NoError(t, err)
	require.Equal(t, int64(0), readResponse.ReadState.UnreadCount)
	require.Equal(t, discussionResponse.Message.ID, readResponse.ReadState.LastReadEventID)
	require.Equal(t, taskScopeMessageSeqForTest(t, ctx, fixture.pool, task.TaskScopeID, discussionResponse.Message.ID), taskScopeReadSeqForTest(t, ctx, fixture.pool, task.TaskScopeID, fixture.ownerMemberID))

	selfDiscussionResponse, err := fixture.service.SendTaskScopeMessage(ctx, SendTaskScopeMessageRequest{
		Context: NewUserServiceContext(fixture.workspaceID, fixture.ownerUserID, fixture.ownerDisplayName),
		Task:    TaskRef{TaskID: task.ID},
		Message: TaskScopeMessageInput{
			Content: "Self-authored update",
		},
	})
	require.NoError(t, err)

	detailResponse, err = fixture.service.GetTaskDetail(ctx, GetTaskDetailRequest{
		Context: NewUserServiceContext(fixture.workspaceID, fixture.ownerUserID, fixture.ownerDisplayName),
		Task:    TaskRef{TaskID: task.ID},
	})
	require.NoError(t, err)
	require.NotNil(t, detailResponse.Detail.Task.ReadState)
	require.Equal(t, int64(0), detailResponse.Detail.Task.ReadState.UnreadCount)
	require.Equal(t, selfDiscussionResponse.Message.ID, detailResponse.Detail.Task.ReadState.LatestEventID)

	checkpointText := "Checkpoint remains typed activity without a synthetic discussion message"
	_, err = fixture.service.AddTaskCheckpoint(ctx, AddTaskCheckpointRequest{
		Context: NewAgentServiceContext(fixture.workspaceID, fixture.agentID, fixture.agentMemberID),
		Board:   BoardRef{Channel: "#all"},
		Task:    TaskRef{TaskNumber: task.TaskNumber},
		Checkpoint: TaskCheckpointInput{
			Text: checkpointText,
		},
	})
	require.NoError(t, err)

	detailResponse, err = fixture.service.GetTaskDetail(ctx, GetTaskDetailRequest{
		Context: NewUserServiceContext(fixture.workspaceID, fixture.ownerUserID, fixture.ownerDisplayName),
		Task:    TaskRef{TaskID: task.ID},
	})
	require.NoError(t, err)
	require.NotNil(t, detailResponse.Detail.Task.ReadState)
	require.Equal(t, int64(0), detailResponse.Detail.Task.ReadState.UnreadCount, "store read state leaves targeted typed-Activity unread to the runtime overlay")
	require.Equal(t, selfDiscussionResponse.Message.ID, detailResponse.Detail.Task.ReadState.LatestEventID)

	var checkpointMessageCount int
	require.NoError(t, fixture.pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_messages
WHERE scope_id = $1
  AND content = $2
`, task.TaskScopeID, checkpointText).Scan(&checkpointMessageCount))
	require.Zero(t, checkpointMessageCount, "checkpoint remains a typed task activity and must not create a task-scope message row")

	timelineResponse, err := fixture.service.GetTaskTimeline(ctx, GetTaskTimelineRequest{
		Context: NewUserServiceContext(fixture.workspaceID, fixture.ownerUserID, fixture.ownerDisplayName),
		Task:    TaskRef{TaskID: task.ID},
	})
	require.NoError(t, err)
	activityEvents := taskTimelineActivityEvents(timelineResponse)
	require.NotEmpty(t, activityEvents)
	require.Equal(t, taskEventCheckpointAdded, activityEvents[0].Type)
	require.Equal(t, taskTimelineSourceKindActivity, activityEvents[0].SourceKind)
	require.Empty(t, activityEvents[0].SourceMessageID)
	require.NotEmpty(t, activityEvents[0].SourceActivityID)
	require.Equal(t, checkpointText, activityEvents[0].Payload)
}

func TestTaskDetailInitialTimelineReadsAreBounded(t *testing.T) {
	ctx := context.Background()
	fixture := seedTaskboardUserFixture(t, ctx)

	createdResponse, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: NewUserServiceContext(fixture.workspaceID, fixture.ownerUserID, fixture.ownerDisplayName),
		Board:   BoardRef{ChannelID: fixture.channelID},
		Titles:  []string{"Bound task detail timeline"},
	})
	require.NoError(t, err)
	require.Len(t, createdResponse.Tasks, 1)

	taskID := createdResponse.Tasks[0].ID
	totalEntries := taskDetailTimelinePageSize + 1
	for index := 1; index <= totalEntries; index++ {
		_, err := fixture.service.SendTaskScopeMessage(ctx, SendTaskScopeMessageRequest{
			Context: NewUserServiceContext(fixture.workspaceID, fixture.ownerUserID, fixture.ownerDisplayName),
			Task:    TaskRef{TaskID: taskID},
			Message: TaskScopeMessageInput{
				Content: fmt.Sprintf("Timeline entry %02d", index),
			},
		})
		require.NoError(t, err)
	}

	detailResponse, err := fixture.service.GetTaskDetail(ctx, GetTaskDetailRequest{
		Context: NewUserServiceContext(fixture.workspaceID, fixture.ownerUserID, fixture.ownerDisplayName),
		Task:    TaskRef{TaskID: taskID},
	})
	require.NoError(t, err)

	require.Len(t, taskDetailDiscussionEntries(detailResponse.Detail), taskDetailTimelinePageSize)
	require.True(t, detailResponse.Detail.TimelineHasMore)
	require.NotEmpty(t, detailResponse.Detail.TimelineNextCursor)
	require.Equal(t, fmt.Sprintf("Timeline entry %02d", totalEntries), taskDetailDiscussionEntries(detailResponse.Detail)[0].Body)
	require.Equal(t, "Timeline entry 02", taskDetailDiscussionEntries(detailResponse.Detail)[len(taskDetailDiscussionEntries(detailResponse.Detail))-1].Body)

	require.Len(t, detailResponse.Detail.Timeline, taskDetailTimelinePageSize)
}

func TestTaskCreateDefaultsProjectFoundationFields(t *testing.T) {
	ctx := context.Background()
	fixture := seedTaskboardUserFixture(t, ctx)

	createdResponse, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: NewUserServiceContext(fixture.workspaceID, fixture.ownerUserID, fixture.ownerDisplayName),
		Board:   BoardRef{ChannelID: fixture.channelID},
		Titles:  []string{"Create foundation contract"},
	})
	require.NoError(t, err)
	require.Len(t, createdResponse.Tasks, 1)

	task := createdResponse.Tasks[0]
	require.Nil(t, task.AssigneeProfile)
	require.NotNil(t, task.KindField)
	require.True(t, task.KindField.IsNone)
	require.NotNil(t, task.PriorityField)
	require.True(t, task.PriorityField.IsNone)
	require.Zero(t, task.CheckpointProgress.Total)
	require.Zero(t, task.SubtaskProgress.Total)
	require.Zero(t, task.DiscussionCount)
}

func TestTaskCheckpointProjectionIncludesFoundationContract(t *testing.T) {
	ctx := context.Background()
	fixture := seedTaskboardUserFixture(t, ctx)

	createdResponse, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: NewUserServiceContext(fixture.workspaceID, fixture.ownerUserID, fixture.ownerDisplayName),
		Board:   BoardRef{ChannelID: fixture.channelID},
		Titles:  []string{"Ship foundation contract"},
	})
	require.NoError(t, err)
	require.Len(t, createdResponse.Tasks, 1)
	taskID := createdResponse.Tasks[0].ID

	initialTask := createdResponse.Tasks[0]
	require.Nil(t, initialTask.AssigneeProfile)
	require.NotNil(t, initialTask.KindField)
	require.True(t, initialTask.KindField.IsNone)
	require.NotNil(t, initialTask.PriorityField)
	require.True(t, initialTask.PriorityField.IsNone)

	_, err = fixture.service.CreateSubtasks(ctx, CreateSubtasksRequest{
		Context: NewUserServiceContext(fixture.workspaceID, fixture.ownerUserID, fixture.ownerDisplayName),
		Task:    TaskRef{TaskID: taskID},
		Titles:  []string{"Foundation subtask A", "Foundation subtask B"},
	})
	require.NoError(t, err)

	_, err = fixture.service.AddTaskCheckpoint(ctx, AddTaskCheckpointRequest{
		Context:    NewUserServiceContext(fixture.workspaceID, fixture.ownerUserID, fixture.ownerDisplayName),
		Task:       TaskRef{TaskID: taskID},
		Checkpoint: TaskCheckpointInput{Text: "Wire the structured summary projection"},
	})
	require.NoError(t, err)
	_, err = fixture.service.AddTaskCheckpoint(ctx, AddTaskCheckpointRequest{
		Context:    NewUserServiceContext(fixture.workspaceID, fixture.ownerUserID, fixture.ownerDisplayName),
		Task:       TaskRef{TaskID: taskID},
		Checkpoint: TaskCheckpointInput{Text: "Expose checkpoints in detail payload"},
	})
	require.NoError(t, err)

	fieldOptions, err := fixture.service.ListTaskFieldOptions(ctx, ListTaskFieldOptionsRequest{
		Context: NewUserServiceContext(fixture.workspaceID, fixture.ownerUserID, fixture.ownerDisplayName),
	})
	require.NoError(t, err)
	kindOption := findTaskFieldOption(t, fieldOptions.KindOptions, "feature")
	priorityOption := findTaskFieldOption(t, fieldOptions.PriorityOptions, "p1")

	mustExecTaskboard(t, ctx, fixture.pool, `
UPDATE tasks
SET assignee_member_id = $2,
    assigned_at = NOW(),
    claimed_by_member_id = $2,
    claimed_by_ref = $2,
    claimed_at = NOW(),
    kind_option_id = $3,
    priority_option_id = $4,
    discussion_count = 3
WHERE id = $1
`, taskID, fixture.agentMemberID, kindOption.ID, priorityOption.ID)

	listResponse, err := fixture.service.ListTasks(ctx, ListTasksRequest{
		Context: NewUserServiceContext(fixture.workspaceID, fixture.ownerUserID, fixture.ownerDisplayName),
		Board:   BoardRef{ChannelID: fixture.channelID},
	})
	require.NoError(t, err)
	require.Len(t, listResponse.Tasks, 1)

	summary := listResponse.Tasks[0]
	require.Equal(t, taskID, summary.ID)

	require.Equal(t, kindOption.Label, summary.Kind)
	require.Equal(t, priorityOption.Label, summary.Priority)
	require.Equal(t, int64(3), summary.DiscussionCount)

	require.NotNil(t, summary.AssigneeProfile)
	require.Equal(t, "agent", summary.AssigneeProfile.ActorType)
	require.Equal(t, fixture.agentID, summary.AssigneeProfile.PrincipalID)
	require.Equal(t, fixture.agentDisplayName, summary.AssigneeProfile.DisplayName)

	require.NotNil(t, summary.KindField)
	require.Equal(t, kindOption.ID, summary.KindField.OptionID)
	require.Equal(t, kindOption.Key, summary.KindField.Key)
	require.Equal(t, kindOption.Label, summary.KindField.Label)

	require.NotNil(t, summary.PriorityField)
	require.Equal(t, priorityOption.ID, summary.PriorityField.OptionID)
	require.Equal(t, priorityOption.Key, summary.PriorityField.Key)
	require.Equal(t, priorityOption.Label, summary.PriorityField.Label)

	require.Equal(t, int64(2), summary.CheckpointProgress.Total)
	require.Equal(t, int64(0), summary.CheckpointProgress.Done)
	require.Equal(t, int64(2), summary.SubtaskProgress.Total)
	require.Equal(t, int64(0), summary.SubtaskProgress.Done)

	detailResponse, err := fixture.service.GetTaskDetail(ctx, GetTaskDetailRequest{
		Context: NewUserServiceContext(fixture.workspaceID, fixture.ownerUserID, fixture.ownerDisplayName),
		Task:    TaskRef{TaskID: taskID},
	})
	require.NoError(t, err)

	require.Len(t, detailResponse.Detail.Checkpoints, 2)
	require.Equal(t, "Wire the structured summary projection", detailResponse.Detail.Checkpoints[0].Text)
	require.Equal(t, "Expose checkpoints in detail payload", detailResponse.Detail.Checkpoints[1].Text)
}

func seedTaskboardUserFixture(t *testing.T, ctx context.Context) taskboardUserFixture {
	t.Helper()

	return seedTaskboardUserFixtureOnDB(t, ctx, startTaskboardTestPool(t, ctx))
}

func seedTaskboardUserFixtureOnDB(t *testing.T, ctx context.Context, pool coredb.DB) taskboardUserFixture {
	t.Helper()

	now := time.Date(2026, 4, 13, 9, 0, 0, 0, time.UTC)

	fixture := taskboardUserFixture{
		pool:                pool,
		service:             New(pool),
		workspaceID:         "wsp_task_detail",
		channelID:           "chn_task_detail",
		ownerUserID:         "usr_task_detail_owner",
		ownerDisplayName:    "Task Detail Owner",
		ownerAvatarURL:      "https://example.test/task-detail-owner.png",
		ownerMemberID:       "mem_task_detail_owner",
		reviewerUserID:      "usr_task_detail_reviewer",
		reviewerDisplayName: "Task Detail Reviewer",
		reviewerAvatarURL:   "https://example.test/task-detail-reviewer.png",
		reviewerMemberID:    "mem_task_detail_reviewer",
		agentID:             "agt_task_detail_agent",
		agentDisplayName:    "Task Detail Agent",
		agentAvatarURL:      "https://example.test/task-detail-agent.png",
		agentMemberID:       "mem_task_detail_agent",
		machineID:           "mac_task_detail_agent",
	}

	mustExecTaskboard(t, ctx, pool, `
INSERT INTO users (id, email, name, display_name, avatar_url, is_admin, password_hash, email_verified_at, created_at, updated_at)
VALUES
    ($1, 'task-detail-owner@example.com', 'task-detail-owner', $2, $3, FALSE, 'hash', $4, $4, $4),
    ($5, 'task-detail-reviewer@example.com', 'task-detail-reviewer', $6, $7, FALSE, 'hash', $4, $4, $4)
`, fixture.ownerUserID, fixture.ownerDisplayName, fixture.ownerAvatarURL, now, fixture.reviewerUserID, fixture.reviewerDisplayName, fixture.reviewerAvatarURL)

	mustExecTaskboard(t, ctx, pool, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, $2, 'task-detail', 'Task Detail Workspace', 0, $3, $3)
`, fixture.workspaceID, testfixture.SeedWorkspaceOrganization(t, ctx, pool, fixture.workspaceID, now), now)

	mustExecTaskboard(t, ctx, pool, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, status, created_at, updated_at)
VALUES
    ($1, $2, 'human', $3, $4, 'owner', 'active', $5, $5),
    ($6, $2, 'human', $7, $8, 'member', 'active', $5, $5)
`, fixture.ownerMemberID, fixture.workspaceID, fixture.ownerDisplayName, fixture.ownerUserID, now, fixture.reviewerMemberID, fixture.reviewerDisplayName, fixture.reviewerUserID)

	mustExecTaskboard(t, ctx, pool, `
INSERT INTO members (id, workspace_id, type, display_name, status, created_at, updated_at)
VALUES ($1, $2, 'agent', $3, 'active', $4, $4)
`, fixture.agentMemberID, fixture.workspaceID, fixture.agentDisplayName, now)

	mustExecTaskboard(t, ctx, pool, `
INSERT INTO scopes (
  id, workspace_id, type, identity_key, latest_seq, latest_global_seq,
  latest_message_at, status, created_by_member_id, created_at, updated_at
) VALUES ($1, $2, 'channel', $3, 0, 0, NULL, 'active', $4, $5, $5)
`, fixture.channelID, fixture.workspaceID, "channel:all", fixture.ownerMemberID, now)
	mustExecTaskboard(t, ctx, pool, `
INSERT INTO scope_channel_profiles (
  scope_id, workspace_id, slug, display_name, description, visibility,
  owner_member_id, is_all_channel, created_at, updated_at
) VALUES ($1, $2, 'all', 'all', '', 'public', NULL, TRUE, $3, $3)
`, fixture.channelID, fixture.workspaceID, now)
	mustExecTaskboard(t, ctx, pool, `
INSERT INTO scope_memberships (
  scope_id, member_id, visibility, owner_member_id, last_read_seq, last_read_global_seq,
  owner_context_dismissed_through_global_seq, created_at, updated_at, removed_at, archived_at
) VALUES
  ($1, $2, 'public', NULL, 0, 0, 0, $5, $5, NULL, NULL),
  ($1, $3, 'public', NULL, 0, 0, 0, $5, $5, NULL, NULL),
  ($1, $4, 'public', NULL, 0, 0, 0, $5, $5, NULL, NULL)
`, fixture.channelID, fixture.ownerMemberID, fixture.reviewerMemberID, fixture.agentMemberID, now)
	mustExecTaskboard(t, ctx, pool, `
INSERT INTO scope_notification_settings (scope_id, member_id, state, created_at, updated_at)
VALUES ($1, $2, 'active', $5, $5), ($1, $3, 'active', $5, $5), ($1, $4, 'auto-wake', $5, $5)
`, fixture.channelID, fixture.ownerMemberID, fixture.reviewerMemberID, fixture.agentMemberID, now)

	mustExecTaskboard(t, ctx, pool, `
INSERT INTO machines (id, workspace_id, created_by_user_id, name, api_key, connected_at, created_at, updated_at)
VALUES ($1, $2, $3, 'task-detail-machine', 'api_key_task_detail', $4, $4, $4)
`, fixture.machineID, fixture.workspaceID, fixture.ownerUserID, now)

	mustExecTaskboard(t, ctx, pool, `
INSERT INTO agents (id, workspace_id, machine_id, member_id, name, avatar_url, runtime, model, reasoning_effort, description, session_id, auto_wake_enabled, status, activity, activity_detail, created_at, updated_at)
VALUES ($1, $2, $3, $4, 'task-detail-agent', $5, 'codex', 'gpt-5.4', '', '', '', TRUE, 'active', 'online', '', $6, $6)
`, fixture.agentID, fixture.workspaceID, fixture.machineID, fixture.agentMemberID, fixture.agentAvatarURL, now)

	return fixture
}

type testStatusMoveResult struct {
	Task    Task
	Changed bool
}

func moveTaskToDefaultStatusForTest(t *testing.T, ctx context.Context, service *Store, serviceContext ServiceContext, task TaskRef, targetStatus Status) testStatusMoveResult {
	t.Helper()

	return moveTaskToStatusForTest(t, ctx, service, serviceContext, BoardRef{}, task, targetStatus)
}

func executeNextStatusProgressionForTest(t *testing.T, ctx context.Context, service *Store, serviceContext ServiceContext, board BoardRef, task TaskRef) testStatusMoveResult {
	t.Helper()

	detail, err := service.GetTaskDetail(ctx, GetTaskDetailRequest{
		Context: serviceContext,
		Board:   board,
		Task:    task,
	})
	require.NoError(t, err)
	nextStatus, ok := defaultNextStatusForTest(Status(detail.Detail.Task.Status))
	if !ok {
		return testStatusMoveResult{Task: detail.Detail.Task, Changed: false}
	}
	return moveTaskToStatusForTest(t, ctx, service, serviceContext, board, task, nextStatus)
}

func moveTaskToStatusForTest(t *testing.T, ctx context.Context, service *Store, serviceContext ServiceContext, board BoardRef, task TaskRef, targetStatus Status) testStatusMoveResult {
	t.Helper()

	detail, err := service.GetTaskDetail(ctx, GetTaskDetailRequest{
		Context: serviceContext,
		Board:   board,
		Task:    task,
	})
	require.NoError(t, err)
	current := Status(detail.Detail.Task.Status)
	if current == targetStatus {
		return testStatusMoveResult{Task: detail.Detail.Task, Changed: false}
	}

	workspaceID, err := serviceContext.requireWorkspaceID()
	require.NoError(t, err)
	actorMemberID := taskStatusUpdateActorMemberIDForTest(t, ctx, service.db, serviceContext)
	var response testStatusMoveResult
	err = coredb.WithTx(ctx, service.db, func(tx coredb.Tx) error {
		txs := service.InTx(tx)
		_, rawStatus, _, err := txs.lockTaskRow(ctx, tx, workspaceID, detail.Detail.Task.ID)
		if err != nil {
			return err
		}
		now := time.Now().UTC()
		if err := txs.applyTaskStatusUpdate(ctx, tx, workspaceID, detail.Detail.Task.ID, actorMemberID, Status(rawStatus), targetStatus, now); err != nil {
			return err
		}
		updated, err := txs.taskByID(ctx, tx, workspaceID, detail.Detail.Task.ID)
		if err != nil {
			return err
		}
		response = testStatusMoveResult{Task: updated, Changed: true}
		return nil
	})
	require.NoError(t, err)
	return response
}

func taskStatusUpdateActorMemberIDForTest(t *testing.T, ctx context.Context, db coredb.Queryer, serviceContext ServiceContext) string {
	t.Helper()

	workspaceID, err := serviceContext.requireWorkspaceID()
	require.NoError(t, err)
	switch serviceContext.principalType() {
	case PrincipalTypeAgent:
		agent, err := serviceContext.requireAgentActor()
		require.NoError(t, err)
		return agent.MemberID
	case PrincipalTypeUser:
		userID, _, err := serviceContext.requireUser()
		require.NoError(t, err)
		var memberID string
		require.NoError(t, db.QueryRow(ctx, `
SELECT id
FROM members
WHERE workspace_id = $1
  AND user_id = $2
  AND status = 'active'
ORDER BY created_at ASC, id ASC
LIMIT 1
`, workspaceID, userID).Scan(&memberID))
		return memberID
	default:
		require.FailNow(t, "unsupported task status update principal", "principalType=%s", serviceContext.principalType())
		return ""
	}
}

func defaultNextStatusForTest(current Status) (Status, bool) {
	switch current {
	case StatusTodo:
		return StatusInProgress, true
	case StatusInProgress:
		return StatusInReview, true
	case StatusInReview:
		return StatusDone, true
	default:
		return "", false
	}
}

func mustExecTaskboard(t *testing.T, ctx context.Context, db interface {
	Exec(context.Context, string, ...any) (coredb.Result, error)
}, query string, args ...any) {
	t.Helper()

	_, err := db.Exec(ctx, query, args...)
	require.NoError(t, err)
}

func stringPointer(value string) *string {
	return &value
}

func updateTaskStatusForTest(t *testing.T, ctx context.Context, pool queryExecer, taskID string, targetStatus Status, updatedAt time.Time) {
	t.Helper()

	require.NoError(t, validateKnownStatus(targetStatus))
	if targetStatus == StatusDone {
		_, err := pool.Exec(ctx, `
UPDATE tasks
SET status = $2,
    completed_at = $3,
    last_activity_at = $3,
    updated_at = $3
WHERE id = $1
	`, taskID, targetStatus, updatedAt)
		require.NoError(t, err)
		return
	}
	_, err := pool.Exec(ctx, `
UPDATE tasks
SET status = $2,
    completed_at = NULL,
    last_activity_at = $3,
    updated_at = $3
WHERE id = $1
	`, taskID, targetStatus, updatedAt)
	require.NoError(t, err)
}

func taskScopeMessageSeqForTest(t *testing.T, ctx context.Context, db coredb.QueryRower, taskScopeID string, messageID string) int64 {
	t.Helper()

	var seq int64
	err := db.QueryRow(ctx, `
SELECT seq
FROM scope_messages
WHERE scope_id = $1
  AND id = $2
`, taskScopeID, messageID).Scan(&seq)
	require.NoError(t, err)
	return seq
}

func taskScopeReadSeqForTest(t *testing.T, ctx context.Context, db coredb.QueryRower, taskScopeID string, memberID string) int64 {
	t.Helper()

	var seq int64
	err := db.QueryRow(ctx, `
SELECT last_read_seq
FROM scope_memberships
WHERE scope_id = $1
  AND member_id = $2
  AND effective_to_seq IS NULL
  AND removed_at IS NULL
  AND archived_at IS NULL
`, taskScopeID, memberID).Scan(&seq)
	require.NoError(t, err)
	return seq
}
