package app

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	"github.com/tidbcloud/AgentTeam/internal/core/runeffect"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/service/appruntime"
	"github.com/tidbcloud/AgentTeam/internal/store/app/registry"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
)

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	appStore := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	now := time.Now().UTC()
	fixture := seedAppAdminFixture(t, pool, "bot_mention_delivery_channel", now)
	installation := installAppAppForTargetBindingTests(t, ctx, pool, appStore, fixture, "mention_channel", now)
	channelID := "chn_bot_mention_delivery_channel"
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, channelID, "app-mention-channel", "App mention channel", "private", fixture.MemberID, fixture.MemberID, now)

	_, err := appStore.AddChannelServiceForUserWithEvent(ctx, fixture.UserID, fixture.WorkspaceID, channelID, installation.MemberID)
	require.NoError(t, err)

	content := "@deploy-app-target-mention-channel upgrade --version 1.2.3"
	appMessage, delivery, err := appStore.SendMessageForUser(ctx, model.User{ID: fixture.UserID, Name: fixture.UserID, DisplayName: "App Operator"}, fixture.WorkspaceID, channelID, content, nil)
	require.NoError(t, err)
	require.Equal(t, appMessage.ID, delivery.MessageID)
	require.Empty(t, delivery.FollowupDeliveries)

	record := loadAppMentionDeliveryForTest(t, ctx, pool, fixture.WorkspaceID, appMessage.ID, installation.ID)
	require.Equal(t, serviceactor.SubscriptionKindAppMention, record.SubscriptionKind)
	require.Equal(t, "channel", record.TargetKind)
	require.Equal(t, channelID, record.TargetRef)
	require.Equal(t, channelID, record.DeliveryScopeID)
	require.Equal(t, AppMentionExternalResourceType, record.ExternalResourceType)
	require.Equal(t, appMessage.ID+":"+installation.ID, record.ExternalResourceKey)
	require.Equal(t, "agentteam.message.mention", record.EventType)
	require.Equal(t, "mention", record.EventAction)
	require.Equal(t, appMessage.ID, record.EventID)
	require.Equal(t, serviceactor.SubscriptionKindAppMention+":"+appMessage.ID+":"+installation.ID, record.IdempotencyKey)
	require.Equal(t, "running", record.Status)

	payload := decodeAppMentionPayloadForTest(t, record.EventPayload)
	require.Equal(t, fixture.WorkspaceID, payload["workspace"].(map[string]any)["id"])
	require.Equal(t, appMessage.ID, payload["message"].(map[string]any)["id"])
	require.Equal(t, content, payload["message"].(map[string]any)["text"])
	require.Equal(t, "human", payload["message"].(map[string]any)["author"].(map[string]any)["type"])
	mentions := payload["message"].(map[string]any)["mentions"].([]any)
	require.Len(t, mentions, 1)
	require.Equal(t, installation.MemberID, mentions[0].(map[string]any)["member_id"])
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	appStore := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	now := time.Now().UTC()
	fixture := seedAppAdminFixture(t, pool, "bot_mention_delivery_service_link", now)
	installation := installAppAppForTargetBindingTests(t, ctx, pool, appStore, fixture, "mention_service_link", now)
	channelID := "chn_bot_mention_delivery_service_link"
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, channelID, "app-mention-service-link", "App mention service link", "private", fixture.MemberID, fixture.MemberID, now)

	_, err := appStore.AddChannelServiceForUserWithEvent(ctx, fixture.UserID, fixture.WorkspaceID, channelID, installation.MemberID)
	require.NoError(t, err)

	token := "display-token-does-not-match-app-handle"
	content := fmt.Sprintf("[@%s](/mentions/service/%s) deploy main", token, installation.MemberID)
	appMessage, _, err := appStore.SendMessageForUser(ctx, model.User{ID: fixture.UserID, Name: fixture.UserID, DisplayName: "App Operator"}, fixture.WorkspaceID, channelID, content, nil)
	require.NoError(t, err)

	record := loadAppMentionDeliveryForTest(t, ctx, pool, fixture.WorkspaceID, appMessage.ID, installation.ID)
	payload := decodeAppMentionPayloadForTest(t, record.EventPayload)
	mentions := payload["message"].(map[string]any)["mentions"].([]any)
	require.Len(t, mentions, 1)
	mention := mentions[0].(map[string]any)
	require.Equal(t, token, mention["token"])
	require.Equal(t, installation.MemberID, mention["member_id"])
	require.Equal(t, map[string]any{
		"start": float64(1),
		"end":   float64(len(token) + 2),
	}, mention["range"])

	wrongMemberContent := fmt.Sprintf("[@%s](/mentions/service/%s-other) deploy main", token, installation.MemberID)
	wrongMemberMessage, _, err := appStore.SendMessageForUser(ctx, model.User{ID: fixture.UserID, Name: fixture.UserID, DisplayName: "App Operator"}, fixture.WorkspaceID, channelID, wrongMemberContent, nil)
	require.NoError(t, err)
	requireAppMentionDeliveryCountForTest(t, ctx, pool, fixture.WorkspaceID, wrongMemberMessage.ID, installation.ID, 0)

	inertContent := fmt.Sprintf("`[@%s](/mentions/service/%s)`\n> [@%s](/mentions/service/%s)", token, installation.MemberID, token, installation.MemberID)
	inertMessage, _, err := appStore.SendMessageForUser(ctx, model.User{ID: fixture.UserID, Name: fixture.UserID, DisplayName: "App Operator"}, fixture.WorkspaceID, channelID, inertContent, nil)
	require.NoError(t, err)
	requireAppMentionDeliveryCountForTest(t, ctx, pool, fixture.WorkspaceID, inertMessage.ID, installation.ID, 0)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	appStore := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	now := time.Now().UTC()
	fixture := seedAppAdminFixture(t, pool, "bot_mention_delivery_legacy_all", now)
	installation := installAppAppForTargetBindingTests(t, ctx, pool, appStore, fixture, "legacy_all", now)

	seedLegacyAppMentionTargetForTest(t, ctx, pool, fixture, installation, "channel", fixture.AllChannelID, fixture.AllChannelID, fixture.AllChannelID, "all_channel", now)
	requireAppMentionTargetBindingCountForTest(t, ctx, pool, fixture.WorkspaceID, installation.ID, "channel", fixture.AllChannelID, 1)
	requireAppMentionSubscriptionCountForTest(t, ctx, pool, fixture.WorkspaceID, installation.ID, "channel", fixture.AllChannelID, 1)

	allMessage, _, err := appStore.SendMessageForUser(ctx, model.User{
		ID: fixture.UserID, Name: fixture.UserID, DisplayName: "App Operator",
	}, fixture.WorkspaceID, fixture.AllChannelID, "@deploy-app-target-legacy-all must not run in all", nil)
	require.NoError(t, err)
	requireAppMentionDeliveryCountForTest(t, ctx, pool, fixture.WorkspaceID, allMessage.ID, installation.ID, 0)

	parentMessage, _, err := appStore.SendMessageForUser(ctx, model.User{
		ID: fixture.UserID, Name: fixture.UserID, DisplayName: "App Operator",
	}, fixture.WorkspaceID, fixture.AllChannelID, "legacy #all thread parent", nil)
	require.NoError(t, err)
	replyThread, err := message.OpenReplyThreadForMember(ctx, pool, fixture.MemberID, fixture.WorkspaceID, fixture.AllChannelID, parentMessage.ID)
	require.NoError(t, err)
	threadID := replyThread.ThreadChannelID
	require.NotEmpty(t, threadID)

	seedLegacyAppMentionTargetForTest(t, ctx, pool, fixture, installation, "thread", threadID, threadID, fixture.AllChannelID, "all_thread", now.Add(time.Second))
	requireAppMentionTargetBindingCountForTest(t, ctx, pool, fixture.WorkspaceID, installation.ID, "thread", threadID, 1)
	requireAppMentionSubscriptionCountForTest(t, ctx, pool, fixture.WorkspaceID, installation.ID, "thread", threadID, 1)

	threadMessage, _, err := appStore.SendMessageForUser(ctx, model.User{
		ID: fixture.UserID, Name: fixture.UserID, DisplayName: "App Operator",
	}, fixture.WorkspaceID, threadID, "@deploy-app-target-legacy-all must not run in an all thread", nil)
	require.NoError(t, err)
	requireAppMentionDeliveryCountForTest(t, ctx, pool, fixture.WorkspaceID, threadMessage.ID, installation.ID, 0)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	appStore := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	now := time.Now().UTC()
	fixture := seedAppAdminFixture(t, pool, "bot_mention_delivery_approved", now)
	slug := "deploy-app-approved"

	insertExternalAppAppForTests(t, pool, externalAppAppSeed{
		AppID:       "app_deploy_bot_approved",
		VersionID:   "appver_deploy_bot_approved_1_0_0",
		Slug:        slug,
		Name:        "Deploy App Approved",
		Visibility:  "unlisted",
		Version:     "1.0.0",
		Scopes:      []string{"app.mentions.read", "messages.write"},
		CallbackURL: "https://example.com/approved-app",
		CreatedAt:   now,
		Manifest: map[string]any{
			"schema_version": "1",
			"slug":           slug,
			"name":           "Deploy App Approved",
			"version":        "1.0.0",
			"scopes":         []string{"app.mentions.read", "messages.write"},
			"callback":       map[string]any{"url": "https://example.com/approved-app"},
			"capabilities": map[string]any{
				"runtime": map[string]any{
					"runtime": map[string]any{
						"transports":        []string{serviceactor.RuntimeTransportKindCallback},
						"default_transport": serviceactor.RuntimeTransportKindCallback,
					},
					"events": []string{"agentteam.message.mention"},
					"scopes": []string{"app.mentions.read", "messages.write"},
				},
			},
		},
	})

	installation := requestAndApproveExternalAppInstallationForTests(
		t, ctx, appStore, fixture.UserID, fixture.UserID, fixture.WorkspaceID,
		"app_deploy_bot_approved", slug, nil,
	)
	channelID := "chn_bot_mention_delivery_approved"
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, channelID, "app-mention-approved", "App mention approved", "private", fixture.MemberID, fixture.MemberID, now)

	_, err := appStore.AddChannelServiceForUserWithEvent(ctx, fixture.UserID, fixture.WorkspaceID, channelID, installation.MemberID)
	require.NoError(t, err)

	appMessage, _, err := appStore.SendMessageForUser(ctx, model.User{ID: fixture.UserID, Name: fixture.UserID, DisplayName: "App Operator"}, fixture.WorkspaceID, channelID, "@deploy-app-approved ping", nil)
	require.NoError(t, err)

	record := loadAppMentionDeliveryForTest(t, ctx, pool, fixture.WorkspaceID, appMessage.ID, installation.ID)
	require.Equal(t, serviceactor.SubscriptionKindAppMention, record.SubscriptionKind)
	require.Equal(t, "running", record.Status)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	appStore := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	now := time.Now().UTC()
	fixture := seedAppAdminFixture(t, pool, "bot_mention_delivery_binding", now)
	installation := installAppAppForTargetBindingTests(t, ctx, pool, appStore, fixture, "mention_binding", now)
	channelID := "chn_bot_mention_delivery_binding"
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, channelID, "app-mention-binding", "App mention binding", "private", fixture.MemberID, fixture.MemberID, now)

	_, err := appStore.AddChannelServiceForUserWithEvent(ctx, fixture.UserID, fixture.WorkspaceID, channelID, installation.MemberID)
	require.NoError(t, err)
	mustExec(t, pool, `
UPDATE service_target_bindings
SET status = 'disabled',
    disabled_reason = 'test disabled binding',
    active_binding_key = NULL,
    updated_at = $4
WHERE workspace_id = $1
  AND service_installation_id = $2
  AND target_kind = 'channel'
  AND target_ref = $3
`, fixture.WorkspaceID, installation.ID, channelID, now.Add(time.Second))

	appMessage, _, err := appStore.SendMessageForUser(ctx, model.User{ID: fixture.UserID, Name: fixture.UserID, DisplayName: "App Operator"}, fixture.WorkspaceID, channelID, "@deploy-app-target-mention-binding upgrade", nil)
	require.NoError(t, err)
	requireAppMentionDeliveryCountForTest(t, ctx, pool, fixture.WorkspaceID, appMessage.ID, installation.ID, 0)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	appStore := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	now := time.Now().UTC()
	fixture := seedAppAdminFixture(t, pool, "bot_mention_delivery_inert", now)
	installation := installAppAppForTargetBindingTests(t, ctx, pool, appStore, fixture, "mention_inert", now)
	channelID := "chn_bot_mention_delivery_inert"
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, channelID, "app-mention-inert", "App mention inert", "private", fixture.MemberID, fixture.MemberID, now)

	_, err := appStore.AddChannelServiceForUserWithEvent(ctx, fixture.UserID, fixture.WorkspaceID, channelID, installation.MemberID)
	require.NoError(t, err)

	content := "please @deploy-app-target-mention-inert run\n`@deploy-app-target-mention-inert ignored`\n> @deploy-app-target-mention-inert ignored\nagain @deploy-app-target-mention-inert"
	appMessage, _, err := appStore.SendMessageForUser(ctx, model.User{ID: fixture.UserID, Name: fixture.UserID, DisplayName: "App Operator"}, fixture.WorkspaceID, channelID, content, nil)
	require.NoError(t, err)

	record := loadAppMentionDeliveryForTest(t, ctx, pool, fixture.WorkspaceID, appMessage.ID, installation.ID)
	payload := decodeAppMentionPayloadForTest(t, record.EventPayload)
	mentions := payload["message"].(map[string]any)["mentions"].([]any)
	require.Len(t, mentions, 2)
	requireAppMentionDeliveryCountForTest(t, ctx, pool, fixture.WorkspaceID, appMessage.ID, installation.ID, 1)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	appStore := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	now := time.Now().UTC()
	fixture := seedAppAdminFixture(t, pool, "bot_mention_delivery_limit", now)
	installation := installAppAppForTargetBindingTests(t, ctx, pool, appStore, fixture, "mention_limit", now)
	channelID := "chn_bot_mention_delivery_limit"
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, channelID, "app-mention-limit", "App mention limit", "private", fixture.MemberID, fixture.MemberID, now)

	_, err := appStore.AddChannelServiceForUserWithEvent(ctx, fixture.UserID, fixture.WorkspaceID, channelID, installation.MemberID)
	require.NoError(t, err)

	overflowTokens := make([]string, 0, message.MaxScopeDirectMentionTokens+1)
	for i := 0; i < message.MaxScopeDirectMentionTokens; i++ {
		overflowTokens = append(overflowTokens, fmt.Sprintf("@noise-%02d", i))
	}
	appToken := "@deploy-app-target-mention-limit"
	overflowTokens = append(overflowTokens, appToken)
	overflowMessage, _, err := appStore.SendMessageForUser(ctx, model.User{ID: fixture.UserID, Name: fixture.UserID, DisplayName: "App Operator"}, fixture.WorkspaceID, channelID, strings.Join(overflowTokens, " "), nil)
	require.NoError(t, err)
	requireAppMentionDeliveryCountForTest(t, ctx, pool, fixture.WorkspaceID, overflowMessage.ID, installation.ID, 0)

	boundedTokens := make([]string, 0, message.MaxScopeDirectMentionTokens+1)
	for i := 0; i < message.MaxScopeDirectMentionTokens-1; i++ {
		boundedTokens = append(boundedTokens, fmt.Sprintf("@noise-%02d", i))
	}
	boundedTokens = append(boundedTokens, appToken, appToken)
	boundedMessage, _, err := appStore.SendMessageForUser(ctx, model.User{ID: fixture.UserID, Name: fixture.UserID, DisplayName: "App Operator"}, fixture.WorkspaceID, channelID, strings.Join(boundedTokens, " "), nil)
	require.NoError(t, err)

	record := loadAppMentionDeliveryForTest(t, ctx, pool, fixture.WorkspaceID, boundedMessage.ID, installation.ID)
	payload := decodeAppMentionPayloadForTest(t, record.EventPayload)
	mentions := payload["message"].(map[string]any)["mentions"].([]any)
	require.Len(t, mentions, 2)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	appStore := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	fanout := &recordingCommittedMessageFanout{}
	appStore.SetCommittedMessageFanout(fanout)
	now := time.Now().UTC()
	fixture := seedAppAdminFixture(t, pool, "bot_mention_delivery_task", now)
	installation := installAppAppForTargetBindingTests(t, ctx, pool, appStore, fixture, "mention_task", now)
	taskID := "tsk_bot_mention_delivery_task"
	seedTaskTargetForAppBindingTests(t, pool, fixture, taskID, now)

	_, err := appStore.bindAppInstallationTargetForUser(ctx, fixture.UserID, fixture.WorkspaceID, installation.ID, "task:"+taskID, AppMentionCreatedViaTarget)
	require.NoError(t, err)

	appMessage, _, err := appStore.SendMessageForUser(
		ctx,
		model.User{ID: fixture.UserID, Name: fixture.UserID, DisplayName: "App Operator"},
		fixture.WorkspaceID,
		taskID,
		"@deploy-app-target-mention-task deploy staging",
		nil,
	)
	require.NoError(t, err)

	record := loadAppMentionDeliveryForTest(t, ctx, pool, fixture.WorkspaceID, appMessage.ID, installation.ID)
	require.Equal(t, "task", record.TargetKind)
	require.Equal(t, taskID, record.TargetRef)
	require.Equal(t, taskID, record.DeliveryScopeID)
	require.Equal(t, fixture.AllChannelID, record.PermissionCheckScopeID)

	credential, err := appStore.CreateAppRunnerCredentialForUser(ctx, fixture.UserID, fixture.WorkspaceID, installation.ID, AppRunnerCredentialCreateParams{})
	require.NoError(t, err)
	runnerAuth, err := newRunnerServiceForTest(appStore).AuthenticateRunner(ctx, credential.Token)
	require.NoError(t, err)
	claimed, err := newRunnerServiceForTest(appStore).ClaimDeliveries(ctx, runnerAuth, appruntime.RunnerClaimParams{
		RunnerID: "runner-task-reply", RunnerVersion: "1.0.0", Limit: 1, LeaseDuration: time.Minute,
	})
	require.NoError(t, err)
	require.Len(t, claimed.Envelopes, 1)
	envelope := claimed.Envelopes[0]
	deliveryAuth, err := appStore.AuthenticateAppRuntimeDelivery(ctx, envelope.DeliveryToken, envelope.Delivery.ID, envelope.Attempt.ID)
	require.NoError(t, err)
	// The operator mention message above is already one Task discussion; capture
	// the durable baseline before the App reply so idempotent replay can prove it
	// does not add a second App reply.
	var baselineDiscussionCount int
	require.NoError(t, pool.QueryRow(ctx, `SELECT discussion_count FROM tasks WHERE id = $1`, taskID).Scan(&baselineDiscussionCount))
	var baselineTimelineCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_activity_events ae
JOIN tasks t ON t.task_scope_id = ae.scope_id
WHERE t.id = $1
  AND ae.activity_type = 'task.discussion_added'
`, taskID).Scan(&baselineTimelineCount))

	reply, err := appStore.ReplyAppRuntimeDelivery(ctx, deliveryAuth, AppRuntimeReplyParams{
		AttemptID: envelope.Attempt.ID, Content: "task-scoped App reply", IdempotencyKey: "task-progress-1",
	})
	require.NoError(t, err)
	require.Equal(t, string(runeffect.EffectKindMessage), reply.Effect.EffectKind)
	require.Equal(t, reply.MessageID, reply.Effect.MessageID)
	require.NotEmpty(t, reply.MessageID)
	var outboxCount int64
	require.NoError(t, pool.QueryRow(ctx, `SELECT COUNT(*) FROM scope_fanout_outbox WHERE message_id = $1`, reply.MessageID).Scan(&outboxCount))
	require.Equal(t, int64(1), outboxCount, "App Task reply must write the resolved scope_fanout outbox row")
	var outboxPayloadSet int64
	require.NoError(t, pool.QueryRow(ctx, `SELECT COUNT(*) FROM scope_fanout_outbox WHERE message_id = $1 AND payload IS NOT NULL`, reply.MessageID).Scan(&outboxPayloadSet))
	require.Equal(t, int64(1), outboxPayloadSet, "App Task reply outbox row must carry the recoverable post-commit payload")
	require.Len(t, fanout.deliveries, 1, "App Task reply must emit the store-owned recoverable post-commit delivery")
	require.Equal(t, reply.MessageID, fanout.deliveries[0].MessageID)
	require.True(t, fanout.deliveries[0].HumanRecipientsTargeted, "App Task reply delivery must freeze the targeted Task Human audience")

	replayed, err := appStore.ReplyAppRuntimeDelivery(ctx, deliveryAuth, AppRuntimeReplyParams{
		AttemptID: envelope.Attempt.ID, Content: "task-scoped App reply", IdempotencyKey: "task-progress-1",
	})
	require.NoError(t, err)
	require.Equal(t, reply.Effect.ID, replayed.Effect.ID)
	require.Equal(t, reply.MessageID, replayed.MessageID)
	require.Len(t, fanout.deliveries, 1, "idempotent App reply replay must not emit a second post-commit delivery")

	// Idempotent replay must not duplicate any durable Task fact, outbox row,
	// timeline entry, or unread/notification/wake side effect.
	var outboxAfter int64
	require.NoError(t, pool.QueryRow(ctx, `SELECT COUNT(*) FROM scope_fanout_outbox WHERE message_id = $1`, reply.MessageID).Scan(&outboxAfter))
	require.Equal(t, int64(1), outboxAfter, "idempotent App reply replay must not create a second outbox row")
	var taskDiscussionCount int
	require.NoError(t, pool.QueryRow(ctx, `SELECT discussion_count FROM tasks WHERE id = $1`, taskID).Scan(&taskDiscussionCount))
	require.Equal(t, baselineDiscussionCount+1, taskDiscussionCount, "idempotent App reply replay must not re-touch the Task discussion counter")
	var timelineCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_activity_events ae
JOIN tasks t ON t.task_scope_id = ae.scope_id
WHERE t.id = $1
  AND ae.activity_type = 'task.discussion_added'
`, taskID).Scan(&timelineCount))
	require.Equal(t, baselineTimelineCount+1, timelineCount, "idempotent App reply replay must not write a duplicate Task timeline entry")
	var messageCount int
	require.NoError(t, pool.QueryRow(ctx, `SELECT COUNT(*) FROM scope_messages WHERE id = $1`, reply.MessageID).Scan(&messageCount))
	require.Equal(t, 1, messageCount, "idempotent App reply replay must not create a duplicate Task discussion message")
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	appStore := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	now := time.Now().UTC()
	fixture := seedAppAdminFixture(t, pool, "bot_mention_delivery_service", now)
	installation := installAppAppForTargetBindingTests(t, ctx, pool, appStore, fixture, "mention_service", now)
	channelID := "chn_bot_mention_delivery_service"
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, channelID, "app-mention-service", "App mention service", "private", fixture.MemberID, fixture.MemberID, now)

	_, err := appStore.AddChannelServiceForUserWithEvent(ctx, fixture.UserID, fixture.WorkspaceID, channelID, installation.MemberID)
	require.NoError(t, err)
	installationView, err := appStore.AppInstallationViewByID(ctx, fixture.WorkspaceID, installation.ID)
	require.NoError(t, err)

	err = coredb.WithTx(ctx, pool, func(tx coredb.Tx) error {
		_, _, appendErr := appStore.appendAppServiceMessageWithOptions(ctx, tx, model.RuntimeAppInstallation{
			InstallationID: installationView.ID,
			AppID:          installationView.AppID,
			AppSlug:        installationView.AppSlug,
			AppName:        installationView.AppName,
			MemberID:       installationView.MemberID,
			Handle:         installationView.Handle,
			WorkspaceID:    fixture.WorkspaceID,
		}, message.ThreadAccess{
			ThreadID: channelID, ThreadType: domain.ThreadTypeChannel,
			ChannelID: channelID, ChannelName: "app-mention-service",
		}, "@deploy-app-target-mention-service should not route", nil, false,
			appendMessageOptions{SuppressAgentRouting: true},
			appServiceMessageProviderEvent,
			nil,
		)
		return appendErr
	})
	require.NoError(t, err)
	requireAppMentionDeliveryCountForTest(t, ctx, pool, fixture.WorkspaceID, "", installation.ID, 0)

	require.False(t, appMentionAuthorTypeCanTrigger(string(domain.MemberTypeService)))
}

type appMentionDeliveryTestRecord struct {
	SubscriptionKind       string
	TargetKind             string
	TargetRef              string
	DeliveryScopeID        string
	PermissionCheckScopeID string
	ExternalResourceType   string
	ExternalResourceKey    string
	EventType              string
	EventAction            string
	EventID                string
	IdempotencyKey         string
	Status                 string
	EventPayload           []byte
}

func loadAppMentionDeliveryForTest(t *testing.T, ctx context.Context, queryer coredb.Queryer, workspaceID string, sourceMessageID string, installationID string) appMentionDeliveryTestRecord {
	t.Helper()
	rows, err := queryer.Query(ctx, `
SELECT subscription_kind, target_kind, target_ref, delivery_scope_id, permission_check_scope_id,
       external_resource_type, external_resource_key, event_type, event_action, event_id,
       idempotency_key, status, event_payload
FROM service_event_deliveries
WHERE workspace_id = $1
  AND event_id = $2
  AND service_installation_id = $3
  AND subscription_kind = $4
ORDER BY created_at ASC, id ASC
`, workspaceID, sourceMessageID, installationID, serviceactor.SubscriptionKindAppMention)
	require.NoError(t, err)
	defer rows.Close()
	var records []appMentionDeliveryTestRecord
	for rows.Next() {
		var record appMentionDeliveryTestRecord
		require.NoError(t, rows.Scan(
			&record.SubscriptionKind,
			&record.TargetKind,
			&record.TargetRef,
			&record.DeliveryScopeID,
			&record.PermissionCheckScopeID,
			&record.ExternalResourceType,
			&record.ExternalResourceKey,
			&record.EventType,
			&record.EventAction,
			&record.EventID,
			&record.IdempotencyKey,
			&record.Status,
			&record.EventPayload,
		))
		records = append(records, record)
	}
	require.NoError(t, rows.Err())
	require.Len(t, records, 1)
	return records[0]
}

func requireAppMentionDeliveryCountForTest(t *testing.T, ctx context.Context, queryer coredb.Queryer, workspaceID string, sourceMessageID string, installationID string, want int) {
	t.Helper()
	query := `
SELECT COUNT(*)
FROM service_event_deliveries
WHERE workspace_id = $1
  AND service_installation_id = $2
  AND subscription_kind = $3`
	args := []any{workspaceID, installationID, serviceactor.SubscriptionKindAppMention}
	if sourceMessageID != "" {
		query += "\n  AND event_id = $4"
		args = append(args, sourceMessageID)
	}
	var got int
	require.NoError(t, queryer.QueryRow(ctx, query, args...).Scan(&got))
	require.Equal(t, want, got)
}

func seedLegacyAppMentionTargetForTest(
	t *testing.T,
	ctx context.Context,
	pool coredb.QueryExecer,
	fixture appAdminFixture,
	installation appTargetInstallationFixture,
	targetKind string,
	targetRef string,
	deliveryScopeID string,
	permissionScopeID string,
	suffix string,
	now time.Time,
) {
	t.Helper()
	var serviceDefinitionID, serviceMemberID string
	require.NoError(t, pool.QueryRow(ctx, `
SELECT service_definition_id, id
FROM service_members
WHERE workspace_id = $1
  AND service_installation_id = $2
  AND member_id = $3
  AND service_kind = 'app'
LIMIT 1
`, fixture.WorkspaceID, installation.ID, installation.MemberID).Scan(&serviceDefinitionID, &serviceMemberID))
	require.NoError(t, message.EnsureScopeMembership(ctx, pool, deliveryScopeID, installation.MemberID, "public", "", "no-wake", 0, 0, now))

	bindingID := "svctb_legacy_bot_mention_" + suffix
	subscriptionID := "svcesub_legacy_bot_mention_" + suffix
	mustExec(t, pool, `
INSERT INTO service_target_bindings (
    id, workspace_id, service_definition_id, service_installation_id,
    service_member_id, service_kind, target_kind, target_ref,
    delivery_scope_id, permission_check_scope_id,
    status, disabled_reason, created_by_member_id, created_by_actor_type,
    created_by_actor_id, created_via, scope_snapshot, metadata,
    created_at, updated_at, active_binding_key
) VALUES (
    $1, $2, $3, $4,
    $5, 'app', $6, $7,
    $8, $9,
    'active', '', $10, 'human',
    $11, 'legacy_install', '{}', '{}',
    $12, $12, $13
)
`, bindingID, fixture.WorkspaceID, serviceDefinitionID, installation.ID, serviceMemberID, targetKind, targetRef, deliveryScopeID, permissionScopeID, fixture.MemberID, fixture.UserID, now, "legacy-binding-"+suffix)
	mustExec(t, pool, `
INSERT INTO service_event_subscriptions (
    id, workspace_id, service_definition_id, service_installation_id,
    service_member_id, service_connection_ref, service_target_binding_id,
    service_kind, subscription_kind, target_kind, target_ref,
    delivery_scope_id, permission_check_scope_id, owner_member_id,
    authorizer_member_id, created_by_actor_type, created_by_actor_id,
    created_via, external_resource_type, external_resource_key,
    event_filter_key, event_filters, delivery_policy, notify_principal,
    status, active_subscription_key, last_failure_detail, metadata,
    created_at, updated_at
) VALUES (
    $1, $2, $3, $4,
    $5, $6, $7,
    'app', $8, $9, $10,
    $11, $12, $13,
    $13, 'human', $14,
    'legacy_install', $15, $16,
    $17, '{"events":["agentteam.message.mention"]}', $18, FALSE,
    'active', $19, '', '{}',
    $20, $20
)
`, subscriptionID, fixture.WorkspaceID, serviceDefinitionID, installation.ID, serviceMemberID, AppMentionServiceConnectionRef, bindingID, serviceactor.SubscriptionKindAppMention, targetKind, targetRef, deliveryScopeID, permissionScopeID, fixture.MemberID, fixture.UserID, AppMentionExternalResourceType, "legacy:"+suffix, registry.AppEventMessageMention, AppMentionDeliveryPolicy, "legacy-subscription-"+suffix, now)
}

func decodeAppMentionPayloadForTest(t *testing.T, payload []byte) map[string]any {
	t.Helper()
	var decoded map[string]any
	require.NoError(t, json.Unmarshal(payload, &decoded))
	return decoded
}
