package taskboard

import (
	"context"
	"testing"

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

// TestTaskSubtasksKeysetPageOrderingAndContinuation verifies the direct-children
// page contract: newest-first (created_at DESC, id DESC) ordering, the unique id
// tie-breaker in both ORDER BY and the strict continuation predicate, hasMore,
// and a nextCursor derived from the last returned row. Equal timestamps,
// concurrent inserts ahead of the cursor, and repeated reads must neither
// duplicate nor omit rows.
func TestTaskSubtasksKeysetPageOrderingAndContinuation(t *testing.T) {
	ctx := context.Background()
	fixture := seedTaskboardUserFixture(t, ctx)
	userContext := taskboardUserContext(fixture)

	parentResponse, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: userContext,
		Board:   BoardRef{ChannelID: fixture.channelID},
		Titles:  []string{"Paged parent"},
	})
	require.NoError(t, err)
	require.Len(t, parentResponse.Tasks, 1)
	parentID := parentResponse.Tasks[0].ID

	// Create 18 subtasks so the default 10-row page cannot cover the set.
	childTitles := make([]string, 0, 18)
	for index := 0; index < 18; index++ {
		childTitles = append(childTitles, titleWithIndex("subtask", index))
	}
	childResponse, err := fixture.service.CreateSubtasks(ctx, CreateSubtasksRequest{
		Context: userContext,
		Task:    TaskRef{TaskID: parentID},
		Titles:  childTitles,
	})
	require.NoError(t, err)
	require.Len(t, childResponse.Tasks, 18)

	// Walk the full set through the keyset page and confirm no dup/omission.
	seen := make(map[string]struct{}, 18)
	collected := make([]Task, 0, 18)
	var cursor SubtaskPageCursor
	pages := 0
	for {
		page, err := fixture.service.ListTaskSubtasks(ctx, ListTaskSubtasksRequest{
			Context: userContext,
			Task:    TaskRef{TaskID: parentID},
			Limit:   7,
			Cursor:  cursor,
		})
		require.NoError(t, err)
		require.LessOrEqual(t, len(page.Tasks), 7)
		for _, child := range page.Tasks {
			require.NotEqual(t, parentID, child.ID)
			_, dup := seen[child.ID]
			require.False(t, dup, "duplicate subtask across pages: %s", child.ID)
			seen[child.ID] = struct{}{}
			collected = append(collected, child)
		}
		pages++
		if !page.HasMore {
			break
		}
		require.False(t, page.NextCursor.IsZero(), "hasMore page must carry a nextCursor")
		cursor = page.NextCursor
	}
	require.Equal(t, 18, len(collected))
	require.GreaterOrEqual(t, pages, 3)

	// Newest-first: createdAt must be non-increasing; a tie on createdAt must be
	// broken by descending id (the id tie-breaker).
	for index := 1; index < len(collected); index++ {
		prev := collected[index-1]
		current := collected[index]
		require.False(t, current.CreatedAt.After(prev.CreatedAt), "page order must be newest-first at index %d", index)
		if current.CreatedAt.Equal(prev.CreatedAt) {
			require.True(t, current.ID < prev.ID, "createdAt tie must be broken by id DESC at index %d", index)
		}
	}

	// First page must match Task detail's initial subtask page.
	detail, err := fixture.service.GetTaskDetail(ctx, GetTaskDetailRequest{
		Context: userContext,
		Task:    TaskRef{TaskID: parentID},
	})
	require.NoError(t, err)
	require.Equal(t, TaskSubtasksDefaultPageSize, len(detail.Detail.Subtasks))
	require.True(t, detail.Detail.SubtasksHasMore)
	require.False(t, detail.Detail.SubtasksNextCursor.IsZero())
	firstPage, err := fixture.service.ListTaskSubtasks(ctx, ListTaskSubtasksRequest{
		Context: userContext,
		Task:    TaskRef{TaskID: parentID},
	})
	require.NoError(t, err)
	require.Len(t, firstPage.Tasks, TaskSubtasksDefaultPageSize)
	require.True(t, firstPage.HasMore)
	for index := range firstPage.Tasks {
		require.Equal(t, detail.Detail.Subtasks[index].ID, firstPage.Tasks[index].ID)
		require.Equal(t, detail.Detail.Subtasks[index].CreatedAt, firstPage.Tasks[index].CreatedAt)
	}
}

// TestTaskSubtasksKeysetPageConcurrentInsertNoDuplicate proves that a cursor
// captured from a page of N rows continues to the exact next rows even when a
// new subtask is inserted after the cursor was captured. The id tie-breaker
// keeps equal-timestamp rows deterministic, so no row is skipped or repeated.
func TestTaskSubtasksKeysetPageConcurrentInsertNoDuplicate(t *testing.T) {
	ctx := context.Background()
	fixture := seedTaskboardUserFixture(t, ctx)
	userContext := taskboardUserContext(fixture)

	parentResponse, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: userContext,
		Board:   BoardRef{ChannelID: fixture.channelID},
		Titles:  []string{"Concurrent parent"},
	})
	require.NoError(t, err)
	parentID := parentResponse.Tasks[0].ID

	titles := make([]string, 0, 12)
	for index := 0; index < 12; index++ {
		titles = append(titles, titleWithIndex("subtask", index))
	}
	_, err = fixture.service.CreateSubtasks(ctx, CreateSubtasksRequest{
		Context: userContext,
		Task:    TaskRef{TaskID: parentID},
		Titles:  titles,
	})
	require.NoError(t, err)

	// Read page 1 (7 rows), capture the cursor.
	first, err := fixture.service.ListTaskSubtasks(ctx, ListTaskSubtasksRequest{
		Context: userContext,
		Task:    TaskRef{TaskID: parentID},
		Limit:   7,
	})
	require.NoError(t, err)
	require.Len(t, first.Tasks, 7)
	require.True(t, first.HasMore)
	cursor := first.NextCursor
	require.False(t, cursor.IsZero())

	// A new subtask lands after the first page was read.
	_, err = fixture.service.CreateSubtasks(ctx, CreateSubtasksRequest{
		Context: userContext,
		Task:    TaskRef{TaskID: parentID},
		Titles:  []string{"late insert"},
	})
	require.NoError(t, err)

	// A fresh walk from a blank cursor must cover all 13 rows exactly once in
	// canonical newest-first order. When the late insert carries a genuinely
	// newer timestamp it sorts ahead of the previously captured cursor (a fresh
	// first page picks it up); when all rows share the same created_at second,
	// the unique id tie-breaker decides placement. In both cases the strict
	// continuation predicate guarantees no row is duplicated or omitted and each
	// page stays newest-first with id DESC on a timestamp tie.
	seen := make(map[string]struct{}, 13)
	collected := make([]Task, 0, 13)
	var currentCursor SubtaskPageCursor
	for {
		page, err := fixture.service.ListTaskSubtasks(ctx, ListTaskSubtasksRequest{
			Context: userContext,
			Task:    TaskRef{TaskID: parentID},
			Limit:   7,
			Cursor:  currentCursor,
		})
		require.NoError(t, err)
		require.LessOrEqual(t, len(page.Tasks), 7)
		for _, task := range page.Tasks {
			_, dup := seen[task.ID]
			require.False(t, dup, "continuation returned a duplicate row: %s", task.ID)
			seen[task.ID] = struct{}{}
			collected = append(collected, task)
		}
		if !page.HasMore {
			require.True(t, page.NextCursor.IsZero())
			break
		}
		require.False(t, page.NextCursor.IsZero(), "hasMore page must carry a nextCursor")
		currentCursor = page.NextCursor
	}
	require.Equal(t, 13, len(collected))
	require.Len(t, seen, 13)

	// The combined set must be canonical newest-first: non-increasing
	// created_at, and id DESC on a timestamp tie. The id tie-breaker is what
	// makes the continuation safe under equal timestamps.
	for index := 1; index < len(collected); index++ {
		prev := collected[index-1]
		current := collected[index]
		require.False(t, current.CreatedAt.After(prev.CreatedAt), "combined page order must be newest-first at index %d", index)
		if current.CreatedAt.Equal(prev.CreatedAt) {
			require.True(t, current.ID < prev.ID, "createdAt tie must be broken by id DESC at index %d", index)
		}
	}
}

// TestTaskSubtasksKeysetPageScopedToParentAndWorkspace verifies that the cursor
// cannot leak children of another parent or workspace.
func TestTaskSubtasksKeysetPageScopedToParentAndWorkspace(t *testing.T) {
	ctx := context.Background()
	fixture := seedTaskboardUserFixture(t, ctx)
	userContext := taskboardUserContext(fixture)

	parentResponse, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: userContext,
		Board:   BoardRef{ChannelID: fixture.channelID},
		Titles:  []string{"Parent one", "Parent two"},
	})
	require.NoError(t, err)
	require.Len(t, parentResponse.Tasks, 2)
	parentOne := parentResponse.Tasks[0].ID
	parentTwo := parentResponse.Tasks[1].ID

	_, err = fixture.service.CreateSubtasks(ctx, CreateSubtasksRequest{
		Context: userContext,
		Task:    TaskRef{TaskID: parentOne},
		Titles:  []string{"one-a", "one-b"},
	})
	require.NoError(t, err)
	_, err = fixture.service.CreateSubtasks(ctx, CreateSubtasksRequest{
		Context: userContext,
		Task:    TaskRef{TaskID: parentTwo},
		Titles:  []string{"two-a"},
	})
	require.NoError(t, err)

	// A cursor derived from parent two must not enumerate parent one's children.
	pageTwo, err := fixture.service.ListTaskSubtasks(ctx, ListTaskSubtasksRequest{
		Context: userContext,
		Task:    TaskRef{TaskID: parentTwo},
		Limit:   10,
	})
	require.NoError(t, err)
	require.Len(t, pageTwo.Tasks, 1)
	cursor := pageTwo.NextCursor
	if !cursor.IsZero() {
		pageOne, err := fixture.service.ListTaskSubtasks(ctx, ListTaskSubtasksRequest{
			Context: userContext,
			Task:    TaskRef{TaskID: parentOne},
			Limit:   10,
			Cursor:  cursor,
		})
		require.NoError(t, err)
		require.Empty(t, pageOne.Tasks)
	}

	// Non-readable parent (another workspace) must still be rejected, not
	// enumerated.
	otherFixture := seedTaskboardUserFixture(t, ctx)
	otherUserContext := taskboardUserContext(otherFixture)
	otherParent, err := otherFixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: otherUserContext,
		Board:   BoardRef{ChannelID: otherFixture.channelID},
		Titles:  []string{"Other workspace parent"},
	})
	require.NoError(t, err)
	otherParentID := otherParent.Tasks[0].ID

	_, err = fixture.service.ListTaskSubtasks(ctx, ListTaskSubtasksRequest{
		Context: userContext,
		Task:    TaskRef{TaskID: otherParentID},
	})
	require.Error(t, err)
}

func titleWithIndex(prefix string, index int) string {
	return prefix + "-" + string(rune('A'+index%26)) + string(rune('0'+index/26))
}
