package taskboard

import (
	"context"
	"database/sql"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/tasktimeline"
)

func TestTaskAttentionIncludesMigratedAssigneeWithoutTaskScopeMembership(t *testing.T) {
	ctx := context.Background()
	fixture := seedTaskboardUserFixture(t, ctx)
	userContext := NewUserServiceContext(fixture.workspaceID, fixture.ownerUserID, fixture.ownerDisplayName)
	assignedAt := time.Date(2026, 7, 29, 3, 0, 0, 0, time.UTC)

	tests := []struct {
		name             string
		assigneeMemberID string
		assertPayload    func(*testing.T, message.ScopeDeliveryIntentPayload)
	}{
		{
			name:             "human assignee receives unread attention",
			assigneeMemberID: fixture.reviewerMemberID,
			assertPayload: func(t *testing.T, payload message.ScopeDeliveryIntentPayload) {
				require.True(t, payload.HumanRecipientsTargeted)
				require.Equal(t, []string{fixture.reviewerMemberID}, payload.HumanRecipientMemberIDs)
				require.NotNil(t, payload.Conversation)
				require.Empty(t, payload.WakeRecipients)
			},
		},
		{
			name:             "agent assignee receives wake attention",
			assigneeMemberID: fixture.agentMemberID,
			assertPayload: func(t *testing.T, payload message.ScopeDeliveryIntentPayload) {
				require.Empty(t, payload.HumanRecipientMemberIDs)
				require.Len(t, payload.WakeRecipients, 1)
				require.Equal(t, fixture.agentMemberID, payload.WakeRecipients[0].AgentMemberID)
				require.Equal(t, fixture.agentID, payload.WakeRecipients[0].AgentID)
				require.Equal(t, message.AgentDeliveryReasonTaskAssignee, payload.WakeRecipients[0].DeliveryReason)
				require.False(t, payload.WakeRecipients[0].AutoWakeEnabled)
			},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			created, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
				Context: userContext,
				Board:   BoardRef{ChannelID: fixture.channelID},
				Titles:  []string{"Migrated assignee without Task membership"},
			})
			require.NoError(t, err)
			require.Len(t, created.Tasks, 1)
			task := created.Tasks[0]

			// Model an untouched pre-canonical-scope Task after migration 00289:
			// its responsibility projection is valid and backfilled, but migration
			// 00135 did not create Task-scope membership rows for existing Tasks.
			_, err = fixture.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
`, task.ID, test.assigneeMemberID, assignedAt)
			require.NoError(t, err)

			var membershipCount int64
			require.NoError(t, fixture.pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_memberships
WHERE scope_id = $1
  AND member_id = $2
`, task.TaskScopeID, test.assigneeMemberID).Scan(&membershipCount))
			require.Zero(t, membershipCount)

			_, err = fixture.service.AddTaskCheckpoint(ctx, AddTaskCheckpointRequest{
				Context:    userContext,
				Task:       TaskRef{TaskID: task.ID},
				Checkpoint: TaskCheckpointInput{Text: "Migrated assignee attention"},
			})
			require.NoError(t, err)

			require.NoError(t, fixture.pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_memberships
WHERE scope_id = $1
  AND member_id = $2
  AND removed_at IS NULL
  AND archived_at IS NULL
  AND effective_to_seq IS NULL
`, task.TaskScopeID, test.assigneeMemberID).Scan(&membershipCount))
			require.Equal(t, int64(1), membershipCount, "attention freeze must make the assignee deliverable in the same transaction")

			var notificationRows int64
			var taskWatchRows int64
			require.NoError(t, fixture.pool.QueryRow(ctx, `
SELECT
  COUNT(*),
  COUNT(task_watch_state)
FROM scope_notification_settings
WHERE scope_id = $1
  AND member_id = $2
`, task.TaskScopeID, test.assigneeMemberID).Scan(&notificationRows, &taskWatchRows))
			require.Zero(t, taskWatchRows, "assignment attention must not silently become Watch")

			var rawPayload []byte
			require.NoError(t, fixture.pool.QueryRow(ctx, `
SELECT payload
FROM scope_fanout_outbox
WHERE scope_id = $1
  AND event_ref_type = 'activity_event'
ORDER BY created_at DESC, id DESC
LIMIT 1
`, task.TaskScopeID).Scan(&rawPayload))
			payload, err := message.UnmarshalScopeDeliveryIntentPayload(rawPayload)
			require.NoError(t, err)
			test.assertPayload(t, payload)
		})
	}
}

func TestTaskAssigneeRollingBridgeReadsOldNodeReassignAndUnassign(t *testing.T) {
	ctx := context.Background()
	fixture := seedTaskboardUserFixture(t, ctx)
	userContext := NewUserServiceContext(fixture.workspaceID, fixture.ownerUserID, fixture.ownerDisplayName)

	created, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: userContext,
		Board:   BoardRef{ChannelID: fixture.channelID},
		Task: &TaskCreateInput{
			Title:       "Rolling assignee bridge",
			AssigneeRef: fixture.reviewerMemberID,
		},
	})
	require.NoError(t, err)
	require.Len(t, created.Tasks, 1)
	task := created.Tasks[0]
	require.NotNil(t, task.AssigneeProfile)
	require.Equal(t, fixture.reviewerUserID, task.AssigneeProfile.PrincipalID)
	require.NotNil(t, task.AssignedAt)

	assertRollingTaskAssigneeColumns(t, ctx, fixture, task.ID, rollingAssigneeColumns{
		CanonicalRef: fixture.reviewerMemberID,
		LegacyRef:    fixture.reviewerMemberID,
		Assigned:     true,
	})

	oldNodeReassignedAt := time.Date(2026, 7, 29, 3, 10, 0, 0, time.UTC)
	_, err = fixture.pool.Exec(ctx, `
UPDATE tasks
SET claimed_by_member_id = $2,
    claimed_by_ref = $2,
    claimed_at = $3,
    updated_at = $3
WHERE id = $1
`, task.ID, fixture.agentMemberID, oldNodeReassignedAt)
	require.NoError(t, err)

	_, err = fixture.pool.Exec(ctx, `
INSERT INTO scope_memberships (
  scope_id, member_id, visibility, owner_member_id,
  last_read_seq, last_read_global_seq, owner_context_dismissed_through_global_seq,
  created_at, updated_at, removed_at, archived_at
) VALUES ($1, $2, 'public', NULL, 0, 0, 0, $3, $3, NULL, NULL)
ON DUPLICATE KEY UPDATE
  visibility = 'public',
  removed_at = NULL,
  archived_at = NULL,
  updated_at = VALUES(updated_at)
`, task.TaskScopeID, fixture.agentMemberID, oldNodeReassignedAt)
	require.NoError(t, err)
	_, err = fixture.pool.Exec(ctx, `
INSERT INTO scope_notification_settings (scope_id, member_id, state, created_at, updated_at)
VALUES ($1, $2, 'no-wake', $3, $3)
ON DUPLICATE KEY UPDATE state = 'no-wake', updated_at = VALUES(updated_at)
`, task.TaskScopeID, fixture.agentMemberID, oldNodeReassignedAt)
	require.NoError(t, err)

	detail, err := fixture.service.GetTaskDetail(ctx, GetTaskDetailRequest{
		Context: userContext,
		Task:    TaskRef{TaskID: task.ID},
	})
	require.NoError(t, err)
	require.NotNil(t, detail.Detail.Task.AssigneeProfile)
	require.Equal(t, fixture.agentID, detail.Detail.Task.AssigneeProfile.PrincipalID)
	require.NotNil(t, detail.Detail.Task.AssignedAt)
	require.Equal(t, oldNodeReassignedAt, detail.Detail.Task.AssignedAt.UTC())
	attention, err := tasktimeline.LoadTaskAttentionSnapshotForScope(
		ctx,
		fixture.pool,
		fixture.workspaceID,
		task.TaskScopeID,
		fixture.ownerMemberID,
		nil,
		nil,
		taskEventUpdated,
		"rolling assignee attention",
		oldNodeReassignedAt,
	)
	require.NoError(t, err)
	require.Len(t, attention.AgentRecipients, 1)
	require.Equal(t, fixture.agentMemberID, attention.AgentRecipients[0].AgentMemberID)
	require.Equal(t, fixture.agentID, attention.AgentRecipients[0].AgentID)

	oldNodeUnassignedAt := oldNodeReassignedAt.Add(time.Minute)
	_, err = fixture.pool.Exec(ctx, `
UPDATE tasks
SET claimed_by_member_id = NULL,
    claimed_by_ref = NULL,
    claimed_at = NULL,
    updated_at = $2
WHERE id = $1
`, task.ID, oldNodeUnassignedAt)
	require.NoError(t, err)

	detail, err = fixture.service.GetTaskDetail(ctx, GetTaskDetailRequest{
		Context: userContext,
		Task:    TaskRef{TaskID: task.ID},
	})
	require.NoError(t, err)
	require.Nil(t, detail.Detail.Task.AssigneeProfile)
	require.Nil(t, detail.Detail.Task.AssignedAt)
	attention, err = tasktimeline.LoadTaskAttentionSnapshotForScope(
		ctx,
		fixture.pool,
		fixture.workspaceID,
		task.TaskScopeID,
		fixture.ownerMemberID,
		nil,
		nil,
		taskEventUpdated,
		"rolling unassign attention",
		oldNodeUnassignedAt,
	)
	require.NoError(t, err)
	require.Empty(t, attention.AgentRecipients)

	// The canonical mirror intentionally remains stale during the overlap.
	// Runtime readers use the legacy projection until the contract migration.
	var canonicalRef sql.NullString
	var canonicalAssignedAt sql.NullTime
	require.NoError(t, fixture.pool.QueryRow(ctx, `
SELECT assignee_member_id, assigned_at
FROM tasks
WHERE id = $1
`, task.ID).Scan(&canonicalRef, &canonicalAssignedAt))
	require.Equal(t, fixture.reviewerMemberID, canonicalRef.String)
	require.True(t, canonicalAssignedAt.Valid)
}

func TestTaskAssigneeRollingBridgeReadsLowerPrecedenceOldNodeFields(t *testing.T) {
	ctx := context.Background()
	fixture := seedTaskboardUserFixture(t, ctx)
	userContext := NewUserServiceContext(fixture.workspaceID, fixture.ownerUserID, fixture.ownerDisplayName)

	created, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: userContext,
		Board:   BoardRef{ChannelID: fixture.channelID},
		Titles:  []string{"Legacy lower precedence bridge"},
	})
	require.NoError(t, err)
	task := created.Tasks[0]

	currentAgentAssignedAt := time.Date(2026, 7, 29, 3, 20, 0, 0, time.UTC)
	_, err = fixture.pool.Exec(ctx, `
UPDATE tasks
SET current_agent_ref = $2,
    updated_at = $3
WHERE id = $1
`, task.ID, fixture.agentMemberID, currentAgentAssignedAt)
	require.NoError(t, err)

	detail, err := fixture.service.GetTaskDetail(ctx, GetTaskDetailRequest{
		Context: userContext,
		Task:    TaskRef{TaskID: task.ID},
	})
	require.NoError(t, err)
	require.NotNil(t, detail.Detail.Task.AssigneeProfile)
	require.Equal(t, fixture.agentID, detail.Detail.Task.AssigneeProfile.PrincipalID)
	require.NotNil(t, detail.Detail.Task.AssignedAt)
	require.Equal(t, currentAgentAssignedAt, detail.Detail.Task.AssignedAt.UTC())

	suggestedAssignedAt := currentAgentAssignedAt.Add(time.Minute)
	_, err = fixture.pool.Exec(ctx, `
UPDATE tasks
SET current_agent_ref = NULL,
    suggested_assignee_ref = $2,
    updated_at = $3
WHERE id = $1
`, task.ID, fixture.reviewerMemberID, suggestedAssignedAt)
	require.NoError(t, err)

	detail, err = fixture.service.GetTaskDetail(ctx, GetTaskDetailRequest{
		Context: userContext,
		Task:    TaskRef{TaskID: task.ID},
	})
	require.NoError(t, err)
	require.NotNil(t, detail.Detail.Task.AssigneeProfile)
	require.Equal(t, fixture.reviewerUserID, detail.Detail.Task.AssigneeProfile.PrincipalID)
	require.NotNil(t, detail.Detail.Task.AssignedAt)
	require.Equal(t, suggestedAssignedAt, detail.Detail.Task.AssignedAt.UTC())

	_, err = fixture.pool.Exec(ctx, `
UPDATE tasks
SET suggested_assignee_ref = '',
    updated_at = $2
WHERE id = $1
`, task.ID, suggestedAssignedAt.Add(time.Minute))
	require.NoError(t, err)

	detail, err = fixture.service.GetTaskDetail(ctx, GetTaskDetailRequest{
		Context: userContext,
		Task:    TaskRef{TaskID: task.ID},
	})
	require.NoError(t, err)
	require.Nil(t, detail.Detail.Task.AssigneeProfile)
	require.Nil(t, detail.Detail.Task.AssignedAt)
}

func TestTaskAssigneeRollingBridgeDualWritesPatchAndUnassign(t *testing.T) {
	ctx := context.Background()
	fixture := seedTaskboardUserFixture(t, ctx)
	userContext := NewUserServiceContext(fixture.workspaceID, fixture.ownerUserID, fixture.ownerDisplayName)

	created, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: userContext,
		Board:   BoardRef{ChannelID: fixture.channelID},
		Titles:  []string{"New node dual write"},
	})
	require.NoError(t, err)
	task := created.Tasks[0]

	updated, err := fixture.service.UpdateTask(ctx, UpdateTaskRequest{
		Context: userContext,
		Task:    TaskRef{TaskID: task.ID},
		Patch: TaskUpdatePatch{
			AssigneeRef: stringPointer(fixture.agentMemberID),
		},
	})
	require.NoError(t, err)
	require.NotNil(t, updated.Task.AssigneeProfile)
	require.Equal(t, fixture.agentID, updated.Task.AssigneeProfile.PrincipalID)
	require.NotNil(t, updated.Task.AssignedAt)
	assertRollingTaskAssigneeColumns(t, ctx, fixture, task.ID, rollingAssigneeColumns{
		CanonicalRef: fixture.agentMemberID,
		LegacyRef:    fixture.agentMemberID,
		Assigned:     true,
	})

	empty := ""
	updated, err = fixture.service.UpdateTask(ctx, UpdateTaskRequest{
		Context: userContext,
		Task:    TaskRef{TaskID: task.ID},
		Patch: TaskUpdatePatch{
			AssigneeRef: &empty,
		},
	})
	require.NoError(t, err)
	require.Nil(t, updated.Task.AssigneeProfile)
	require.Nil(t, updated.Task.AssignedAt)
	assertRollingTaskAssigneeColumns(t, ctx, fixture, task.ID, rollingAssigneeColumns{})
}

type rollingAssigneeColumns struct {
	CanonicalRef string
	LegacyRef    string
	Assigned     bool
}

func assertRollingTaskAssigneeColumns(t *testing.T, ctx context.Context, fixture taskboardUserFixture, taskID string, expected rollingAssigneeColumns) {
	t.Helper()
	var canonicalRef sql.NullString
	var assignedAt sql.NullTime
	var claimedMemberRef sql.NullString
	var claimedRef sql.NullString
	var claimedAt sql.NullTime
	var currentAgentRef sql.NullString
	var suggestedAssigneeRef string
	var reviewerRef string
	require.NoError(t, fixture.pool.QueryRow(ctx, `
SELECT
  assignee_member_id,
  assigned_at,
  claimed_by_member_id,
  claimed_by_ref,
  claimed_at,
  current_agent_ref,
  suggested_assignee_ref,
  reviewer_ref
FROM tasks
WHERE id = $1
`, taskID).Scan(
		&canonicalRef,
		&assignedAt,
		&claimedMemberRef,
		&claimedRef,
		&claimedAt,
		&currentAgentRef,
		&suggestedAssigneeRef,
		&reviewerRef,
	))
	require.Equal(t, expected.CanonicalRef, canonicalRef.String)
	require.Equal(t, expected.LegacyRef, claimedMemberRef.String)
	require.Equal(t, expected.LegacyRef, claimedRef.String)
	require.Equal(t, expected.Assigned, assignedAt.Valid)
	require.Equal(t, expected.Assigned, claimedAt.Valid)
	require.False(t, currentAgentRef.Valid)
	require.Empty(t, suggestedAssigneeRef)
	require.Empty(t, reviewerRef)
}
