package taskboard

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

	"github.com/stretchr/testify/require"
)

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

	created, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: taskboardUserContext(fixture),
		Board:   BoardRef{ChannelID: fixture.channelID},
		Titles:  []string{"Thread root task"},
		Metadata: &CreateTaskMetadata{
			SourceThreadChannelID:       threadChannelID,
			SourceThreadParentChannelID: fixture.channelID,
			SourceThreadParentMessageID: "msg_task_collaborator_parent",
			SourceThreadRef:             "#all:task-collab",
		},
	})
	require.NoError(t, err)
	require.Len(t, created.Tasks, 1)
	root := created.Tasks[0]

	subtasks, err := fixture.service.CreateSubtasks(ctx, CreateSubtasksRequest{
		Context: taskboardUserContext(fixture),
		Task:    TaskRef{TaskID: root.ID},
		Titles:  []string{"Thread child task"},
	})
	require.NoError(t, err)
	require.Len(t, subtasks.Tasks, 1)
	child := subtasks.Tasks[0]

	_, err = fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: taskboardUserContext(fixture),
		Board:   BoardRef{ChannelID: fixture.channelID},
		Titles:  []string{"Unrelated task"},
	})
	require.NoError(t, err)

	_, err = fixture.service.AddTaskCheckpoint(ctx, AddTaskCheckpointRequest{
		Context: NewUserServiceContext(fixture.workspaceID, fixture.reviewerUserID, fixture.reviewerDisplayName),
		Task:    TaskRef{TaskID: root.ID},
		Checkpoint: TaskCheckpointInput{
			Text: "high signal on related root",
		},
	})
	require.NoError(t, err)

	response, err := fixture.service.ListRelatedTasksForThread(ctx, ListRelatedTasksForThreadRequest{
		Context:         taskboardUserContext(fixture),
		ThreadChannelID: threadChannelID,
	})
	require.NoError(t, err)
	require.Len(t, response.Rows, 2)
	require.Equal(t, root.ID, response.Rows[0].TaskID)
	require.Equal(t, child.ID, response.Rows[1].TaskID)
	require.Equal(t, threadChannelID, response.Rows[0].SourceThread.ThreadChannelID)
	require.Equal(t, "/channels/"+fixture.channelID+"/tasks/1", response.Rows[0].Route)
	require.NotNil(t, response.Rows[0].LastSignal)
	require.Equal(t, "checkpoint_added", response.Rows[0].LastSignal.Type)
	require.Equal(t, int64(0), response.Rows[0].Unread.Count, "related-task store rows leave unread serving to runtime overlays")
	require.False(t, response.HasMore)
	require.True(t, response.NextCursor.IsZero())
}

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

	created, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: taskboardUserContext(fixture),
		Board:   BoardRef{ChannelID: fixture.channelID},
		Titles:  []string{"Recursive related root"},
		Metadata: &CreateTaskMetadata{
			SourceThreadChannelID:       threadChannelID,
			SourceThreadParentChannelID: fixture.channelID,
			SourceThreadParentMessageID: "msg_task_collaborator_parent",
			SourceThreadRef:             "#all:task-collab",
		},
	})
	require.NoError(t, err)
	root := created.Tasks[0]
	children, err := fixture.service.CreateSubtasks(ctx, CreateSubtasksRequest{
		Context: taskboardUserContext(fixture),
		Task:    TaskRef{TaskID: root.ID},
		Titles:  []string{"Recursive related child"},
	})
	require.NoError(t, err)
	child := children.Tasks[0]
	grandchildren, err := fixture.service.CreateSubtasks(ctx, CreateSubtasksRequest{
		Context: taskboardUserContext(fixture),
		Task:    TaskRef{TaskID: child.ID},
		Titles:  []string{"Recursive related grandchild"},
	})
	require.NoError(t, err)
	grandchild := grandchildren.Tasks[0]

	response, err := fixture.service.ListRelatedTasksForThread(ctx, ListRelatedTasksForThreadRequest{
		Context:         taskboardUserContext(fixture),
		ThreadChannelID: threadChannelID,
	})
	require.NoError(t, err)
	require.Equal(t, []string{root.ID, child.ID, grandchild.ID}, relatedTaskRowIDsForTest(response.Rows))

	summary, err := fixture.service.RelatedTasksSummaryForThread(ctx, RelatedTasksSummaryForThreadRequest{
		Context:         taskboardUserContext(fixture),
		ThreadChannelID: threadChannelID,
	})
	require.NoError(t, err)
	require.Equal(t, int64(3), summary.TaskCount)

	markTaskDoneForRelatedTest(t, ctx, fixture, grandchild.ID)
	doneResponse, err := fixture.service.ListRelatedTasksForThread(ctx, ListRelatedTasksForThreadRequest{
		Context:         taskboardUserContext(fixture),
		ThreadChannelID: threadChannelID,
		Status:          RelatedTaskStatusDone,
	})
	require.NoError(t, err)
	require.Equal(t, []string{root.ID, child.ID, grandchild.ID}, relatedTaskRowIDsForTest(doneResponse.Rows))
	require.Equal(t, root.ID, doneResponse.Rows[1].ParentTaskID)
	require.Equal(t, child.ID, doneResponse.Rows[2].ParentTaskID)
}

func relatedTaskRowIDsForTest(rows []RelatedTaskRow) []string {
	ids := make([]string, 0, len(rows))
	for _, row := range rows {
		ids = append(ids, row.TaskID)
	}
	return ids
}

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

	created, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: taskboardUserContext(fixture),
		Board:   BoardRef{ChannelID: fixture.channelID},
		Titles:  []string{"Thread root task"},
		Metadata: &CreateTaskMetadata{
			SourceThreadChannelID:       threadChannelID,
			SourceThreadParentChannelID: fixture.channelID,
			SourceThreadParentMessageID: "msg_task_collaborator_parent",
			SourceThreadRef:             "#all:task-collab",
		},
	})
	require.NoError(t, err)
	root := created.Tasks[0]

	subtasks, err := fixture.service.CreateSubtasks(ctx, CreateSubtasksRequest{
		Context: taskboardUserContext(fixture),
		Task:    TaskRef{TaskID: root.ID},
		Titles:  []string{"Thread child task"},
	})
	require.NoError(t, err)
	child := subtasks.Tasks[0]

	// Mark only the child done; the parent stays active.
	markTaskDoneForRelatedTest(t, ctx, fixture, child.ID)

	// Active page filters by each task's OWN status: only the active parent.
	activeResponse, err := fixture.service.ListRelatedTasksForThread(ctx, ListRelatedTasksForThreadRequest{
		Context:         taskboardUserContext(fixture),
		ThreadChannelID: threadChannelID,
	})
	require.NoError(t, err)
	require.Len(t, activeResponse.Rows, 1)
	require.Equal(t, root.ID, activeResponse.Rows[0].TaskID)

	// Done page returns the done child AND its active parent as grouping context,
	// so the panel can nest the done child under the parent. The parent row
	// carries its real active status, not faked as done.
	doneResponse, err := fixture.service.ListRelatedTasksForThread(ctx, ListRelatedTasksForThreadRequest{
		Context:         taskboardUserContext(fixture),
		ThreadChannelID: threadChannelID,
		Status:          RelatedTaskStatusDone,
	})
	require.NoError(t, err)
	require.Len(t, doneResponse.Rows, 2)
	require.Equal(t, root.ID, doneResponse.Rows[0].TaskID)
	require.NotEqual(t, string(StatusDone), doneResponse.Rows[0].Status, "parent keeps its real active status as context")
	require.Equal(t, child.ID, doneResponse.Rows[1].TaskID)
	require.Equal(t, root.ID, doneResponse.Rows[1].ParentTaskID, "done child carries its parent id for grouping")
	require.Equal(t, string(StatusDone), doneResponse.Rows[1].Status)
}

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

	created, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: taskboardUserContext(fixture),
		Board:   BoardRef{ChannelID: fixture.channelID},
		Titles:  []string{"Thread root task"},
		Metadata: &CreateTaskMetadata{
			SourceThreadChannelID:       threadChannelID,
			SourceThreadParentChannelID: fixture.channelID,
			SourceThreadParentMessageID: "msg_task_collaborator_parent",
			SourceThreadRef:             "#all:task-collab",
		},
	})
	require.NoError(t, err)
	root := created.Tasks[0]

	subtasks, err := fixture.service.CreateSubtasks(ctx, CreateSubtasksRequest{
		Context: taskboardUserContext(fixture),
		Task:    TaskRef{TaskID: root.ID},
		Titles:  []string{"Thread child task"},
	})
	require.NoError(t, err)
	child := subtasks.Tasks[0]

	// Mark only the parent done; the child stays active.
	markTaskDoneForRelatedTest(t, ctx, fixture, root.ID)

	// Done page filters by each task's OWN status: only the done parent.
	doneResponse, err := fixture.service.ListRelatedTasksForThread(ctx, ListRelatedTasksForThreadRequest{
		Context:         taskboardUserContext(fixture),
		ThreadChannelID: threadChannelID,
		Status:          RelatedTaskStatusDone,
	})
	require.NoError(t, err)
	require.Len(t, doneResponse.Rows, 1)
	require.Equal(t, root.ID, doneResponse.Rows[0].TaskID)
	require.Equal(t, string(StatusDone), doneResponse.Rows[0].Status)

	// Active page returns the active child AND its done parent as context.
	activeResponse, err := fixture.service.ListRelatedTasksForThread(ctx, ListRelatedTasksForThreadRequest{
		Context:         taskboardUserContext(fixture),
		ThreadChannelID: threadChannelID,
	})
	require.NoError(t, err)
	require.Len(t, activeResponse.Rows, 2)
	require.Equal(t, root.ID, activeResponse.Rows[0].TaskID)
	require.Equal(t, string(StatusDone), activeResponse.Rows[0].Status, "parent keeps its real done status as context")
	require.Equal(t, child.ID, activeResponse.Rows[1].TaskID)
	require.Equal(t, root.ID, activeResponse.Rows[1].ParentTaskID, "active child carries its parent id for grouping")
	require.NotEqual(t, string(StatusDone), activeResponse.Rows[1].Status)
}

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

	created, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: taskboardUserContext(fixture),
		Board:   BoardRef{ChannelID: fixture.channelID},
		Titles:  []string{"Done thread task", "Active thread task A", "Active thread task B"},
		Metadata: &CreateTaskMetadata{
			SourceThreadChannelID:       threadChannelID,
			SourceThreadParentChannelID: fixture.channelID,
			SourceThreadParentMessageID: "msg_task_collaborator_parent",
			SourceThreadRef:             "#all:task-collab",
		},
	})
	require.NoError(t, err)
	require.Len(t, created.Tasks, 3)
	markTaskDoneForRelatedTest(t, ctx, fixture, created.Tasks[0].ID)

	first, err := fixture.service.ListRelatedTasksForThread(ctx, ListRelatedTasksForThreadRequest{
		Context:         taskboardUserContext(fixture),
		ThreadChannelID: threadChannelID,
		Limit:           1,
	})
	require.NoError(t, err)
	require.Len(t, first.Rows, 1)
	require.Equal(t, created.Tasks[1].ID, first.Rows[0].TaskID)
	require.NotEqual(t, string(StatusDone), first.Rows[0].Status)
	require.True(t, first.HasMore)
	require.False(t, first.NextCursor.IsZero())

	second, err := fixture.service.ListRelatedTasksForThread(ctx, ListRelatedTasksForThreadRequest{
		Context:         taskboardUserContext(fixture),
		ThreadChannelID: threadChannelID,
		Limit:           1,
		Cursor:          first.NextCursor,
	})
	require.NoError(t, err)
	require.Len(t, second.Rows, 1)
	require.Equal(t, created.Tasks[2].ID, second.Rows[0].TaskID)
	require.False(t, second.HasMore)
	require.True(t, second.NextCursor.IsZero())

	donePage, err := fixture.service.ListRelatedTasksForThread(ctx, ListRelatedTasksForThreadRequest{
		Context:         taskboardUserContext(fixture),
		ThreadChannelID: threadChannelID,
		Limit:           5,
		Status:          RelatedTaskStatusDone,
	})
	require.NoError(t, err)
	require.Len(t, donePage.Rows, 1)
	require.Equal(t, created.Tasks[0].ID, donePage.Rows[0].TaskID)
	require.Equal(t, string(StatusDone), donePage.Rows[0].Status)
}

func TestListRelatedExecutionTasksForAgentFiltersAssignee(t *testing.T) {
	ctx := context.Background()
	fixture := seedTaskboardUserFixture(t, ctx)
	ownedAgent := seedTaskboardOwnedAgentForTest(t, ctx, fixture, "related_exec")

	firstAssigned, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: taskboardUserContext(fixture),
		Board:   BoardRef{ChannelID: fixture.channelID},
		Task: &TaskCreateInput{
			Title:       "First assigned execution",
			AssigneeRef: ownedAgent.AgentMemberID,
		},
	})
	require.NoError(t, err)
	require.Len(t, firstAssigned.Tasks, 1)

	secondAssigned, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: taskboardUserContext(fixture),
		Board:   BoardRef{ChannelID: fixture.channelID},
		Task: &TaskCreateInput{
			Title:       "Second assigned execution",
			AssigneeRef: ownedAgent.AgentMemberID,
		},
	})
	require.NoError(t, err)
	require.Len(t, secondAssigned.Tasks, 1)

	unassigned, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: taskboardUserContext(fixture),
		Board:   BoardRef{ChannelID: fixture.channelID},
		Task: &TaskCreateInput{
			Title: "Unassigned task",
		},
	})
	require.NoError(t, err)
	require.Len(t, unassigned.Tasks, 1)

	response, err := fixture.service.ListRelatedExecutionTasksForAgent(ctx, ListRelatedExecutionTasksForAgentRequest{
		Context: NewUserServiceContext(fixture.workspaceID, ownedAgent.OwnerUserID, ownedAgent.OwnerName),
		AgentID: ownedAgent.AgentID,
	})
	require.NoError(t, err)
	require.Len(t, response.Rows, 2)
	require.Equal(t, firstAssigned.Tasks[0].ID, response.Rows[0].TaskID)
	require.Equal(t, secondAssigned.Tasks[0].ID, response.Rows[1].TaskID)
	for _, row := range response.Rows {
		require.NotEqual(t, unassigned.Tasks[0].ID, row.TaskID)
		require.NotNil(t, row.AssigneeProfile)
	}
	require.False(t, response.HasMore)
	require.True(t, response.NextCursor.IsZero())
}

func TestListRelatedExecutionTasksForAgentDefaultsActiveAndFullCountSummary(t *testing.T) {
	ctx := context.Background()
	fixture := seedTaskboardUserFixture(t, ctx)
	ownedAgent := seedTaskboardOwnedAgentForTest(t, ctx, fixture, "related_exec_active")

	active, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: taskboardUserContext(fixture),
		Board:   BoardRef{ChannelID: fixture.channelID},
		Task: &TaskCreateInput{
			Title:       "Active execution",
			AssigneeRef: ownedAgent.AgentMemberID,
		},
	})
	require.NoError(t, err)
	require.Len(t, active.Tasks, 1)

	done, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: taskboardUserContext(fixture),
		Board:   BoardRef{ChannelID: fixture.channelID},
		Task: &TaskCreateInput{
			Title:       "Historical execution",
			AssigneeRef: ownedAgent.AgentMemberID,
		},
	})
	require.NoError(t, err)
	require.Len(t, done.Tasks, 1)
	markTaskDoneForRelatedTest(t, ctx, fixture, done.Tasks[0].ID)

	insertTaskManualUnreadStateForTest(t, ctx, fixture.pool, active.Tasks[0].TaskScopeID, ownedAgent.OwnerMemberID)
	insertTaskManualUnreadStateForTest(t, ctx, fixture.pool, done.Tasks[0].TaskScopeID, ownedAgent.OwnerMemberID)

	defaultPage, err := fixture.service.ListRelatedExecutionTasksForAgent(ctx, ListRelatedExecutionTasksForAgentRequest{
		Context: NewUserServiceContext(fixture.workspaceID, ownedAgent.OwnerUserID, ownedAgent.OwnerName),
		AgentID: ownedAgent.AgentID,
		Limit:   10,
	})
	require.NoError(t, err)
	require.Len(t, defaultPage.Rows, 1)
	require.Equal(t, active.Tasks[0].ID, defaultPage.Rows[0].TaskID)
	require.NotEqual(t, string(StatusDone), defaultPage.Rows[0].Status)

	summary, err := fixture.service.RelatedExecutionTasksSummaryForAgent(ctx, RelatedExecutionTasksSummaryForAgentRequest{
		Context: NewUserServiceContext(fixture.workspaceID, ownedAgent.OwnerUserID, ownedAgent.OwnerName),
		AgentID: ownedAgent.AgentID,
	})
	require.NoError(t, err)
	require.Equal(t, int64(2), summary.TaskCount, "summary is full-count (active + done), not a bounded page")
	require.Equal(t, int64(1), summary.StatusCounts[string(StatusDone)], "summary statusCounts includes done tasks")
	require.False(t, summary.Unread.ManualUnread, "store summary does not serve DB manual-unread markers")

	donePage, err := fixture.service.ListRelatedExecutionTasksForAgent(ctx, ListRelatedExecutionTasksForAgentRequest{
		Context: NewUserServiceContext(fixture.workspaceID, ownedAgent.OwnerUserID, ownedAgent.OwnerName),
		AgentID: ownedAgent.AgentID,
		Limit:   10,
		Status:  RelatedTaskStatusDone,
	})
	require.NoError(t, err)
	require.Len(t, donePage.Rows, 1)
	require.Equal(t, done.Tasks[0].ID, donePage.Rows[0].TaskID)
	require.Equal(t, string(StatusDone), donePage.Rows[0].Status)
}

func TestRelatedTaskSQLShapesArePagedActiveAndBatchHydrated(t *testing.T) {
	t.Parallel()

	threadQuery, _, err := buildRelatedThreadTaskIDsQuery("wsp-1", "thread-1", RelatedTaskPage{Limit: 20, Status: RelatedTaskStatusActive})
	require.NoError(t, err)
	require.Contains(t, threadQuery, "node.status <> 'done'")
	require.Equal(t, 1, strings.Count(threadQuery, "LIMIT $3"))
	require.Contains(t, threadQuery, "WITH RECURSIVE related_tree")
	require.Contains(t, threadQuery, "UNION ALL")
	require.Contains(t, threadQuery, "root.source_thread_channel_id = $2")
	require.Contains(t, threadQuery, "child.parent_task_ref = related_tree.id")
	require.Contains(t, threadQuery, "related_tree.depth < 64")

	agentQuery, _, err := buildRelatedAgentExecutionTaskIDsQuery("wsp-1", "mem-agent", RelatedTaskPage{Limit: 20, Status: RelatedTaskStatusActive})
	require.NoError(t, err)
	require.Contains(t, agentQuery, "t.status <> 'done'")
	require.Equal(t, 1, strings.Count(agentQuery, "LIMIT $3"))
	require.NotContains(t, agentQuery, "UNION")
	require.Contains(t, agentQuery, "NULLIF(t.claimed_by_ref, '')")
	require.Contains(t, agentQuery, "NULLIF(t.reviewer_ref, '')) = $2")
	require.NotContains(t, agentQuery, "CASE")
	require.NotContains(t, agentQuery, " OR ")

	lastSignalQuery, _, err := buildRelatedTaskLastSignalsQuery("wsp-1", []string{"scope-1", "scope-2"})
	require.NoError(t, err)
	require.Contains(t, lastSignalQuery, "requested_scopes")
	require.Contains(t, lastSignalQuery, "ROW_NUMBER() OVER (PARTITION BY ae.scope_id")
}

func TestRelatedTaskHydrationPreservesCandidateOrderForCursor(t *testing.T) {
	t.Parallel()

	candidateIDs := []string{"tsk-root-1", "tsk-root-2", "tsk-child-1"}
	dbHydratedOrder := []Task{
		{ID: "tsk-root-1", TaskNumber: 1},
		{ID: "tsk-child-1", TaskNumber: 1},
		{ID: "tsk-root-2", TaskNumber: 2},
	}

	pageTasks := orderRelatedTasksByIDs(dbHydratedOrder, candidateIDs[:2])
	require.Equal(t, []string{"tsk-root-1", "tsk-root-2"}, relatedTaskIDsForTest(pageTasks))
	require.Equal(t, RelatedTaskCursor{TaskNumber: 2, TaskID: "tsk-root-2"}, relatedTaskNextCursor(true, pageTasks))

	nextPageTasks := orderRelatedTasksByIDs(dbHydratedOrder, candidateIDs[2:])
	require.Equal(t, []string{"tsk-child-1"}, relatedTaskIDsForTest(nextPageTasks))
	require.NotContains(t, relatedTaskIDsForTest(nextPageTasks), pageTasks[len(pageTasks)-1].ID)
}

func relatedTaskIDsForTest(tasks []Task) []string {
	ids := make([]string, 0, len(tasks))
	for _, task := range tasks {
		ids = append(ids, task.ID)
	}
	return ids
}

func markTaskDoneForRelatedTest(t *testing.T, ctx context.Context, fixture taskboardUserFixture, taskID string) {
	t.Helper()
	completedAt := time.Now().UTC()
	_, err := fixture.pool.Exec(ctx, `
UPDATE tasks
SET status = 'done',
    completed_at = $2
WHERE id = $1
`, taskID, completedAt)
	require.NoError(t, err)
	updateTaskStatusForTest(t, ctx, fixture.pool, taskID, StatusDone, completedAt)
}
