package taskboard

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

	"github.com/stretchr/testify/require"

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

func TestDeleteTaskWithSubtasksRequiresSubtaskMode(t *testing.T) {
	ctx := context.Background()
	fixture := seedTaskboardUserFixture(t, ctx)
	parent, children := createTaskboardParentWithSubtasks(t, ctx, fixture)

	_, err := fixture.service.DeleteTask(ctx, DeleteTaskRequest{
		Context: taskboardUserContext(fixture),
		Task:    TaskRef{TaskID: parent.ID},
	})
	require.ErrorIs(t, err, ErrBadRequest)
	require.Contains(t, err.Error(), "subtaskMode")

	require.Equal(t, int64(1), countTaskRowsByID(t, ctx, fixture, parent.ID))
	for _, child := range children {
		require.Equal(t, parent.ID, taskParentRef(t, ctx, fixture, child.ID))
	}
}

func TestDeleteTaskPromotesSubtasks(t *testing.T) {
	ctx := context.Background()
	fixture := seedTaskboardUserFixture(t, ctx)
	parent, children := createTaskboardParentWithSubtasks(t, ctx, fixture)

	response, err := fixture.service.DeleteTask(ctx, DeleteTaskRequest{
		Context:     taskboardUserContext(fixture),
		Task:        TaskRef{TaskID: parent.ID},
		SubtaskMode: DeleteTaskSubtaskModePromote,
	})
	require.NoError(t, err)
	require.Equal(t, fixture.channelID, response.ChannelID)
	require.Equal(t, []string{parent.ID}, response.DeletedTaskIDs)
	require.Len(t, response.PromotedTasks, len(children))
	for _, promoted := range response.PromotedTasks {
		require.Empty(t, promoted.ParentTaskRef)
	}

	require.Zero(t, countTaskRowsByID(t, ctx, fixture, parent.ID))
	require.Equal(t, "deleted", taskScopeStatus(t, ctx, fixture, parent.TaskScopeID))
	for _, child := range children {
		require.Equal(t, int64(1), countTaskRowsByID(t, ctx, fixture, child.ID))
		require.Empty(t, taskParentRef(t, ctx, fixture, child.ID))
		require.Equal(t, "active", taskScopeStatus(t, ctx, fixture, child.TaskScopeID))
	}
}

func TestDeleteTaskDeletesSubtasks(t *testing.T) {
	ctx := context.Background()
	fixture := seedTaskboardUserFixture(t, ctx)
	parent, children := createTaskboardParentWithSubtasks(t, ctx, fixture)
	deletedTasks := append([]Task{parent}, children...)
	statuses := []string{"pending", "leased", "failed"}
	for index, task := range deletedTasks {
		seedDeletedTaskConvergenceState(
			t,
			ctx,
			fixture,
			task.TaskScopeID,
			statuses[index],
		)
	}
	seedTaskFanoutOutbox(
		t,
		ctx,
		fixture,
		parent.TaskScopeID,
		"delivered",
		"delivered",
	)

	response, err := fixture.service.DeleteTask(ctx, DeleteTaskRequest{
		Context:     taskboardUserContext(fixture),
		Task:        TaskRef{TaskID: parent.ID},
		SubtaskMode: DeleteTaskSubtaskModeDelete,
	})
	require.NoError(t, err)
	require.Equal(t, fixture.channelID, response.ChannelID)
	require.Empty(t, response.PromotedTasks)
	require.ElementsMatch(t, []string{children[0].ID, children[1].ID, parent.ID}, response.DeletedTaskIDs)

	require.Zero(t, countTaskRowsByID(t, ctx, fixture, parent.ID))
	require.Equal(t, "deleted", taskScopeStatus(t, ctx, fixture, parent.TaskScopeID))
	for _, child := range children {
		require.Zero(t, countTaskRowsByID(t, ctx, fixture, child.ID))
		require.Equal(t, "deleted", taskScopeStatus(t, ctx, fixture, child.TaskScopeID))
	}
	for index, task := range deletedTasks {
		var archived bool
		require.NoError(t, fixture.pool.QueryRow(ctx, `
SELECT archived_at IS NOT NULL
FROM im_conversation_index
WHERE workspace_id = $1
  AND member_id = $2
  AND conversation_id = $3
`, fixture.workspaceID, fixture.ownerMemberID, "task:"+task.TaskScopeID).Scan(&archived))
		require.True(t, archived)

		var status string
		var reason string
		var suppressed bool
		var leaseCleared bool
		require.NoError(t, fixture.pool.QueryRow(ctx, `
SELECT status,
       suppression_reason,
       suppressed_at IS NOT NULL,
       lease_owner IS NULL AND lease_expires_at IS NULL
FROM scope_fanout_outbox
WHERE id = $1
`, taskFanoutOutboxID(task.TaskScopeID, statuses[index])).Scan(
			&status,
			&reason,
			&suppressed,
			&leaseCleared,
		))
		require.Equal(t, "suppressed", status)
		require.Equal(t, "task_deleted", reason)
		require.True(t, suppressed)
		require.True(t, leaseCleared)
	}

	var deliveredStatus string
	var deliveredSuppressed bool
	require.NoError(t, fixture.pool.QueryRow(ctx, `
SELECT status, suppressed_at IS NOT NULL
FROM scope_fanout_outbox
WHERE id = $1
`, taskFanoutOutboxID(parent.TaskScopeID, "delivered")).Scan(&deliveredStatus, &deliveredSuppressed))
	require.Equal(t, "delivered", deliveredStatus)
	require.False(t, deliveredSuppressed)
}

func TestDeleteTaskDeletesSubtasksRequiresChildDeletePermission(t *testing.T) {
	ctx := context.Background()
	fixture := seedTaskboardUserFixture(t, ctx)
	reviewerContext := NewUserServiceContext(fixture.workspaceID, fixture.reviewerUserID, fixture.reviewerDisplayName)
	parent, children := createTaskboardParentWithSubtasksForContext(t, ctx, fixture, reviewerContext)

	mustExecTaskboard(t, ctx, fixture.pool, `
UPDATE tasks
SET created_by_member_id = $2,
    created_by_ref = $2
WHERE id = $1
`, children[0].ID, fixture.ownerMemberID)

	_, err := fixture.service.DeleteTask(ctx, DeleteTaskRequest{
		Context:     reviewerContext,
		Task:        TaskRef{TaskID: parent.ID},
		SubtaskMode: DeleteTaskSubtaskModeDelete,
	})
	require.ErrorIs(t, err, ErrForbidden)
	require.Equal(t, int64(1), countTaskRowsByID(t, ctx, fixture, parent.ID))
	for _, child := range children {
		require.Equal(t, int64(1), countTaskRowsByID(t, ctx, fixture, child.ID))
		require.Equal(t, parent.ID, taskParentRef(t, ctx, fixture, child.ID))
		require.Equal(t, "active", taskScopeStatus(t, ctx, fixture, child.TaskScopeID))
	}
}

func TestAgentDeleteTaskDeletesOwnLeafByBoardNumber(t *testing.T) {
	ctx := context.Background()
	fixture := seedTaskboardUserFixture(t, ctx)
	agentContext := NewAgentServiceContext(fixture.workspaceID, fixture.agentID, fixture.agentMemberID)

	created, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: agentContext,
		Board:   BoardRef{Channel: "#all"},
		Titles:  []string{"Agent-owned disposable task"},
	})
	require.NoError(t, err)
	require.Len(t, created.Tasks, 1)
	task := created.Tasks[0]

	response, err := fixture.service.DeleteTask(ctx, DeleteTaskRequest{
		Context: agentContext,
		Board:   BoardRef{Channel: "#all"},
		Task:    TaskRef{TaskNumber: task.TaskNumber},
	})
	require.NoError(t, err)
	require.Equal(t, fixture.channelID, response.ChannelID)
	require.Equal(t, []string{task.ID}, response.DeletedTaskIDs)
	require.Zero(t, countTaskRowsByID(t, ctx, fixture, task.ID))
	require.Equal(t, "deleted", taskScopeStatus(t, ctx, fixture, task.TaskScopeID))

	_, err = fixture.service.DeleteTask(ctx, DeleteTaskRequest{
		Context: agentContext,
		Board:   BoardRef{Channel: "#all"},
		Task:    TaskRef{TaskNumber: task.TaskNumber},
	})
	require.ErrorIs(t, err, ErrNotFound, "delete replay must not disclose deleted task state")
}

func TestAgentDeleteTaskRejectsTaskCreatedByAnotherMember(t *testing.T) {
	ctx := context.Background()
	fixture := seedTaskboardUserFixture(t, ctx)
	created, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: taskboardUserContext(fixture),
		Board:   BoardRef{ChannelID: fixture.channelID},
		Titles:  []string{"Human-owned protected task"},
	})
	require.NoError(t, err)
	task := created.Tasks[0]

	_, err = fixture.service.DeleteTask(ctx, DeleteTaskRequest{
		Context: NewAgentServiceContext(fixture.workspaceID, fixture.agentID, fixture.agentMemberID),
		Board:   BoardRef{Channel: "#all"},
		Task:    TaskRef{TaskNumber: task.TaskNumber},
	})
	require.ErrorIs(t, err, ErrForbidden)
	require.Equal(t, int64(1), countTaskRowsByID(t, ctx, fixture, task.ID))
	require.Equal(t, "active", taskScopeStatus(t, ctx, fixture, task.TaskScopeID))
}

func TestAgentDeleteTaskRejectsInvalidModeAndInaccessibleBoard(t *testing.T) {
	ctx := context.Background()
	fixture := seedTaskboardUserFixture(t, ctx)
	agentContext := NewAgentServiceContext(fixture.workspaceID, fixture.agentID, fixture.agentMemberID)
	created, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: agentContext,
		Board:   BoardRef{Channel: "#all"},
		Titles:  []string{"Agent delete validation task"},
	})
	require.NoError(t, err)
	task := created.Tasks[0]

	_, err = fixture.service.DeleteTask(ctx, DeleteTaskRequest{
		Context:     agentContext,
		Board:       BoardRef{Channel: "#all"},
		Task:        TaskRef{TaskNumber: task.TaskNumber},
		SubtaskMode: DeleteTaskSubtaskMode("cascade"),
	})
	require.ErrorIs(t, err, ErrBadRequest)
	require.Equal(t, int64(1), countTaskRowsByID(t, ctx, fixture, task.ID))

	_, err = fixture.service.DeleteTask(ctx, DeleteTaskRequest{
		Context: agentContext,
		Board:   BoardRef{Channel: "#unknown-private-board"},
		Task:    TaskRef{TaskNumber: task.TaskNumber},
	})
	require.ErrorIs(t, err, ErrForbidden)
	require.Equal(t, int64(1), countTaskRowsByID(t, ctx, fixture, task.ID))
}

func TestAgentDeleteTaskWithSubtasksRequiresMode(t *testing.T) {
	ctx := context.Background()
	fixture := seedTaskboardUserFixture(t, ctx)
	agentContext := NewAgentServiceContext(fixture.workspaceID, fixture.agentID, fixture.agentMemberID)
	parent, children := createTaskboardParentWithSubtasksForContext(t, ctx, fixture, agentContext)

	_, err := fixture.service.DeleteTask(ctx, DeleteTaskRequest{
		Context: agentContext,
		Board:   BoardRef{Channel: "#all"},
		Task:    TaskRef{TaskNumber: parent.TaskNumber},
	})
	require.ErrorIs(t, err, ErrBadRequest)
	require.Equal(t, int64(1), countTaskRowsByID(t, ctx, fixture, parent.ID))
	for _, child := range children {
		require.Equal(t, parent.ID, taskParentRef(t, ctx, fixture, child.ID))
	}
}

func TestAgentDeleteTaskPromotesSubtasksWithoutChildOwnership(t *testing.T) {
	ctx := context.Background()
	fixture := seedTaskboardUserFixture(t, ctx)
	sink := &recordingTaskChangeSink{}
	fixture.service.taskChangeSink = sink
	agentContext := NewAgentServiceContext(fixture.workspaceID, fixture.agentID, fixture.agentMemberID)
	parent, children := createTaskboardParentWithSubtasksForContext(t, ctx, fixture, agentContext)
	sink.events = nil
	mustExecTaskboard(t, ctx, fixture.pool, `
UPDATE tasks SET created_by_member_id = $2, created_by_ref = $2 WHERE id = $1
`, children[0].ID, fixture.ownerMemberID)

	response, err := fixture.service.DeleteTask(ctx, DeleteTaskRequest{
		Context:     agentContext,
		Board:       BoardRef{Channel: "#all"},
		Task:        TaskRef{TaskNumber: parent.TaskNumber},
		SubtaskMode: DeleteTaskSubtaskModePromote,
	})
	require.NoError(t, err)
	require.Equal(t, []int64{parent.TaskNumber}, response.DeletedTaskNumbers)
	require.ElementsMatch(t, []int64{children[0].TaskNumber, children[1].TaskNumber}, response.PromotedTaskNumbers)
	require.Zero(t, countTaskRowsByID(t, ctx, fixture, parent.ID))
	for _, child := range children {
		require.Equal(t, int64(1), countTaskRowsByID(t, ctx, fixture, child.ID))
		require.Empty(t, taskParentRef(t, ctx, fixture, child.ID))
	}
	require.Len(t, sink.events, 3)
	require.Equal(t, []TaskChangeType{TaskChangeUpdated, TaskChangeUpdated, TaskChangeDeleted}, []TaskChangeType{
		sink.events[0].EventType, sink.events[1].EventType, sink.events[2].EventType,
	})
	require.ElementsMatch(t, []string{children[0].ID, children[1].ID}, []string{sink.events[0].Task.ID, sink.events[1].Task.ID})
	require.Equal(t, parent.ID, sink.events[2].TaskID)
}

func TestAgentDeleteTaskDeletesOwnedTree(t *testing.T) {
	ctx := context.Background()
	fixture := seedTaskboardUserFixture(t, ctx)
	sink := &recordingTaskChangeSink{}
	fixture.service.taskChangeSink = sink
	agentContext := NewAgentServiceContext(fixture.workspaceID, fixture.agentID, fixture.agentMemberID)
	parent, children := createTaskboardParentWithSubtasksForContext(t, ctx, fixture, agentContext)
	sink.events = nil

	response, err := fixture.service.DeleteTask(ctx, DeleteTaskRequest{
		Context:     agentContext,
		Board:       BoardRef{Channel: "#all"},
		Task:        TaskRef{TaskNumber: parent.TaskNumber},
		SubtaskMode: DeleteTaskSubtaskModeDelete,
	})
	require.NoError(t, err)
	require.ElementsMatch(t,
		[]int64{parent.TaskNumber, children[0].TaskNumber, children[1].TaskNumber},
		response.DeletedTaskNumbers,
	)
	require.Empty(t, response.PromotedTaskNumbers)
	require.Zero(t, countTaskRowsByID(t, ctx, fixture, parent.ID))
	require.Equal(t, "deleted", taskScopeStatus(t, ctx, fixture, parent.TaskScopeID))
	for _, child := range children {
		require.Zero(t, countTaskRowsByID(t, ctx, fixture, child.ID))
		require.Equal(t, "deleted", taskScopeStatus(t, ctx, fixture, child.TaskScopeID))
	}
	require.Len(t, sink.events, 3)
	for _, event := range sink.events {
		require.Equal(t, TaskChangeDeleted, event.EventType)
	}
	require.Equal(t, []string{children[0].ID, children[1].ID, parent.ID}, []string{
		sink.events[0].TaskID, sink.events[1].TaskID, sink.events[2].TaskID,
	})
}

func TestAgentDeleteTaskDeletesArbitraryDepthTree(t *testing.T) {
	ctx := context.Background()
	fixture := seedTaskboardUserFixture(t, ctx)
	sink := &recordingTaskChangeSink{}
	fixture.service.taskChangeSink = sink
	agentContext := NewAgentServiceContext(fixture.workspaceID, fixture.agentID, fixture.agentMemberID)
	parent, children := createTaskboardParentWithSubtasksForContext(t, ctx, fixture, agentContext)
	grandchildResponse, err := fixture.service.CreateSubtasks(ctx, CreateSubtasksRequest{
		Context: agentContext,
		Board:   BoardRef{Channel: "#all"},
		Task:    TaskRef{TaskNumber: children[0].TaskNumber},
		Titles:  []string{"Grandchild to delete"},
	})
	require.NoError(t, err)
	require.Len(t, grandchildResponse.Tasks, 1)
	grandchild := grandchildResponse.Tasks[0]
	sink.events = nil

	response, err := fixture.service.DeleteTask(ctx, DeleteTaskRequest{
		Context:     agentContext,
		Board:       BoardRef{Channel: "#all"},
		Task:        TaskRef{TaskNumber: parent.TaskNumber},
		SubtaskMode: DeleteTaskSubtaskModeDelete,
	})
	require.NoError(t, err)
	require.ElementsMatch(t, []int64{parent.TaskNumber, children[0].TaskNumber, children[1].TaskNumber, grandchild.TaskNumber}, response.DeletedTaskNumbers)
	require.Empty(t, response.PromotedTaskNumbers)
	for _, task := range append(children, grandchild) {
		require.Zero(t, countTaskRowsByID(t, ctx, fixture, task.ID))
		require.Equal(t, "deleted", taskScopeStatus(t, ctx, fixture, task.TaskScopeID))
	}
	require.Zero(t, countTaskRowsByID(t, ctx, fixture, parent.ID))
	require.Len(t, sink.events, 4)
	require.Equal(t, []TaskChangeType{TaskChangeDeleted, TaskChangeDeleted, TaskChangeDeleted, TaskChangeDeleted}, []TaskChangeType{
		sink.events[0].EventType, sink.events[1].EventType, sink.events[2].EventType, sink.events[3].EventType,
	})
}

func TestAgentDeleteTaskRejectsMixedOwnershipAtomically(t *testing.T) {
	ctx := context.Background()
	fixture := seedTaskboardUserFixture(t, ctx)
	sink := &recordingTaskChangeSink{}
	fixture.service.taskChangeSink = sink
	agentContext := NewAgentServiceContext(fixture.workspaceID, fixture.agentID, fixture.agentMemberID)
	parent, children := createTaskboardParentWithSubtasksForContext(t, ctx, fixture, agentContext)
	sink.events = nil
	mustExecTaskboard(t, ctx, fixture.pool, `
UPDATE tasks SET created_by_member_id = $2, created_by_ref = $2 WHERE id = $1
`, children[0].ID, fixture.ownerMemberID)

	_, err := fixture.service.DeleteTask(ctx, DeleteTaskRequest{
		Context:     agentContext,
		Board:       BoardRef{Channel: "#all"},
		Task:        TaskRef{TaskNumber: parent.TaskNumber},
		SubtaskMode: DeleteTaskSubtaskModeDelete,
	})
	require.ErrorIs(t, err, ErrForbidden)
	require.Equal(t, int64(1), countTaskRowsByID(t, ctx, fixture, parent.ID))
	require.Equal(t, "active", taskScopeStatus(t, ctx, fixture, parent.TaskScopeID))
	for _, child := range children {
		require.Equal(t, int64(1), countTaskRowsByID(t, ctx, fixture, child.ID))
		require.Equal(t, parent.ID, taskParentRef(t, ctx, fixture, child.ID))
		require.Equal(t, "active", taskScopeStatus(t, ctx, fixture, child.TaskScopeID))
	}
	require.Empty(t, sink.events, "failed atomic delete must not emit committed Task changes")
}

func TestAgentDeleteOwnershipCheckRejectsMissingAndDuplicateTaskIDs(t *testing.T) {
	ctx := context.Background()
	fixture := seedTaskboardUserFixture(t, ctx)
	agentContext := NewAgentServiceContext(fixture.workspaceID, fixture.agentID, fixture.agentMemberID)
	created, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: agentContext,
		Board:   BoardRef{Channel: "#all"},
		Titles:  []string{"Ownership completeness task"},
	})
	require.NoError(t, err)
	task := created.Tasks[0]

	err = coredb.WithTx(ctx, fixture.pool, func(tx coredb.Tx) error {
		allowed, err := fixture.service.InTx(tx).tasksCreatedByMember(
			ctx,
			tx,
			fixture.agentMemberID,
			[]string{task.ID, task.ID, "tsk_missing"},
		)
		require.NoError(t, err)
		require.False(t, allowed, "missing ids must never be treated as owned")
		return nil
	})
	require.NoError(t, err)
}

func createTaskboardParentWithSubtasks(t *testing.T, ctx context.Context, fixture taskboardUserFixture) (Task, []Task) {
	return createTaskboardParentWithSubtasksForContext(t, ctx, fixture, taskboardUserContext(fixture))
}

func createTaskboardParentWithSubtasksForContext(t *testing.T, ctx context.Context, fixture taskboardUserFixture, serviceContext ServiceContext) (Task, []Task) {
	t.Helper()

	parentResponse, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: serviceContext,
		Board:   taskboardBoardRefForContext(serviceContext, fixture.channelID),
		Titles:  []string{"Delete parent task"},
	})
	require.NoError(t, err)
	require.Len(t, parentResponse.Tasks, 1)
	parentRef := TaskRef{TaskID: parentResponse.Tasks[0].ID}
	if serviceContext.principalType() == PrincipalTypeAgent {
		parentRef = TaskRef{TaskNumber: parentResponse.Tasks[0].TaskNumber}
	}

	subtaskResponse, err := fixture.service.CreateSubtasks(ctx, CreateSubtasksRequest{
		Context: serviceContext,
		Board:   taskboardBoardRefForContext(serviceContext, fixture.channelID),
		Task:    parentRef,
		Titles:  []string{"Keep or delete child one", "Keep or delete child two"},
	})
	require.NoError(t, err)
	require.Len(t, subtaskResponse.Tasks, 2)
	return parentResponse.Tasks[0], subtaskResponse.Tasks
}

func taskboardBoardRefForContext(serviceContext ServiceContext, channelID string) BoardRef {
	if serviceContext.principalType() == PrincipalTypeAgent {
		return BoardRef{Channel: "#all"}
	}
	return BoardRef{ChannelID: channelID}
}

func countTaskRowsByID(t *testing.T, ctx context.Context, fixture taskboardUserFixture, taskID string) int64 {
	t.Helper()

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

func taskParentRef(t *testing.T, ctx context.Context, fixture taskboardUserFixture, taskID string) string {
	t.Helper()

	var parentTaskRef string
	require.NoError(t, fixture.pool.QueryRow(ctx, `
SELECT parent_task_ref
FROM tasks
WHERE id = $1
`, taskID).Scan(&parentTaskRef))
	return parentTaskRef
}

func taskScopeStatus(t *testing.T, ctx context.Context, fixture taskboardUserFixture, taskScopeID string) string {
	t.Helper()

	var status string
	require.NoError(t, fixture.pool.QueryRow(ctx, `
SELECT status
FROM scopes
WHERE id = $1
`, taskScopeID).Scan(&status))
	return status
}

func seedDeletedTaskConvergenceState(
	t *testing.T,
	ctx context.Context,
	fixture taskboardUserFixture,
	taskScopeID string,
	outboxStatus string,
) {
	t.Helper()
	now := time.Date(2026, 7, 29, 1, 0, 0, 0, time.UTC)
	mustExecTaskboard(t, ctx, fixture.pool, `
INSERT INTO im_conversation_index (
    workspace_id,
    member_id,
    conversation_id,
    scope_id,
    kind,
    sort_at,
    last_event_seq,
    projection_json,
    created_at,
    updated_at
) VALUES ($1, $2, $3, $4, 'task', $5, 1, JSON_OBJECT('taskId', $4), $5, $5)
`, fixture.workspaceID, fixture.ownerMemberID, "task:"+taskScopeID, taskScopeID, now)
	seedTaskFanoutOutbox(t, ctx, fixture, taskScopeID, outboxStatus, outboxStatus)
}

func seedTaskFanoutOutbox(
	t *testing.T,
	ctx context.Context,
	fixture taskboardUserFixture,
	taskScopeID string,
	status string,
	suffix string,
) {
	t.Helper()
	now := time.Date(2026, 7, 29, 1, 0, 0, 0, time.UTC)
	var leaseOwner any
	var leaseExpiresAt any
	if status == "leased" {
		leaseOwner = "worker-before-delete"
		leaseExpiresAt = now.Add(time.Minute)
	}
	mustExecTaskboard(t, ctx, fixture.pool, `
INSERT INTO scope_fanout_outbox (
    id,
    workspace_id,
    scope_id,
    message_id,
    event_ref_type,
    event_ref_id,
    fanout_type,
    dedupe_key,
    payload,
    status,
    attempts,
    max_attempts,
    next_attempt_at,
    lease_owner,
    lease_expires_at,
    created_at,
    delivered_at,
    suppressed_at,
    suppression_reason,
    last_error
) VALUES (
    $1, $2, $3, NULL, 'activity_event', $4, 'scope_fanout', $5, JSON_OBJECT(),
    $6, 1, 3, $7, $8, $9, $7,
    CASE WHEN $6 = 'delivered' THEN $7 ELSE NULL END,
    NULL, '', CASE WHEN $6 = 'failed' THEN 'exhausted before delete' ELSE '' END
)
`, taskFanoutOutboxID(taskScopeID, suffix), fixture.workspaceID, taskScopeID, "event:"+suffix, "dedupe:"+taskScopeID+":"+suffix, status, now, leaseOwner, leaseExpiresAt)
}

func taskFanoutOutboxID(taskScopeID string, suffix string) string {
	return fmt.Sprintf("out_delete_%s_%s", taskScopeID, suffix)
}
