package app

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

	"github.com/google/uuid"
	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/service/serviceaccount"
	"github.com/tidbcloud/AgentTeam/internal/store/attachment"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	serviceaccountstore "github.com/tidbcloud/AgentTeam/internal/store/serviceaccount"
)

func serviceAccountPostTestOptions(clientMessageID string, extra map[string]any) message.HumanMessageCreateOptions {
	metadata := map[string]any{
		serviceAccountPostMetadataKey: map[string]any{"title": "Publisher update"},
	}
	for key, value := range extra {
		metadata[key] = value
	}
	return message.HumanMessageCreateOptions{
		ClientMessageID: clientMessageID,
		MessageType:     message.MessageTypeServiceAccountPost,
		Metadata:        metadata,
	}
}

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

	ctx := context.Background()
	db := startAppRollbackDB(t, ctx)
	store := New(db, Options{})
	now := time.Date(2026, 7, 11, 1, 0, 0, 0, time.UTC)
	owner := seedAppAdminFixture(t, db, "service_account_publisher_owner", now)
	publisher := seedWorkspaceMemberFixture(t, db, owner.WorkspaceID, "service_account_publisher_one", now)
	secondPublisher := seedWorkspaceMemberFixture(t, db, owner.WorkspaceID, "service_account_publisher_two", now)
	audienceMember := seedWorkspaceMemberFixture(t, db, owner.WorkspaceID, "service_account_audience_member", now)
	accountID := "sacct_publisher-news"
	scopeID := "sasc_publisher-news"
	authorMemberID := "mem_sa_publisher-news"

	flow := serviceaccount.New(db)
	materialized, err := flow.MaterializeWorkspace(ctx, serviceaccount.WorkspaceMaterialization{
		Identity: serviceaccount.IdentityRefs{
			AccountID:             accountID,
			ServiceInstallationID: "svcinst_sa_publisher-news",
			ServiceMemberID:       "svcmem_sa_publisher-news",
			AuthorMemberID:        authorMemberID,
			MessageScopeID:        scopeID,
			NameClaimID:           "sachc_publisher-news",
		},
		WorkspaceID:       owner.WorkspaceID,
		CreatorMemberID:   publisher.MemberID,
		Name:              "Publisher News",
		ManagerMemberIDs:  []string{publisher.MemberID, secondPublisher.MemberID},
		AuthorizationPath: serviceaccountstore.AuthorizationPathDirectCreate,
		Now:               now,
	})
	require.NoError(t, err)
	ready, err := flow.ReconcileAllWorkspaceHumans(ctx, owner.WorkspaceID, accountID, materialized.Account.Version, materialized.Account.AudiencePolicyVersion, now.Add(time.Second))
	require.NoError(t, err)
	require.Equal(t, serviceaccountstore.AudienceStateReady, ready.AudienceState)

	var beforeWorkspaceSeq int64
	require.NoError(t, db.QueryRow(ctx, `SELECT latest_message_seq FROM workspaces WHERE id = $1`, owner.WorkspaceID).Scan(&beforeWorkspaceSeq))
	clientMessageID := uuid.NewString()
	user := model.User{ID: publisher.UserID, Name: "service_account_publisher_one", DisplayName: "Publisher One"}
	created, delivery, err := store.SendMessageForUserWithOptions(ctx, user, owner.WorkspaceID, scopeID, "Service account publisher message", nil, serviceAccountPostTestOptions(clientMessageID, map[string]any{"clientFact": "retained"}))
	require.NoError(t, err)
	require.False(t, delivery.IdempotentReplay)
	require.NotEmpty(t, created.ID)
	require.Equal(t, int64(1), created.Seq)
	require.Equal(t, scopeID, created.ChannelID)
	require.Equal(t, serviceaccountstore.ServiceKind, created.ChannelType)
	require.Equal(t, message.AuthorActorTypeService, created.AuthorProfile.ActorType)
	stats, err := loadMessageParticipationStats(ctx, db, owner.WorkspaceID, scopeID, publisher.MemberID)
	require.NoError(t, err)
	require.Equal(t, message.MessageParticipationStats{}, stats)
	require.Equal(t, message.AuthorServiceKindServiceAccount, created.AuthorProfile.ServiceKind)
	require.Equal(t, message.MessageTypeServiceAccountPost, created.MessageType)
	require.Equal(t, accountID, created.AuthorProfile.PrincipalID)
	require.Equal(t, authorMemberID, created.AuthorProfile.MemberID)
	require.Equal(t, "service-account:"+accountID, created.AuthorProfile.TargetID)
	require.Equal(t, message.ContributionSourceKindServiceDirectPost, created.Source.Kind)
	require.Equal(t, message.ContributionSourceRefTypeMessage, created.Source.RefType)
	require.Equal(t, created.ID, created.Source.RefID)
	require.Equal(t, authorMemberID, delivery.NotifyPrincipalMemberID)
	require.Equal(t, beforeWorkspaceSeq+1, delivery.GlobalSeq)

	publisherMetadata, ok := created.Metadata[serviceAccountPublisherMetadataKey].(map[string]any)
	require.True(t, ok)
	byline, ok := publisherMetadata["bylineActorRef"].(map[string]any)
	require.True(t, ok)
	require.Equal(t, publisher.MemberID, byline["memberId"])
	require.Equal(t, publisher.UserID, byline["principalId"])
	managerPermissionBasis, ok := publisherMetadata["permissionBasis"].(map[string]any)
	require.True(t, ok)
	require.Equal(t, serviceAccountMessageManagerAuditBasis, managerPermissionBasis["kind"])
	require.NotEmpty(t, managerPermissionBasis["managerAssignmentId"])
	require.NotContains(t, managerPermissionBasis, "workspaceRole")
	require.Equal(t, "retained", created.Metadata["clientFact"])
	require.Equal(t, "Publisher update", created.Metadata[serviceAccountPostMetadataKey].(map[string]any)["title"])

	managerPage, err := store.ListMessagesForUser(ctx, publisher.UserID, owner.WorkspaceID, scopeID, 20, nil, nil)
	require.NoError(t, err)
	require.Len(t, managerPage.Messages, 1)
	require.Contains(t, managerPage.Messages[0].Metadata, serviceAccountPublisherMetadataKey)
	adminPage, err := store.ListMessagesForUser(ctx, owner.UserID, owner.WorkspaceID, scopeID, 20, nil, nil)
	require.NoError(t, err)
	require.Len(t, adminPage.Messages, 1)
	require.Contains(t, adminPage.Messages[0].Metadata, serviceAccountPublisherMetadataKey, "workspace admins share manager conversation moderation visibility")
	audiencePage, err := store.ListMessagesForUser(ctx, audienceMember.UserID, owner.WorkspaceID, scopeID, 20, nil, nil)
	require.NoError(t, err)
	require.Len(t, audiencePage.Messages, 1)
	require.NotContains(t, audiencePage.Messages[0].Metadata, serviceAccountPublisherMetadataKey)

	var storedAuthorID, storedAuthorType, notifyPrincipalID, idempotencySubjectID, storedClientID string
	var storedGlobalSeq int64
	var storedMetadata []byte
	require.NoError(t, db.QueryRow(ctx, `
SELECT author_member_id, author_type, notify_principal_member_id,
       idempotency_subject_member_id, client_message_id,
       workspace_global_seq, metadata
FROM scope_messages
WHERE id = $1
`, created.ID).Scan(&storedAuthorID, &storedAuthorType, &notifyPrincipalID, &idempotencySubjectID, &storedClientID, &storedGlobalSeq, &storedMetadata))
	require.Equal(t, authorMemberID, storedAuthorID)
	require.Equal(t, string(domain.MemberTypeService), storedAuthorType)
	require.Equal(t, authorMemberID, notifyPrincipalID)
	require.Equal(t, authorMemberID, idempotencySubjectID)
	require.Equal(t, clientMessageID, storedClientID)
	require.Equal(t, delivery.GlobalSeq, storedGlobalSeq)
	var storedMetadataObject map[string]any
	require.NoError(t, json.Unmarshal(storedMetadata, &storedMetadataObject))
	require.Contains(t, storedMetadataObject, serviceAccountPublisherMetadataKey)

	var workspaceSeq, scopeLatestSeq, scopeLatestGlobalSeq int64
	require.NoError(t, db.QueryRow(ctx, `SELECT latest_message_seq FROM workspaces WHERE id = $1`, owner.WorkspaceID).Scan(&workspaceSeq))
	require.NoError(t, db.QueryRow(ctx, `SELECT latest_seq, latest_global_seq FROM scopes WHERE id = $1`, scopeID).Scan(&scopeLatestSeq, &scopeLatestGlobalSeq))
	require.Equal(t, beforeWorkspaceSeq+1, workspaceSeq)
	require.Equal(t, int64(1), scopeLatestSeq)
	require.Equal(t, delivery.GlobalSeq, scopeLatestGlobalSeq)

	var auditCount, outboxCount, openAudienceCount int64
	require.NoError(t, db.QueryRow(ctx, `
SELECT COUNT(*)
FROM service_account_audit_events
WHERE service_account_id = $1 AND event_type = $2 AND subject_id = $3
`, accountID, serviceAccountMessageAuditType, created.ID).Scan(&auditCount))
	require.Equal(t, int64(1), auditCount)
	require.NoError(t, db.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_fanout_outbox
WHERE scope_id = $1 AND message_id = $2 AND payload IS NOT NULL
`, scopeID, created.ID).Scan(&outboxCount))
	require.Equal(t, int64(1), outboxCount)
	require.NoError(t, db.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_memberships sm
JOIN members m ON m.id = sm.member_id
WHERE sm.scope_id = $1
  AND m.type = 'human' AND m.status = 'active'
  AND sm.effective_to_seq IS NULL AND sm.removed_at IS NULL AND sm.archived_at IS NULL
`, scopeID).Scan(&openAudienceCount))
	require.Equal(t, int64(4), openAudienceCount)

	replayed, replayDelivery, err := store.SendMessageForUserWithOptions(ctx, user, owner.WorkspaceID, scopeID, "Service account publisher message", nil, serviceAccountPostTestOptions(clientMessageID, map[string]any{"clientFact": "retained"}))
	require.NoError(t, err)
	require.True(t, replayDelivery.IdempotentReplay)
	require.Equal(t, created.ID, replayed.ID)
	require.Equal(t, created.Source, replayed.Source)
	require.NoError(t, db.QueryRow(ctx, `SELECT latest_message_seq FROM workspaces WHERE id = $1`, owner.WorkspaceID).Scan(&workspaceSeq))
	require.Equal(t, beforeWorkspaceSeq+1, workspaceSeq, "replay must not allocate another workspace-global sequence")
	require.NoError(t, db.QueryRow(ctx, `SELECT COUNT(*) FROM service_account_audit_events WHERE service_account_id = $1 AND event_type = $2`, accountID, serviceAccountMessageAuditType).Scan(&auditCount))
	require.Equal(t, int64(1), auditCount)
	require.NoError(t, db.QueryRow(ctx, `SELECT COUNT(*) FROM scope_fanout_outbox WHERE scope_id = $1 AND message_id = $2`, scopeID, created.ID).Scan(&outboxCount))
	require.Equal(t, int64(1), outboxCount)

	_, _, err = store.SendMessageForUserWithOptions(ctx, user, owner.WorkspaceID, scopeID, "different payload", nil, serviceAccountPostTestOptions(clientMessageID, map[string]any{"clientFact": "retained"}))
	require.ErrorIs(t, err, ErrConflict)
	_, _, err = store.SendMessageForUserWithOptions(ctx, model.User{ID: secondPublisher.UserID}, owner.WorkspaceID, scopeID, "Service account publisher message", nil, serviceAccountPostTestOptions(clientMessageID, map[string]any{"clientFact": "retained"}))
	require.ErrorIs(t, err, ErrConflict, "all managers share the service author idempotency namespace")

	nonPublisher := audienceMember
	_, _, err = store.SendMessageForUserWithOptions(ctx, model.User{ID: nonPublisher.UserID}, owner.WorkspaceID, scopeID, "not authorized", nil, serviceAccountPostTestOptions(uuid.NewString(), nil))
	require.ErrorIs(t, err, ErrForbidden)
	_, _, err = store.SendMessageForUserWithOptions(ctx, user, owner.WorkspaceID, scopeID, "missing uuid", nil, serviceAccountPostTestOptions("not-a-uuid", nil))
	require.ErrorIs(t, err, ErrBadRequest)
	_, _, err = store.SendMessageForUserWithOptions(ctx, user, owner.WorkspaceID, scopeID, "reserved metadata", nil, serviceAccountPostTestOptions(uuid.NewString(), map[string]any{serviceAccountPublisherMetadataKey: map[string]any{"memberId": "forged"}}))
	require.ErrorIs(t, err, ErrBadRequest)
	_, _, err = store.SendMessageForUserWithOptions(ctx, user, owner.WorkspaceID, scopeID, "forged workflow authority", nil, serviceAccountPostTestOptions(uuid.NewString(), map[string]any{message.WorkflowContextMetadataRunIDKey: "wri_forged"}))
	require.ErrorIs(t, err, ErrBadRequest)
	require.ErrorContains(t, err, message.WorkflowContextMetadataRunIDKey)
	_, _, err = store.SendMessageForUserWithOptions(ctx, user, owner.WorkspaceID, scopeID, "plain row", nil, message.HumanMessageCreateOptions{ClientMessageID: uuid.NewString()})
	require.ErrorIs(t, err, ErrBadRequest, "service-account publishing requires the card message type")
	_, _, err = store.SendMessageForUserWithOptions(ctx, user, owner.WorkspaceID, scopeID, "missing title", nil, message.HumanMessageCreateOptions{
		ClientMessageID: uuid.NewString(),
		MessageType:     message.MessageTypeServiceAccountPost,
	})
	require.ErrorIs(t, err, ErrBadRequest, "service-account post cards require a title")

	_, err = store.RecallMessageForUser(ctx, model.User{ID: audienceMember.UserID}, owner.WorkspaceID, scopeID, created.ID, message.HumanMessageRecallOptions{})
	require.ErrorIs(t, err, ErrForbidden)
	recalled, err := store.RecallMessageForUser(ctx, user, owner.WorkspaceID, scopeID, created.ID, message.HumanMessageRecallOptions{})
	require.NoError(t, err)
	require.True(t, recalled.Changed)
	require.Equal(t, "Publisher News 撤回了一篇文章", recalled.Message.Content)
	require.True(t, message.MessageMetadataIsRecalled(recalled.Message.Metadata))
	recallMetadata, ok := recalled.Message.Metadata[message.MessageRecallMetadataKey].(map[string]any)
	require.True(t, ok)
	require.Equal(t, authorMemberID, recallMetadata["recalledByMemberId"], "the shared tombstone must not expose the Human manager")
	var recallAuditCount int64
	require.NoError(t, db.QueryRow(ctx, `
SELECT COUNT(*)
FROM service_account_audit_events
WHERE service_account_id = $1 AND event_type = $2 AND subject_id = $3
`, accountID, serviceAccountRecallAuditType, created.ID).Scan(&recallAuditCount))
	require.Equal(t, int64(1), recallAuditCount)
	replayedRecall, err := store.RecallMessageForUser(ctx, model.User{ID: secondPublisher.UserID}, owner.WorkspaceID, scopeID, created.ID, message.HumanMessageRecallOptions{})
	require.NoError(t, err)
	require.False(t, replayedRecall.Changed)
	require.NoError(t, db.QueryRow(ctx, `
SELECT COUNT(*)
FROM service_account_audit_events
WHERE service_account_id = $1 AND event_type = $2 AND subject_id = $3
`, accountID, serviceAccountRecallAuditType, created.ID).Scan(&recallAuditCount))
	require.Equal(t, int64(1), recallAuditCount)

	var implicitAdminManagerCount int64
	require.NoError(t, db.QueryRow(ctx, `
SELECT COUNT(*) FROM service_account_managers
WHERE workspace_id = $1 AND service_account_id = $2 AND member_id = $3
`, owner.WorkspaceID, accountID, owner.MemberID).Scan(&implicitAdminManagerCount))
	require.Equal(t, int64(0), implicitAdminManagerCount, "workspace admins must not need a synthetic manager assignment")
	adminUser := model.User{ID: owner.UserID}
	adminCreated, _, err := store.SendMessageForUserWithOptions(
		ctx, adminUser, owner.WorkspaceID, scopeID, "Workspace admin publisher message", nil,
		serviceAccountPostTestOptions(uuid.NewString(), nil),
	)
	require.NoError(t, err)
	adminPublisherMetadata, ok := adminCreated.Metadata[serviceAccountPublisherMetadataKey].(map[string]any)
	require.True(t, ok)
	adminPermissionBasis, ok := adminPublisherMetadata["permissionBasis"].(map[string]any)
	require.True(t, ok)
	require.Equal(t, serviceAccountMessageWorkspaceRoleAuditBasis, adminPermissionBasis["kind"])
	require.Equal(t, "admin", adminPermissionBasis["workspaceRole"])
	require.NotContains(t, adminPermissionBasis, "managerAssignmentId")
	var adminPublishBasisKind, adminPublishBasisRef string
	require.NoError(t, db.QueryRow(ctx, `
SELECT permission_basis_kind, permission_basis_ref
FROM service_account_audit_events
WHERE service_account_id = $1 AND event_type = $2 AND subject_id = $3
`, accountID, serviceAccountMessageAuditType, adminCreated.ID).Scan(&adminPublishBasisKind, &adminPublishBasisRef))
	require.Equal(t, serviceAccountMessageWorkspaceRoleAuditBasis, adminPublishBasisKind)
	require.Contains(t, adminPublishBasisRef, ":admin")
	adminRecalled, err := store.RecallMessageForUser(
		ctx, adminUser, owner.WorkspaceID, scopeID, adminCreated.ID, message.HumanMessageRecallOptions{},
	)
	require.NoError(t, err)
	require.True(t, adminRecalled.Changed)
	var adminRecallBasisKind, adminRecallBasisRef string
	require.NoError(t, db.QueryRow(ctx, `
SELECT permission_basis_kind, permission_basis_ref
FROM service_account_audit_events
WHERE service_account_id = $1 AND event_type = $2 AND subject_id = $3
`, accountID, serviceAccountRecallAuditType, adminCreated.ID).Scan(&adminRecallBasisKind, &adminRecallBasisRef))
	require.Equal(t, workspaceRoleRecallBasis, adminRecallBasisKind)
	require.Equal(t, "admin", adminRecallBasisRef)

	adminManager, err := flow.AddManagerForUser(
		ctx, owner.UserID, owner.WorkspaceID, accountID, owner.MemberID, now.Add(time.Minute),
	)
	require.NoError(t, err)
	require.Equal(t, owner.MemberID, adminManager.MemberID)
	adminWithManagerCreated, _, err := store.SendMessageForUserWithOptions(
		ctx, adminUser, owner.WorkspaceID, scopeID, "Workspace admin publisher message with manager row", nil,
		serviceAccountPostTestOptions(uuid.NewString(), nil),
	)
	require.NoError(t, err)
	adminWithManagerMetadata, ok := adminWithManagerCreated.Metadata[serviceAccountPublisherMetadataKey].(map[string]any)
	require.True(t, ok)
	adminWithManagerBasis, ok := adminWithManagerMetadata["permissionBasis"].(map[string]any)
	require.True(t, ok)
	require.Equal(t, serviceAccountMessageWorkspaceRoleAuditBasis, adminWithManagerBasis["kind"])
	require.Equal(t, "admin", adminWithManagerBasis["workspaceRole"])
	require.NotContains(t, adminWithManagerBasis, "managerAssignmentId", "workspace-role provenance wins even when an admin also has a manager row")
	require.NoError(t, db.QueryRow(ctx, `
SELECT permission_basis_kind, permission_basis_ref
FROM service_account_audit_events
WHERE service_account_id = $1 AND event_type = $2 AND subject_id = $3
`, accountID, serviceAccountMessageAuditType, adminWithManagerCreated.ID).Scan(&adminPublishBasisKind, &adminPublishBasisRef))
	require.Equal(t, serviceAccountMessageWorkspaceRoleAuditBasis, adminPublishBasisKind)
	require.Contains(t, adminPublishBasisRef, ":admin")

	mustExec(t, db, `UPDATE service_grants SET revoked_at = $2 WHERE service_installation_id = $1 AND scope = 'message.write'`, materialized.Account.ServiceInstallationID, now.Add(2*time.Second))
	_, _, err = store.SendMessageForUserWithOptions(ctx, user, owner.WorkspaceID, scopeID, "revoked", nil, serviceAccountPostTestOptions(uuid.NewString(), nil))
	require.ErrorIs(t, err, ErrForbidden)
}

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

	ctx := context.Background()
	db := startAppRollbackDB(t, ctx)
	store := New(db, Options{})
	now := time.Date(2026, 7, 10, 8, 0, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, db, "service_account_reader", now)
	serviceScopeID := seedAppServiceAccountMessageFixture(t, db, fixture, "reader-news", now)

	page, err := store.ListMessagesForUser(ctx, fixture.UserID, fixture.WorkspaceID, serviceScopeID, 20, nil, nil)
	require.NoError(t, err)
	require.Len(t, page.Messages, 1)
	require.Equal(t, "Service account message", page.Messages[0].Content)
	require.Equal(t, "service", page.Messages[0].AuthorProfile.ActorType)
	require.Equal(t, "service_account", page.Messages[0].AuthorProfile.ServiceKind)
	_, _, err = store.SendAppMessageForUserWithOptions(
		ctx,
		model.User{ID: fixture.UserID, Name: "service_account_reader", DisplayName: "service_account_reader"},
		fixture.WorkspaceID,
		serviceScopeID,
		"must not be Human-authored",
		nil,
		SendAppMessageOptions{ClientMessageID: "human-writer-denied"},
	)
	require.ErrorIs(t, err, ErrForbidden)

	uploadMemberID, uploadScopeID, canUpload, err := resolveServiceAccountAttachmentUploadScopeForUser(ctx, db, fixture.UserID, fixture.WorkspaceID, serviceScopeID)
	require.NoError(t, err)
	require.True(t, canUpload, "workspace admins share manager attachment authority without a manager row")
	require.Equal(t, "mem_sa_reader-news", uploadMemberID, "attachment ownership uses the durable service author")
	require.Equal(t, serviceScopeID, uploadScopeID)
	uploaded, err := store.attachmentStore().UploadAttachmentsForUser(ctx, fixture.UserID, fixture.WorkspaceID, serviceScopeID, []attachment.CreateAttachmentParams{{
		Filename:    "service-news.txt",
		ContentType: "text/plain",
		Blob:        []byte("service attachment"),
	}})
	require.NoError(t, err)
	require.Len(t, uploaded, 1)
	var uploadedByMemberID string
	require.NoError(t, db.QueryRow(ctx, `SELECT uploaded_by_member_id FROM attachments WHERE id = $1`, uploaded[0].ID).Scan(&uploadedByMemberID))
	require.Equal(t, "mem_sa_reader-news", uploadedByMemberID)
	claimed, err := store.attachmentStore().ClaimAttachmentsForMessage(ctx, fixture.WorkspaceID, serviceScopeID, "mem_sa_reader-news", "msg_sa_reader-news", []string{uploaded[0].ID})
	require.NoError(t, err)
	require.Len(t, claimed, 1, "the durable service author claims publisher-uploaded attachments")
	reaction, err := store.Reaction().AddReactionForUser(ctx, fixture.UserID, fixture.WorkspaceID, "msg_sa_reader-news", "👍")
	require.NoError(t, err)
	require.True(t, reaction.Changed)
	require.Equal(t, "service_account", reaction.ChannelType)
	removedReaction, err := store.Reaction().RemoveReactionForUser(ctx, fixture.UserID, fixture.WorkspaceID, "msg_sa_reader-news", "👍")
	require.NoError(t, err)
	require.True(t, removedReaction.Changed)
	require.Equal(t, "service_account", removedReaction.ChannelType)
	retainedReaction, err := store.Reaction().AddReactionForUser(ctx, fixture.UserID, fixture.WorkspaceID, "msg_sa_reader-news", "🎉")
	require.NoError(t, err)
	require.True(t, retainedReaction.Changed)
	mustExec(t, db, `UPDATE service_grants SET revoked_at = $2 WHERE service_installation_id = $1 AND scope = 'message.write'`, "svcinst_sa_reader-news", now)
	_, _, canUpload, err = resolveServiceAccountAttachmentUploadScopeForUser(ctx, db, fixture.UserID, fixture.WorkspaceID, serviceScopeID)
	require.NoError(t, err)
	require.False(t, canUpload, "a revoked message.write grant closes the upload authority")
	mustExec(t, db, `UPDATE service_grants SET revoked_at = NULL WHERE service_installation_id = $1 AND scope = 'message.write'`, "svcinst_sa_reader-news")
	mustExec(t, db, `UPDATE service_target_bindings SET authority_subject_member_id = $2 WHERE service_installation_id = $1`, "svcinst_sa_reader-news", fixture.MemberID)
	_, _, canUpload, err = resolveServiceAccountAttachmentUploadScopeForUser(ctx, db, fixture.UserID, fixture.WorkspaceID, serviceScopeID)
	require.NoError(t, err)
	require.False(t, canUpload, "the target binding must name the durable service author")
	mustExec(t, db, `UPDATE service_target_bindings SET authority_subject_member_id = $2 WHERE service_installation_id = $1`, "svcinst_sa_reader-news", "mem_sa_reader-news")
	mustExec(t, db, `UPDATE service_target_bindings SET status = 'inactive' WHERE service_installation_id = $1`, "svcinst_sa_reader-news")
	_, _, canUpload, err = resolveServiceAccountAttachmentUploadScopeForUser(ctx, db, fixture.UserID, fixture.WorkspaceID, serviceScopeID)
	require.NoError(t, err)
	require.False(t, canUpload, "a disabled target binding closes the upload authority")
	mustExec(t, db, `UPDATE service_target_bindings SET status = 'active' WHERE service_installation_id = $1`, "svcinst_sa_reader-news")
	mustExec(t, db, `UPDATE service_members SET status = 'inactive' WHERE service_installation_id = $1`, "svcinst_sa_reader-news")
	_, _, canUpload, err = resolveServiceAccountAttachmentUploadScopeForUser(ctx, db, fixture.UserID, fixture.WorkspaceID, serviceScopeID)
	require.NoError(t, err)
	require.False(t, canUpload, "an inactive service-member projection closes the upload authority")
	mustExec(t, db, `UPDATE service_members SET status = 'active' WHERE service_installation_id = $1`, "svcinst_sa_reader-news")
	_, err = store.Reaction().RemoveReactionForUser(ctx, fixture.UserID, fixture.WorkspaceID, "msg_sa_reader-news", "🎉")
	require.NoError(t, err)
	messagePage, err := store.ListMessagesForUser(ctx, fixture.UserID, fixture.WorkspaceID, serviceScopeID, 20, nil, nil)
	require.NoError(t, err)
	require.Len(t, messagePage.Messages, 1)

	readSeq, err := store.UpdateChannelReadStateByGlobalSeq(ctx, fixture.UserID, fixture.WorkspaceID, serviceScopeID, 1)
	require.NoError(t, err)
	require.Equal(t, int64(1), readSeq)

	channelID, channelType, err := store.LookupMessageChannel(ctx, fixture.WorkspaceID, "msg_sa_reader-news")
	require.NoError(t, err)
	require.Equal(t, serviceScopeID, channelID)
	require.Equal(t, "service_account", channelType)

	_, _, err = store.SendAppMessageForUserWithOptions(
		ctx,
		model.User{ID: fixture.UserID, Name: "service_account_reader", DisplayName: "service_account_reader"},
		fixture.WorkspaceID,
		serviceScopeID,
		"must not be Human-authored",
		nil,
		SendAppMessageOptions{ClientMessageID: "human-writer-denied"},
	)
	require.ErrorIs(t, err, ErrForbidden)

	outsider := seedWorkspaceMemberFixture(t, db, fixture.WorkspaceID, "service_account_outsider", now)
	_, err = store.ListMessagesForUser(ctx, outsider.UserID, fixture.WorkspaceID, serviceScopeID, 20, nil, nil)
	require.ErrorIs(t, err, ErrForbidden)

	mustExec(t, db, `UPDATE scope_memberships SET removed_at = $3, effective_to_seq = 1, updated_at = $3 WHERE scope_id = $1 AND member_id = $2`, serviceScopeID, fixture.MemberID, now.Add(time.Minute))
	_, err = store.Reaction().AddReactionForUser(ctx, fixture.UserID, fixture.WorkspaceID, "msg_sa_reader-news", "👍")
	require.ErrorIs(t, err, ErrForbidden, "closed audience membership revokes reaction authority")
	_, err = store.Reaction().GetMessageReactionsForUser(ctx, fixture.UserID, fixture.WorkspaceID, "msg_sa_reader-news")
	require.ErrorIs(t, err, ErrForbidden, "closed audience membership revokes reaction reads")
}

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

	ctx := context.Background()
	db := startAppRollbackDB(t, ctx)
	store := New(db, Options{})
	now := time.Date(2026, 7, 21, 9, 0, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, db, "platform_service_account_workspace_admin_denied", now)
	scopeID := seedAppServiceAccountMessageFixture(t, db, fixture, "platform-admin-denied", now)
	accountID := "sacct_platform-admin-denied"
	platformAccountID := "psacct_platform-admin-denied"

	mustExec(t, db, `
INSERT INTO platform_service_accounts (
  id, name, description, avatar_asset_id, profile_version, version,
  provisioning_mode, provisioning_state, applied_provisioning_generation,
  audience_policy_version, latest_broadcast_seq,
  created_by_operator_user_id, created_at, updated_at
) VALUES (
  $1, 'Platform Admin Denied', '', '', 1, 1,
  'all_workspaces', 'ready', 1,
  1, 0,
  $2, $3, $3
)
`, platformAccountID, fixture.UserID, now)
	mustExec(t, db, `
UPDATE service_accounts
SET principal_id = $2,
    management_kind = 'platform',
    platform_service_account_id = $2,
    created_by_member_id = NULL,
    created_by_operator_user_id = $3,
    updated_at = $4
WHERE workspace_id = $1 AND id = $5
`, fixture.WorkspaceID, platformAccountID, fixture.UserID, now.Add(time.Second), accountID)

	var workspaceAdminManagerCount int64
	require.NoError(t, db.QueryRow(ctx, `
SELECT COUNT(*) FROM service_account_managers
WHERE workspace_id = $1 AND service_account_id = $2 AND member_id = $3
`, fixture.WorkspaceID, accountID, fixture.MemberID).Scan(&workspaceAdminManagerCount))
	require.Zero(t, workspaceAdminManagerCount)

	_, _, err := store.SendMessageForUserWithOptions(
		ctx, model.User{ID: fixture.UserID}, fixture.WorkspaceID, scopeID,
		"workspace admin must not publish as a platform account", nil,
		serviceAccountPostTestOptions(uuid.NewString(), nil),
	)
	require.ErrorIs(t, err, ErrForbidden)

	_, _, canUpload, err := resolveServiceAccountAttachmentUploadScopeForUser(
		ctx, db, fixture.UserID, fixture.WorkspaceID, scopeID,
	)
	require.NoError(t, err)
	require.False(t, canUpload)
	_, err = store.attachmentStore().UploadAttachmentsForUser(
		ctx, fixture.UserID, fixture.WorkspaceID, scopeID,
		[]attachment.CreateAttachmentParams{{
			Filename: "forbidden-platform-upload.txt", ContentType: "text/plain",
			Blob: []byte("workspace admin cannot upload as a platform account"),
		}},
	)
	require.Error(t, err)
}

func seedAppServiceAccountMessageFixture(t *testing.T, db coredb.DB, fixture appAdminFixture, handle string, now time.Time) string {
	t.Helper()
	scopeID := "sasc_" + handle
	serviceMemberID := "mem_sa_" + handle
	installationID := "svcinst_sa_" + handle
	serviceProjectionID := "svcmem_sa_" + handle
	accountID := "sacct_" + handle
	publisher := seedWorkspaceMemberFixture(t, db, fixture.WorkspaceID, "service_account_seed_publisher_"+handle, now)
	flow := serviceaccount.New(db)
	result, err := flow.MaterializeWorkspace(context.Background(), serviceaccount.WorkspaceMaterialization{
		Identity: serviceaccount.IdentityRefs{
			AccountID:             accountID,
			ServiceInstallationID: installationID,
			ServiceMemberID:       serviceProjectionID,
			AuthorMemberID:        serviceMemberID,
			MessageScopeID:        scopeID,
			NameClaimID:           "sachc_" + handle,
		},
		WorkspaceID:       fixture.WorkspaceID,
		CreatorMemberID:   publisher.MemberID,
		Name:              "Reader News",
		ManagerMemberIDs:  []string{publisher.MemberID},
		AuthorizationPath: "direct_create",
		Now:               now,
	})
	require.NoError(t, err)
	require.Equal(t, accountID, result.Account.ID)
	ready, err := flow.ReconcileAllWorkspaceHumans(
		context.Background(), fixture.WorkspaceID, accountID,
		result.Account.Version, result.Account.AudiencePolicyVersion, now,
	)
	require.NoError(t, err)
	require.Equal(t, serviceaccountstore.AudienceStateReady, ready.AudienceState)
	mustExec(t, db, `
UPDATE scopes
SET latest_seq = 1, latest_global_seq = 1,
    visible_message_count = 1, visible_last_message_at = $2,
    latest_message_at = $2, updated_at = $2
WHERE id = $1
`, scopeID, now)
	mustExec(t, db, `
INSERT INTO scope_messages (
    id, workspace_id, scope_id, seq, workspace_global_seq,
    author_member_id, author_type, client_message_id, message_type,
    content, metadata, created_at, updated_at
) VALUES ($1, $2, $3, 1, 1, $4, 'service', 'service-message-1', '', $5, JSON_OBJECT(), $6, $6)
`, "msg_sa_"+handle, fixture.WorkspaceID, scopeID, serviceMemberID, "Service account message", now)
	mustExec(t, db, `UPDATE workspaces SET latest_message_seq = 1 WHERE id = $1`, fixture.WorkspaceID)
	return scopeID
}
