package taskboard

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

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

func TestTaskScopeParticipantsDefaultUnwatchedOnCreateClaimAndDiscussion(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},
		Task: &TaskCreateInput{
			Title:       "Default task notification policy",
			AssigneeRef: fixture.agentMemberID,
		},
	})
	require.NoError(t, err)
	require.Len(t, created.Tasks, 1)
	task := created.Tasks[0]

	requireTaskScopeMemberForTest(t, ctx, fixture.pool, task.TaskScopeID, fixture.ownerMemberID)
	requireTaskScopeNotificationForTest(t, ctx, fixture.pool, task.TaskScopeID, fixture.ownerMemberID, "mute")
	requireTaskScopeMemberForTest(t, ctx, fixture.pool, task.TaskScopeID, fixture.agentMemberID)
	requireTaskScopeNotificationForTest(t, ctx, fixture.pool, task.TaskScopeID, fixture.agentMemberID, "no-wake")

	agentCreated, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: NewAgentServiceContext(fixture.workspaceID, fixture.agentID, fixture.agentMemberID),
		Board:   BoardRef{Channel: "#all"},
		Titles:  []string{"Agent-created task participant"},
	})
	require.NoError(t, err)
	require.Len(t, agentCreated.Tasks, 1)
	requireTaskScopeMemberForTest(t, ctx, fixture.pool, agentCreated.Tasks[0].TaskScopeID, fixture.agentMemberID)
	requireTaskScopeNotificationForTest(t, ctx, fixture.pool, agentCreated.Tasks[0].TaskScopeID, fixture.agentMemberID, "no-wake")

	claimTarget, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: taskboardUserContext(fixture),
		Board:   BoardRef{ChannelID: fixture.channelID},
		Titles:  []string{"Claimed task remains unwatched"},
	})
	require.NoError(t, err)
	require.Len(t, claimTarget.Tasks, 1)
	claimTask := claimTarget.Tasks[0]

	_, claimResults, changedTaskNumbers, err := fixture.service.ClaimForAgent(ctx, AgentActor{
		WorkspaceID: fixture.workspaceID,
		MemberID:    fixture.agentMemberID,
	}, "#all", []int64{claimTask.TaskNumber})
	require.NoError(t, err)
	require.Len(t, claimResults, 1)
	require.True(t, claimResults[0].Success)
	require.Equal(t, []int64{claimTask.TaskNumber}, changedTaskNumbers)
	requireTaskScopeMemberForTest(t, ctx, fixture.pool, claimTask.TaskScopeID, fixture.agentMemberID)
	requireTaskScopeNotificationForTest(t, ctx, fixture.pool, claimTask.TaskScopeID, fixture.agentMemberID, "no-wake")

	_, err = fixture.service.SendTaskScopeMessage(ctx, SendTaskScopeMessageRequest{
		Context: NewAgentServiceContext(fixture.workspaceID, fixture.agentID, fixture.agentMemberID),
		Board:   BoardRef{Channel: "#all"},
		Task:    TaskRef{TaskNumber: claimTask.TaskNumber},
		Message: TaskScopeMessageInput{Content: "Implementation update from the assigned agent."},
	})
	require.NoError(t, err)
	requireTaskScopeNotificationForTest(t, ctx, fixture.pool, claimTask.TaskScopeID, fixture.agentMemberID, "no-wake")
}

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

	for _, test := range []struct {
		name  string
		state string
	}{
		{name: "unwatched", state: "no-wake"},
		{name: "watching", state: "auto-wake"},
	} {
		t.Run(test.name, func(t *testing.T) {
			created, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
				Context: taskboardUserContext(fixture),
				Board:   BoardRef{ChannelID: fixture.channelID},
				Titles:  []string{"Assignment preserves " + test.state},
			})
			require.NoError(t, err)
			require.Len(t, created.Tasks, 1)
			task := created.Tasks[0]

			seedTaskScopeNotificationForTest(t, ctx, fixture.pool, task.TaskScopeID, fixture.agentMemberID, test.state)
			_, err = fixture.service.UpdateTask(ctx, UpdateTaskRequest{
				Context: taskboardUserContext(fixture),
				Task:    TaskRef{TaskID: task.ID},
				Patch: TaskUpdatePatch{
					AssigneeRef: stringPointer(fixture.agentMemberID),
				},
			})
			require.NoError(t, err)
			requireTaskScopeMemberForTest(t, ctx, fixture.pool, task.TaskScopeID, fixture.agentMemberID)
			requireTaskScopeNotificationForTest(t, ctx, fixture.pool, task.TaskScopeID, fixture.agentMemberID, test.state)
		})
	}
}

func TestTaskWatchV2IgnoresLegacyClaimWakeDuringRollingDeploy(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{"Rolling Task Watch bridge"},
	})
	require.NoError(t, err)
	require.Len(t, created.Tasks, 1)
	task := created.Tasks[0]

	// A direct mention creates Task participation but does not create a
	// persistent Watch. It also gives this fixture a realistic Agent recipient
	// with an active machine.
	_, err = fixture.service.SendTaskScopeMessage(ctx, SendTaskScopeMessageRequest{
		Context: taskboardUserContext(fixture),
		Task:    TaskRef{TaskID: task.ID},
		Message: TaskScopeMessageInput{Content: "@task-detail-agent join without watching."},
	})
	require.NoError(t, err)
	requireTaskScopeNotificationForTest(t, ctx, fixture.pool, task.TaskScopeID, fixture.agentMemberID, "no-wake")

	// Simulate an overlapping old binary claiming/selecting the Agent. It only
	// knows the legacy column and rewrites that projection to auto-wake.
	mustExecTaskboard(t, ctx, fixture.pool, `
UPDATE scope_notification_settings
SET state = 'auto-wake', updated_at = $3
WHERE scope_id = $1 AND member_id = $2
`, task.TaskScopeID, fixture.agentMemberID, time.Now().UTC())

	before := countTaskAttentionOutboxesForTest(t, ctx, fixture.pool, task.TaskScopeID)
	_, err = fixture.service.AddTaskCheckpoint(ctx, AddTaskCheckpointRequest{
		Context:    taskboardUserContext(fixture),
		Task:       TaskRef{TaskID: task.ID},
		Checkpoint: TaskCheckpointInput{Text: "Legacy state must not wake"},
	})
	require.NoError(t, err)
	require.Equal(t, before, countTaskAttentionOutboxesForTest(t, ctx, fixture.pool, task.TaskScopeID))
	requireTaskScopeWatchStateForTest(t, ctx, fixture.pool, task.TaskScopeID, fixture.agentMemberID, "auto-wake", "no-wake")

	// A new explicit Watch writes the v2 authority and mirrors legacy state.
	require.NoError(t, message.UpsertTaskWatchNotificationState(
		ctx,
		fixture.pool,
		fixture.agentMemberID,
		task.TaskScopeID,
		"auto-wake",
		time.Now().UTC(),
	))
	_, err = fixture.service.AddTaskCheckpoint(ctx, AddTaskCheckpointRequest{
		Context:    taskboardUserContext(fixture),
		Task:       TaskRef{TaskID: task.ID},
		Checkpoint: TaskCheckpointInput{Text: "Explicit v2 Watch wakes"},
	})
	require.NoError(t, err)
	require.Equal(t, before+1, countTaskAttentionOutboxesForTest(t, ctx, fixture.pool, task.TaskScopeID))

	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)
	require.Len(t, payload.WakeRecipients, 1)
	require.Equal(t, fixture.agentMemberID, payload.WakeRecipients[0].AgentMemberID)
	require.Equal(t, message.AgentDeliveryReasonTaskWatch, payload.WakeRecipients[0].DeliveryReason)
	require.True(t, payload.WakeRecipients[0].AutoWakeEnabled)
}

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

	mentionTarget, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: taskboardUserContext(fixture),
		Board:   BoardRef{ChannelID: fixture.channelID},
		Titles:  []string{"Mentioned task participant"},
	})
	require.NoError(t, err)
	mentionTask := mentionTarget.Tasks[0]
	_, err = fixture.service.SendTaskScopeMessage(ctx, SendTaskScopeMessageRequest{
		Context: taskboardUserContext(fixture),
		Task:    TaskRef{TaskID: mentionTask.ID},
		Message: TaskScopeMessageInput{Content: "@task-detail-agent please inspect this task."},
	})
	require.NoError(t, err)
	requireTaskScopeMemberForTest(t, ctx, fixture.pool, mentionTask.TaskScopeID, fixture.agentMemberID)
	requireTaskScopeNotificationForTest(t, ctx, fixture.pool, mentionTask.TaskScopeID, fixture.agentMemberID, "no-wake")

	collaboratorTarget, err := fixture.service.CreateTasks(ctx, CreateTasksRequest{
		Context: taskboardUserContext(fixture),
		Board:   BoardRef{ChannelID: fixture.channelID},
		Titles:  []string{"Collaborator task participant"},
	})
	require.NoError(t, err)
	collaboratorTask := collaboratorTarget.Tasks[0]
	_, err = fixture.service.AddTaskCollaborator(ctx, AddTaskCollaboratorRequest{
		Context: taskboardUserContext(fixture),
		Task:    TaskRef{TaskID: collaboratorTask.ID},
		Collaborator: TaskCollaboratorInput{
			MemberID: fixture.reviewerMemberID,
			Role:     taskCollaboratorRoleContributor,
		},
	})
	require.NoError(t, err)
	requireTaskScopeMemberForTest(t, ctx, fixture.pool, collaboratorTask.TaskScopeID, fixture.reviewerMemberID)
	requireTaskScopeNotificationForTest(t, ctx, fixture.pool, collaboratorTask.TaskScopeID, fixture.reviewerMemberID, "mute")
}

type taskboardOwnedAgentFixture struct {
	OwnerUserID   string
	OwnerMemberID string
	AgentID       string
	AgentMemberID string
	MachineID     string
	OwnerName     string
	AgentName     string
}

func seedTaskboardOwnedAgentForTest(t *testing.T, ctx context.Context, fixture taskboardUserFixture, suffix string) taskboardOwnedAgentFixture {
	t.Helper()

	now := time.Date(2026, 5, 20, 8, 30, 0, 0, time.UTC)
	owned := taskboardOwnedAgentFixture{
		OwnerUserID:   "usr_task_detail_owner_" + suffix,
		OwnerMemberID: "mem_task_detail_owner_" + suffix,
		AgentID:       "agt_task_detail_" + suffix,
		AgentMemberID: "mem_task_detail_" + suffix,
		MachineID:     "mac_task_detail_" + suffix,
		OwnerName:     "Execution Owner " + suffix,
		AgentName:     "execution-agent-" + suffix,
	}
	mustExecTaskboard(t, ctx, fixture.pool, `
INSERT INTO users (id, email, name, display_name, avatar_url, is_admin, password_hash, email_verified_at, created_at, updated_at)
VALUES ($1, $2, $3, $4, '', FALSE, 'hash', $5, $5, $5)
`, owned.OwnerUserID, owned.OwnerUserID+"@example.com", owned.OwnerUserID, owned.OwnerName, now)
	mustExecTaskboard(t, ctx, fixture.pool, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, status, created_at, updated_at)
VALUES ($1, $2, 'human', $3, $4, 'member', 'active', $5, $5)
`, owned.OwnerMemberID, fixture.workspaceID, owned.OwnerName, owned.OwnerUserID, now)
	mustExecTaskboard(t, ctx, fixture.pool, `
INSERT INTO members (id, workspace_id, type, display_name, status, created_at, updated_at)
VALUES ($1, $2, 'agent', $3, 'active', $4, $4)
`, owned.AgentMemberID, fixture.workspaceID, owned.AgentName, now)
	mustExecTaskboard(t, ctx, fixture.pool, `
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)
`, fixture.channelID, owned.AgentMemberID, now)
	mustExecTaskboard(t, ctx, fixture.pool, `
INSERT INTO machines (id, workspace_id, created_by_user_id, name, api_key, connected_at, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $6, $6)
`, owned.MachineID, fixture.workspaceID, owned.OwnerUserID, owned.MachineID, "api_key_"+owned.MachineID, now)
	mustExecTaskboard(t, ctx, fixture.pool, `
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, $5, '', 'codex', 'gpt-5.4', '', '', '', TRUE, 'active', 'online', '', $6, $6)
`, owned.AgentID, fixture.workspaceID, owned.MachineID, owned.AgentMemberID, owned.AgentName, now)
	return owned
}

func seedTaskScopeNotificationForTest(t *testing.T, ctx context.Context, db coredb.QueryExecer, scopeID string, memberID string, state string) {
	t.Helper()

	now := time.Date(2026, 5, 20, 8, 0, 0, 0, time.UTC)
	_, err := db.Exec(ctx, `
INSERT INTO scope_notification_settings (
  scope_id, member_id, state, task_watch_state, created_at, updated_at
) VALUES ($1, $2, $3, $3, $4, $4)
`, scopeID, memberID, state, now)
	require.NoError(t, err)
}

func requireTaskScopeMemberForTest(t *testing.T, ctx context.Context, db coredb.QueryRower, scopeID string, memberID string) {
	t.Helper()

	var count int64
	require.NoError(t, db.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_memberships
WHERE scope_id = $1
  AND member_id = $2
  AND visibility = 'public'
  AND effective_to_seq IS NULL
  AND removed_at IS NULL
`, scopeID, memberID).Scan(&count))
	require.Equal(t, int64(1), count)
}

func requireTaskScopeNotificationForTest(t *testing.T, ctx context.Context, db coredb.QueryRower, scopeID string, memberID string, expectedState string) {
	t.Helper()

	var count int64
	var state string
	var taskWatchState string
	require.NoError(t, db.QueryRow(ctx, `
SELECT COUNT(*),
       CASE WHEN MAX(state) IS NULL THEN '' ELSE MAX(state) END,
       CASE WHEN MAX(task_watch_state) IS NULL THEN '' ELSE MAX(task_watch_state) END
FROM scope_notification_settings
WHERE scope_id = $1
  AND member_id = $2
`, scopeID, memberID).Scan(&count, &state, &taskWatchState))
	require.Equal(t, int64(1), count)
	require.Equal(t, expectedState, state)
	require.Equal(t, expectedState, taskWatchState)
}

func requireTaskScopeWatchStateForTest(
	t *testing.T,
	ctx context.Context,
	db coredb.QueryRower,
	scopeID string,
	memberID string,
	expectedLegacyState string,
	expectedTaskWatchState string,
) {
	t.Helper()

	var legacyState, taskWatchState string
	require.NoError(t, db.QueryRow(ctx, `
SELECT state, COALESCE(task_watch_state, '')
FROM scope_notification_settings
WHERE scope_id = $1 AND member_id = $2
`, scopeID, memberID).Scan(&legacyState, &taskWatchState))
	require.Equal(t, expectedLegacyState, legacyState)
	require.Equal(t, expectedTaskWatchState, taskWatchState)
}

func countTaskAttentionOutboxesForTest(t *testing.T, ctx context.Context, db coredb.QueryRower, scopeID string) int64 {
	t.Helper()

	var count int64
	require.NoError(t, db.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_fanout_outbox
WHERE scope_id = $1
  AND event_ref_type = 'activity_event'
`, scopeID).Scan(&count))
	return count
}

func requireNoTaskInboxProjectionForTest(t *testing.T, ctx context.Context, db coredb.QueryRower, scopeID string, memberID string) {
	t.Helper()

	require.Equal(t, int64(0), countConversationMemberStatesForTest(t, ctx, db, scopeID, memberID), "participant ensure must not create or refresh Task Inbox projection rows")
}
