package taskboard

import (
	"context"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

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

func TestCreateTasksInitializesTaskBoardOnMigratedSchema(t *testing.T) {
	ctx := context.Background()
	pool := startTaskboardTestPool(t, ctx)

	now := time.Date(2026, 3, 19, 8, 0, 0, 0, time.UTC)
	workspaceID := "wsp_taskboard"
	userID := "usr_taskboard"
	memberID := "mem_taskboard"
	channelID := "chn_taskboard"

	var err error
	_, err = pool.Exec(ctx, `
INSERT INTO users (id, email, name, display_name, avatar_url, is_admin, password_hash, email_verified_at, created_at, updated_at)
VALUES ($1, 'taskboard@example.com', 'taskboard-user', 'Taskboard User', '', FALSE, 'hash', $2, $2, $2)
`, userID, now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, $2, 'taskboard', 'Taskboard Workspace', 0, $3, $3)
`, workspaceID, testfixture.SeedWorkspaceOrganization(t, ctx, pool, workspaceID, now), now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, created_at, updated_at)
VALUES ($1, $2, 'human', 'Taskboard User', $3, 'owner', $4, $4)
`, memberID, workspaceID, userID, now)
	require.NoError(t, err)

	seedTaskboardChannelScope(t, ctx, pool, workspaceID, channelID, "all", "", "", "public", "", now)

	_, err = pool.Exec(ctx, `
INSERT IGNORE 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, $3, $3, NULL, NULL)
`, channelID, memberID, now)
	require.NoError(t, err)

	seedTaskboardChannelScopeFacts(t, ctx, pool, workspaceID, now)

	service := New(pool)
	response, err := service.CreateTasks(ctx, CreateTasksRequest{
		Context: NewUserServiceContext(workspaceID, userID, "Taskboard User"),
		Board:   BoardRef{ChannelID: channelID},
		Titles:  []string{"Ship taskboard regression"},
	})
	require.NoError(t, err)
	require.Len(t, response.Tasks, 1)
	require.Equal(t, channelID, response.ChannelID)
	require.Equal(t, int64(1), response.Tasks[0].TaskNumber)
	require.Equal(t, "Ship taskboard regression", response.Tasks[0].Title)
	require.NotEmpty(t, response.Tasks[0].TaskScopeID)
	require.Len(t, response.CreatedTasks, 1)
	require.Equal(t, response.Tasks[0].TaskScopeID, response.CreatedTasks[0].TaskScopeID)

	var taskScopeType string
	var taskScopeIdentityKey string
	require.NoError(t, pool.QueryRow(ctx, `
SELECT type, identity_key
FROM scopes
WHERE id = $1
`, response.Tasks[0].TaskScopeID).Scan(&taskScopeType, &taskScopeIdentityKey))
	require.Equal(t, "task", taskScopeType)
	require.Equal(t, "scope/task/"+response.Tasks[0].ID, taskScopeIdentityKey)

	var boardID string
	var boardWorkspaceID string
	var boardChannelID string
	require.NoError(t, pool.QueryRow(ctx, `
SELECT id, workspace_id, channel_id
FROM task_boards
WHERE channel_id = $1
`, channelID).Scan(&boardID, &boardWorkspaceID, &boardChannelID))
	require.NotEmpty(t, boardID)
	require.Equal(t, workspaceID, boardWorkspaceID)
	require.Equal(t, channelID, boardChannelID)
}

func TestDeleteTaskTombstonesTaskScope(t *testing.T) {
	ctx := context.Background()
	pool := startTaskboardTestPool(t, ctx)

	now := time.Date(2026, 5, 19, 11, 40, 0, 0, time.UTC)
	workspaceID := "wsp_task_scope_delete"
	userID := "usr_task_scope_delete"
	memberID := "mem_task_scope_delete"
	channelID := "chn_task_scope_delete"

	_, err := pool.Exec(ctx, `
INSERT INTO users (id, email, name, display_name, avatar_url, is_admin, password_hash, email_verified_at, created_at, updated_at)
VALUES ($1, 'task-scope-delete@example.com', 'task-scope-delete-user', 'Task Scope Delete User', '', FALSE, 'hash', $2, $2, $2)
`, userID, now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, $2, 'task-scope-delete', 'Task Scope Delete Workspace', 0, $3, $3)
`, workspaceID, testfixture.SeedWorkspaceOrganization(t, ctx, pool, workspaceID, now), now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, created_at, updated_at)
VALUES ($1, $2, 'human', 'Task Scope Delete User', $3, 'owner', $4, $4)
`, memberID, workspaceID, userID, now)
	require.NoError(t, err)

	seedTaskboardChannelScope(t, ctx, pool, workspaceID, channelID, "all", "", "", "public", "", now)

	_, err = pool.Exec(ctx, `
INSERT IGNORE 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, $3, $3, NULL, NULL)
`, channelID, memberID, now)
	require.NoError(t, err)

	seedTaskboardChannelScopeFacts(t, ctx, pool, workspaceID, now)

	service := New(pool)
	created, err := service.CreateTasks(ctx, CreateTasksRequest{
		Context: NewUserServiceContext(workspaceID, userID, "Task Scope Delete User"),
		Board:   BoardRef{ChannelID: channelID},
		Titles:  []string{"Delete should tombstone resolved task scope"},
	})
	require.NoError(t, err)
	require.Len(t, created.Tasks, 1)
	taskID := created.Tasks[0].ID
	taskScopeID := created.Tasks[0].TaskScopeID
	require.NotEmpty(t, taskScopeID)

	deleteResponse, err := service.DeleteTask(ctx, DeleteTaskRequest{
		Context: NewUserServiceContext(workspaceID, userID, "Task Scope Delete User"),
		Task:    TaskRef{TaskID: taskID},
	})
	require.NoError(t, err)
	require.Equal(t, channelID, deleteResponse.ChannelID)

	var taskCount int64
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM tasks
WHERE id = $1
`, taskID).Scan(&taskCount))
	require.Zero(t, taskCount)

	var scopeStatus string
	var scopeArchived bool
	require.NoError(t, pool.QueryRow(ctx, `
SELECT status, archived_at IS NOT NULL
FROM scopes
WHERE id = $1
`, taskScopeID).Scan(&scopeStatus, &scopeArchived))
	require.Equal(t, "deleted", scopeStatus)
	require.True(t, scopeArchived)

	var activeOrphanScopes int64
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scopes s
LEFT JOIN tasks t ON t.task_scope_id = s.id
WHERE s.type = 'task'
  AND s.status = 'active'
  AND t.id IS NULL
`).Scan(&activeOrphanScopes))
	require.Zero(t, activeOrphanScopes)
}

func TestAgentTaskLifecycleUsesResolvedTeamBoard(t *testing.T) {
	ctx := context.Background()
	pool := startTaskboardTestPool(t, ctx)

	now := time.Date(2026, 3, 21, 8, 0, 0, 0, time.UTC)
	workspaceID := "wsp_agent_tasks"
	agentMemberID := "ag7tsk01"
	channelID := "chn_agent_tasks"

	var err error
	_, err = pool.Exec(ctx, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, $2, 'agent-tasks', 'Agent Tasks Workspace', 0, $3, $3)
`, workspaceID, testfixture.SeedWorkspaceOrganization(t, ctx, pool, workspaceID, now), now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO users (id, email, name, display_name, avatar_url, is_admin, password_hash, email_verified_at, created_at, updated_at)
VALUES ($1, 'agent-tasks-owner@example.com', 'agent-tasks-owner', 'Agent Tasks Owner', '', FALSE, 'hash', $2, $2, $2)
`, "usr_agent_tasks_owner", now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO members (id, workspace_id, type, display_name, status, created_at, updated_at)
VALUES ($1, $2, 'agent', 'Task Agent', 'active', $3, $3)
`, agentMemberID, workspaceID, now)
	require.NoError(t, err)

	// This test resolves a Team pseudo-target to a concrete ChannelID and drives
	// the ID-based taskboard path, which authorizes through the resolved
	// Agent+Machine+Member channel-membership gate. Seed the agent and its
	// machine so the resolved authorizer sees a real active runtime Agent.
	_, err = pool.Exec(ctx, `
INSERT INTO machines (id, workspace_id, created_by_user_id, name, api_key, connected_at, created_at, updated_at)
VALUES ($1, $2, $3, 'task-agent-machine', 'k', $4, $4, $4)
`, "mac_agent_tasks", workspaceID, "usr_agent_tasks_owner", now)
	require.NoError(t, err)
	_, err = pool.Exec(ctx, `
INSERT INTO agents (id, workspace_id, machine_id, member_id, name, runtime, model, description,
  session_id, auto_wake_enabled, status, activity, activity_detail, created_at, updated_at)
VALUES ($1, $2, $3, $4, 'Task Agent', 'codex', 'gpt', '', '', TRUE, 'active', 'online', '', $5, $5)
`, "agt_agent_tasks", workspaceID, "mac_agent_tasks", agentMemberID, now)
	require.NoError(t, err)

	seedTaskboardChannelScope(t, ctx, pool, workspaceID, channelID, "all", "", "", "public", "", now)

	_, err = pool.Exec(ctx, `
INSERT IGNORE 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, $3, $3, NULL, NULL)
`, channelID, agentMemberID, now)
	require.NoError(t, err)

	seedTaskboardChannelScopeFacts(t, ctx, pool, workspaceID, now)

	service := New(pool)
	agentContext := NewAgentServiceContext(workspaceID, "agt_agent_tasks", agentMemberID)
	resolvedTeamBoard := BoardRef{
		ChannelID: channelID,
		// The resolved Agent API resolves this Team pseudo-target to ChannelID
		// before entering taskboard. The resolved identity must take precedence
		// over the author-facing alias.
		Channel: "#team_abcdefghijklmn",
	}
	created, err := service.CreateTasks(ctx, CreateTasksRequest{
		Context: agentContext,
		Board:   resolvedTeamBoard,
		Titles:  []string{"Investigate daemon regression"},
	})
	require.NoError(t, err)
	require.Len(t, created.CreatedTasks, 1)
	require.NotEmpty(t, created.CreatedTasks[0].TaskScopeID)
	subtasks, err := service.CreateSubtasks(ctx, CreateSubtasksRequest{
		Context: agentContext,
		Board:   resolvedTeamBoard,
		Task:    TaskRef{TaskNumber: 1},
		Titles:  []string{"Verify daemon subtask"},
	})
	require.NoError(t, err)
	require.Len(t, subtasks.CreatedTasks, 1)
	require.NotEmpty(t, subtasks.CreatedTasks[0].TaskScopeID)

	// Agent parentTaskScope is a canonical task-scope locator, not a
	// top-level-only shortcut. Exercise the production store path with a real
	// child-of-child create so public Agent Task creation supports arbitrary
	// nesting without fixture SQL or a second root child.
	grandchildren, err := service.CreateSubtasks(ctx, CreateSubtasksRequest{
		Context: agentContext,
		ParentTaskScope: &domain.MessageScopeValue{
			Kind:    domain.MessageScopeKindTask,
			ScopeID: subtasks.CreatedTasks[0].TaskScopeID,
		},
		Titles: []string{"Verify nested daemon subtask"},
	})
	require.NoError(t, err)
	require.Len(t, grandchildren.CreatedTasks, 1)
	require.Len(t, grandchildren.Tasks, 1)
	require.Equal(t, subtasks.Tasks[0].ID, grandchildren.Tasks[0].ParentTaskRef)

	detail, err := service.GetTaskDetail(ctx, GetTaskDetailRequest{
		Context: agentContext,
		Board:   resolvedTeamBoard,
		Task:    TaskRef{TaskNumber: 1},
	})
	require.NoError(t, err)
	require.Equal(t, "Investigate daemon regression", detail.Detail.Task.Title)

	updated, err := service.UpdateTask(ctx, UpdateTaskRequest{
		Context: agentContext,
		Board:   resolvedTeamBoard,
		Task:    TaskRef{TaskNumber: 1},
		Patch: TaskUpdatePatch{
			Title: stringPointer("Investigate Team daemon regression"),
		},
	})
	require.NoError(t, err)
	require.Equal(t, "Investigate Team daemon regression", updated.Task.Title)

	status, err := service.UpdateTaskStatus(ctx, UpdateTaskStatusRequest{
		Context: agentContext,
		Board:   resolvedTeamBoard,
		Task:    TaskRef{TaskNumber: 1},
		Status:  string(StatusInProgress),
	})
	require.NoError(t, err)
	require.True(t, status.Changed)
	require.Equal(t, string(StatusInProgress), status.Task.Status)

	checkpoint, err := service.AddTaskCheckpoint(ctx, AddTaskCheckpointRequest{
		Context:    agentContext,
		Board:      resolvedTeamBoard,
		Task:       TaskRef{TaskNumber: 1},
		Checkpoint: TaskCheckpointInput{Text: "Verified Team board resolution"},
	})
	require.NoError(t, err)
	require.Equal(t, int64(1), checkpoint.Task.CheckpointProgress.Total)

	blocked, err := service.BlockTask(ctx, BlockTaskRequest{
		Context: agentContext,
		Board:   resolvedTeamBoard,
		Task:    TaskRef{TaskNumber: 1},
		Block:   TaskBlockInput{Reason: "Waiting for Team validation"},
	})
	require.NoError(t, err)
	require.True(t, blocked.Changed)

	unblocked, err := service.UnblockTask(ctx, UnblockTaskRequest{
		Context: agentContext,
		Board:   resolvedTeamBoard,
		Task:    TaskRef{TaskNumber: 1},
	})
	require.NoError(t, err)
	require.True(t, unblocked.Changed)

	options, err := service.ListTaskOptions(ctx, ListTaskOptionsRequest{
		Context: agentContext,
		Board:   resolvedTeamBoard,
	})
	require.NoError(t, err)
	require.NotEmpty(t, options.PriorityOptions)

	reviewQueue, err := service.ListTaskReviewQueue(ctx, ListTaskReviewQueueRequest{
		Context: agentContext,
		Board:   resolvedTeamBoard,
	})
	require.NoError(t, err)
	require.Empty(t, reviewQueue.Reviews)

	claimed, err := service.ClaimTasks(ctx, ClaimTasksRequest{
		Context: agentContext,
		Board:   resolvedTeamBoard,
		Tasks:   []TaskRef{{TaskNumber: 1}},
	})
	require.NoError(t, err)
	require.Len(t, claimed.Results, 1)
	require.True(t, claimed.Results[0].Success)

	unclaimed, err := service.UnclaimTask(ctx, UnclaimTaskRequest{
		Context: agentContext,
		Board:   resolvedTeamBoard,
		Task:    TaskRef{TaskNumber: 1},
	})
	require.NoError(t, err)
	require.Nil(t, unclaimed.Task.AssigneeProfile)

	discussion, err := service.SendTaskScopeMessage(ctx, SendTaskScopeMessageRequest{
		Context: agentContext,
		Board:   resolvedTeamBoard,
		Task:    TaskRef{TaskNumber: 1},
		Message: TaskScopeMessageInput{Content: "Team board lifecycle is reachable."},
	})
	require.NoError(t, err)
	require.Equal(t, int64(1), discussion.Task.DiscussionCount)

	deleted, err := service.DeleteTask(ctx, DeleteTaskRequest{
		Context:     agentContext,
		Board:       resolvedTeamBoard,
		Task:        TaskRef{TaskNumber: 2},
		SubtaskMode: DeleteTaskSubtaskModeDelete,
	})
	require.NoError(t, err)
	require.Equal(t, []int64{3, 2}, deleted.DeletedTaskNumbers)

	response, err := service.ListTasks(ctx, ListTasksRequest{
		Context: agentContext,
		Board:   resolvedTeamBoard,
		Status:  "all",
	})
	require.NoError(t, err)
	require.Len(t, response.TaskViews, 1)
	require.Equal(t, created.CreatedTasks[0].TaskScopeID, response.TaskViews[0].TaskScopeID)
	require.Equal(t, int64(1), response.TaskViews[0].TaskNumber)
	require.Equal(t, "Investigate Team daemon regression", response.TaskViews[0].Title)
	require.Equal(t, string(StatusInProgress), response.TaskViews[0].Status)

	browse, err := service.BrowseChannelTasks(ctx, BrowseChannelTasksRequest{
		Context: agentContext,
		Board:   resolvedTeamBoard,
		Query:   TaskBrowseQuery{StatusFilter: "all"},
	})
	require.NoError(t, err)
	require.Len(t, browse.Rows, 1)
	require.Equal(t, created.CreatedTasks[0].TaskNumber, browse.Rows[0].TaskNumber)
}

func TestAgentListTasksUsesBoundedBrowsePage(t *testing.T) {
	ctx := context.Background()
	pool := startTaskboardTestPool(t, ctx)

	now := time.Date(2026, 5, 30, 8, 30, 0, 0, time.UTC)
	workspaceID := "wsp_agent_tasks_bounded"
	agentMemberID := "ag7tskbounded"
	channelID := "chn_agent_tasks_bounded"

	var err error
	_, err = pool.Exec(ctx, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, $2, 'agent-tasks-bounded', 'Agent Tasks Bounded Workspace', 0, $3, $3)
`, workspaceID, testfixture.SeedWorkspaceOrganization(t, ctx, pool, workspaceID, now), now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO members (id, workspace_id, type, display_name, status, created_at, updated_at)
VALUES ($1, $2, 'agent', 'Task Agent', 'active', $3, $3)
`, agentMemberID, workspaceID, now)
	require.NoError(t, err)

	// The Agent authority fence requires an active Machine + Agent chain.
	_, err = pool.Exec(ctx, `
INSERT INTO users (id, email, name, display_name, avatar_url, is_admin, password_hash, email_verified_at, created_at, updated_at)
VALUES ($1, 'agent-bounded@example.com', 'agent-bounded', 'Agent Bounded', '', FALSE, 'hash', $2, $2, $2)
`, "usr_agent_tasks_bounded", now)
	require.NoError(t, err)
	_, err = pool.Exec(ctx, `
INSERT INTO machines (id, workspace_id, created_by_user_id, name, api_key, connected_at, created_at, updated_at)
VALUES ($1, $2, $3, 'agent-tasks-bounded-machine', 'api_key_agent_bounded', $4, $4, $4)
`, "mac_agent_tasks_bounded", workspaceID, "usr_agent_tasks_bounded", now)
	require.NoError(t, err)
	_, err = pool.Exec(ctx, `
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, 'agent-tasks-bounded', '', 'codex', 'gpt-5.4', '', '', '', TRUE, 'active', 'online', '', $5, $5)
`, "agt_agent_tasks_bounded", workspaceID, "mac_agent_tasks_bounded", agentMemberID, now)
	require.NoError(t, err)

	seedTaskboardChannelScope(t, ctx, pool, workspaceID, channelID, "all", "", "", "public", "", now)

	_, err = pool.Exec(ctx, `
INSERT IGNORE 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, $3, $3, NULL, NULL)
`, channelID, agentMemberID, now)
	require.NoError(t, err)

	seedTaskboardChannelScopeFacts(t, ctx, pool, workspaceID, now)

	titles := make([]string, 20)
	for i := range titles {
		titles[i] = "Bounded task"
	}

	service := New(pool)
	created, err := service.CreateTasks(ctx, CreateTasksRequest{
		Context: NewAgentServiceContext(workspaceID, "agt_agent_tasks_bounded", agentMemberID),
		Board:   BoardRef{Channel: "#all"},
		Titles:  titles,
	})
	require.NoError(t, err)
	require.Len(t, created.CreatedTasks, len(titles))
	subtasks, err := service.CreateSubtasks(ctx, CreateSubtasksRequest{
		Context: NewAgentServiceContext(workspaceID, "agt_agent_tasks_bounded", agentMemberID),
		Board:   BoardRef{Channel: "#all"},
		Task:    TaskRef{TaskNumber: 1},
		Titles:  []string{"Bounded subtask"},
	})
	require.NoError(t, err)
	require.Len(t, subtasks.CreatedTasks, 1)

	response, err := service.ListTasks(ctx, ListTasksRequest{
		Context:         NewAgentServiceContext(workspaceID, "agt_agent_tasks_bounded", agentMemberID),
		Board:           BoardRef{Channel: "#all"},
		Status:          "all",
		IncludeSubtasks: true,
	})
	require.NoError(t, err)
	require.Equal(t, taskBrowseDefaultLimit, response.Limit)
	require.Len(t, response.TaskViews, taskBrowseDefaultLimit)
	require.True(t, response.HasMore)
	require.Equal(t, int64(len(titles)+1), response.TotalCount)
	require.NotEmpty(t, response.NextCursor.TaskID)
	require.Equal(t, int64(1), response.TaskViews[0].TaskNumber)
	require.Equal(t, int64(taskBrowseDefaultLimit), response.TaskViews[len(response.TaskViews)-1].TaskNumber)
}

func TestAgentUsesPrivateChannelEntryForOwnTaskTools(t *testing.T) {
	ctx := context.Background()
	pool := startTaskboardTestPool(t, ctx)

	now := time.Date(2026, 5, 1, 8, 0, 0, 0, time.UTC)
	workspaceID := "wsp_agent_private_tasks"
	ownerMemberID := "mem_agent_private_owner"
	agentMemberID := "mem_agent_private_task"
	channelID := "chn_agent_private_tasks"

	var err error
	_, err = pool.Exec(ctx, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, $2, 'agent-private-tasks', 'Agent Private Tasks Workspace', 0, $3, $3)
`, workspaceID, testfixture.SeedWorkspaceOrganization(t, ctx, pool, workspaceID, now), now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO members (id, workspace_id, type, display_name, status, created_at, updated_at)
VALUES
  ($1, $3, 'human', 'Private Owner', 'active', $4, $4),
  ($2, $3, 'agent', 'Private Task Agent', 'active', $4, $4)
`, ownerMemberID, agentMemberID, workspaceID, now)
	require.NoError(t, err)

	// The Agent authority fence requires an active Machine + Agent chain for the
	// same-workspace agent member. The machine references a user.
	_, err = pool.Exec(ctx, `
INSERT INTO users (id, email, name, display_name, avatar_url, is_admin, password_hash, email_verified_at, created_at, updated_at)
VALUES ($1, 'agent-private-owner@example.com', 'agent-private-owner', 'Agent Private Owner', '', FALSE, 'hash', $2, $2, $2)
`, "usr_agent_private_owner", now)
	require.NoError(t, err)
	_, err = pool.Exec(ctx, `
INSERT INTO machines (id, workspace_id, created_by_user_id, name, api_key, connected_at, created_at, updated_at)
VALUES ($1, $2, $3, 'agent-private-machine', 'api_key_agent_private', $4, $4, $4)
`, "mac_agent_private_tasks", workspaceID, "usr_agent_private_owner", now)
	require.NoError(t, err)
	_, err = pool.Exec(ctx, `
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, 'agent-private-task', '', 'codex', 'gpt-5.4', '', '', '', TRUE, 'active', 'online', '', $5, $5)
`, "agt_agent_private_task", workspaceID, "mac_agent_private_tasks", agentMemberID, now)
	require.NoError(t, err)

	seedTaskboardChannelScope(t, ctx, pool, workspaceID, channelID, "all", "", "", "public", "", now)

	_, err = pool.Exec(ctx, `
INSERT IGNORE 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, $3, $3, NULL, NULL)
`, channelID, ownerMemberID, now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO scope_memberships (scope_id, member_id, visibility, owner_member_id, created_at, updated_at)
VALUES ($1, $2, 'private', $3, $4, $4)
ON DUPLICATE KEY UPDATE
  visibility = VALUES(visibility),
  owner_member_id = VALUES(owner_member_id),
  removed_at = NULL,
  updated_at = VALUES(updated_at)
`, channelID, agentMemberID, ownerMemberID, now)
	require.NoError(t, err)

	seedTaskboardChannelScopeFacts(t, ctx, pool, workspaceID, now)

	service := New(pool)
	created, err := service.CreateTasks(ctx, CreateTasksRequest{
		Context: NewAgentServiceContext(workspaceID, "agt_agent_private_task", agentMemberID),
		Board:   BoardRef{Channel: "#all"},
		Titles:  []string{"Private agent should manage its fallback channel task"},
	})
	require.NoError(t, err)
	require.Len(t, created.CreatedTasks, 1)

	claimed, err := service.ClaimTasks(ctx, ClaimTasksRequest{
		Context: NewAgentServiceContext(workspaceID, "agt_agent_private_task", agentMemberID),
		Board:   BoardRef{Channel: "#all"},
		Tasks:   []TaskRef{{TaskNumber: 1}},
	})
	require.NoError(t, err)
	require.Len(t, claimed.Results, 1)
	require.True(t, claimed.Results[0].Success)

	listed, err := service.ListTasks(ctx, ListTasksRequest{
		Context: NewAgentServiceContext(workspaceID, "agt_agent_private_task", agentMemberID),
		Board:   BoardRef{Channel: "#all"},
		Status:  "all",
	})
	require.NoError(t, err)
	require.Len(t, listed.TaskViews, 1)
	require.Equal(t, int64(1), listed.TaskViews[0].TaskNumber)
}

func TestListWorkspaceTasksForUserOnlyReturnsJoinedChannelTasks(t *testing.T) {
	ctx := context.Background()
	pool := startTaskboardTestPool(t, ctx)

	now := time.Date(2026, 4, 15, 8, 0, 0, 0, time.UTC)
	workspaceID := "wsp_workspace_tasks"
	ownerUserID := "usr_workspace_owner"
	memberUserID := "usr_workspace_member"
	ownerMemberID := "mem_workspace_owner"
	memberMemberID := "mem_workspace_member"
	allChannelID := "chn_workspace_all"
	privateChannelID := "chn_workspace_private"

	var err error
	_, err = pool.Exec(ctx, `
INSERT INTO users (id, email, name, display_name, avatar_url, is_admin, password_hash, email_verified_at, created_at, updated_at)
VALUES
    ($1, 'workspace-owner@example.com', 'workspace-owner', 'Workspace Owner', '', FALSE, 'hash', $3, $3, $3),
    ($2, 'workspace-member@example.com', 'workspace-member', 'Workspace Member', '', FALSE, 'hash', $3, $3, $3)
`, ownerUserID, memberUserID, now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, $2, 'workspace-tasks', 'Workspace Tasks', 0, $3, $3)
`, workspaceID, testfixture.SeedWorkspaceOrganization(t, ctx, pool, workspaceID, now), now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, status, created_at, updated_at)
VALUES
    ($1, $3, 'human', 'Workspace Owner', $4, 'owner', 'active', $5, $5),
    ($2, $3, 'human', 'Workspace Member', $6, 'member', 'active', $5, $5)
`, ownerMemberID, memberMemberID, workspaceID, ownerUserID, now, memberUserID)
	require.NoError(t, err)

	seedTaskboardChannelScope(t, ctx, pool, workspaceID, allChannelID, "all", "", "", "public", "", now)
	seedTaskboardChannelScope(t, ctx, pool, workspaceID, privateChannelID, "exec-room", "", "", "private", ownerMemberID, now)

	_, err = pool.Exec(ctx, `
INSERT IGNORE 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, $3, 'public', NULL, 0, 0, 0, $5, $5, NULL, NULL),
    ($1, $4, 'public', NULL, 0, 0, 0, $5, $5, NULL, NULL),
    ($2, $3, 'public', NULL, 0, 0, 0, $5, $5, NULL, NULL)
`, allChannelID, privateChannelID, ownerMemberID, memberMemberID, now)
	require.NoError(t, err)

	seedTaskboardChannelScopeFacts(t, ctx, pool, workspaceID, now)

	service := New(pool)
	rootResponse, err := service.CreateTasks(ctx, CreateTasksRequest{
		Context: NewUserServiceContext(workspaceID, ownerUserID, "Workspace Owner"),
		Board:   BoardRef{ChannelID: allChannelID},
		Titles:  []string{"Visible task"},
	})
	require.NoError(t, err)
	_, err = service.CreateSubtasks(ctx, CreateSubtasksRequest{
		Context: NewUserServiceContext(workspaceID, ownerUserID, "Workspace Owner"),
		Board:   BoardRef{ChannelID: allChannelID},
		Task:    TaskRef{TaskID: rootResponse.Tasks[0].ID},
		Titles:  []string{"Visible child task"},
	})
	require.NoError(t, err)
	_, err = service.CreateTasks(ctx, CreateTasksRequest{
		Context: NewUserServiceContext(workspaceID, ownerUserID, "Workspace Owner"),
		Board:   BoardRef{ChannelID: privateChannelID},
		Titles:  []string{"Hidden task"},
	})
	require.NoError(t, err)

	response, err := service.ListWorkspaceTasks(ctx, ListWorkspaceTasksRequest{
		Context: NewUserServiceContext(workspaceID, memberUserID, "Workspace Member"),
	})
	require.NoError(t, err)
	require.Len(t, response.Tasks, 1)
	require.Equal(t, allChannelID, response.Tasks[0].ChannelID)
	require.Equal(t, "Visible task", response.Tasks[0].Title)

	response, err = service.ListWorkspaceTasks(ctx, ListWorkspaceTasksRequest{
		Context:         NewUserServiceContext(workspaceID, memberUserID, "Workspace Member"),
		IncludeSubtasks: true,
	})
	require.NoError(t, err)
	require.Len(t, response.Tasks, 2)
	require.Equal(t, "Visible task", response.Tasks[0].Title)
	require.Equal(t, "Visible child task", response.Tasks[1].Title)
}

func TestBrowseWorkspaceRowsPaginatesVisibleRootRows(t *testing.T) {
	ctx := context.Background()
	pool := startTaskboardTestPool(t, ctx)

	now := time.Date(2026, 4, 23, 8, 0, 0, 0, time.UTC)
	workspaceID := "wsp_browse_workspace"
	ownerUserID := "usr_browse_workspace_owner"
	memberUserID := "usr_browse_workspace_member"
	ownerMemberID := "mem_browse_workspace_owner"
	memberMemberID := "mem_browse_workspace_member"
	alphaChannelID := "chn_browse_alpha"
	bravoChannelID := "chn_browse_bravo"
	privateChannelID := "chn_browse_private"

	var err error
	_, err = pool.Exec(ctx, `
INSERT INTO users (id, email, name, display_name, avatar_url, is_admin, password_hash, email_verified_at, created_at, updated_at)
VALUES
    ($1, 'browse-workspace-owner@example.com', 'browse-workspace-owner', 'Browse Workspace Owner', '', FALSE, 'hash', $3, $3, $3),
    ($2, 'browse-workspace-member@example.com', 'browse-workspace-member', 'Browse Workspace Member', '', FALSE, 'hash', $3, $3, $3)
`, ownerUserID, memberUserID, now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, $2, 'browse-workspace', 'Browse Workspace', 0, $3, $3)
`, workspaceID, testfixture.SeedWorkspaceOrganization(t, ctx, pool, workspaceID, now), now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, status, created_at, updated_at)
VALUES
    ($1, $3, 'human', 'Browse Workspace Owner', $4, 'owner', 'active', $5, $5),
    ($2, $3, 'human', 'Browse Workspace Member', $6, 'member', 'active', $5, $5)
`, ownerMemberID, memberMemberID, workspaceID, ownerUserID, now, memberUserID)
	require.NoError(t, err)

	seedTaskboardChannelScope(t, ctx, pool, workspaceID, alphaChannelID, "alpha", "", "", "public", "", now)
	seedTaskboardChannelScope(t, ctx, pool, workspaceID, bravoChannelID, "bravo", "", "", "public", "", now)
	seedTaskboardChannelScope(t, ctx, pool, workspaceID, privateChannelID, "secret", "", "", "private", ownerMemberID, now)

	_, err = pool.Exec(ctx, `
INSERT IGNORE 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, $4, 'public', NULL, 0, 0, 0, $6, $6, NULL, NULL),
    ($1, $5, 'public', NULL, 0, 0, 0, $6, $6, NULL, NULL),
    ($2, $4, 'public', NULL, 0, 0, 0, $6, $6, NULL, NULL),
    ($2, $5, 'public', NULL, 0, 0, 0, $6, $6, NULL, NULL),
    ($3, $4, 'public', NULL, 0, 0, 0, $6, $6, NULL, NULL)
`, alphaChannelID, bravoChannelID, privateChannelID, ownerMemberID, memberMemberID, now)
	require.NoError(t, err)

	seedTaskboardChannelScopeFacts(t, ctx, pool, workspaceID, now)

	service := New(pool)
	alphaResponse, err := service.CreateTasks(ctx, CreateTasksRequest{
		Context: NewUserServiceContext(workspaceID, ownerUserID, "Browse Workspace Owner"),
		Board:   BoardRef{ChannelID: alphaChannelID},
		Titles:  []string{"Visible Alpha One", "Visible Alpha Two"},
	})
	require.NoError(t, err)
	require.Len(t, alphaResponse.Tasks, 2)

	bravoResponse, err := service.CreateTasks(ctx, CreateTasksRequest{
		Context: NewUserServiceContext(workspaceID, ownerUserID, "Browse Workspace Owner"),
		Board:   BoardRef{ChannelID: bravoChannelID},
		Titles:  []string{"Visible Bravo One"},
	})
	require.NoError(t, err)
	require.Len(t, bravoResponse.Tasks, 1)

	privateResponse, err := service.CreateTasks(ctx, CreateTasksRequest{
		Context: NewUserServiceContext(workspaceID, ownerUserID, "Browse Workspace Owner"),
		Board:   BoardRef{ChannelID: privateChannelID},
		Titles:  []string{"Visible Private Hidden"},
	})
	require.NoError(t, err)
	require.Len(t, privateResponse.Tasks, 1)

	fieldOptions, err := service.ListTaskFieldOptions(ctx, ListTaskFieldOptionsRequest{
		Context: NewUserServiceContext(workspaceID, ownerUserID, "Browse Workspace Owner"),
	})
	require.NoError(t, err)
	kindOption := findTaskFieldOption(t, fieldOptions.KindOptions, "feature")
	priorityOption := findTaskFieldOption(t, fieldOptions.PriorityOptions, "p1")
	_, err = pool.Exec(ctx, `
UPDATE tasks
SET assignee_member_id = $2,
    kind_option_id = $3,
    priority_option_id = $4,
    updated_at = $5
WHERE id = $1
`, alphaResponse.Tasks[0].ID, memberMemberID, kindOption.ID, priorityOption.ID, now)
	require.NoError(t, err)

	_, err = service.CreateSubtasks(ctx, CreateSubtasksRequest{
		Context: NewUserServiceContext(workspaceID, ownerUserID, "Browse Workspace Owner"),
		Board:   BoardRef{ChannelID: alphaChannelID},
		Task:    TaskRef{TaskID: alphaResponse.Tasks[0].ID},
		Titles:  []string{"Visible Alpha Child"},
	})
	require.NoError(t, err)

	for _, update := range []struct {
		taskID  string
		at      time.Time
		channel string
	}{
		{taskID: alphaResponse.Tasks[0].ID, at: now.Add(2 * time.Hour), channel: alphaChannelID},
		{taskID: alphaResponse.Tasks[1].ID, at: now.Add(1 * time.Hour), channel: alphaChannelID},
		{taskID: bravoResponse.Tasks[0].ID, at: now.Add(3 * time.Hour), channel: bravoChannelID},
		{taskID: privateResponse.Tasks[0].ID, at: now.Add(4 * time.Hour), channel: privateChannelID},
	} {
		_, err = pool.Exec(ctx, `
UPDATE tasks
SET created_at = $2,
    last_activity_at = $2,
    updated_at = $2
WHERE id = $1
`, update.taskID, update.at)
		require.NoError(t, err)
	}

	firstPage, err := service.BrowseWorkspaceTasks(ctx, BrowseWorkspaceTasksRequest{
		Context: NewUserServiceContext(workspaceID, memberUserID, "Browse Workspace Member"),
		Query: TaskBrowseQuery{
			Limit:       2,
			SearchQuery: " visible ",
			SortMode:    "recently_updated",
		},
	})
	require.NoError(t, err)
	require.Len(t, firstPage.Rows, 2)
	require.Equal(t, int64(3), firstPage.TotalCount)
	require.True(t, firstPage.HasMore)
	require.NotEmpty(t, firstPage.NextCursor)
	require.Equal(t, []string{"Visible Bravo One", "Visible Alpha One"}, []string{
		firstPage.Rows[0].Title,
		firstPage.Rows[1].Title,
	})

	secondPage, err := service.BrowseWorkspaceTasks(ctx, BrowseWorkspaceTasksRequest{
		Context: NewUserServiceContext(workspaceID, memberUserID, "Browse Workspace Member"),
		Query: TaskBrowseQuery{
			Limit:       2,
			SearchQuery: "visible",
			SortMode:    "recently_updated",
			Cursor:      firstPage.NextCursor,
		},
	})
	require.NoError(t, err)
	require.Len(t, secondPage.Rows, 1)
	require.Equal(t, int64(3), secondPage.TotalCount)
	require.False(t, secondPage.HasMore)
	require.Empty(t, secondPage.NextCursor)
	require.Equal(t, "Visible Alpha Two", secondPage.Rows[0].Title)

	alphaReviewAt := now.Add(5 * time.Hour)
	alphaDoneAt := now.Add(6 * time.Hour)
	bravoProgressAt := now.Add(7 * time.Hour)

	_, err = pool.Exec(ctx, `
UPDATE tasks
SET status = 'in_review',
    reviewer_ref = $2,
    updated_at = $3
WHERE id = $1
`, alphaResponse.Tasks[0].ID, memberMemberID, alphaReviewAt)
	require.NoError(t, err)
	updateTaskStatusForTest(t, ctx, pool, alphaResponse.Tasks[0].ID, StatusInReview, alphaReviewAt)

	_, err = pool.Exec(ctx, `
UPDATE tasks
SET status = 'done',
    completed_at = $2,
    updated_at = $2
WHERE id = $1
`, alphaResponse.Tasks[1].ID, alphaDoneAt)
	require.NoError(t, err)
	updateTaskStatusForTest(t, ctx, pool, alphaResponse.Tasks[1].ID, StatusDone, alphaDoneAt)

	_, err = pool.Exec(ctx, `
UPDATE tasks
SET status = 'in_progress',
    updated_at = $2
WHERE id = $1
`, bravoResponse.Tasks[0].ID, bravoProgressAt)
	require.NoError(t, err)
	updateTaskStatusForTest(t, ctx, pool, bravoResponse.Tasks[0].ID, StatusInProgress, bravoProgressAt)

	boardPage, err := service.BrowseWorkspaceTaskBoard(ctx, BrowseWorkspaceTaskBoardRequest{
		Context: NewUserServiceContext(workspaceID, memberUserID, "Browse Workspace Member"),
		Query: TaskBrowseQuery{
			Limit:       1,
			SearchQuery: "visible",
			SortMode:    "recently_updated",
		},
	})
	require.NoError(t, err)
	require.Equal(t, int64(3), boardPage.TotalCount)
	require.Equal(t, int64(0), boardPage.ColumnCounts[string(TaskBrowseColumnTodo)])
	require.Equal(t, int64(1), boardPage.ColumnCounts[string(TaskBrowseColumnInProgress)])
	require.Equal(t, int64(1), boardPage.ColumnCounts[string(TaskBrowseColumnInReview)])
	require.Equal(t, int64(1), boardPage.ColumnCounts[string(TaskBrowseColumnDone)])
	require.Len(t, boardPage.Columns[string(TaskBrowseColumnInProgress)].Tasks, 1)
	require.Equal(t, "Visible Bravo One", boardPage.Columns[string(TaskBrowseColumnInProgress)].Tasks[0].Title)
	require.Len(t, boardPage.Columns[string(TaskBrowseColumnInReview)].Tasks, 1)
	require.Equal(t, "Visible Alpha One", boardPage.Columns[string(TaskBrowseColumnInReview)].Tasks[0].Title)
	require.Len(t, boardPage.Columns[string(TaskBrowseColumnDone)].Tasks, 1)
	require.Equal(t, "Visible Alpha Two", boardPage.Columns[string(TaskBrowseColumnDone)].Tasks[0].Title)

	for _, test := range []struct {
		name   string
		filter map[string][]string
		taskID string
	}{
		{name: "channel", filter: map[string][]string{"channel": {bravoChannelID}}, taskID: bravoResponse.Tasks[0].ID},
		{name: "status", filter: map[string][]string{"status": {string(StatusDone)}}, taskID: alphaResponse.Tasks[1].ID},
		{name: "assignee", filter: map[string][]string{"assignee": {memberUserID}}, taskID: alphaResponse.Tasks[0].ID},
		{name: "kind", filter: map[string][]string{"kind": {kindOption.Key}}, taskID: alphaResponse.Tasks[0].ID},
		{name: "priority", filter: map[string][]string{"priority": {priorityOption.Key}}, taskID: alphaResponse.Tasks[0].ID},
	} {
		t.Run("structured filter "+test.name, func(t *testing.T) {
			page, err := service.BrowseWorkspaceTasks(ctx, BrowseWorkspaceTasksRequest{
				Context: NewUserServiceContext(workspaceID, memberUserID, "Browse Workspace Member"),
				Query: TaskBrowseQuery{
					StructuredFilters: test.filter,
				},
			})
			require.NoError(t, err)
			require.Equal(t, int64(1), page.TotalCount)
			require.Len(t, page.Rows, 1)
			require.Equal(t, test.taskID, page.Rows[0].ID)
			require.Equal(t, test.filter, page.QueryEcho.StructuredFilters)
		})
	}
}

func TestBrowseWorkspaceRowsSearchesAssigneeProfile(t *testing.T) {
	ctx := context.Background()
	pool := startTaskboardTestPool(t, ctx)

	now := time.Date(2026, 4, 23, 9, 0, 0, 0, time.UTC)
	workspaceID := "wsp_browse_profile_search"
	userID := "usr_browse_profile_search"
	assigneeUserID := "usr_browse_profile_assignee"
	memberID := "mem_browse_profile_search"
	assigneeMemberID := "mem_browse_profile_assignee"
	channelID := "chn_browse_profile_search"

	var err error
	_, err = pool.Exec(ctx, `
INSERT INTO users (id, email, name, display_name, avatar_url, is_admin, password_hash, email_verified_at, created_at, updated_at)
VALUES
    ($1, 'browse-profile@example.com', 'browse-profile', 'Browse Profile User', '', FALSE, 'hash', $3, $3, $3),
    ($2, 'browse-profile-assignee@example.com', 'browse-profile-assignee', 'Resolved Assignee Needle', '', FALSE, 'hash', $3, $3, $3)
`, userID, assigneeUserID, now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, $2, 'browse-profile', 'Browse Profile Workspace', 0, $3, $3)
`, workspaceID, testfixture.SeedWorkspaceOrganization(t, ctx, pool, workspaceID, now), now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, status, created_at, updated_at)
VALUES
    ($1, $3, 'human', 'Browse Profile User', $4, 'owner', 'active', $6, $6),
    ($2, $3, 'human', 'Legacy Assignee Alias', $5, 'member', 'active', $6, $6)
`, memberID, assigneeMemberID, workspaceID, userID, assigneeUserID, now)
	require.NoError(t, err)

	seedTaskboardChannelScope(t, ctx, pool, workspaceID, channelID, "profile-search", "", "", "public", "", now)
	_, err = pool.Exec(ctx, `
INSERT IGNORE 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, $3, $3, NULL, NULL)
`, channelID, memberID, now)
	require.NoError(t, err)
	seedTaskboardChannelScopeFacts(t, ctx, pool, workspaceID, now)

	service := New(pool)
	response, err := service.CreateTasks(ctx, CreateTasksRequest{
		Context: NewUserServiceContext(workspaceID, userID, "Browse Profile User"),
		Board:   BoardRef{ChannelID: channelID},
		Titles:  []string{"Profile Search Target", "Profile Search Decoy"},
	})
	require.NoError(t, err)
	require.Len(t, response.Tasks, 2)

	_, err = pool.Exec(ctx, `
UPDATE tasks
SET assignee_member_id = $2,
    assigned_at = $3,
    claimed_by_member_id = $2,
    claimed_by_ref = $2,
    claimed_at = $3,
    updated_at = $3
WHERE id = $1
`, response.Tasks[0].ID, assigneeMemberID, now.Add(time.Hour))
	require.NoError(t, err)

	page, err := service.BrowseWorkspaceTasks(ctx, BrowseWorkspaceTasksRequest{
		Context: NewUserServiceContext(workspaceID, userID, "Browse Profile User"),
		Query: TaskBrowseQuery{
			SearchQuery: "resolved assignee needle",
			SortMode:    "title",
		},
	})
	require.NoError(t, err)
	require.Equal(t, int64(1), page.TotalCount)
	require.Len(t, page.Rows, 1)
	require.Equal(t, response.Tasks[0].ID, page.Rows[0].ID)
	require.NotNil(t, page.Rows[0].AssigneeProfile)
	require.Equal(t, "Resolved Assignee Needle", page.Rows[0].AssigneeProfile.DisplayName)
	require.Equal(t, "resolved assignee needle", page.QueryEcho.SearchQuery)
}

func TestBrowseChannelRowsFiltersBeforePagination(t *testing.T) {
	ctx := context.Background()
	pool := startTaskboardTestPool(t, ctx)

	now := time.Date(2026, 4, 23, 10, 0, 0, 0, time.UTC)
	workspaceID := "wsp_browse_channel"
	userID := "usr_browse_channel"
	memberID := "mem_browse_channel"
	channelID := "chn_browse_channel"

	var err error
	_, err = pool.Exec(ctx, `
INSERT INTO users (id, email, name, display_name, avatar_url, is_admin, password_hash, email_verified_at, created_at, updated_at)
VALUES ($1, 'browse-channel@example.com', 'browse-channel', 'Browse Channel User', '', FALSE, 'hash', $2, $2, $2)
`, userID, now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, $2, 'browse-channel', 'Browse Channel', 0, $3, $3)
`, workspaceID, testfixture.SeedWorkspaceOrganization(t, ctx, pool, workspaceID, now), now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, status, created_at, updated_at)
VALUES ($1, $2, 'human', 'Browse Channel User', $3, 'owner', 'active', $4, $4)
`, memberID, workspaceID, userID, now)
	require.NoError(t, err)

	seedTaskboardChannelScope(t, ctx, pool, workspaceID, channelID, "all", "", "", "public", "", now)

	_, err = pool.Exec(ctx, `
INSERT IGNORE 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, $3, $3, NULL, NULL)
`, channelID, memberID, now)
	require.NoError(t, err)

	seedTaskboardChannelScopeFacts(t, ctx, pool, workspaceID, now)

	service := New(pool)
	rootResponse, err := service.CreateTasks(ctx, CreateTasksRequest{
		Context: NewUserServiceContext(workspaceID, userID, "Browse Channel User"),
		Board:   BoardRef{ChannelID: channelID},
		Titles:  []string{"Blocked One", "Blocked Two", "Blocked Three", "Open Four"},
	})
	require.NoError(t, err)
	require.Len(t, rootResponse.Tasks, 4)

	_, err = service.CreateSubtasks(ctx, CreateSubtasksRequest{
		Context: NewUserServiceContext(workspaceID, userID, "Browse Channel User"),
		Board:   BoardRef{ChannelID: channelID},
		Task:    TaskRef{TaskID: rootResponse.Tasks[0].ID},
		Titles:  []string{"Blocked Child"},
	})
	require.NoError(t, err)

	for index, task := range rootResponse.Tasks[:3] {
		at := now.Add(time.Duration(index+1) * time.Hour)
		_, err = pool.Exec(ctx, `
UPDATE tasks
SET blocked_reason = 'Waiting on blocker',
    created_at = $2,
    last_activity_at = $2,
    updated_at = $2
WHERE id = $1
`, task.ID, at)
		require.NoError(t, err)
	}
	_, err = pool.Exec(ctx, `
UPDATE tasks
SET created_at = $2,
    last_activity_at = $2,
    updated_at = $2
WHERE id = $1
`, rootResponse.Tasks[3].ID, now.Add(4*time.Hour))
	require.NoError(t, err)

	firstPage, err := service.BrowseChannelTasks(ctx, BrowseChannelTasksRequest{
		Context: NewUserServiceContext(workspaceID, userID, "Browse Channel User"),
		Board:   BoardRef{ChannelID: channelID},
		Query: TaskBrowseQuery{
			Limit:        2,
			QuickFilters: []string{"blocked"},
			SortMode:     "recently_updated",
		},
	})
	require.NoError(t, err)
	require.Len(t, firstPage.Rows, 2)
	require.Equal(t, int64(3), firstPage.TotalCount)
	require.True(t, firstPage.HasMore)
	require.NotEmpty(t, firstPage.NextCursor)
	require.Equal(t, []string{"Blocked Three", "Blocked Two"}, []string{
		firstPage.Rows[0].Title,
		firstPage.Rows[1].Title,
	})

	secondPage, err := service.BrowseChannelTasks(ctx, BrowseChannelTasksRequest{
		Context: NewUserServiceContext(workspaceID, userID, "Browse Channel User"),
		Board:   BoardRef{ChannelID: channelID},
		Query: TaskBrowseQuery{
			Limit:        2,
			QuickFilters: []string{"blocked"},
			SortMode:     "recently_updated",
			Cursor:       firstPage.NextCursor,
		},
	})
	require.NoError(t, err)
	require.Len(t, secondPage.Rows, 1)
	require.Equal(t, int64(3), secondPage.TotalCount)
	require.False(t, secondPage.HasMore)
	require.Equal(t, "Blocked One", secondPage.Rows[0].Title)
}

func TestBrowseChannelBoardRejectsBoardGlobalCursor(t *testing.T) {
	ctx := context.Background()
	pool := startTaskboardTestPool(t, ctx)

	now := time.Date(2026, 4, 24, 6, 0, 0, 0, time.UTC)
	workspaceID := "wsp_browse_board_cursor"
	userID := "usr_browse_board_cursor"
	memberID := "mem_browse_board_cursor"
	channelID := "chn_browse_board_cursor"

	var err error
	_, err = pool.Exec(ctx, `
INSERT INTO users (id, email, name, display_name, avatar_url, is_admin, password_hash, email_verified_at, created_at, updated_at)
VALUES ($1, 'browse-board-cursor@example.com', 'browse-board-cursor', 'Browse Board Cursor User', '', FALSE, 'hash', $2, $2, $2)
`, userID, now)
	require.NoError(t, err)
	_, err = pool.Exec(ctx, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, $2, 'browse-board-cursor', 'Browse Board Cursor', 0, $3, $3)
`, workspaceID, testfixture.SeedWorkspaceOrganization(t, ctx, pool, workspaceID, now), now)
	require.NoError(t, err)
	_, err = pool.Exec(ctx, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, status, created_at, updated_at)
VALUES ($1, $2, 'human', 'Browse Board Cursor User', $3, 'owner', 'active', $4, $4)
`, memberID, workspaceID, userID, now)
	require.NoError(t, err)
	seedTaskboardChannelScope(t, ctx, pool, workspaceID, channelID, "all", "", "", "public", "", now)
	_, err = pool.Exec(ctx, `
INSERT IGNORE 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, $3, $3, NULL, NULL)
`, channelID, memberID, now)
	require.NoError(t, err)

	seedTaskboardChannelScopeFacts(t, ctx, pool, workspaceID, now)

	service := New(pool)
	_, err = service.BrowseTaskBoard(ctx, BrowseTaskBoardRequest{
		Context: NewUserServiceContext(workspaceID, userID, "Browse Board Cursor User"),
		Board:   BoardRef{ChannelID: channelID},
		Query: TaskBrowseQuery{
			Cursor: TaskBrowseCursor{
				SortMode:   "manual",
				TaskID:     "tsk_any",
				CreatedAt:  now,
				TaskNumber: 1,
			},
		},
	})
	require.ErrorContains(t, err, "board browse does not support cursor")
}

func TestBrowseChannelBoardUsesColumnPaginationAndCountFirstDone(t *testing.T) {
	ctx := context.Background()
	pool := startTaskboardTestPool(t, ctx)

	now := time.Date(2026, 4, 24, 7, 0, 0, 0, time.UTC)
	workspaceID := "wsp_browse_board"
	userID := "usr_browse_board"
	memberID := "mem_browse_board"
	channelID := "chn_browse_board"

	var err error
	_, err = pool.Exec(ctx, `
INSERT INTO users (id, email, name, display_name, avatar_url, is_admin, password_hash, email_verified_at, created_at, updated_at)
VALUES ($1, 'browse-board@example.com', 'browse-board', 'Browse Board User', '', FALSE, 'hash', $2, $2, $2)
`, userID, now)
	require.NoError(t, err)
	_, err = pool.Exec(ctx, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, $2, 'browse-board', 'Browse Board', 0, $3, $3)
`, workspaceID, testfixture.SeedWorkspaceOrganization(t, ctx, pool, workspaceID, now), now)
	require.NoError(t, err)
	_, err = pool.Exec(ctx, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, status, created_at, updated_at)
VALUES ($1, $2, 'human', 'Browse Board User', $3, 'owner', 'active', $4, $4)
`, memberID, workspaceID, userID, now)
	require.NoError(t, err)
	seedTaskboardChannelScope(t, ctx, pool, workspaceID, channelID, "all", "", "", "public", "", now)
	_, err = pool.Exec(ctx, `
INSERT IGNORE 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, $3, $3, NULL, NULL)
`, channelID, memberID, now)
	require.NoError(t, err)

	titles := []string{
		"Alpha Todo",
		"Alpha Claimed Todo",
		"Alpha Review",
		"Alpha Done One",
		"Alpha Done Two",
		"Beta Ignored",
	}
	for index := 1; index <= 11; index++ {
		titles = append(titles, "Alpha In Progress "+time.Date(2026, 1, index, 0, 0, 0, 0, time.UTC).Format("02"))
	}

	seedTaskboardChannelScopeFacts(t, ctx, pool, workspaceID, now)

	service := New(pool)
	createResponse, err := service.CreateTasks(ctx, CreateTasksRequest{
		Context: NewUserServiceContext(workspaceID, userID, "Browse Board User"),
		Board:   BoardRef{ChannelID: channelID},
		Titles:  titles,
	})
	require.NoError(t, err)
	require.Len(t, createResponse.Tasks, len(titles))

	tasksByTitle := make(map[string]Task, len(createResponse.Tasks))
	for _, task := range createResponse.Tasks {
		tasksByTitle[task.Title] = task
	}

	taskTimes := map[string]time.Time{
		"Alpha Todo":         now.Add(1 * time.Hour),
		"Alpha Claimed Todo": now.Add(2 * time.Hour),
		"Alpha Review":       now.Add(3 * time.Hour),
		"Alpha Done One":     now.Add(4 * time.Hour),
		"Alpha Done Two":     now.Add(5 * time.Hour),
		"Beta Ignored":       now.Add(20 * time.Hour),
	}
	for index := 1; index <= 11; index++ {
		title := "Alpha In Progress " + time.Date(2026, 1, index, 0, 0, 0, 0, time.UTC).Format("02")
		taskTimes[title] = now.Add(time.Duration(6+index) * time.Hour)
	}

	for title, at := range taskTimes {
		task := tasksByTitle[title]
		status := "todo"
		var assigneeMemberID any
		var completedAt any
		switch title {
		case "Alpha Claimed Todo":
			assigneeMemberID = memberID
		case "Alpha Review":
			status = "in_review"
			assigneeMemberID = memberID
		case "Alpha Done One", "Alpha Done Two":
			status = "done"
			completedAt = at
		case "Beta Ignored":
			status = "in_progress"
		default:
			if strings.HasPrefix(title, "Alpha In Progress ") {
				status = "in_progress"
			}
		}

		_, err = pool.Exec(ctx, `
UPDATE tasks
SET status = $2,
    assignee_member_id = $3,
    assigned_at = CASE WHEN $3 IS NULL THEN NULL ELSE $6 END,
    claimed_by_member_id = $3,
    claimed_by_ref = $3,
    claimed_at = CASE WHEN $3 IS NULL THEN NULL ELSE $6 END,
    completed_at = $4,
    created_at = $5,
    last_activity_at = $5,
    updated_at = $5
WHERE id = $1
`, task.ID, status, assigneeMemberID, completedAt, at, at)
		require.NoError(t, err)
		updateTaskStatusForTest(t, ctx, pool, task.ID, Status(status), at)
	}

	firstPage, err := service.BrowseTaskBoard(ctx, BrowseTaskBoardRequest{
		Context: NewUserServiceContext(workspaceID, userID, "Browse Board User"),
		Board:   BoardRef{ChannelID: channelID},
		Query: TaskBrowseQuery{
			SearchQuery: "alpha",
			SortMode:    "recently_updated",
		},
	})
	require.NoError(t, err)
	require.Equal(t, 10, firstPage.QueryEcho.Limit)
	require.Equal(t, int64(16), firstPage.TotalCount)
	require.Equal(t, int64(2), firstPage.ColumnCounts[string(TaskBrowseColumnTodo)])
	require.Equal(t, int64(11), firstPage.ColumnCounts[string(TaskBrowseColumnInProgress)])
	require.Equal(t, int64(1), firstPage.ColumnCounts[string(TaskBrowseColumnInReview)])
	require.Equal(t, int64(2), firstPage.ColumnCounts[string(TaskBrowseColumnDone)])

	require.Len(t, firstPage.Columns[string(TaskBrowseColumnTodo)].Tasks, 2)
	require.Len(t, firstPage.Columns[string(TaskBrowseColumnInProgress)].Tasks, 10)
	require.Len(t, firstPage.Columns[string(TaskBrowseColumnInReview)].Tasks, 1)
	require.Len(t, firstPage.Columns[string(TaskBrowseColumnDone)].Tasks, 2)
	require.Equal(t, int64(2), firstPage.Columns[string(TaskBrowseColumnDone)].Count)
	require.False(t, firstPage.Columns[string(TaskBrowseColumnDone)].HasMore)
	require.Equal(t, "Alpha Done Two", firstPage.Columns[string(TaskBrowseColumnDone)].Tasks[0].Title)
	require.Equal(t, "Alpha Done One", firstPage.Columns[string(TaskBrowseColumnDone)].Tasks[1].Title)
	require.True(t, firstPage.Columns[string(TaskBrowseColumnInProgress)].HasMore)
	require.NotEmpty(t, firstPage.Columns[string(TaskBrowseColumnInProgress)].NextCursor)
	require.Equal(t, "Alpha In Progress 11", firstPage.Columns[string(TaskBrowseColumnInProgress)].Tasks[0].Title)
	require.Equal(t, "Alpha In Progress 02", firstPage.Columns[string(TaskBrowseColumnInProgress)].Tasks[9].Title)

	secondPage, err := service.BrowseTaskBoard(ctx, BrowseTaskBoardRequest{
		Context: NewUserServiceContext(workspaceID, userID, "Browse Board User"),
		Board:   BoardRef{ChannelID: channelID},
		Query: TaskBrowseQuery{
			SearchQuery: "alpha",
			SortMode:    "recently_updated",
			ColumnCursors: map[string]TaskBrowseCursor{
				string(TaskBrowseColumnInProgress): firstPage.Columns[string(TaskBrowseColumnInProgress)].NextCursor,
			},
		},
	})
	require.NoError(t, err)
	require.Equal(t, int64(16), secondPage.TotalCount)
	require.Equal(t, int64(2), secondPage.Columns[string(TaskBrowseColumnTodo)].Count)
	require.Empty(t, secondPage.Columns[string(TaskBrowseColumnTodo)].Tasks)
	require.Len(t, secondPage.Columns[string(TaskBrowseColumnInProgress)].Tasks, 1)
	require.Equal(t, "Alpha In Progress 01", secondPage.Columns[string(TaskBrowseColumnInProgress)].Tasks[0].Title)
	require.False(t, secondPage.Columns[string(TaskBrowseColumnInProgress)].HasMore)
	require.Equal(t, int64(1), secondPage.Columns[string(TaskBrowseColumnInReview)].Count)
	require.Empty(t, secondPage.Columns[string(TaskBrowseColumnInReview)].Tasks)
	require.Equal(t, int64(2), secondPage.Columns[string(TaskBrowseColumnDone)].Count)
	require.Empty(t, secondPage.Columns[string(TaskBrowseColumnDone)].Tasks)
	require.True(t, secondPage.Columns[string(TaskBrowseColumnDone)].HasMore)
}

func TestListTasksForUserIncludeSubtasksIsOptIn(t *testing.T) {
	ctx := context.Background()
	pool := startTaskboardTestPool(t, ctx)

	now := time.Date(2026, 4, 15, 8, 0, 0, 0, time.UTC)
	workspaceID := "wsp_channel_tasks"
	userID := "usr_channel_tasks"
	memberID := "mem_channel_tasks"
	channelID := "chn_channel_tasks"

	var err error
	_, err = pool.Exec(ctx, `
INSERT INTO users (id, email, name, display_name, avatar_url, is_admin, password_hash, email_verified_at, created_at, updated_at)
VALUES ($1, 'channel-tasks@example.com', 'channel-tasks', 'Channel Tasks User', '', FALSE, 'hash', $2, $2, $2)
`, userID, now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, $2, 'channel-tasks', 'Channel Tasks', 0, $3, $3)
`, workspaceID, testfixture.SeedWorkspaceOrganization(t, ctx, pool, workspaceID, now), now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, status, created_at, updated_at)
VALUES ($1, $2, 'human', 'Channel Tasks User', $3, 'owner', 'active', $4, $4)
`, memberID, workspaceID, userID, now)
	require.NoError(t, err)

	seedTaskboardChannelScope(t, ctx, pool, workspaceID, channelID, "all", "", "Channel tasks board", "public", "", now)

	_, err = pool.Exec(ctx, `
INSERT IGNORE 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, $3, $3, NULL, NULL)
`, channelID, memberID, now)
	require.NoError(t, err)

	seedTaskboardChannelScopeFacts(t, ctx, pool, workspaceID, now)

	service := New(pool)
	rootResponse, err := service.CreateTasks(ctx, CreateTasksRequest{
		Context: NewUserServiceContext(workspaceID, userID, "Channel Tasks User"),
		Board:   BoardRef{ChannelID: channelID},
		Titles:  []string{"Visible root task"},
	})
	require.NoError(t, err)
	require.Len(t, rootResponse.Tasks, 1)

	_, err = service.CreateSubtasks(ctx, CreateSubtasksRequest{
		Context: NewUserServiceContext(workspaceID, userID, "Channel Tasks User"),
		Board:   BoardRef{ChannelID: channelID},
		Task:    TaskRef{TaskID: rootResponse.Tasks[0].ID},
		Titles:  []string{"Visible child task"},
	})
	require.NoError(t, err)

	response, err := service.ListTasks(ctx, ListTasksRequest{
		Context: NewUserServiceContext(workspaceID, userID, "Channel Tasks User"),
		Board:   BoardRef{ChannelID: channelID},
	})
	require.NoError(t, err)
	require.Len(t, response.Tasks, 1)
	require.Equal(t, "Visible root task", response.Tasks[0].Title)

	response, err = service.ListTasks(ctx, ListTasksRequest{
		Context:         NewUserServiceContext(workspaceID, userID, "Channel Tasks User"),
		Board:           BoardRef{ChannelID: channelID},
		IncludeSubtasks: true,
	})
	require.NoError(t, err)
	require.Len(t, response.Tasks, 2)
	require.Equal(t, "Visible root task", response.Tasks[0].Title)
	require.Equal(t, "Visible child task", response.Tasks[1].Title)
}

func TestCreateTasksPersistsCollaborationMetadata(t *testing.T) {
	ctx := context.Background()
	pool := startTaskboardTestPool(t, ctx)

	now := time.Date(2026, 3, 23, 8, 0, 0, 0, time.UTC)
	workspaceID := "wsp_task_metadata"
	userID := "usr_task_metadata"
	userMemberID := "mem_task_metadata_user"
	agentMemberID := "mem_task_metadata_agent"
	agentID := "agt_task_metadata"
	machineID := "mac_task_metadata"
	channelID := "chn_task_metadata"

	var err error
	_, err = pool.Exec(ctx, `
INSERT INTO users (id, email, name, display_name, avatar_url, is_admin, password_hash, email_verified_at, created_at, updated_at)
VALUES ($1, 'task-metadata@example.com', 'task-metadata-user', 'Task Metadata User', '', FALSE, 'hash', $2, $2, $2)
`, userID, now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, $2, 'task-metadata', 'Task Metadata Workspace', 0, $3, $3)
`, workspaceID, testfixture.SeedWorkspaceOrganization(t, ctx, pool, workspaceID, now), now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, status, created_at, updated_at)
VALUES ($1, $2, 'human', 'Task Metadata User', $3, 'owner', 'active', $4, $4)
`, userMemberID, workspaceID, userID, now)
	require.NoError(t, err)

	seedTaskboardChannelScope(t, ctx, pool, workspaceID, channelID, "all", "", "", "public", "", now)

	_, err = pool.Exec(ctx, `
INSERT INTO machines (id, workspace_id, created_by_user_id, name, api_key, connected_at, created_at, updated_at)
VALUES ($1, $2, $3, 'task-metadata-machine', 'api_key_task_metadata', $4, $4, $4)
`, machineID, workspaceID, userID, now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO members (id, workspace_id, type, display_name, status, created_at, updated_at)
VALUES ($1, $2, 'agent', 'Suggested Agent Display', 'active', $3, $3)
`, agentMemberID, workspaceID, now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT IGNORE 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, $3, $3, NULL, NULL), ($1, $4, 'public', NULL, 0, 0, 0, $3, $3, NULL, NULL)
`, channelID, userMemberID, now, agentMemberID)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
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, 'suggested-agent', '', 'codex', 'gpt-5.4', '', '', '', TRUE, 'active', 'online', '', $5, $5)
`, agentID, workspaceID, machineID, agentMemberID, now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO scope_memberships (scope_id, member_id, visibility, owner_member_id, created_at, updated_at)
VALUES ($1, $2, 'public', NULL, $3, $3)
ON DUPLICATE KEY UPDATE
  visibility = VALUES(visibility),
  owner_member_id = VALUES(owner_member_id),
  removed_at = NULL,
  updated_at = VALUES(updated_at)
`, channelID, agentMemberID, now)
	require.NoError(t, err)

	seedTaskboardChannelScopeFacts(t, ctx, pool, workspaceID, now)

	service := New(pool)
	response, err := service.CreateTasks(ctx, CreateTasksRequest{
		Context: NewUserServiceContext(workspaceID, userID, "Task Metadata User"),
		Board:   BoardRef{ChannelID: channelID},
		Task: &TaskCreateInput{
			Title:       "Persist collaboration metadata",
			AssigneeRef: agentMemberID,
		},
		Metadata: &CreateTaskMetadata{
			SourceThreadChannelID:       "thr_task_metadata",
			SourceThreadParentChannelID: channelID,
			SourceThreadParentMessageID: "msg_parent_task_metadata",
			SourceThreadRef:             "#all:abcd1234",
		},
	})
	require.NoError(t, err)
	require.Len(t, response.Tasks, 1)
	require.NotNil(t, response.Tasks[0].AssigneeProfile)
	require.Equal(t, "agent", response.Tasks[0].AssigneeProfile.ActorType)
	require.Equal(t, agentID, response.Tasks[0].AssigneeProfile.PrincipalID)
	require.Equal(t, "Suggested Agent Display", response.Tasks[0].AssigneeProfile.DisplayName)
	require.NotNil(t, response.Tasks[0].SourceThread)
	require.Equal(t, "thr_task_metadata", response.Tasks[0].SourceThread.ThreadChannelID)
	require.Equal(t, channelID, response.Tasks[0].SourceThread.ParentChannelID)
	require.Equal(t, "msg_parent_task_metadata", response.Tasks[0].SourceThread.ParentMessageID)
	require.Equal(t, "#all:abcd1234", response.Tasks[0].SourceThread.Ref)
	require.Equal(t, message.ContributionSourceKindHumanCompose, response.Tasks[0].Source.Kind)
	require.Equal(t, message.ContributionSourceRefTypeMessage, response.Tasks[0].Source.RefType)
	require.Equal(t, "msg_parent_task_metadata", response.Tasks[0].Source.RefID)

	var assigneeMemberID string
	var sourceThreadChannelID string
	var sourceThreadParentChannelID string
	var sourceThreadParentMessageID string
	var sourceThreadRef string
	require.NoError(t, pool.QueryRow(ctx, `
SELECT assignee_member_id, source_thread_channel_id, source_thread_parent_channel_id, source_thread_parent_message_id, source_thread_ref
FROM tasks
WHERE id = $1
`, response.Tasks[0].ID).Scan(&assigneeMemberID, &sourceThreadChannelID, &sourceThreadParentChannelID, &sourceThreadParentMessageID, &sourceThreadRef))
	require.Equal(t, agentMemberID, assigneeMemberID)
	require.Equal(t, "thr_task_metadata", sourceThreadChannelID)
	require.Equal(t, channelID, sourceThreadParentChannelID)
	require.Equal(t, "msg_parent_task_metadata", sourceThreadParentMessageID)
	require.Equal(t, "#all:abcd1234", sourceThreadRef)

	listed, err := service.ListTasks(ctx, ListTasksRequest{
		Context: NewUserServiceContext(workspaceID, userID, "Task Metadata User"),
		Board:   BoardRef{ChannelID: channelID},
	})
	require.NoError(t, err)
	require.Len(t, listed.Tasks, 1)
	require.NotNil(t, listed.Tasks[0].AssigneeProfile)
	require.Equal(t, agentID, listed.Tasks[0].AssigneeProfile.PrincipalID)
	require.NotNil(t, listed.Tasks[0].SourceThread)
	require.Equal(t, "thr_task_metadata", listed.Tasks[0].SourceThread.ThreadChannelID)
	require.Equal(t, "msg_parent_task_metadata", listed.Tasks[0].SourceThread.ParentMessageID)
	require.Equal(t, message.ContributionSourceKindHumanCompose, listed.Tasks[0].Source.Kind)
	require.Equal(t, message.ContributionSourceRefTypeMessage, listed.Tasks[0].Source.RefType)
	require.Equal(t, "msg_parent_task_metadata", listed.Tasks[0].Source.RefID)
}

func TestTaskProjectionUsesHumanDisplayNamesInsteadOfLegacyAliases(t *testing.T) {
	ctx := context.Background()
	pool := startTaskboardTestPool(t, ctx)

	now := time.Date(2026, 3, 25, 8, 0, 0, 0, time.UTC)
	workspaceID := "wsp_task_display"
	userID := "usr_task_display"
	memberID := "mem_task_display"
	channelID := "chn_task_display"

	var err error
	_, err = pool.Exec(ctx, `
INSERT INTO users (id, email, name, display_name, avatar_url, is_admin, password_hash, email_verified_at, created_at, updated_at)
VALUES ($1, 'task-display@example.com', 'legacy-task-user', 'Visible Task User', '', FALSE, 'hash', $2, $2, $2)
`, userID, now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, $2, 'task-display', 'Task Display Workspace', 0, $3, $3)
`, workspaceID, testfixture.SeedWorkspaceOrganization(t, ctx, pool, workspaceID, now), now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, created_at, updated_at)
VALUES ($1, $2, 'human', 'Workspace Task User', $3, 'owner', $4, $4)
`, memberID, workspaceID, userID, now)
	require.NoError(t, err)

	seedTaskboardChannelScope(t, ctx, pool, workspaceID, channelID, "all", "", "", "public", "", now)

	_, err = pool.Exec(ctx, `
INSERT IGNORE 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, $3, $3, NULL, NULL)
`, channelID, memberID, now)
	require.NoError(t, err)

	seedTaskboardChannelScopeFacts(t, ctx, pool, workspaceID, now)

	service := New(pool)
	createResponse, err := service.CreateTasks(ctx, CreateTasksRequest{
		Context: NewUserServiceContext(workspaceID, userID, "Visible Task User"),
		Board:   BoardRef{ChannelID: channelID},
		Titles:  []string{"Ship visible task labels"},
	})
	require.NoError(t, err)
	require.Len(t, createResponse.Tasks, 1)
	require.Equal(t, userID, createResponse.Tasks[0].CreatedByID)
	require.Equal(t, "Visible Task User", createResponse.Tasks[0].CreatedByName)

	claimedTask, err := service.ClaimForUser(ctx, userID, workspaceID, createResponse.Tasks[0].ID)
	require.NoError(t, err)
	require.Equal(t, userID, claimedTask.AssigneeProfile.PrincipalID)
	require.Equal(t, "Visible Task User", claimedTask.AssigneeProfile.DisplayName)
}

func TestTaskProjectionUsesHumanDisplayNamesForAssignee(t *testing.T) {
	ctx := context.Background()
	pool := startTaskboardTestPool(t, ctx)

	now := time.Date(2026, 3, 25, 9, 0, 0, 0, time.UTC)
	workspaceID := "wsp_task_suggested_human"
	creatorUserID := "usr_task_creator"
	creatorMemberID := "mem_task_creator"
	targetUserID := "usr_task_target"
	targetMemberID := "mem_task_target"
	channelID := "chn_task_suggested_human"

	var err error
	_, err = pool.Exec(ctx, `
INSERT INTO users (id, email, name, display_name, avatar_url, is_admin, password_hash, email_verified_at, created_at, updated_at)
VALUES
  ($1, 'creator@example.com', 'legacy-creator', 'Visible Creator', '', FALSE, 'hash', $3, $3, $3),
  ($2, 'target@example.com', 'legacy-target', 'Visible Target', '', FALSE, 'hash', $3, $3, $3)
`, creatorUserID, targetUserID, now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, $2, 'task-suggested-human', 'Task Suggested Human Workspace', 0, $3, $3)
`, workspaceID, testfixture.SeedWorkspaceOrganization(t, ctx, pool, workspaceID, now), now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, created_at, updated_at)
VALUES
  ($1, $3, 'human', 'Workspace Creator', $4, 'owner', $5, $5),
  ($2, $3, 'human', 'Workspace Target', $6, 'member', $5, $5)
`, creatorMemberID, targetMemberID, workspaceID, creatorUserID, now, targetUserID)
	require.NoError(t, err)

	seedTaskboardChannelScope(t, ctx, pool, workspaceID, channelID, "all", "", "", "public", "", now)

	_, err = pool.Exec(ctx, `
INSERT IGNORE 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, $4, $4, NULL, NULL), ($1, $3, 'public', NULL, 0, 0, 0, $4, $4, NULL, NULL)
`, channelID, creatorMemberID, targetMemberID, now)
	require.NoError(t, err)

	seedTaskboardChannelScopeFacts(t, ctx, pool, workspaceID, now)

	service := New(pool)
	response, err := service.CreateTasks(ctx, CreateTasksRequest{
		Context: NewUserServiceContext(workspaceID, creatorUserID, "Visible Creator"),
		Board:   BoardRef{ChannelID: channelID},
		Task: &TaskCreateInput{
			Title:       "Assign the visible target",
			AssigneeRef: targetMemberID,
		},
	})
	require.NoError(t, err)
	require.Len(t, response.Tasks, 1)
	require.NotNil(t, response.Tasks[0].AssigneeProfile)
	require.Equal(t, "human", response.Tasks[0].AssigneeProfile.ActorType)
	require.Equal(t, targetUserID, response.Tasks[0].AssigneeProfile.PrincipalID)
	require.Equal(t, "Visible Target", response.Tasks[0].AssigneeProfile.DisplayName)
}

func TestAgentTaskViewUsesDisplayLabelsForHumanActors(t *testing.T) {
	ctx := context.Background()
	pool := startTaskboardTestPool(t, ctx)

	now := time.Date(2026, 3, 25, 10, 0, 0, 0, time.UTC)
	workspaceID := "wsp_agent_task_display"
	userID := "usr_agent_task_display"
	userMemberID := "mem_agent_task_display_user"
	agentID := "agt_agent_task_display"
	agentMemberID := "mem_agent_task_display_agent"
	machineID := "mac_agent_task_display"
	channelID := "chn_agent_task_display"

	var err error
	_, err = pool.Exec(ctx, `
INSERT INTO users (id, email, name, display_name, avatar_url, is_admin, password_hash, email_verified_at, created_at, updated_at)
VALUES ($1, 'agent-task-display@example.com', 'legacy-agent-task-user', 'Visible Agent Task User', '', FALSE, 'hash', $2, $2, $2)
`, userID, now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, $2, 'agent-task-display', 'Agent Task Display Workspace', 0, $3, $3)
`, workspaceID, testfixture.SeedWorkspaceOrganization(t, ctx, pool, workspaceID, now), now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, status, created_at, updated_at)
VALUES
  ($1, $3, 'human', 'Workspace Agent Task User', $4, 'owner', 'active', $5, $5),
  ($2, $3, 'agent', 'Agent Task App', NULL, NULL, 'active', $5, $5)
`, userMemberID, agentMemberID, workspaceID, userID, now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO machines (id, workspace_id, created_by_user_id, name, api_key, connected_at, created_at, updated_at)
VALUES ($1, $2, $3, 'agent-task-machine', 'api_key_agent_task', $4, $4, $4)
`, machineID, workspaceID, userID, now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
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-agent', '', 'codex', 'gpt-5.4', '', '', '', TRUE, 'active', 'online', '', $5, $5)
`, agentID, workspaceID, machineID, agentMemberID, now)
	require.NoError(t, err)

	seedTaskboardChannelScope(t, ctx, pool, workspaceID, channelID, "all", "", "", "public", "", now)

	_, err = pool.Exec(ctx, `
INSERT IGNORE 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, $4, $4, NULL, NULL), ($1, $3, 'public', NULL, 0, 0, 0, $4, $4, NULL, NULL)
`, channelID, userMemberID, agentMemberID, now)
	require.NoError(t, err)

	seedTaskboardChannelScopeFacts(t, ctx, pool, workspaceID, now)

	service := New(pool)
	createResponse, err := service.CreateTasks(ctx, CreateTasksRequest{
		Context: NewUserServiceContext(workspaceID, userID, "Visible Agent Task User"),
		Board:   BoardRef{ChannelID: channelID},
		Titles:  []string{"Show display labels to agents"},
	})
	require.NoError(t, err)

	_, err = service.ClaimForUser(ctx, userID, workspaceID, createResponse.Tasks[0].ID)
	require.NoError(t, err)

	listResponse, err := service.ListTasks(ctx, ListTasksRequest{
		Context: NewAgentServiceContext(workspaceID, agentID, agentMemberID),
		Board:   BoardRef{Channel: "#all"},
		Status:  "all",
	})
	require.NoError(t, err)
	require.Len(t, listResponse.TaskViews, 1)
	require.Equal(t, "Visible Agent Task User", listResponse.TaskViews[0].CreatedByName)
	require.Equal(t, "Visible Agent Task User", listResponse.TaskViews[0].AssigneeProfile.DisplayName)
}

func TestTaskProjectionUsesDeletedAgentDisplayFallback(t *testing.T) {
	ctx := context.Background()
	pool := startTaskboardTestPool(t, ctx)

	now := time.Date(2026, 3, 25, 11, 0, 0, 0, time.UTC)
	workspaceID := "wsp_deleted_agent_task_display"
	userID := "usr_deleted_agent_task_display"
	userMemberID := "mem_deleted_agent_task_display_user"
	agentID := "agt_deleted_agent_task_display"
	agentMemberID := "mem_deleted_agent_task_display_agent"
	machineID := "mac_deleted_agent_task_display"
	channelID := "chn_deleted_agent_task_display"

	var err error
	_, err = pool.Exec(ctx, `
INSERT INTO users (id, email, name, display_name, avatar_url, is_admin, password_hash, email_verified_at, created_at, updated_at)
VALUES ($1, 'deleted-agent-task-display@example.com', 'legacy-deleted-agent-user', 'Visible Deleted Agent User', '', FALSE, 'hash', $2, $2, $2)
`, userID, now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, $2, 'deleted-agent-task-display', 'Deleted Agent Task Display Workspace', 0, $3, $3)
`, workspaceID, testfixture.SeedWorkspaceOrganization(t, ctx, pool, workspaceID, now), now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, status, created_at, updated_at)
VALUES
  ($1, $3, 'human', 'Workspace Deleted Agent User', $4, 'owner', 'active', $5, $5),
  ($2, $3, 'agent', 'Deleted Agent Display', NULL, NULL, 'active', $5, $5)
`, userMemberID, agentMemberID, workspaceID, userID, now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO machines (id, workspace_id, created_by_user_id, name, api_key, connected_at, created_at, updated_at)
VALUES ($1, $2, $3, 'deleted-agent-task-machine', 'api_key_deleted_agent_task', $4, $4, $4)
`, machineID, workspaceID, userID, now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
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, 'deleted-agent-handle', '', 'codex', 'gpt-5.4', '', '', '', TRUE, 'active', 'online', '', $5, $5)
`, agentID, workspaceID, machineID, agentMemberID, now)
	require.NoError(t, err)

	seedTaskboardChannelScope(t, ctx, pool, workspaceID, channelID, "all", "", "", "public", "", now)

	_, err = pool.Exec(ctx, `
INSERT IGNORE 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, $4, $4, NULL, NULL), ($1, $3, 'public', NULL, 0, 0, 0, $4, $4, NULL, NULL)
`, channelID, userMemberID, agentMemberID, now)
	require.NoError(t, err)

	seedTaskboardChannelScopeFacts(t, ctx, pool, workspaceID, now)

	service := New(pool)
	createResponse, err := service.CreateTasks(ctx, CreateTasksRequest{
		Context: NewAgentServiceContext(workspaceID, agentID, agentMemberID),
		Board:   BoardRef{Channel: "#all"},
		Titles:  []string{"Keep deleted-agent task labels readable"},
	})
	require.NoError(t, err)
	require.Len(t, createResponse.Tasks, 1)

	_, claimResults, _, err := service.ClaimForAgent(ctx, AgentActor{
		WorkspaceID: workspaceID,
		MemberID:    agentMemberID,
	}, "#all", []int64{createResponse.Tasks[0].TaskNumber})
	require.NoError(t, err)
	require.Len(t, claimResults, 1)
	require.True(t, claimResults[0].Success)

	listWhileAgentExists, err := service.ListTasks(ctx, ListTasksRequest{
		Context: NewUserServiceContext(workspaceID, userID, "Visible Deleted Agent User"),
		Board:   BoardRef{ChannelID: channelID},
	})
	require.NoError(t, err)
	require.Len(t, listWhileAgentExists.Tasks, 1)
	require.Equal(t, "Deleted Agent Display", listWhileAgentExists.Tasks[0].CreatedByName)
	require.Equal(t, "Deleted Agent Display", listWhileAgentExists.Tasks[0].AssigneeProfile.DisplayName)

	_, err = pool.Exec(ctx, `DELETE FROM agents WHERE workspace_id = $1 AND id = $2`, workspaceID, agentID)
	require.NoError(t, err)

	listResponse, err := service.ListTasks(ctx, ListTasksRequest{
		Context: NewUserServiceContext(workspaceID, userID, "Visible Deleted Agent User"),
		Board:   BoardRef{ChannelID: channelID},
	})
	require.NoError(t, err)
	require.Len(t, listResponse.Tasks, 1)
	require.Equal(t, "agent", listResponse.Tasks[0].CreatedByType)
	require.Equal(t, "Deleted Agent Display", listResponse.Tasks[0].CreatedByName)
	require.Equal(t, "agent", listResponse.Tasks[0].AssigneeProfile.ActorType)
	require.Equal(t, "Deleted Agent Display", listResponse.Tasks[0].AssigneeProfile.DisplayName)
}

func TestAgentTaskViewUsesAgentDisplayLabelsInsteadOfHandles(t *testing.T) {
	ctx := context.Background()
	pool := startTaskboardTestPool(t, ctx)

	now := time.Date(2026, 3, 25, 12, 0, 0, 0, time.UTC)
	workspaceID := "wsp_agent_task_actor_display"
	userID := "usr_agent_task_actor_display"
	userMemberID := "mem_agent_task_actor_display_user"
	agentID := "agt_agent_task_actor_display"
	agentMemberID := "mem_agent_task_actor_display_agent"
	machineID := "mac_agent_task_actor_display"
	channelID := "chn_agent_task_actor_display"

	var err error
	_, err = pool.Exec(ctx, `
INSERT INTO users (id, email, name, display_name, avatar_url, is_admin, password_hash, email_verified_at, created_at, updated_at)
VALUES ($1, 'agent-task-actor-display@example.com', 'legacy-agent-task-actor-user', 'Visible Agent Task Actor User', '', FALSE, 'hash', $2, $2, $2)
`, userID, now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, $2, 'agent-task-actor-display', 'Agent Task Actor Display Workspace', 0, $3, $3)
`, workspaceID, testfixture.SeedWorkspaceOrganization(t, ctx, pool, workspaceID, now), now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, status, created_at, updated_at)
VALUES
  ($1, $3, 'human', 'Workspace Agent Task Actor User', $4, 'owner', 'active', $5, $5),
  ($2, $3, 'agent', 'Visible Agent Actor', NULL, NULL, 'active', $5, $5)
`, userMemberID, agentMemberID, workspaceID, userID, now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
INSERT INTO machines (id, workspace_id, created_by_user_id, name, api_key, connected_at, created_at, updated_at)
VALUES ($1, $2, $3, 'agent-task-actor-machine', 'api_key_agent_task_actor', $4, $4, $4)
`, machineID, workspaceID, userID, now)
	require.NoError(t, err)

	_, err = pool.Exec(ctx, `
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, 'agent-task-handle', '', 'codex', 'gpt-5.4', '', '', '', TRUE, 'active', 'online', '', $5, $5)
`, agentID, workspaceID, machineID, agentMemberID, now)
	require.NoError(t, err)

	seedTaskboardChannelScope(t, ctx, pool, workspaceID, channelID, "all", "", "", "public", "", now)

	_, err = pool.Exec(ctx, `
INSERT IGNORE 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, $4, $4, NULL, NULL), ($1, $3, 'public', NULL, 0, 0, 0, $4, $4, NULL, NULL)
`, channelID, userMemberID, agentMemberID, now)
	require.NoError(t, err)

	seedTaskboardChannelScopeFacts(t, ctx, pool, workspaceID, now)

	service := New(pool)
	createResponse, err := service.CreateTasks(ctx, CreateTasksRequest{
		Context: NewAgentServiceContext(workspaceID, agentID, agentMemberID),
		Board:   BoardRef{Channel: "#all"},
		Titles:  []string{"Show agent display labels to agents"},
	})
	require.NoError(t, err)
	require.Len(t, createResponse.Tasks, 1)

	_, claimResults, _, err := service.ClaimForAgent(ctx, AgentActor{
		WorkspaceID: workspaceID,
		MemberID:    agentMemberID,
	}, "#all", []int64{createResponse.Tasks[0].TaskNumber})
	require.NoError(t, err)
	require.Len(t, claimResults, 1)
	require.True(t, claimResults[0].Success)

	listResponse, err := service.ListTasks(ctx, ListTasksRequest{
		Context: NewAgentServiceContext(workspaceID, agentID, agentMemberID),
		Board:   BoardRef{Channel: "#all"},
		Status:  "all",
	})
	require.NoError(t, err)
	require.Len(t, listResponse.TaskViews, 1)
	require.Equal(t, "Visible Agent Actor", listResponse.TaskViews[0].CreatedByName)
	require.Equal(t, "Visible Agent Actor", listResponse.TaskViews[0].AssigneeProfile.DisplayName)

	detailResponse, err := service.GetTaskDetail(ctx, GetTaskDetailRequest{
		Context: NewAgentServiceContext(workspaceID, agentID, agentMemberID),
		Board:   BoardRef{Channel: "#all"},
		Task:    TaskRef{TaskNumber: createResponse.Tasks[0].TaskNumber},
	})
	require.NoError(t, err)
	require.Len(t, taskDetailActivityEvents(detailResponse.Detail), 2)
	require.Equal(t, agentMemberID, taskDetailActivityEvents(detailResponse.Detail)[0].ActorRef)
	require.Equal(t, "Visible Agent Actor", taskDetailActivityEvents(detailResponse.Detail)[0].ActorDisplayName)
	require.Equal(t, agentMemberID, taskDetailActivityEvents(detailResponse.Detail)[1].ActorRef)
	require.Equal(t, "Visible Agent Actor", taskDetailActivityEvents(detailResponse.Detail)[1].ActorDisplayName)
}

func seedTaskboardChannelScope(t *testing.T, ctx context.Context, pool interface {
	Exec(context.Context, string, ...any) (coredb.Result, error)
}, workspaceID string, channelID string, slug string, displayName string, description string, visibility string, ownerMemberID string, now time.Time) {
	t.Helper()
	if displayName == "" {
		displayName = slug
	}
	if visibility == "" {
		visibility = "public"
	}
	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', CONCAT('channel:', $3), 0, 0, NULL, 'active', NULLIF($7, ''), $8, $8)
ON DUPLICATE KEY UPDATE
  identity_key = VALUES(identity_key),
  status = VALUES(status),
  created_by_member_id = VALUES(created_by_member_id),
  updated_at = VALUES(updated_at)
`, channelID, workspaceID, slug, displayName, description, visibility, 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, $3, $4, $5, $6, NULLIF($7, ''), $3 = 'all', $8, $8)
ON DUPLICATE KEY UPDATE
  slug = VALUES(slug),
  display_name = VALUES(display_name),
  description = VALUES(description),
  visibility = VALUES(visibility),
  owner_member_id = VALUES(owner_member_id),
  is_all_channel = VALUES(is_all_channel),
  updated_at = VALUES(updated_at)
`, channelID, workspaceID, slug, displayName, description, visibility, ownerMemberID, now)
}

func seedTaskboardChannelScopeFacts(t *testing.T, ctx context.Context, pool interface {
	Exec(context.Context, string, ...any) (coredb.Result, error)
}, workspaceID string, now time.Time) {
	t.Helper()
	// Scope facts are seeded explicitly by seedTaskboardChannelScope in these tests.
}
