package app

import (
	"context"
	"errors"
	"fmt"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/access"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/attachment"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/rewind"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
	"github.com/tidbcloud/AgentTeam/internal/store/taskboard"
	"github.com/tidbcloud/AgentTeam/internal/store/team"
)

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 7, 12, 0, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "client_message_id_replay", now)
	user := model.User{ID: fixture.UserID, Name: "client_message_id_replay", DisplayName: "client_message_id_replay"}

	firstMessage, firstDelivery, err := store.SendAppMessageForUserWithOptions(
		ctx,
		user,
		fixture.WorkspaceID,
		fixture.AllChannelID,
		"hello",
		nil,
		SendAppMessageOptions{ClientMessageID: "client-replay-1"},
	)
	require.NoError(t, err)
	require.False(t, firstDelivery.IdempotentReplay)

	replayedMessage, replayedDelivery, err := store.SendAppMessageForUserWithOptions(
		ctx,
		user,
		fixture.WorkspaceID,
		fixture.AllChannelID,
		"hello",
		nil,
		SendAppMessageOptions{ClientMessageID: "client-replay-1"},
	)
	require.NoError(t, err)
	require.True(t, replayedDelivery.IdempotentReplay)
	require.Equal(t, firstMessage.ID, replayedMessage.ID)

	_, _, err = store.SendAppMessageForUserWithOptions(
		ctx,
		user,
		fixture.WorkspaceID,
		fixture.AllChannelID,
		"changed",
		nil,
		SendAppMessageOptions{ClientMessageID: "client-replay-1"},
	)
	require.True(t, errors.Is(err, ErrConflict), "got %v", err)
	require.ErrorContains(t, err, "clientMessageId")

	var messageCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_messages
WHERE workspace_id = $1
  AND scope_id = $2
  AND author_member_id = $3
  AND client_message_id = $4
`, fixture.WorkspaceID, fixture.AllChannelID, fixture.MemberID, "client-replay-1").Scan(&messageCount))
	require.Equal(t, 1, messageCount)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 8, 9, 12, 0, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "desktop_onboarding_mock", now)
	user := model.User{ID: fixture.UserID, Name: "desktop_onboarding_mock", DisplayName: "desktop_onboarding_mock"}
	agentID := "agt_desktop_onboarding_mock"
	agentMemberID := "mem_agent_desktop_onboarding_mock"
	agentName := "my_assistant"
	seedAppAgentParticipant(
		t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID,
		fixture.AllChannelID, "mch_desktop_onboarding_mock", agentID, agentMemberID, agentName, now,
	)

	created, delivery, err := store.SendMessageForUserWithOptions(
		ctx,
		user,
		fixture.WorkspaceID,
		fixture.AllChannelID,
		"@my_assistant please build the game",
		nil,
		message.HumanMessageCreateOptions{
			MentionHints:    []message.MessageMentionHint{{Type: "agent", ID: agentID, Token: agentName}},
			ClientMessageID: "desktop-onboarding-mock-message", StartAgentThread: true,
			SuppressAgentDelivery: true,
		},
	)
	require.NoError(t, err)
	require.NotEmpty(t, created.ID)
	require.NotNil(t, delivery.StartedReplyThread)
	require.Empty(t, delivery.Recipients)
	require.Empty(t, delivery.WakeRecipients)
	require.Equal(t, []message.PersistedMention{{Type: "agent", ID: agentID, Token: agentName}}, message.ExtractPersistedMentionMetadata(created.Metadata))

	var outboxPayload []byte
	require.NoError(t, pool.QueryRow(ctx, `
SELECT payload
FROM scope_fanout_outbox
WHERE workspace_id = $1
  AND message_id = $2
  AND fanout_type = $3
LIMIT 1
`, fixture.WorkspaceID, created.ID, message.ScopeOutboxFanoutScope).Scan(&outboxPayload))
	payload, err := message.UnmarshalScopeFanoutPayload(outboxPayload)
	require.NoError(t, err)
	require.Empty(t, payload.Delivery.Recipients)
	require.Empty(t, payload.Delivery.WakeRecipients)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 7, 29, 4, 20, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "task_dispatch_read_only", now)
	user := model.User{ID: fixture.UserID, Name: "task_dispatch_read_only", DisplayName: "task_dispatch_read_only"}

	var messageCountBefore int64
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_messages
WHERE workspace_id = $1
  AND scope_id = $2
  AND message_type = 'task_dispatch'
`, fixture.WorkspaceID, fixture.AllChannelID).Scan(&messageCountBefore))
	var outboxCountBefore int64
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_fanout_outbox
WHERE workspace_id = $1
  AND scope_id = $2
`, fixture.WorkspaceID, fixture.AllChannelID).Scan(&outboxCountBefore))

	_, _, err := store.SendAppMessageForUserWithOptions(
		ctx,
		user,
		fixture.WorkspaceID,
		fixture.AllChannelID,
		"must not create a new hidden dispatch carrier",
		nil,
		SendAppMessageOptions{MessageType: message.MessageTypeTaskDispatch, SkipContentModeration: true},
	)
	require.ErrorContains(t, err, "message type is not allowed for active writes")

	var messageCountAfter int64
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_messages
WHERE workspace_id = $1
  AND scope_id = $2
  AND message_type = 'task_dispatch'
`, fixture.WorkspaceID, fixture.AllChannelID).Scan(&messageCountAfter))
	require.Equal(t, messageCountBefore, messageCountAfter)
	var outboxCountAfter int64
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_fanout_outbox
WHERE workspace_id = $1
  AND scope_id = $2
`, fixture.WorkspaceID, fixture.AllChannelID).Scan(&outboxCountAfter))
	require.Equal(t, outboxCountBefore, outboxCountAfter)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 29, 18, 0, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "urgent_agent_delivery", now)
	peer := seedWorkspaceMemberFixture(t, pool, fixture.WorkspaceID, "urgent_agent_delivery_peer", now)
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, fixture.AllChannelID, "all", "", "public", fixture.MemberID, peer.MemberID, now)
	user := model.User{ID: fixture.UserID, Name: "urgent_agent_delivery", DisplayName: "urgent_agent_delivery"}

	const (
		machineID     = "mach_urgent_agent_delivery"
		agentID       = "agt_urgent_agent_delivery"
		agentMemberID = "mem_agent_urgent_delivery"
		agentName     = "urgent-agent-delivery"
	)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, machineID, agentID, agentMemberID, agentName, now)

	sentMessage, originalDelivery, err := store.SendAppMessageForUserWithOptions(
		ctx,
		user,
		fixture.WorkspaceID,
		fixture.AllChannelID,
		"@urgent-agent-delivery please take this now",
		nil,
		SendAppMessageOptions{
			MentionHints: []message.MessageMentionHint{{
				Type:  "agent",
				ID:    agentID,
				Token: agentName,
			}},
		},
	)
	require.NoError(t, err)
	require.False(t, message.MessageHasAgentDeliveryInterruptPolicy(sentMessage.Metadata, message.AgentInterruptPolicyCurrentRun))
	require.Len(t, originalDelivery.WakeRecipients, 1)
	require.Equal(t, agentID, originalDelivery.WakeRecipients[0].AgentID)
	require.Equal(t, agentMemberID, originalDelivery.WakeRecipients[0].AgentMemberID)
	require.Equal(t, agentName, originalDelivery.WakeRecipients[0].MentionToken)
	require.Equal(t, message.AgentDeliveryReasonDirectMention, originalDelivery.WakeRecipients[0].DeliveryReason)

	updatedMessage, urgentDelivery, err := store.SubmitMessageAgentDeliveryForUser(ctx, peer.UserID, fixture.WorkspaceID, sentMessage.ID, message.AgentInterruptPolicyCurrentRun)
	require.NoError(t, err)
	require.Equal(t, sentMessage.ID, updatedMessage.ID)
	require.True(t, message.MessageHasAgentDeliveryInterruptPolicy(updatedMessage.Metadata, message.AgentInterruptPolicyCurrentRun))
	require.Equal(t, message.AgentInterruptPolicyCurrentRun, urgentDelivery.InterruptPolicy)
	require.Equal(t, message.AgentInterruptPolicyCurrentRun, urgentDelivery.Message.InterruptPolicy)
	require.True(t, urgentDelivery.IdempotentReplay)
	require.Equal(t, originalDelivery.WakeRecipients, urgentDelivery.WakeRecipients)
	require.NotNil(t, urgentDelivery.AppMessage)
	require.True(t, message.MessageHasAgentDeliveryInterruptPolicy(urgentDelivery.AppMessage.Metadata, message.AgentInterruptPolicyCurrentRun))

	var metadataType string
	require.NoError(t, pool.QueryRow(ctx, `
SELECT JSON_TYPE(metadata)
FROM scope_messages
WHERE id = $1
`, sentMessage.ID).Scan(&metadataType))
	require.Equal(t, "OBJECT", metadataType)

	var rawPayload []byte
	require.NoError(t, pool.QueryRow(ctx, `
SELECT payload
FROM scope_fanout_outbox
WHERE message_id = $1
`, sentMessage.ID).Scan(&rawPayload))
	payload, err := message.UnmarshalScopeFanoutPayload(rawPayload)
	require.NoError(t, err)
	require.Empty(t, payload.Delivery.InterruptPolicy)
	require.Empty(t, payload.Delivery.Message.InterruptPolicy)
	require.False(t, message.MessageHasAgentDeliveryInterruptPolicy(payload.AppMessage.Metadata, message.AgentInterruptPolicyCurrentRun))
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 29, 18, 2, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "urgent_agent_delivery_prepare", now)
	peer := seedWorkspaceMemberFixture(t, pool, fixture.WorkspaceID, "urgent_agent_delivery_prepare_peer", now)
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, fixture.AllChannelID, "all", "", "public", fixture.MemberID, peer.MemberID, now)
	user := model.User{ID: fixture.UserID, Name: "urgent_agent_delivery_prepare", DisplayName: "urgent_agent_delivery_prepare"}

	const (
		machineID     = "mach_urgent_agent_delivery_prepare"
		agentID       = "agt_urgent_agent_delivery_prepare"
		agentMemberID = "mem_agent_urgent_delivery_prepare"
		agentName     = "urgent-agent-delivery-prepare"
	)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, machineID, agentID, agentMemberID, agentName, now)

	sentMessage, _, err := store.SendAppMessageForUserWithOptions(
		ctx,
		user,
		fixture.WorkspaceID,
		fixture.AllChannelID,
		"@urgent-agent-delivery-prepare please take this now",
		nil,
		SendAppMessageOptions{
			MentionHints: []message.MessageMentionHint{{
				Type:  "agent",
				ID:    agentID,
				Token: agentName,
			}},
		},
	)
	require.NoError(t, err)

	preparedMessage, preparedDelivery, err := store.PrepareMessageAgentDeliveryForUser(ctx, peer.UserID, fixture.WorkspaceID, sentMessage.ID, message.AgentInterruptPolicyCurrentRun)
	require.NoError(t, err)
	require.True(t, message.MessageHasAgentDeliveryInterruptPolicy(preparedMessage.Metadata, message.AgentInterruptPolicyCurrentRun))
	require.Equal(t, message.AgentInterruptPolicyCurrentRun, preparedDelivery.InterruptPolicy)

	var rawMetadata []byte
	require.NoError(t, pool.QueryRow(ctx, `
SELECT metadata
FROM scope_messages
WHERE id = $1
`, sentMessage.ID).Scan(&rawMetadata))
	persistedMetadata, err := message.DecodeMessageMetadata(rawMetadata)
	require.NoError(t, err)
	require.False(t, message.MessageHasAgentDeliveryInterruptPolicy(persistedMetadata, message.AgentInterruptPolicyCurrentRun))

	var rawPayload []byte
	require.NoError(t, pool.QueryRow(ctx, `
SELECT payload
FROM scope_fanout_outbox
WHERE message_id = $1
`, sentMessage.ID).Scan(&rawPayload))
	payload, err := message.UnmarshalScopeFanoutPayload(rawPayload)
	require.NoError(t, err)
	require.Empty(t, payload.Delivery.InterruptPolicy)
	require.False(t, message.MessageHasAgentDeliveryInterruptPolicy(payload.AppMessage.Metadata, message.AgentInterruptPolicyCurrentRun))

	pendingMessage, err := store.MarkMessageAgentDeliveryStatusForUser(ctx, peer.UserID, fixture.WorkspaceID, sentMessage.ID, message.AgentInterruptPolicyCurrentRun, message.AgentDeliveryStatusPending)
	require.NoError(t, err)
	require.True(t, message.MessageHasAgentDeliveryInterruptPolicy(pendingMessage.Metadata, message.AgentInterruptPolicyCurrentRun))
	require.Equal(t, map[string]any{
		"interruptPolicy": message.AgentInterruptPolicyCurrentRun,
		"status":          message.AgentDeliveryStatusPending,
	}, pendingMessage.Metadata[message.AgentDeliveryMetadataKey])

	var metadataType string
	require.NoError(t, pool.QueryRow(ctx, `
SELECT JSON_TYPE(metadata)
FROM scope_messages
WHERE id = $1
`, sentMessage.ID).Scan(&metadataType))
	require.Equal(t, "OBJECT", metadataType)

	require.NoError(t, pool.QueryRow(ctx, `
SELECT metadata
FROM scope_messages
WHERE id = $1
`, sentMessage.ID).Scan(&rawMetadata))
	persistedMetadata, err = message.DecodeMessageMetadata(rawMetadata)
	require.NoError(t, err)
	require.Equal(t, map[string]any{
		"interruptPolicy": message.AgentInterruptPolicyCurrentRun,
		"status":          message.AgentDeliveryStatusPending,
	}, persistedMetadata[message.AgentDeliveryMetadataKey])

	require.NoError(t, pool.QueryRow(ctx, `
SELECT payload
FROM scope_fanout_outbox
WHERE message_id = $1
`, sentMessage.ID).Scan(&rawPayload))
	payload, err = message.UnmarshalScopeFanoutPayload(rawPayload)
	require.NoError(t, err)
	require.Empty(t, payload.Delivery.InterruptPolicy)
	require.False(t, message.MessageHasAgentDeliveryInterruptPolicy(payload.AppMessage.Metadata, message.AgentInterruptPolicyCurrentRun))
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 29, 18, 5, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "urgent_agent_delivery_durable", now)
	peer := seedWorkspaceMemberFixture(t, pool, fixture.WorkspaceID, "urgent_agent_delivery_durable_peer", now)
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, fixture.AllChannelID, "all", "", "public", fixture.MemberID, peer.MemberID, now)
	user := model.User{ID: fixture.UserID, Name: "urgent_agent_delivery_durable", DisplayName: "urgent_agent_delivery_durable"}

	const (
		machineID     = "mach_urgent_agent_delivery_durable"
		agentID       = "agt_urgent_agent_delivery_durable"
		agentMemberID = "mem_agent_urgent_delivery_durable"
		agentName     = "urgent-agent-delivery-durable"
	)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, machineID, agentID, agentMemberID, agentName, now)
	upsertScopeSubscriptionForTests(t, pool, fixture.AllChannelID, agentMemberID, "no-wake", now)

	sentMessage, originalDelivery, err := store.SendMessageForUser(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "plain message for a no-wake default agent", nil)
	require.NoError(t, err)
	require.Empty(t, originalDelivery.WakeRecipients)
	require.Len(t, originalDelivery.Recipients, 1)
	require.Equal(t, agentID, originalDelivery.Recipients[0].AgentID)

	updatedMessage, urgentDelivery, err := store.SubmitMessageAgentDeliveryForUser(ctx, peer.UserID, fixture.WorkspaceID, sentMessage.ID, message.AgentInterruptPolicyCurrentRun)
	require.NoError(t, err)
	require.True(t, message.MessageHasAgentDeliveryInterruptPolicy(updatedMessage.Metadata, message.AgentInterruptPolicyCurrentRun))
	require.Equal(t, originalDelivery.Recipients, urgentDelivery.WakeRecipients)
	require.Equal(t, message.AgentInterruptPolicyCurrentRun, urgentDelivery.Message.InterruptPolicy)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 29, 18, 10, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "urgent_agent_delivery_recompute", now)
	peer := seedWorkspaceMemberFixture(t, pool, fixture.WorkspaceID, "urgent_agent_delivery_recompute_peer", now)
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, fixture.AllChannelID, "all", "", "public", fixture.MemberID, peer.MemberID, now)
	user := model.User{ID: fixture.UserID, Name: "urgent_agent_delivery_recompute", DisplayName: "urgent_agent_delivery_recompute"}

	const (
		machineID     = "mach_urgent_agent_delivery_recompute"
		agentID       = "agt_urgent_agent_delivery_recompute"
		agentMemberID = "mem_agent_urgent_delivery_recompute"
		agentName     = "urgent-agent-delivery-recompute"
	)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, machineID, agentID, agentMemberID, agentName, now)

	sentMessage, originalDelivery, err := store.SendAppMessageForUserWithOptions(
		ctx,
		user,
		fixture.WorkspaceID,
		fixture.AllChannelID,
		"@urgent-agent-delivery-recompute please take this now",
		nil,
		SendAppMessageOptions{
			MentionHints: []message.MessageMentionHint{{
				Type:  "agent",
				ID:    agentID,
				Token: agentName,
			}},
		},
	)
	require.NoError(t, err)
	require.Len(t, originalDelivery.WakeRecipients, 1)
	require.Equal(t, agentID, originalDelivery.WakeRecipients[0].AgentID)

	mustExec(t, pool, `
UPDATE agents
SET name = $2, updated_at = $3
WHERE id = $1
`, agentID, "urgent-agent-delivery-recompute-renamed", now)
	seedAppAgentParticipant(
		t,
		pool,
		fixture.WorkspaceID,
		fixture.UserID,
		fixture.MemberID,
		fixture.AllChannelID,
		"mach_urgent_agent_delivery_recompute_reused",
		"agt_urgent_agent_delivery_recompute_reused",
		"mem_agent_urgent_delivery_recompute_reused",
		agentName,
		now,
	)

	mustExec(t, pool, `
UPDATE scope_fanout_outbox
SET payload = NULL
WHERE message_id = $1
`, sentMessage.ID)

	updatedMessage, urgentDelivery, err := store.SubmitMessageAgentDeliveryForUser(ctx, peer.UserID, fixture.WorkspaceID, sentMessage.ID, message.AgentInterruptPolicyCurrentRun)
	require.NoError(t, err)
	require.True(t, message.MessageHasAgentDeliveryInterruptPolicy(updatedMessage.Metadata, message.AgentInterruptPolicyCurrentRun))
	require.Len(t, urgentDelivery.WakeRecipients, 1)
	require.Equal(t, agentID, urgentDelivery.WakeRecipients[0].AgentID)
	require.NotEqual(t, "agt_urgent_agent_delivery_recompute_reused", urgentDelivery.WakeRecipients[0].AgentID)
	require.Equal(t, message.AgentInterruptPolicyCurrentRun, urgentDelivery.InterruptPolicy)

	var payloadIsNull bool
	require.NoError(t, pool.QueryRow(ctx, `
SELECT payload IS NULL
FROM scope_fanout_outbox
WHERE message_id = $1
`, sentMessage.ID).Scan(&payloadIsNull))
	require.True(t, payloadIsNull)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 29, 18, 12, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "urgent_agent_delivery_missing_metadata", now)
	peer := seedWorkspaceMemberFixture(t, pool, fixture.WorkspaceID, "urgent_agent_delivery_missing_metadata_peer", now)
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, fixture.AllChannelID, "all", "", "public", fixture.MemberID, peer.MemberID, now)
	user := model.User{ID: fixture.UserID, Name: "urgent_agent_delivery_missing_metadata", DisplayName: "urgent_agent_delivery_missing_metadata"}

	const (
		machineID     = "mach_urgent_agent_delivery_missing_metadata"
		agentID       = "agt_urgent_agent_delivery_missing_metadata"
		agentMemberID = "mem_agent_urgent_delivery_missing_metadata"
		agentName     = "urgent-agent-delivery-missing-metadata"
	)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, machineID, agentID, agentMemberID, agentName, now)

	sentMessage, originalDelivery, err := store.SendMessageForUser(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "@urgent-agent-delivery-missing-metadata please take this now", nil)
	require.NoError(t, err)
	require.Len(t, originalDelivery.WakeRecipients, 1)
	require.Equal(t, agentID, originalDelivery.WakeRecipients[0].AgentID)

	mustExec(t, pool, `
UPDATE scope_messages
SET metadata = '{}', updated_at = $2
WHERE id = $1
`, sentMessage.ID, now)
	mustExec(t, pool, `
UPDATE agents
SET name = $2, updated_at = $3
WHERE id = $1
`, agentID, "urgent-agent-delivery-missing-metadata-renamed", now)
	seedAppAgentParticipant(
		t,
		pool,
		fixture.WorkspaceID,
		fixture.UserID,
		fixture.MemberID,
		fixture.AllChannelID,
		"mach_urgent_agent_delivery_missing_metadata_reused",
		"agt_urgent_agent_delivery_missing_metadata_reused",
		"mem_agent_urgent_delivery_missing_metadata_reused",
		agentName,
		now,
	)
	mustExec(t, pool, `
UPDATE scope_fanout_outbox
SET payload = NULL
WHERE message_id = $1
`, sentMessage.ID)

	_, _, err = store.SubmitMessageAgentDeliveryForUser(ctx, peer.UserID, fixture.WorkspaceID, sentMessage.ID, message.AgentInterruptPolicyCurrentRun)
	require.ErrorContains(t, err, "message has no replayable agent delivery payload")
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 29, 18, 14, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "urgent_agent_delivery_missing_default", now)
	peer := seedWorkspaceMemberFixture(t, pool, fixture.WorkspaceID, "urgent_agent_delivery_missing_default_peer", now)
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, fixture.AllChannelID, "all", "", "public", fixture.MemberID, peer.MemberID, now)
	user := model.User{ID: fixture.UserID, Name: "urgent_agent_delivery_missing_default", DisplayName: "urgent_agent_delivery_missing_default"}

	const (
		machineID     = "mach_urgent_agent_delivery_missing_default"
		agentID       = "agt_urgent_agent_delivery_missing_default"
		agentMemberID = "mem_agent_urgent_delivery_missing_default"
		agentName     = "urgent-agent-delivery-missing-default"
	)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, machineID, agentID, agentMemberID, agentName, now)
	upsertScopeSubscriptionForTests(t, pool, fixture.AllChannelID, agentMemberID, "no-wake", now)

	sentMessage, originalDelivery, err := store.SendMessageForUser(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "plain message for a no-wake default agent", nil)
	require.NoError(t, err)
	require.Empty(t, originalDelivery.WakeRecipients)
	require.Len(t, originalDelivery.Recipients, 1)
	require.Equal(t, agentID, originalDelivery.Recipients[0].AgentID)

	seedAppAgentParticipant(
		t,
		pool,
		fixture.WorkspaceID,
		fixture.UserID,
		fixture.MemberID,
		fixture.AllChannelID,
		"mach_urgent_agent_delivery_missing_default_new",
		"agt_urgent_agent_delivery_missing_default_new",
		"mem_agent_urgent_delivery_missing_default_new",
		"urgent-agent-delivery-missing-default-new",
		now,
	)
	mustExec(t, pool, `
UPDATE scope_fanout_outbox
SET payload = NULL
WHERE message_id = $1
`, sentMessage.ID)

	_, _, err = store.SubmitMessageAgentDeliveryForUser(ctx, peer.UserID, fixture.WorkspaceID, sentMessage.ID, message.AgentInterruptPolicyCurrentRun)
	require.ErrorContains(t, err, "message has no replayable agent delivery payload")
}

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

	cases := []struct {
		name   string
		target message.ThreadAccess
	}{
		{
			name: "channel default",
			target: message.ThreadAccess{
				ThreadType:  domain.ThreadTypeChannel,
				ChannelID:   "chn_replay_default",
				ChannelName: "all",
			},
		},
		{
			name: "reply thread participants",
			target: message.ThreadAccess{
				ThreadType: domain.ThreadTypeChannel,
				ChannelID:  "chn_replay_thread",
				ReplyThread: &message.ReplyThreadTarget{
					ParentChannelID:   "chn_replay_parent",
					ParentChannelName: "all",
					ParentMessageID:   "msg_replay_parent",
					ShortID:           "abcdef12",
				},
			},
		},
		{
			name: "dm peer",
			target: message.ThreadAccess{
				ThreadType: domain.ThreadTypeDM,
				DMThreadID: "dm_replay_peer",
			},
		},
	}
	for _, tt := range cases {
		t.Run(tt.name, func(t *testing.T) {
			_, err := replayHumanMessageRoutingPlan(context.Background(), nil, tt.target, "mem_author", "", "plain message", nil)
			require.ErrorContains(t, err, "message has no replayable agent delivery payload")
		})
	}
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 20, 10, 0, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "moderation_block", now)
	user := model.User{ID: fixture.UserID, Name: "moderation_block", DisplayName: "moderation_block"}
	const blockedContent = "please check HighRisk phrase"

	mustExec(t, pool, `
INSERT INTO moderation_keywords (id, keyword, enabled, created_at, updated_at)
VALUES ('modkw_moderation_block', 'highrisk', 1, $1, $1)
`, now)

	_, _, err := store.SendMessageForUser(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, blockedContent, nil)
	require.ErrorIs(t, err, message.ErrContentModerationBlocked)

	var messageCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_messages
WHERE workspace_id = $1
  AND scope_id = $2
  AND content = $3
`, fixture.WorkspaceID, fixture.AllChannelID, blockedContent).Scan(&messageCount))
	require.Equal(t, 0, messageCount)

	var outboxCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_fanout_outbox
WHERE workspace_id = $1
  AND scope_id = $2
`, fixture.WorkspaceID, fixture.AllChannelID).Scan(&outboxCount))
	require.Equal(t, 0, outboxCount)

	var blockWorkspaceID, blockScopeID, blockScopeType, blockUserID, blockMemberID, blockContent, matchedKeyword string
	require.NoError(t, pool.QueryRow(ctx, `
SELECT workspace_id, scope_id, scope_type, user_id, member_id, content, matched_keyword
FROM moderation_blocks
WHERE workspace_id = $1
`, fixture.WorkspaceID).Scan(&blockWorkspaceID, &blockScopeID, &blockScopeType, &blockUserID, &blockMemberID, &blockContent, &matchedKeyword))
	require.Equal(t, fixture.WorkspaceID, blockWorkspaceID)
	require.Equal(t, fixture.AllChannelID, blockScopeID)
	require.Equal(t, "channel", blockScopeType)
	require.Equal(t, fixture.UserID, blockUserID)
	require.Equal(t, fixture.MemberID, blockMemberID)
	require.Equal(t, blockedContent, blockContent)
	require.Equal(t, "highrisk", matchedKeyword)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 20, 10, 2, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "moderation_typed", now)
	user := model.User{ID: fixture.UserID, Name: "moderation_typed", DisplayName: "moderation_typed"}
	const blockedContent = "typed highrisk content"

	mustExec(t, pool, `
INSERT INTO moderation_keywords (id, keyword, enabled, created_at, updated_at)
VALUES ('modkw_moderation_typed', 'highrisk', 1, $1, $1)
`, now)

	_, _, err := store.SendMessageForUserWithType(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, blockedContent, nil, MessageTypeSystem)
	require.ErrorIs(t, err, message.ErrContentModerationBlocked)

	var messageCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_messages
WHERE workspace_id = $1
  AND scope_id = $2
  AND content = $3
`, fixture.WorkspaceID, fixture.AllChannelID, blockedContent).Scan(&messageCount))
	require.Equal(t, 0, messageCount)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 28, 8, 30, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "system_card_retired_writer", now)
	user := model.User{ID: fixture.UserID, Name: "system_card_retired_writer", DisplayName: "system_card_retired_writer"}

	for _, cardKind := range []string{"embedded_task", "channel_share_card", "future_card"} {
		cardKind := cardKind
		t.Run(cardKind, func(t *testing.T) {
			_, _, err := store.SendSystemMessageForUserWithMetadata(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "retired system card", map[string]any{
				"card": map[string]any{"kind": cardKind},
			}, nil, nil)
			require.Error(t, err)
			require.ErrorContains(t, err, "system card message metadata is retired")
		})
	}
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 28, 9, 10, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "internal_workflow_retired_writer", now)
	user := model.User{ID: fixture.UserID, Name: "internal_workflow_retired_writer", DisplayName: "internal_workflow_retired_writer"}

	_, _, err := store.SendAppMessageForUserWithOptions(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "retired internal workflow", nil, SendAppMessageOptions{
		MessageType:           "internal_workflow",
		SkipContentModeration: true,
	})
	require.Error(t, err)
	require.ErrorContains(t, err, "unsupported messageType")

	var messageCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_messages
WHERE workspace_id = $1
  AND scope_id = $2
  AND message_type = $3
`, fixture.WorkspaceID, fixture.AllChannelID, "internal_workflow").Scan(&messageCount))
	require.Equal(t, 0, messageCount)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 20, 10, 5, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "moderation_disabled", now)
	user := model.User{ID: fixture.UserID, Name: "moderation_disabled", DisplayName: "moderation_disabled"}

	mustExec(t, pool, `
INSERT INTO moderation_keywords (id, keyword, enabled, created_at, updated_at)
VALUES ('modkw_moderation_disabled', 'disabledword', 0, $1, $1)
`, now)

	sentMessage, _, err := store.SendMessageForUser(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "contains disabledword only", nil)
	require.NoError(t, err)
	require.NotEmpty(t, sentMessage.ID)

	var blockCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM moderation_blocks
WHERE workspace_id = $1
`, fixture.WorkspaceID).Scan(&blockCount))
	require.Equal(t, 0, blockCount)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 6, 19, 10, 0, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "edit_message_content", now)
	user := model.User{ID: fixture.UserID, Name: "edit_message_content", DisplayName: "edit_message_content"}
	const (
		machineID     = "mach_edit_message_content"
		agentID       = "agt_edit_message_content"
		agentMemberID = "mem_agent_edit_message_content"
		agentName     = "edit-agent"
	)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, machineID, agentID, agentMemberID, agentName, now)
	original, delivery, err := store.SendMessageForUser(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "original content", nil)
	require.NoError(t, err)
	require.Empty(t, delivery.WakeRecipients)

	var outboxBefore int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_fanout_outbox
WHERE workspace_id = $1
`, fixture.WorkspaceID).Scan(&outboxBefore))

	edited, err := store.EditMessageForUser(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, original.ID, "edited @edit-agent content", message.HumanMessageEditOptions{
		MentionHints: []message.MessageMentionHint{{
			Type:  "agent",
			ID:    agentID,
			Token: agentName,
		}},
	})

	require.NoError(t, err)
	require.Equal(t, original.ID, edited.ID)
	require.Equal(t, original.Seq, edited.Seq)
	require.Equal(t, "edited @edit-agent content", edited.Content)
	require.NotNil(t, edited.EditedAt)
	require.Equal(t, fixture.MemberID, edited.EditedByMemberID)
	require.EqualValues(t, 1, edited.EditRevision)
	require.Len(t, message.ExtractPersistedMentionMetadata(edited.Metadata), 1)

	var storedContent, searchContent string
	var editRevision int64
	var editedByMemberID string
	require.NoError(t, pool.QueryRow(ctx, `
SELECT content, edited_by_member_id, edit_revision
FROM scope_messages
WHERE id = $1
`, original.ID).Scan(&storedContent, &editedByMemberID, &editRevision))
	require.Equal(t, "edited @edit-agent content", storedContent)
	require.Equal(t, fixture.MemberID, editedByMemberID)
	require.EqualValues(t, 1, editRevision)
	require.NoError(t, pool.QueryRow(ctx, `
SELECT content
FROM scope_message_search_docs
WHERE message_id = $1
`, original.ID).Scan(&searchContent))
	require.Equal(t, "edited @edit-agent content", searchContent)

	var outboxAfter int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_fanout_outbox
WHERE workspace_id = $1
`, fixture.WorkspaceID).Scan(&outboxAfter))
	require.Equal(t, outboxBefore, outboxAfter)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	board := taskboard.New(pool)
	now := time.Date(2026, 6, 20, 15, 25, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "edit_task_discussion", now)
	user := model.User{ID: fixture.UserID, Name: "edit_task_discussion", DisplayName: "edit_task_discussion"}

	created, err := board.CreateTasks(ctx, taskboard.CreateTasksRequest{
		Context: taskboard.NewUserServiceContext(fixture.WorkspaceID, fixture.UserID, user.DisplayName),
		Board:   taskboard.BoardRef{ChannelID: fixture.AllChannelID},
		Titles:  []string{"Edit task discussion"},
	})
	require.NoError(t, err)
	require.Len(t, created.Tasks, 1)
	task := created.Tasks[0]
	discussion, err := board.SendTaskScopeMessage(ctx, taskboard.SendTaskScopeMessageRequest{
		Context: taskboard.NewUserServiceContext(fixture.WorkspaceID, fixture.UserID, user.DisplayName),
		Task:    taskboard.TaskRef{TaskID: task.ID},
		Message: taskboard.TaskScopeMessageInput{
			Content: "original task discussion",
		},
	})
	require.NoError(t, err)

	var activityBefore, outboxBefore int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_activity_events
WHERE workspace_id = $1
  AND scope_id = $2
`, fixture.WorkspaceID, task.TaskScopeID).Scan(&activityBefore))
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_fanout_outbox
WHERE workspace_id = $1
`, fixture.WorkspaceID).Scan(&outboxBefore))

	edited, err := store.EditMessageForUser(ctx, user, fixture.WorkspaceID, task.TaskScopeID, discussion.Message.ID, "edited task discussion", message.HumanMessageEditOptions{})
	require.NoError(t, err)
	require.Equal(t, discussion.Message.ID, edited.ID)
	require.Equal(t, message.MessageTypeTaskDiscussion, edited.MessageType)
	require.Equal(t, "edited task discussion", edited.Content)
	require.NotNil(t, edited.EditedAt)
	require.Equal(t, fixture.MemberID, edited.EditedByMemberID)
	require.EqualValues(t, 1, edited.EditRevision)

	var storedContent, searchContent string
	var editRevision int64
	require.NoError(t, pool.QueryRow(ctx, `
SELECT content, edit_revision
FROM scope_messages
WHERE id = $1
`, discussion.Message.ID).Scan(&storedContent, &editRevision))
	require.Equal(t, "edited task discussion", storedContent)
	require.EqualValues(t, 1, editRevision)
	require.NoError(t, pool.QueryRow(ctx, `
SELECT content
FROM scope_message_search_docs
WHERE message_id = $1
`, discussion.Message.ID).Scan(&searchContent))
	require.Equal(t, "edited task discussion", searchContent)

	var activityAfter, outboxAfter int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_activity_events
WHERE workspace_id = $1
  AND scope_id = $2
`, fixture.WorkspaceID, task.TaskScopeID).Scan(&activityAfter))
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_fanout_outbox
WHERE workspace_id = $1
`, fixture.WorkspaceID).Scan(&outboxAfter))
	require.Equal(t, activityBefore, activityAfter)
	require.Equal(t, outboxBefore, outboxAfter)
}

func TestLookupMessageChannelExcludesTaskDiscussionFromRewindTargets(t *testing.T) {
	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	board := taskboard.New(pool)
	now := time.Date(2026, 7, 29, 8, 0, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "task_discussion_rewind_target", now)
	user := model.User{
		ID:          fixture.UserID,
		Name:        "task_discussion_rewind_target",
		DisplayName: "Task Discussion Rewind Target",
	}

	created, err := board.CreateTasks(ctx, taskboard.CreateTasksRequest{
		Context: taskboard.NewUserServiceContext(fixture.WorkspaceID, fixture.UserID, user.DisplayName),
		Board:   taskboard.BoardRef{ChannelID: fixture.AllChannelID},
		Titles:  []string{"Task discussion is not rewindable"},
	})
	require.NoError(t, err)
	require.Len(t, created.Tasks, 1)
	task := created.Tasks[0]

	discussion, err := board.SendTaskScopeMessage(ctx, taskboard.SendTaskScopeMessageRequest{
		Context: taskboard.NewUserServiceContext(fixture.WorkspaceID, fixture.UserID, user.DisplayName),
		Task:    taskboard.TaskRef{TaskID: task.ID},
		Message: taskboard.TaskScopeMessageInput{Content: "active Task discussion"},
	})
	require.NoError(t, err)

	channelID, channelType, err := store.LookupMessageChannel(ctx, fixture.WorkspaceID, discussion.Message.ID)
	require.NoError(t, err)
	require.Empty(t, channelID)
	require.Empty(t, channelType)

	rewindStore := rewind.New(pool, rewind.Options{
		LookupMessageChannelID: store.LookupMessageChannelID,
	})
	_, err = rewindStore.PrepareRewindForUser(ctx, fixture.UserID, fixture.WorkspaceID, discussion.Message.ID)
	require.ErrorIs(t, err, storeerr.ErrBadRequest)
	require.ErrorContains(t, err, "message is not in hot history or is not visible")
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 6, 19, 10, 10, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "edit_message_author", now)
	peer := seedWorkspaceMemberFixture(t, pool, fixture.WorkspaceID, "edit_message_author_peer", now)
	require.NoError(t, message.EnsureScopeMembership(ctx, pool, fixture.AllChannelID, peer.MemberID, "public", "", "active", 0, 0, now))
	user := model.User{ID: fixture.UserID, Name: "edit_message_author", DisplayName: "edit_message_author"}
	peerUser := model.User{ID: peer.UserID, Name: "edit_message_author_peer", DisplayName: "edit_message_author_peer"}

	original, _, err := store.SendMessageForUser(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "original content", nil)
	require.NoError(t, err)
	_, err = store.EditMessageForUser(ctx, peerUser, fixture.WorkspaceID, fixture.AllChannelID, original.ID, "peer edit", message.HumanMessageEditOptions{})
	require.ErrorIs(t, err, ErrForbidden)

	systemMessage, _, err := store.SendMessageForUserWithType(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "system content", nil, MessageTypeSystem)
	require.NoError(t, err)
	_, err = store.EditMessageForUser(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, systemMessage.ID, "edited system", message.HumanMessageEditOptions{})
	require.ErrorIs(t, err, ErrBadRequest)

	attachmentMessage, _, err := store.SendMessageForUser(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "message with attachment", nil)
	require.NoError(t, err)
	mustExec(t, pool, `
INSERT INTO attachments (id, workspace_id, scope_id, uploaded_by_member_id, message_id, filename, content_type, size_bytes, payload, created_at)
VALUES ('att_edit_message_author', $1, $2, $3, $4, 'edit-attachment.txt', 'text/plain', 1, $5, $6)
`, fixture.WorkspaceID, fixture.AllChannelID, fixture.MemberID, attachmentMessage.ID, []byte("x"), now)
	_, err = store.EditMessageForUser(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, attachmentMessage.ID, "edited attachment message", message.HumanMessageEditOptions{})
	require.ErrorIs(t, err, ErrBadRequest)

	parent, _, err := store.SendMessageForUser(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "thread parent", nil)
	require.NoError(t, err)
	replyThread, err := message.OpenReplyThreadForMember(ctx, pool, fixture.MemberID, fixture.WorkspaceID, fixture.AllChannelID, parent.ID)
	require.NoError(t, err)
	threadMessage, _, err := store.SendMessageForUser(ctx, user, fixture.WorkspaceID, replyThread.ThreadChannelID, "thread ordinary", nil)
	require.NoError(t, err)
	_, err = store.EditMessageForUser(ctx, user, fixture.WorkspaceID, replyThread.ThreadChannelID, threadMessage.ID, "@all edited thread", message.HumanMessageEditOptions{})
	require.ErrorIs(t, err, ErrBadRequest)
	require.ErrorContains(t, err, "threads do not support @all or @online")

	loaded, err := store.ListMessagesForUser(ctx, fixture.UserID, fixture.WorkspaceID, fixture.AllChannelID, 20, nil, nil)
	require.NoError(t, err)
	contentByID := make(map[string]string, len(loaded.Messages))
	for _, msg := range loaded.Messages {
		contentByID[msg.ID] = msg.Content
	}
	require.Equal(t, "original content", contentByID[original.ID])
	require.Equal(t, "system content", contentByID[systemMessage.ID])
	require.Equal(t, "message with attachment", contentByID[attachmentMessage.ID])

	threadPage, err := store.ListMessagesForUser(ctx, fixture.UserID, fixture.WorkspaceID, replyThread.ThreadChannelID, 20, nil, nil)
	require.NoError(t, err)
	threadContentByID := make(map[string]string, len(threadPage.Messages))
	for _, msg := range threadPage.Messages {
		threadContentByID[msg.ID] = msg.Content
	}
	require.Equal(t, "thread ordinary", threadContentByID[threadMessage.ID])
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 20, 10, 10, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "moderation_system", now)
	user := model.User{ID: fixture.UserID, Name: "moderation_system", DisplayName: "moderation_system"}

	mustExec(t, pool, `
INSERT INTO moderation_keywords (id, keyword, enabled, created_at, updated_at)
VALUES ('modkw_moderation_system', 'systemword', 1, $1, $1)
`, now)

	sentMessage, _, err := store.SendSystemMessageForUserWithMetadata(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "contains systemword", nil, nil, nil)
	require.NoError(t, err)
	require.Equal(t, MessageTypeSystem, sentMessage.MessageType)

	var blockCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM moderation_blocks
WHERE workspace_id = $1
`, fixture.WorkspaceID).Scan(&blockCount))
	require.Equal(t, 0, blockCount)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 20, 10, 15, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "moderation_validate", now)
	user := model.User{ID: fixture.UserID, Name: "moderation_validate", DisplayName: "moderation_validate"}

	mustExec(t, pool, `
INSERT INTO moderation_keywords (id, keyword, enabled, created_at, updated_at)
VALUES ('modkw_moderation_validate', 'validateword', 1, $1, $1)
`, now)

	err := store.ValidateMessageForUser(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "contains validateword")
	require.ErrorIs(t, err, message.ErrContentModerationBlocked)

	var blockCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM moderation_blocks
WHERE workspace_id = $1
`, fixture.WorkspaceID).Scan(&blockCount))
	require.Equal(t, 0, blockCount)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 18, 12, 0, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "message_created_at_monotonic", now)
	user := model.User{ID: fixture.UserID, Name: "message_created_at_monotonic", DisplayName: "message_created_at_monotonic"}
	futureLatest := time.Now().UTC().Add(time.Hour).Truncate(time.Second)
	mustExec(t, pool, `
UPDATE scopes
SET latest_message_at = $2,
    visible_last_message_at = $2
WHERE id = $1
`, fixture.AllChannelID, futureLatest)

	sentMessage, delivery, err := store.SendMessageForUser(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "after skewed earlier sender", nil)
	require.NoError(t, err)
	require.WithinDuration(t, futureLatest, sentMessage.CreatedAt.UTC(), time.Second)
	require.WithinDuration(t, futureLatest, delivery.Message.Timestamp.UTC(), time.Second)

	var persistedCreatedAt time.Time
	require.NoError(t, pool.QueryRow(ctx, `
SELECT created_at
FROM scope_messages
WHERE id = $1
`, sentMessage.ID).Scan(&persistedCreatedAt))
	require.WithinDuration(t, futureLatest, persistedCreatedAt.UTC(), time.Second)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 14, 9, 18, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "client_replay_scope_thread", now)
	user := model.User{ID: fixture.UserID, Name: "client_replay_scope_thread", DisplayName: "client_replay_scope_thread"}
	const (
		machineID     = "mach_client_replay_scope_thread"
		agentID       = "agt_client_replay_scope_thread"
		agentMemberID = "mem_agent_client_replay_scope_thread"
	)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, machineID, agentID, agentMemberID, "client-replay-scope-agent", now)

	firstMessage, firstDelivery, err := store.SendAppMessageForUserWithOptions(
		ctx,
		user,
		fixture.WorkspaceID,
		fixture.AllChannelID,
		"@client-replay-scope-agent start a scoped thread",
		nil,
		SendAppMessageOptions{ClientMessageID: "client-replay-scope-thread-1", StartAgentThread: true},
	)
	require.NoError(t, err)
	require.False(t, firstDelivery.IdempotentReplay)
	require.NotNil(t, firstDelivery.StartedReplyThread)
	require.NotEmpty(t, firstDelivery.StartedReplyThread.ThreadChannelID)
	require.Equal(t, firstMessage.ID, firstDelivery.StartedReplyThread.RootMessageID)

	replyMessage, _, err := store.SendMessageForUser(
		ctx,
		user,
		fixture.WorkspaceID,
		firstDelivery.StartedReplyThread.ThreadChannelID,
		"scoped reply after started thread",
		nil,
	)
	require.NoError(t, err)

	replayedMessage, replayedDelivery, err := store.SendAppMessageForUserWithOptions(
		ctx,
		user,
		fixture.WorkspaceID,
		fixture.AllChannelID,
		"@client-replay-scope-agent start a scoped thread",
		nil,
		SendAppMessageOptions{ClientMessageID: "client-replay-scope-thread-1", StartAgentThread: true},
	)
	require.NoError(t, err)
	require.True(t, replayedDelivery.IdempotentReplay)
	require.Equal(t, firstMessage.ID, replayedMessage.ID)
	require.NotNil(t, replayedDelivery.StartedReplyThread)
	require.Equal(t, firstDelivery.StartedReplyThread.ThreadChannelID, replayedDelivery.StartedReplyThread.ThreadChannelID)
	require.Equal(t, firstMessage.ID, replayedDelivery.StartedReplyThread.RootMessageID)
	require.EqualValues(t, 1, replayedDelivery.StartedReplyThread.ReplyCount)
	require.NotNil(t, replayedDelivery.StartedReplyThread.LastReplyAt)
	require.WithinDuration(t, replyMessage.CreatedAt.UTC(), replayedDelivery.StartedReplyThread.LastReplyAt.UTC(), time.Second)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 15, 9, 30, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "reply_visible_summary", now)
	user := model.User{ID: fixture.UserID, Name: "reply_visible_summary", DisplayName: "reply_visible_summary"}

	parent, _, err := store.SendMessageForUser(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "parent message", nil)
	require.NoError(t, err)
	replyThread, err := message.OpenReplyThreadForMember(ctx, pool, fixture.MemberID, fixture.WorkspaceID, fixture.AllChannelID, parent.ID)
	require.NoError(t, err)
	threadChannelID := replyThread.ThreadChannelID

	ordinaryReply, _, err := store.SendMessageForUser(ctx, user, fixture.WorkspaceID, threadChannelID, "ordinary reply", nil)
	require.NoError(t, err)
	seedLegacyTaskDispatchMessageForTest(t, ctx, pool, fixture.WorkspaceID, threadChannelID, fixture.MemberID, "legacy hidden task dispatch")
	page, err := store.ListMessagesForUser(ctx, fixture.UserID, fixture.WorkspaceID, fixture.AllChannelID, 20, nil, nil)
	require.NoError(t, err)
	summary, ok := page.ReplyThreadSummaries[parent.ID]
	require.True(t, ok)
	require.EqualValues(t, 1, summary.ReplyCount)
	require.NotNil(t, summary.LastReplyAt)
	require.WithinDuration(t, ordinaryReply.CreatedAt.UTC(), summary.LastReplyAt.UTC(), time.Second)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 6, 14, 10, 0, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "message_page_has_more", now)
	user := model.User{ID: fixture.UserID, Name: "message_page_has_more", DisplayName: "message_page_has_more"}

	for i := 1; i <= 21; i++ {
		_, _, err := store.SendMessageForUser(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, fmt.Sprintf("message %02d", i), nil)
		require.NoError(t, err)
	}

	page, err := store.ListMessagesForUser(ctx, fixture.UserID, fixture.WorkspaceID, fixture.AllChannelID, 20, nil, nil)
	require.NoError(t, err)
	require.Len(t, page.Messages, 20)
	require.True(t, page.HasMore)
	require.Equal(t, page.Messages[0].Seq, page.NextCursor)
	require.Equal(t, "message 02", page.Messages[0].Content)
	require.Equal(t, "message 21", page.Messages[19].Content)

	before := page.NextCursor
	olderPage, err := store.ListMessagesForUser(ctx, fixture.UserID, fixture.WorkspaceID, fixture.AllChannelID, 20, &before, nil)
	require.NoError(t, err)
	require.Len(t, olderPage.Messages, 1)
	require.False(t, olderPage.HasMore)
	require.Zero(t, olderPage.NextCursor)
	require.Equal(t, "message 01", olderPage.Messages[0].Content)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 6, 16, 8, 45, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "message_pin_state", now)
	user := model.User{ID: fixture.UserID, Name: "message_pin_state", DisplayName: "message_pin_state"}

	sent, _, err := store.SendMessageForUser(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "pinned row marker", nil)
	require.NoError(t, err)

	pinStore := message.New(pool, nil, 0, message.Options{})
	pinned, err := pinStore.PinMessageForUser(ctx, fixture.UserID, fixture.WorkspaceID, fixture.AllChannelID, sent.ID)
	require.NoError(t, err)
	require.NotEmpty(t, pinned.PinID)

	page, err := store.ListMessagesForUser(ctx, fixture.UserID, fixture.WorkspaceID, fixture.AllChannelID, 20, nil, nil)
	require.NoError(t, err)
	var listed *message.AppMessage
	for index := range page.Messages {
		if page.Messages[index].ID == sent.ID {
			listed = &page.Messages[index]
			break
		}
	}
	require.NotNil(t, listed)
	require.NotNil(t, listed.PinState)
	require.True(t, listed.PinState.IsPinned)
	require.Equal(t, pinned.PinID, listed.PinState.PinID)
	require.Equal(t, fixture.MemberID, listed.PinState.PinnedByMemberID)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 20, 14, 0, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "thread_owner_quote_visible", now)
	user := model.User{ID: fixture.UserID, Name: "thread_owner_quote_visible", DisplayName: "thread_owner_quote_visible"}

	parent, _, err := store.SendMessageForUser(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "thread owner quote parent", nil)
	require.NoError(t, err)
	replyThread, err := message.OpenReplyThreadForMember(ctx, pool, fixture.MemberID, fixture.WorkspaceID, fixture.AllChannelID, parent.ID)
	require.NoError(t, err)

	source, _, err := store.SendMessageForUser(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "thread owner quote source", nil)
	require.NoError(t, err)
	ownerQuote, _, err := store.SendMessageForUserWithType(
		ctx,
		user,
		fixture.WorkspaceID,
		replyThread.ThreadChannelID,
		ownerQuoteTestContent(source.ID, "forward thread context", "thread owner quote source"),
		nil,
		MessageTypeOwnerQuote,
	)
	require.NoError(t, err)

	page, err := store.ListMessagesForUser(ctx, fixture.UserID, fixture.WorkspaceID, replyThread.ThreadChannelID, 20, nil, nil)
	require.NoError(t, err)
	require.Len(t, page.Messages, 1)
	require.Equal(t, ownerQuote.ID, page.Messages[0].ID)
	require.Equal(t, MessageTypeOwnerQuote, page.Messages[0].MessageType)
	require.Equal(t, message.AuthorActorTypeHuman, page.Messages[0].AuthorProfile.ActorType)
	require.Empty(t, page.Messages[0].Metadata)
	require.Contains(t, page.Messages[0].Content, "thread owner quote source")
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 13, 13, 0, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "user_display_name_precedence", now)

	mustExec(t, pool, `
UPDATE members
SET display_name = 'Old Member Name'
WHERE id = $1
`, fixture.MemberID)
	mustExec(t, pool, `
UPDATE users
SET display_name = 'New User Display'
WHERE id = $1
`, fixture.UserID)

	user := model.User{
		ID:          fixture.UserID,
		Name:        "user_display_name_precedence",
		DisplayName: "New User Display",
	}

	sentMessage, _, err := store.SendMessageForUser(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "hello", nil)
	require.NoError(t, err)
	require.Equal(t, "New User Display", sentMessage.AuthorProfile.DisplayName)

	page, err := store.ListMessagesForUser(ctx, fixture.UserID, fixture.WorkspaceID, fixture.AllChannelID, 20, nil, nil)
	require.NoError(t, err)
	require.Len(t, page.Messages, 1)
	require.Equal(t, "New User Display", page.Messages[0].AuthorProfile.DisplayName)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 13, 13, 10, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "user_avatar_propagation", now)

	const avatarURL = "https://example.com/avatars/user-avatar.png"
	user := model.User{
		ID:          fixture.UserID,
		Name:        "user_avatar_propagation",
		DisplayName: "Avatar User",
		AvatarURL:   avatarURL,
	}

	sentMessage, delivery, err := store.SendMessageForUser(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "hello", nil)
	require.NoError(t, err)
	require.Equal(t, avatarURL, sentMessage.AuthorProfile.AvatarURL)
	require.Equal(t, avatarURL, delivery.Message.AuthorProfile.AvatarURL)
	require.NotNil(t, delivery.AppMessage)
	require.Equal(t, avatarURL, delivery.AppMessage.AuthorProfile.AvatarURL)

	var payload []byte
	require.NoError(t, pool.QueryRow(ctx, `
SELECT payload
FROM scope_fanout_outbox
WHERE message_id = $1
  AND fanout_type = 'scope_fanout'
LIMIT 1
`, delivery.MessageID).Scan(&payload))
	fanoutPayload, err := message.UnmarshalScopeFanoutPayload(payload)
	require.NoError(t, err)
	require.Equal(t, avatarURL, fanoutPayload.AppMessage.AuthorProfile.AvatarURL)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 20, 11, 0, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "resolved_agent_mentions", now)
	user := model.User{ID: fixture.UserID, Name: "resolved_agent_mentions", DisplayName: "resolved_agent_mentions"}

	const (
		machineID     = "mach_resolved_agent_mentions"
		agentID       = "agt_resolved_agent_mentions"
		agentMemberID = "8drblxlist"
		agentName     = "resolved-agent"
		agentLabel    = "Resolved Agent"
	)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, machineID, agentID, agentMemberID, agentName, now)
	mustExec(t, pool, `
UPDATE members
SET display_name = $2
WHERE id = $1
`, agentMemberID, agentLabel)

	sentMessage, _, err := store.SendMessageForUser(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "@8drblxlist please use the public context", nil)
	require.NoError(t, err)

	page, err := store.ListMessagesForUser(ctx, fixture.UserID, fixture.WorkspaceID, fixture.AllChannelID, 20, nil, nil)
	require.NoError(t, err)
	var listed *message.AppMessage
	for i := range page.Messages {
		if page.Messages[i].ID == sentMessage.ID {
			listed = &page.Messages[i]
			break
		}
	}
	require.NotNil(t, listed)
	require.Equal(t, []message.ResolvedMention{{
		Token:         agentMemberID,
		Type:          "agent",
		ID:            agentID,
		DisplayName:   agentLabel,
		ResolvedToken: agentName,
	}}, listed.ResolvedMentions)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 20, 11, 30, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "resolved_agent_rename", now)
	user := model.User{ID: fixture.UserID, Name: "resolved_agent_rename", DisplayName: "resolved_agent_rename"}

	const (
		machineID         = "mach_resolved_agent_rename"
		agentID           = "agt_resolved_agent_rename"
		agentMemberID     = "mem_agent_resolved_agent_rename"
		reusedMachineID   = "mach_resolved_agent_reused"
		reusedAgentID     = "agt_resolved_agent_reused"
		reusedAgentMember = "mem_resolved_agent_reused"
		agentName         = "before-rename"
		renamedAgent      = "after-rename"
		renamedLabel      = "After Rename"
	)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, machineID, agentID, agentMemberID, agentName, now)

	sentMessage, _, err := store.SendAppMessageForUserWithOptions(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "@before-rename please review", nil, SendAppMessageOptions{
		MentionHints: []message.MessageMentionHint{{
			Type:  "agent",
			ID:    agentID,
			Token: agentName,
		}},
	})
	require.NoError(t, err)
	require.Equal(t, []any{
		map[string]any{
			"type":  "agent",
			"id":    agentID,
			"token": agentName,
		},
	}, sentMessage.Metadata[message.MentionMetadataKey])

	unhintedMessage, _, err := store.SendAppMessageForUserWithOptions(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "@before-rename please keep this binding", nil, SendAppMessageOptions{})
	require.NoError(t, err)
	require.Equal(t, []any{
		map[string]any{
			"type":  "agent",
			"id":    agentID,
			"token": agentName,
		},
	}, unhintedMessage.Metadata[message.MentionMetadataKey])

	mustExec(t, pool, `
UPDATE agents
SET name = $2
WHERE id = $1
`, agentID, renamedAgent)
	mustExec(t, pool, `
UPDATE members
SET display_name = $2
WHERE id = $1
`, agentMemberID, renamedLabel)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, reusedMachineID, reusedAgentID, reusedAgentMember, agentName, now)

	page, err := store.ListMessagesForUser(ctx, fixture.UserID, fixture.WorkspaceID, fixture.AllChannelID, 20, nil, nil)
	require.NoError(t, err)
	var listed *message.AppMessage
	for i := range page.Messages {
		if page.Messages[i].ID == sentMessage.ID {
			listed = &page.Messages[i]
			break
		}
	}
	require.NotNil(t, listed)
	require.Equal(t, []message.ResolvedMention{{
		Token:         agentName,
		Type:          "agent",
		ID:            agentID,
		DisplayName:   renamedLabel,
		ResolvedToken: renamedAgent,
	}}, listed.ResolvedMentions)
	var listedUnhinted *message.AppMessage
	for i := range page.Messages {
		if page.Messages[i].ID == unhintedMessage.ID {
			listedUnhinted = &page.Messages[i]
			break
		}
	}
	require.NotNil(t, listedUnhinted)
	require.Equal(t, []message.ResolvedMention{{
		Token:         agentName,
		Type:          "agent",
		ID:            agentID,
		DisplayName:   renamedLabel,
		ResolvedToken: renamedAgent,
	}}, listedUnhinted.ResolvedMentions)

	staleTokenMessage, staleTokenDelivery, err := store.SendAppMessageForUserWithOptions(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "@before-rename still points at the selected agent", nil, SendAppMessageOptions{
		MentionHints: []message.MessageMentionHint{{
			Type:  "agent",
			ID:    agentID,
			Token: agentName,
		}, {
			Type:  "agent",
			ID:    reusedAgentID,
			Token: agentName,
		}, {
			Type:  "agent",
			ID:    "agt_missing_from_channel",
			Token: agentName,
		}},
	})
	require.NoError(t, err)
	require.Equal(t, []any{
		map[string]any{
			"type":  "agent",
			"id":    agentID,
			"token": agentName,
		},
	}, staleTokenMessage.Metadata[message.MentionMetadataKey])
	staleReasons := agentDeliveryReasonsByMember(staleTokenDelivery.Recipients)
	require.Equal(t, message.AgentDeliveryReasonDirectMention, staleReasons[agentMemberID])
	require.NotEqual(t, message.AgentDeliveryReasonDirectMention, staleReasons[reusedAgentMember])

	invalidHintMessage, _, err := store.SendAppMessageForUserWithOptions(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "@missing-agent should stay plain text", nil, SendAppMessageOptions{
		MentionHints: []message.MessageMentionHint{{
			Type:  "agent",
			ID:    "agt_missing_from_channel",
			Token: "missing-agent",
		}},
	})
	require.NoError(t, err)
	require.Nil(t, invalidHintMessage.Metadata[message.MentionMetadataKey])
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 20, 11, 45, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "human_resolved_payload", now)
	user := model.User{ID: fixture.UserID, Name: "human_resolved_payload", DisplayName: "human_resolved_payload"}

	const (
		machineID     = "mach_human_resolved_payload"
		agentID       = "agt_human_resolved_payload"
		agentMemberID = "mem_agent_human_resolved_payload"
		agentName     = "human-payload-target"
		agentLabel    = "Human Payload Target"
	)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, machineID, agentID, agentMemberID, agentName, now)
	mustExec(t, pool, `
UPDATE members
SET display_name = $2
WHERE id = $1
`, agentMemberID, agentLabel)

	sentMessage, delivery, err := store.SendAppMessageForUserWithOptions(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "@human-payload-target please review", nil, SendAppMessageOptions{
		MentionHints: []message.MessageMentionHint{{
			Type:  "agent",
			ID:    agentID,
			Token: agentName,
		}},
	})
	require.NoError(t, err)
	expected := []message.ResolvedMention{{
		Token:         agentName,
		Type:          "agent",
		ID:            agentID,
		DisplayName:   agentLabel,
		ResolvedToken: agentName,
	}}
	require.Equal(t, expected, sentMessage.ResolvedMentions)
	require.NotNil(t, delivery.AppMessage)
	require.Equal(t, expected, delivery.AppMessage.ResolvedMentions)

	fanoutPayload := requireScopeFanoutPayloadForMessage(t, pool, delivery.MessageID)
	require.Equal(t, expected, fanoutPayload.AppMessage.ResolvedMentions)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 20, 11, 50, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "private_resolved_payload", now)
	peer := seedWorkspaceMemberFixture(t, pool, fixture.WorkspaceID, "private_resolved_payload_peer", now)
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, fixture.AllChannelID, "all", "", "public", fixture.MemberID, peer.MemberID, now)
	user := model.User{ID: fixture.UserID, Name: "private_resolved_payload", DisplayName: "private_resolved_payload"}

	const (
		machineID     = "mach_private_resolved_payload"
		agentID       = "agt_private_resolved_payload"
		agentMemberID = "mem_agent_private_resolved_payload"
		agentName     = "private-payload-target"
		agentLabel    = "Private Payload Target"
	)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, machineID, agentID, agentMemberID, agentName, now)
	mustExec(t, pool, `
UPDATE members
SET display_name = $2
WHERE id = $1
`, agentMemberID, agentLabel)

	sentMessage, delivery, err := store.SendAppMessageForUserWithOptions(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "@private-payload-target please handle this owner-only agent", nil, SendAppMessageOptions{
		MentionHints: []message.MessageMentionHint{{
			Type:  "agent",
			ID:    agentID,
			Token: agentName,
		}},
	})
	require.NoError(t, err)
	require.Equal(t, []any{map[string]any{
		"type":  "agent",
		"id":    agentID,
		"token": agentName,
	}}, sentMessage.Metadata[message.MentionMetadataKey])
	require.Equal(t, message.AgentDeliveryReasonDirectMention, agentDeliveryReasonsByMember(delivery.WakeRecipients)[agentMemberID])

	require.Empty(t, sentMessage.ResolvedMentions)
	require.NotNil(t, delivery.AppMessage)
	require.Empty(t, delivery.AppMessage.ResolvedMentions)
	fanoutPayload := requireScopeFanoutPayloadForMessage(t, pool, delivery.MessageID)
	require.Empty(t, fanoutPayload.AppMessage.ResolvedMentions)

	ownerPage, err := store.ListMessagesForUser(ctx, fixture.UserID, fixture.WorkspaceID, fixture.AllChannelID, 20, nil, nil)
	require.NoError(t, err)
	ownerListed := requireListedAppMessage(t, ownerPage.Messages, sentMessage.ID)
	require.Equal(t, []message.ResolvedMention{{
		Token:         agentName,
		Type:          "agent",
		ID:            agentID,
		DisplayName:   agentLabel,
		ResolvedToken: agentName,
	}}, ownerListed.ResolvedMentions)

	peerPage, err := store.ListMessagesForUser(ctx, peer.UserID, fixture.WorkspaceID, fixture.AllChannelID, 20, nil, nil)
	require.NoError(t, err)
	peerListed := requireListedAppMessage(t, peerPage.Messages, sentMessage.ID)
	require.Empty(t, peerListed.ResolvedMentions)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 20, 12, 5, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "cjk_mention_no_delimiter", now)
	user := model.User{ID: fixture.UserID, Name: "cjk_mention_no_delimiter", DisplayName: "cjk_mention_no_delimiter"}

	const (
		machineID     = "mach_cjk_mention_no_delimiter"
		agentID       = "agt_cjk_mention_no_delimiter"
		agentMemberID = "mem_agent_cjk_mention_no_delimiter"
		agentName     = "billmay-sdr线索洞察"
	)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, machineID, agentID, agentMemberID, agentName, now)

	sentMessage, delivery, err := store.SendAppMessageForUserWithOptions(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "@billmay-sdr线索洞察请给我一个邮件回复内容", nil, SendAppMessageOptions{})
	require.NoError(t, err)
	require.Equal(t, []any{
		map[string]any{
			"type":  "agent",
			"id":    agentID,
			"token": agentName,
		},
	}, sentMessage.Metadata[message.MentionMetadataKey])
	reasons := agentDeliveryReasonsByMember(delivery.Recipients)
	require.Equal(t, message.AgentDeliveryReasonDirectMention, reasons[agentMemberID])

	page, err := store.ListMessagesForUser(ctx, fixture.UserID, fixture.WorkspaceID, fixture.AllChannelID, 20, nil, nil)
	require.NoError(t, err)
	var listed *message.AppMessage
	for i := range page.Messages {
		if page.Messages[i].ID == sentMessage.ID {
			listed = &page.Messages[i]
			break
		}
	}
	require.NotNil(t, listed)
	require.Equal(t, []message.ResolvedMention{{
		Token:         agentName,
		Type:          "agent",
		ID:            agentID,
		DisplayName:   agentName,
		ResolvedToken: agentName,
	}}, listed.ResolvedMentions)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 29, 10, 15, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "agent_mentions_metadata", now)

	const (
		authorMachineID     = "mach_agent_mentions_metadata_author"
		authorAgentID       = "agt_agent_mentions_metadata_author"
		authorAgentMemberID = "mem_agent_mentions_metadata_author"
		authorAgentName     = "agent-mention-author"
		targetMachineID     = "mach_agent_mentions_metadata_target"
		targetAgentID       = "agt_agent_mentions_metadata_target"
		targetAgentMemberID = "mem_agent_mentions_metadata_target"
		targetAgentName     = "agent-mention-target"
		targetAgentLabel    = "Agent Mention Target"
	)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, authorMachineID, authorAgentID, authorAgentMemberID, authorAgentName, now)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, targetMachineID, targetAgentID, targetAgentMemberID, targetAgentName, now)
	mustExec(t, pool, `
UPDATE members
SET display_name = $2
WHERE id = $1
`, targetAgentMemberID, targetAgentLabel)

	delivery, err := store.SendAgentThreadMessage(ctx, agent.AuthenticatedAgent{
		AgentID:     authorAgentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    authorAgentMemberID,
	}, message.SendMessageParams{
		TargetRef: "#all",
		Content:   "@agent-mention-target please review this",
	})
	require.NoError(t, err)
	require.NotNil(t, delivery.AppMessage)
	require.Equal(t, message.AgentDeliveryReasonDirectMention, agentDeliveryReasonsByMember(delivery.Recipients)[targetAgentMemberID])
	require.Equal(t, []any{map[string]any{
		"type":  "agent",
		"id":    targetAgentID,
		"token": targetAgentName,
	}}, delivery.AppMessage.Metadata[message.MentionMetadataKey])
	require.Equal(t, []message.ResolvedMention{{
		Token:         targetAgentName,
		Type:          "agent",
		ID:            targetAgentID,
		DisplayName:   targetAgentLabel,
		ResolvedToken: targetAgentName,
	}}, delivery.AppMessage.ResolvedMentions)

	page, err := store.ListMessagesForUser(ctx, fixture.UserID, fixture.WorkspaceID, fixture.AllChannelID, 20, nil, nil)
	require.NoError(t, err)
	listed := requireListedAppMessage(t, page.Messages, delivery.MessageID)
	require.Equal(t, []message.ResolvedMention{{
		Token:         targetAgentName,
		Type:          "agent",
		ID:            targetAgentID,
		DisplayName:   targetAgentLabel,
		ResolvedToken: targetAgentName,
	}}, listed.ResolvedMentions)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	teamStore := team.New(pool)
	now := time.Date(2026, 6, 23, 13, 30, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "agent_team_invoke", now)

	const (
		authorMachineID     = "mach_agent_team_invoke_author"
		authorAgentID       = "agt_agent_team_invoke_author"
		authorAgentMemberID = "mem_agent_team_invoke_author"
		authorAgentName     = "agent-team-invoke-author"
		targetMachineID     = "mach_agent_team_invoke_target"
		targetAgentID       = "agt_agent_team_invoke_target"
		targetAgentMemberID = "mem_agent_team_invoke_target"
		targetAgentName     = "agent-team-invoke-target"
	)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, authorMachineID, authorAgentID, authorAgentMemberID, authorAgentName, now)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, targetMachineID, targetAgentID, targetAgentMemberID, targetAgentName, now)

	wakeOnMention := false
	team, err := teamStore.CreateTeam(ctx, team.CreateTeamParams{
		ID:          "team_agent_team_invoke",
		WorkspaceID: fixture.WorkspaceID,
		Handle:      "team-invoke",
		DisplayName: "Team Invoke",
		InitialMembers: []team.AddTeamMemberParams{
			{
				ID:        "wtm_agent_team_invoke_author",
				ActorKind: team.ActorKindAgent,
				ActorID:   authorAgentMemberID,
				Role:      team.MemberRoleLeader,
				RoleLabel: "Coordinator",
			},
			{
				ID:                "wtm_agent_team_invoke_target",
				ActorKind:         team.ActorKindAgent,
				ActorID:           targetAgentMemberID,
				Role:              team.MemberRoleMember,
				RoleLabel:         "Builder",
				RoleOverlayPrompt: "Build delegated Team work.",
				WakeOnTeamMention: &wakeOnMention,
			},
		},
		CreatedByMemberID: fixture.MemberID,
		Now:               now.Add(time.Minute),
	})
	require.NoError(t, err)

	delivery, err := store.SendAgentThreadMessage(ctx, agent.AuthenticatedAgent{
		AgentID:     authorAgentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    authorAgentMemberID,
	}, message.SendMessageParams{
		TargetRef:        "#all",
		Content:          "@agent-team-invoke-target please build this slice",
		TeamInvokeTeamID: team.ID,
	})
	require.NoError(t, err)

	require.Equal(t, message.AgentDeliveryReasonDirectMention, agentDeliveryReasonsByMember(delivery.Recipients)[targetAgentMemberID])
	require.Len(t, delivery.Recipients, 1)
	require.Len(t, delivery.Recipients[0].TeamContexts, 1)
	require.Equal(t, team.ID, delivery.Recipients[0].TeamContexts[0].TeamID)
	require.Equal(t, "Builder", delivery.Recipients[0].TeamContexts[0].RoleLabel)

	targetMessages, err := store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{
		AgentID:     targetAgentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    targetAgentMemberID,
	}, 10)
	require.NoError(t, err)
	require.Len(t, targetMessages, 1)
	require.Equal(t, delivery.MessageID, targetMessages[0].MessageID)
	require.Len(t, targetMessages[0].TeamContexts, 1)
	require.Equal(t, team.ID, targetMessages[0].TeamContexts[0].TeamID)
	require.Equal(t, "Build delegated Team work.", targetMessages[0].TeamContexts[0].RoleOverlayPrompt)
	require.Len(t, targetMessages[0].TeamContexts[0].MemberRoster, 1)
	require.Equal(t, targetAgentMemberID, targetMessages[0].TeamContexts[0].MemberRoster[0].ActorID)
	require.True(t, targetMessages[0].TeamContexts[0].MemberRoster[0].Self)
	require.Equal(t, "Build delegated Team work.", targetMessages[0].TeamContexts[0].MemberRoster[0].RoleOverlayPrompt)

	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, "mach_agent_team_invoke_outsider", "agt_agent_team_invoke_outsider", "mem_agent_team_invoke_outsider", "agent-team-invoke-outsider", now)
	_, err = store.SendAgentThreadMessage(ctx, agent.AuthenticatedAgent{
		AgentID:     "agt_agent_team_invoke_outsider",
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    "mem_agent_team_invoke_outsider",
	}, message.SendMessageParams{
		TargetRef:        "#all",
		Content:          "@agent-team-invoke-target please build this slice",
		TeamInvokeTeamID: team.ID,
	})
	require.Error(t, err)
	require.Contains(t, err.Error(), "sender to be an agent member")

	_, err = store.SendAgentThreadMessage(ctx, agent.AuthenticatedAgent{
		AgentID:     authorAgentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    authorAgentMemberID,
	}, message.SendMessageParams{
		TargetRef:        "#all",
		Content:          "@agent-team-invoke-target please build this slice",
		TeamInvokeTeamID: team.Handle,
	})
	require.Error(t, err)
	require.Contains(t, err.Error(), "active Team id")
	require.Contains(t, err.Error(), "not the @handle")
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	teamStore := team.New(pool)
	now := time.Date(2026, 6, 25, 15, 45, 0, 0, time.UTC)

	fixture := seedAppAdminFixture(t, pool, "agent_team_invoke_owner_private", now)
	ownerUser := model.User{ID: fixture.UserID, Name: "agent_team_invoke_owner_private", DisplayName: "agent_team_invoke_owner_private"}

	const (
		leadMachineID     = "mach_agent_team_invoke_private_lead"
		leadAgentID       = "agt_agent_team_invoke_private_lead"
		leadAgentMemberID = "mem_agent_team_invoke_private_lead"
		leadAgentName     = "agent-team-invoke-private-lead"
		devMachineID      = "mach_agent_team_invoke_private_dev"
		devAgentID        = "agt_agent_team_invoke_private_dev"
		devAgentMemberID  = "mem_agent_team_invoke_private_dev"
		devAgentName      = "agent-team-invoke-private-dev"
	)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, leadMachineID, leadAgentID, leadAgentMemberID, leadAgentName, now)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, devMachineID, devAgentID, devAgentMemberID, devAgentName, now)

	team, err := teamStore.CreateTeam(ctx, team.CreateTeamParams{
		ID:          "team_agent_team_invoke_owner_private",
		WorkspaceID: fixture.WorkspaceID,
		Handle:      "team-invoke-owner-private",
		DisplayName: "Team Invoke Owner Private",
		InitialMembers: []team.AddTeamMemberParams{
			{
				ID:        "wtm_agent_team_invoke_private_lead",
				ActorKind: team.ActorKindAgent,
				ActorID:   leadAgentMemberID,
				Role:      team.MemberRoleLeader,
				RoleLabel: "Coordinator",
			},
			{
				ID:                "wtm_agent_team_invoke_private_dev",
				ActorKind:         team.ActorKindAgent,
				ActorID:           devAgentMemberID,
				Role:              team.MemberRoleMember,
				RoleLabel:         "Developer",
				RoleOverlayPrompt: "Build delegated owner-private Team work.",
			},
		},
		CreatedByMemberID: fixture.MemberID,
		Now:               now.Add(time.Minute),
	})
	require.NoError(t, err)

	_, initialDelivery, err := store.SendAppMessageForUserWithOptions(
		ctx,
		ownerUser,
		fixture.WorkspaceID,
		fixture.AllChannelID,
		"@agent-team-invoke-private-lead please coordinate this privately",
		nil,
		SendAppMessageOptions{StartAgentThread: true},
	)
	require.NoError(t, err)
	require.NotNil(t, initialDelivery.StartedReplyThread)
	_, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{
		AgentID:     leadAgentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    leadAgentMemberID,
	}, 10)
	require.NoError(t, err)

	delivery, err := store.SendAgentThreadMessage(ctx, agent.AuthenticatedAgent{
		AgentID:     leadAgentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    leadAgentMemberID,
	}, message.SendMessageParams{
		TargetRef:        "#all:" + initialDelivery.MessageID[:8],
		Content:          "@agent-team-invoke-private-dev please build this slice",
		TeamInvokeTeamID: team.ID,
	})
	require.NoError(t, err)

	require.Equal(t, message.AgentDeliveryReasonDirectMention, agentDeliveryReasonsByMember(delivery.Recipients)[devAgentMemberID])
	require.Len(t, delivery.Recipients, 1)
	require.Len(t, delivery.Recipients[0].TeamContexts, 1)
	require.Equal(t, team.ID, delivery.Recipients[0].TeamContexts[0].TeamID)
	require.Equal(t, "Developer", delivery.Recipients[0].TeamContexts[0].RoleLabel)

	devMessages, err := store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{
		AgentID:     devAgentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    devAgentMemberID,
	}, 10)
	require.NoError(t, err)
	require.Len(t, devMessages, 1)
	require.Equal(t, delivery.MessageID, devMessages[0].MessageID)
	require.Len(t, devMessages[0].TeamContexts, 1)
	require.Equal(t, team.ID, devMessages[0].TeamContexts[0].TeamID)
	require.Equal(t, "Build delegated owner-private Team work.", devMessages[0].TeamContexts[0].RoleOverlayPrompt)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	teamStore := team.New(pool)
	now := time.Date(2026, 6, 25, 11, 25, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "team_root_thread_subscribe", now)
	user := model.User{ID: fixture.UserID, Name: "team_root_thread_subscribe", DisplayName: "team_root_thread_subscribe"}

	const (
		leadMachineID        = "mach_team_root_thread_lead"
		leadAgentID          = "agt_team_root_thread_lead"
		leadAgentMemberID    = "mem_team_root_thread_lead"
		leadAgentName        = "team-root-thread-lead"
		builderMachineID     = "mach_team_root_thread_builder"
		builderAgentID       = "agt_team_root_thread_builder"
		builderAgentMemberID = "mem_team_root_thread_builder"
		builderAgentName     = "team-root-thread-builder"
	)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, leadMachineID, leadAgentID, leadAgentMemberID, leadAgentName, now)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, builderMachineID, builderAgentID, builderAgentMemberID, builderAgentName, now)
	upsertScopeSubscriptionForTests(t, pool, fixture.AllChannelID, builderAgentMemberID, "no-wake", now)

	wakeOnMention := false
	team, err := teamStore.CreateTeam(ctx, team.CreateTeamParams{
		ID:          "team_root_thread_subscribe",
		WorkspaceID: fixture.WorkspaceID,
		Handle:      "team-root-thread-subscribe",
		DisplayName: "Team Root Thread Subscribe",
		InitialMembers: []team.AddTeamMemberParams{
			{
				ID:        "wtm_team_root_thread_lead",
				ActorKind: team.ActorKindAgent,
				ActorID:   leadAgentMemberID,
				Role:      team.MemberRoleLeader,
				RoleLabel: "Lead",
			},
			{
				ID:                "wtm_team_root_thread_builder",
				ActorKind:         team.ActorKindAgent,
				ActorID:           builderAgentMemberID,
				Role:              team.MemberRoleMember,
				RoleLabel:         "Builder",
				WakeOnTeamMention: &wakeOnMention,
			},
		},
		CreatedByMemberID: fixture.MemberID,
		Now:               now.Add(time.Minute),
	})
	require.NoError(t, err)

	parent, delivery, err := store.SendAppMessageForUserWithOptions(
		ctx,
		user,
		fixture.WorkspaceID,
		fixture.AllChannelID,
		"@team-root-thread-subscribe please plan this thread",
		nil,
		SendAppMessageOptions{MentionHints: []message.MessageMentionHint{{
			Type:  "team",
			ID:    team.ID,
			Token: "team-root-thread-subscribe",
		}}},
	)
	require.NoError(t, err)
	require.Equal(t, message.AgentDeliveryReasonTeamMention, agentDeliveryReasonsByMember(delivery.WakeRecipients)[leadAgentMemberID])

	replyThread, err := message.OpenReplyThreadForMember(ctx, pool, fixture.MemberID, fixture.WorkspaceID, fixture.AllChannelID, parent.ID)
	require.NoError(t, err)
	_, _, err = store.SendMessageForUser(ctx, user, fixture.WorkspaceID, replyThread.ThreadChannelID, "thread follow-up should subscribe the root team responder", nil)
	require.NoError(t, err)

	var subscribedCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_notification_settings
WHERE scope_id = $1
  AND member_id = $2
  AND state IN ('active', 'quiet', 'mute', 'auto-wake', 'no-wake')
`, replyThread.ThreadChannelID, leadAgentMemberID).Scan(&subscribedCount))
	require.Equal(t, 1, subscribedCount)
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_memberships
WHERE scope_id = $1
  AND member_id = $2
  AND removed_at IS NULL
  AND effective_to_seq IS NULL
`, replyThread.ThreadChannelID, leadAgentMemberID).Scan(&subscribedCount))
	require.Equal(t, 1, subscribedCount)
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_notification_settings
WHERE scope_id = $1
  AND member_id = $2
  AND state IN ('active', 'quiet', 'mute', 'auto-wake', 'no-wake')
`, replyThread.ThreadChannelID, builderAgentMemberID).Scan(&subscribedCount))
	require.Zero(t, subscribedCount)
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_memberships
WHERE scope_id = $1
  AND member_id = $2
  AND removed_at IS NULL
  AND effective_to_seq IS NULL
`, replyThread.ThreadChannelID, builderAgentMemberID).Scan(&subscribedCount))
	require.Zero(t, subscribedCount)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 29, 10, 30, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "agent_thread_parent_mention", now)
	user := model.User{ID: fixture.UserID, Name: "agent_thread_parent_mention", DisplayName: "agent_thread_parent_mention"}

	root, _, err := store.SendMessageForUser(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "thread root before agents join", nil)
	require.NoError(t, err)

	const (
		authorMachineID     = "mach_agent_thread_parent_author"
		authorAgentID       = "agt_agent_thread_parent_author"
		authorAgentMemberID = "mem_agent_thread_parent_author"
		authorAgentName     = "agent-thread-author"
		targetMachineID     = "mach_agent_thread_parent_target"
		targetAgentID       = "agt_agent_thread_parent_target"
		targetAgentMemberID = "mem_agent_thread_parent_target"
		targetAgentName     = "agent-thread-target"
	)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, authorMachineID, authorAgentID, authorAgentMemberID, authorAgentName, now)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, targetMachineID, targetAgentID, targetAgentMemberID, targetAgentName, now)

	delivery, err := store.SendAgentThreadMessage(ctx, agent.AuthenticatedAgent{
		AgentID:     authorAgentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    authorAgentMemberID,
	}, message.SendMessageParams{
		TargetRef: "#all:" + root.ID[:8],
		Content:   "@agent-thread-target please take this thread",
	})
	require.NoError(t, err)
	require.NotNil(t, delivery.AppMessage)
	threadChannelID := delivery.AppMessage.ChannelID
	require.NotEmpty(t, threadChannelID)
	require.NotEqual(t, fixture.AllChannelID, threadChannelID)
	require.Equal(t, message.AgentDeliveryReasonDirectMention, agentDeliveryReasonsByMember(delivery.Recipients)[targetAgentMemberID])
	require.Equal(t, []any{map[string]any{
		"type":  "agent",
		"id":    targetAgentID,
		"token": targetAgentName,
	}}, delivery.AppMessage.Metadata[message.MentionMetadataKey])

	targetMessages, err := store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{
		AgentID:     targetAgentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    targetAgentMemberID,
	}, 10)
	require.NoError(t, err)
	require.Len(t, targetMessages, 1)
	require.Equal(t, delivery.MessageID, targetMessages[0].MessageID)

	page, err := store.ListMessagesForUser(ctx, fixture.UserID, fixture.WorkspaceID, threadChannelID, 20, nil, nil)
	require.NoError(t, err)
	listed := requireListedAppMessage(t, page.Messages, delivery.MessageID)
	require.Equal(t, []message.ResolvedMention{{
		Token:         targetAgentName,
		Type:          "agent",
		ID:            targetAgentID,
		DisplayName:   targetAgentName,
		ResolvedToken: targetAgentName,
	}}, listed.ResolvedMentions)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 20, 11, 45, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "resolved_agent_no_fallback", now)
	peer := seedWorkspaceMemberFixture(t, pool, fixture.WorkspaceID, "resolved_agent_no_fallback_peer", now)
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, fixture.AllChannelID, "all", "", "public", fixture.MemberID, peer.MemberID, now)

	const (
		privateMachineID     = "mach_resolved_no_fallback_private"
		privateAgentID       = "agt_resolved_no_fallback_private"
		privateAgentMemberID = "mem_resolved_no_fallback_private"
		publicMachineID      = "mach_resolved_no_fallback_public"
		publicAgentID        = "agt_resolved_no_fallback_public"
		publicAgentMemberID  = "mem_resolved_no_fallback_public"
		privateAgentName     = "hidden-selected-agent"
	)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, privateMachineID, privateAgentID, privateAgentMemberID, privateAgentName, now)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, publicMachineID, publicAgentID, publicAgentMemberID, "visible-other-agent", now)
	mustExec(t, pool, `
UPDATE members
SET display_name = $2
WHERE id = $1
`, publicAgentMemberID, privateAgentName)

	user := model.User{ID: fixture.UserID, Name: "resolved_agent_no_fallback", DisplayName: "resolved_agent_no_fallback"}
	sentMessage, _, err := store.SendAppMessageForUserWithOptions(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "@hidden-selected-agent stays bound to the selected agent", nil, SendAppMessageOptions{
		MentionHints: []message.MessageMentionHint{{
			Type:  "agent",
			ID:    privateAgentID,
			Token: privateAgentName,
		}},
	})
	require.NoError(t, err)

	page, err := store.ListMessagesForUser(ctx, peer.UserID, fixture.WorkspaceID, fixture.AllChannelID, 20, nil, nil)
	require.NoError(t, err)
	var listed *message.AppMessage
	for i := range page.Messages {
		if page.Messages[i].ID == sentMessage.ID {
			listed = &page.Messages[i]
			break
		}
	}
	require.NotNil(t, listed)
	require.Empty(t, listed.ResolvedMentions)
}

func TestChannelReadStateUsesScopeMembershipAsCursorBaseline(t *testing.T) {

	t.Parallel()

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 13, 9, 20, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "read_state_baseline", now)
	peer := seedWorkspaceMemberFixture(t, pool, fixture.WorkspaceID, "read_state_baseline_peer", now)
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, fixture.AllChannelID, "all", "", "public", fixture.MemberID, peer.MemberID, now)

	peerUser := model.User{ID: peer.UserID, Name: "read_state_baseline_peer", DisplayName: "read_state_baseline_peer"}
	messageOne, _, err := store.SendMessageForUser(ctx, peerUser, fixture.WorkspaceID, fixture.AllChannelID, "one", nil)
	require.NoError(t, err)
	_, _, err = store.SendMessageForUser(ctx, peerUser, fixture.WorkspaceID, fixture.AllChannelID, "two", nil)
	require.NoError(t, err)

	_, err = store.UpdateChannelReadStateByGlobalSeq(ctx, fixture.UserID, fixture.WorkspaceID, fixture.AllChannelID, messageOne.Seq)
	require.NoError(t, err)

	var scopeReadGlobalSeq int64
	require.NoError(t, pool.QueryRow(ctx, `
SELECT last_read_global_seq
FROM scope_memberships
WHERE member_id = $1 AND scope_id = $2
`, fixture.MemberID, fixture.AllChannelID).Scan(&scopeReadGlobalSeq))
	require.Equal(t, messageOne.Seq, scopeReadGlobalSeq)

	unreadCounts, err := store.UnreadCountsForUser(ctx, fixture.UserID, fixture.WorkspaceID)
	require.NoError(t, err)
	require.Equal(t, int64(1), unreadCounts[fixture.AllChannelID])
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 28, 3, 0, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "read_state_conversation_legacy", now)
	peer := seedWorkspaceMemberFixture(t, pool, fixture.WorkspaceID, "read_state_conversation_legacy_peer", now)
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, fixture.AllChannelID, "all", "", "public", fixture.MemberID, peer.MemberID, now)

	_, _, newMessage := seedMixedLegacyAndLocalMessagesForAppPanelTest(t, pool, store, fixture, peer, now)

	conversationStore := message.New(pool, nil, 0, message.Options{})
	beforePage, err := conversationStore.ListConversationsForUser(ctx, fixture.UserID, fixture.WorkspaceID, 20, message.ConversationCursor{})
	require.NoError(t, err)

	var beforeUnread int64 = -1
	for _, item := range beforePage.Items {
		if item.ScopeID == fixture.AllChannelID {
			beforeUnread = item.UnreadCount
			break
		}
	}
	require.Equal(t, int64(0), beforeUnread, "store conversation rows no longer own unread serving")

	_, err = store.UpdateChannelReadStateByGlobalSeq(ctx, fixture.UserID, fixture.WorkspaceID, fixture.AllChannelID, newMessage.Seq)
	require.NoError(t, err)

	afterPage, err := conversationStore.ListConversationsForUser(ctx, fixture.UserID, fixture.WorkspaceID, 20, message.ConversationCursor{})
	require.NoError(t, err)

	var afterUnread int64 = -1
	for _, item := range afterPage.Items {
		if item.ScopeID == fixture.AllChannelID {
			afterUnread = item.UnreadCount
			break
		}
	}
	require.Equal(t, int64(0), afterUnread, "store conversation rows keep unread suppressed after read-state changes")

	peerUser := model.User{ID: peer.UserID, Name: "read_state_conversation_legacy_peer", DisplayName: "read_state_conversation_legacy_peer"}
	_, _, err = store.SendMessageForUser(ctx, peerUser, fixture.WorkspaceID, fixture.AllChannelID, "fresh after inbox open", nil)
	require.NoError(t, err)

	refreshedPage, err := conversationStore.ListConversationsForUser(ctx, fixture.UserID, fixture.WorkspaceID, 20, message.ConversationCursor{})
	require.NoError(t, err)

	var refreshedUnread int64 = -1
	for _, item := range refreshedPage.Items {
		if item.ScopeID == fixture.AllChannelID {
			refreshedUnread = item.UnreadCount
			break
		}
	}
	require.Equal(t, int64(0), refreshedUnread, "newer messages are acked through UnreadRuntime overlay, not store row counts")
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 13, 10, 5, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "owner_context_scope_read", now)
	peer := seedWorkspaceMemberFixture(t, pool, fixture.WorkspaceID, "owner_context_scope_read_peer", now)
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, fixture.AllChannelID, "all", "", "public", fixture.MemberID, peer.MemberID, now)

	peerUser := model.User{ID: peer.UserID, Name: "owner_context_scope_read_peer", DisplayName: "owner_context_scope_read_peer"}
	_, _, err := store.SendMessageForUser(ctx, peerUser, fixture.WorkspaceID, fixture.AllChannelID, "already read one", nil)
	require.NoError(t, err)
	secondMessage, _, err := store.SendMessageForUser(ctx, peerUser, fixture.WorkspaceID, fixture.AllChannelID, "already read two", nil)
	require.NoError(t, err)

	var legacySeq int64
	var legacyGlobalSeq int64
	require.NoError(t, pool.QueryRow(ctx, `
SELECT seq, workspace_global_seq
FROM scope_messages
WHERE id = $1
`, secondMessage.ID).Scan(&legacySeq, &legacyGlobalSeq))
	require.Greater(t, legacySeq, int64(0))
	require.Greater(t, legacyGlobalSeq, int64(0))

	mustExec(t, pool, `
UPDATE scope_memberships
SET last_read_seq = $3, last_read_global_seq = $4, updated_at = $5
WHERE scope_id = $1 AND member_id = $2
`, fixture.AllChannelID, fixture.MemberID, legacySeq, legacyGlobalSeq, now)

	require.NoError(t, store.DismissPendingOwnerContextForUser(ctx, fixture.UserID, fixture.WorkspaceID, fixture.AllChannelID, legacyGlobalSeq))

	var scopeReadSeq int64
	var scopeReadGlobalSeq int64
	require.NoError(t, pool.QueryRow(ctx, `
SELECT last_read_seq, last_read_global_seq
FROM scope_memberships
WHERE member_id = $1 AND scope_id = $2
`, fixture.MemberID, fixture.AllChannelID).Scan(&scopeReadSeq, &scopeReadGlobalSeq))
	require.Equal(t, legacySeq, scopeReadSeq)
	require.Equal(t, legacyGlobalSeq, scopeReadGlobalSeq)

	unreadCounts, err := store.UnreadCountsForUser(ctx, fixture.UserID, fixture.WorkspaceID)
	require.NoError(t, err)
	require.NotContains(t, unreadCounts, fixture.AllChannelID)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 20, 15, 0, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "unread_effective_from", now)
	peer := seedWorkspaceMemberFixture(t, pool, fixture.WorkspaceID, "unread_effective_from_peer", now)
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, fixture.AllChannelID, "all", "", "public", fixture.MemberID, peer.MemberID, now)

	peerUser := model.User{ID: peer.UserID, Name: "unread_effective_from_peer", DisplayName: "unread_effective_from_peer"}
	beforeJoin, _, err := store.SendMessageForUser(ctx, peerUser, fixture.WorkspaceID, fixture.AllChannelID, "before effective membership", nil)
	require.NoError(t, err)
	afterJoin, _, err := store.SendMessageForUser(ctx, peerUser, fixture.WorkspaceID, fixture.AllChannelID, "after effective membership", nil)
	require.NoError(t, err)

	require.Greater(t, afterJoin.Seq, beforeJoin.Seq)
	mustExec(t, pool, `
UPDATE scope_memberships
SET effective_from_seq = $3,
    last_read_seq = 0,
    last_read_global_seq = 0,
    updated_at = $4
WHERE scope_id = $1 AND member_id = $2
`, fixture.AllChannelID, fixture.MemberID, afterJoin.Seq, now)

	unreadCounts, err := store.UnreadCountsForUser(ctx, fixture.UserID, fixture.WorkspaceID)
	require.NoError(t, err)
	require.Equal(t, int64(1), unreadCounts[fixture.AllChannelID])
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 13, 10, 40, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "mark_unread_scope_read", now)
	peer := seedWorkspaceMemberFixture(t, pool, fixture.WorkspaceID, "mark_unread_scope_read_peer", now)
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, fixture.AllChannelID, "all", "", "public", fixture.MemberID, peer.MemberID, now)

	peerUser := model.User{ID: peer.UserID, Name: "mark_unread_scope_read_peer", DisplayName: "mark_unread_scope_read_peer"}
	firstMessage, _, err := store.SendMessageForUser(ctx, peerUser, fixture.WorkspaceID, fixture.AllChannelID, "already read one", nil)
	require.NoError(t, err)
	secondMessage, _, err := store.SendMessageForUser(ctx, peerUser, fixture.WorkspaceID, fixture.AllChannelID, "already read two", nil)
	require.NoError(t, err)

	var firstSeq int64
	var firstGlobalSeq int64
	require.NoError(t, pool.QueryRow(ctx, `
SELECT seq, workspace_global_seq
FROM scope_messages
WHERE id = $1
`, firstMessage.ID).Scan(&firstSeq, &firstGlobalSeq))
	var legacySeq int64
	var legacyGlobalSeq int64
	require.NoError(t, pool.QueryRow(ctx, `
SELECT seq, workspace_global_seq
FROM scope_messages
WHERE id = $1
`, secondMessage.ID).Scan(&legacySeq, &legacyGlobalSeq))
	require.Greater(t, legacySeq, firstSeq)
	require.Greater(t, legacyGlobalSeq, firstGlobalSeq)

	mustExec(t, pool, `
UPDATE scope_memberships
SET last_read_seq = $3, last_read_global_seq = $4, updated_at = $5
WHERE scope_id = $1 AND member_id = $2
`, fixture.AllChannelID, fixture.MemberID, legacySeq, legacyGlobalSeq, now)
	unreadCount, err := store.MarkChannelUnreadForUser(ctx, fixture.UserID, fixture.WorkspaceID, fixture.AllChannelID)
	require.NoError(t, err)
	require.Equal(t, int64(1), unreadCount)

	var scopeReadSeq int64
	var scopeReadGlobalSeq int64
	require.NoError(t, pool.QueryRow(ctx, `
SELECT last_read_seq, last_read_global_seq
FROM scope_memberships
WHERE member_id = $1 AND scope_id = $2
`, fixture.MemberID, fixture.AllChannelID).Scan(&scopeReadSeq, &scopeReadGlobalSeq))
	require.Equal(t, legacySeq, scopeReadSeq)
	require.Equal(t, legacyGlobalSeq, scopeReadGlobalSeq)

	var manualUnreadSeq int64
	var manualUnread bool
	require.NoError(t, pool.QueryRow(ctx, `
SELECT manual_unread_seq, manual_unread_at IS NOT NULL
FROM conversation_member_states
WHERE member_id = $1 AND scope_id = $2
`, fixture.MemberID, fixture.AllChannelID).Scan(&manualUnreadSeq, &manualUnread))
	require.True(t, manualUnread)
	require.Equal(t, legacySeq, manualUnreadSeq)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 20, 15, 20, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "mark_unread_human_visible", now)
	peer := seedWorkspaceMemberFixture(t, pool, fixture.WorkspaceID, "mark_unread_human_visible_peer", now)
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, fixture.AllChannelID, "all", "", "public", fixture.MemberID, peer.MemberID, now)

	peerUser := model.User{ID: peer.UserID, Name: "mark_unread_human_visible_peer", DisplayName: "mark_unread_human_visible_peer"}
	firstVisible, _, err := store.SendMessageForUser(ctx, peerUser, fixture.WorkspaceID, fixture.AllChannelID, "first visible", nil)
	require.NoError(t, err)
	secondVisible, _, err := store.SendMessageForUser(ctx, peerUser, fixture.WorkspaceID, fixture.AllChannelID, "second visible", nil)
	require.NoError(t, err)
	hidden := seedLegacyTaskDispatchMessageForTest(t, ctx, pool, fixture.WorkspaceID, fixture.AllChannelID, peer.MemberID, "legacy hidden task dispatch")
	require.Greater(t, secondVisible.Seq, firstVisible.Seq)
	require.Greater(t, hidden.Seq, secondVisible.Seq)

	mustExec(t, pool, `
UPDATE scope_memberships
SET last_read_seq = $3,
    last_read_global_seq = $3,
    updated_at = $4
WHERE scope_id = $1 AND member_id = $2
`, fixture.AllChannelID, fixture.MemberID, hidden.Seq, now)

	unreadCount, err := store.MarkChannelUnreadForUser(ctx, fixture.UserID, fixture.WorkspaceID, fixture.AllChannelID)
	require.NoError(t, err)
	require.Equal(t, int64(1), unreadCount)

	var scopeReadSeq int64
	var scopeReadGlobalSeq int64
	require.NoError(t, pool.QueryRow(ctx, `
SELECT last_read_seq, last_read_global_seq
FROM scope_memberships
WHERE member_id = $1 AND scope_id = $2
`, fixture.MemberID, fixture.AllChannelID).Scan(&scopeReadSeq, &scopeReadGlobalSeq))
	require.Equal(t, hidden.Seq, scopeReadSeq)
	require.Equal(t, hidden.Seq, scopeReadGlobalSeq)

	var manualUnreadSeq int64
	var manualUnread bool
	require.NoError(t, pool.QueryRow(ctx, `
SELECT manual_unread_seq, manual_unread_at IS NOT NULL
FROM conversation_member_states
WHERE member_id = $1 AND scope_id = $2
`, fixture.MemberID, fixture.AllChannelID).Scan(&manualUnreadSeq, &manualUnread))
	require.True(t, manualUnread)
	require.Equal(t, secondVisible.Seq, manualUnreadSeq)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 15, 11, 0, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "mixed_legacy_local_history", now)
	peer := seedWorkspaceMemberFixture(t, pool, fixture.WorkspaceID, "mixed_legacy_local_history_peer", now)
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, fixture.AllChannelID, "all", "", "public", fixture.MemberID, peer.MemberID, now)

	legacyMessageID, legacySeq, newMessage := seedMixedLegacyAndLocalMessagesForAppPanelTest(t, pool, store, fixture, peer, now)

	page, err := store.ListMessagesForUser(ctx, fixture.UserID, fixture.WorkspaceID, fixture.AllChannelID, 20, nil, nil)
	require.NoError(t, err)
	require.Len(t, page.Messages, 2)
	require.Equal(t, []string{legacyMessageID, newMessage.ID}, []string{page.Messages[0].ID, page.Messages[1].ID})
	require.Equal(t, []int64{legacySeq, newMessage.Seq}, []int64{page.Messages[0].Seq, page.Messages[1].Seq})

	after := legacySeq
	newerPage, err := store.ListMessagesForUser(ctx, fixture.UserID, fixture.WorkspaceID, fixture.AllChannelID, 20, nil, &after)
	require.NoError(t, err)
	require.Len(t, newerPage.Messages, 1)
	require.Equal(t, newMessage.ID, newerPage.Messages[0].ID)
	require.Equal(t, newMessage.Seq, newerPage.Messages[0].Seq)

	aroundPage, err := store.ListMessagesAroundForUser(ctx, fixture.UserID, fixture.WorkspaceID, fixture.AllChannelID, newMessage.ID, 10)
	require.NoError(t, err)
	require.Len(t, aroundPage.Messages, 2)
	require.Equal(t, []string{legacyMessageID, newMessage.ID}, []string{aroundPage.Messages[0].ID, aroundPage.Messages[1].ID})
	require.Equal(t, []int64{legacySeq, newMessage.Seq}, []int64{aroundPage.Messages[0].Seq, aroundPage.Messages[1].Seq})
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 15, 11, 20, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "mixed_legacy_local_read_state", now)
	peer := seedWorkspaceMemberFixture(t, pool, fixture.WorkspaceID, "mixed_legacy_local_read_state_peer", now)
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, fixture.AllChannelID, "all", "", "public", fixture.MemberID, peer.MemberID, now)

	_, legacySeq, newMessage := seedMixedLegacyAndLocalMessagesForAppPanelTest(t, pool, store, fixture, peer, now)

	_, err := store.UpdateChannelReadStateByGlobalSeq(ctx, fixture.UserID, fixture.WorkspaceID, fixture.AllChannelID, legacySeq)
	require.NoError(t, err)

	var scopeReadSeq int64
	var scopeReadGlobalSeq int64
	require.NoError(t, pool.QueryRow(ctx, `
SELECT last_read_seq, last_read_global_seq
FROM scope_memberships
WHERE member_id = $1 AND scope_id = $2
`, fixture.MemberID, fixture.AllChannelID).Scan(&scopeReadSeq, &scopeReadGlobalSeq))
	require.Equal(t, legacySeq, scopeReadSeq)
	require.Equal(t, legacySeq, scopeReadGlobalSeq)

	unreadCounts, err := store.UnreadCountsForUser(ctx, fixture.UserID, fixture.WorkspaceID)
	require.NoError(t, err)
	require.Equal(t, int64(1), unreadCounts[fixture.AllChannelID])

	_, err = store.MarkChannelReadAllForUser(ctx, fixture.UserID, fixture.WorkspaceID, fixture.AllChannelID)
	require.NoError(t, err)
	require.NoError(t, pool.QueryRow(ctx, `
SELECT last_read_seq, last_read_global_seq
FROM scope_memberships
WHERE member_id = $1 AND scope_id = $2
`, fixture.MemberID, fixture.AllChannelID).Scan(&scopeReadSeq, &scopeReadGlobalSeq))
	require.Equal(t, newMessage.Seq, scopeReadSeq)
	require.Equal(t, newMessage.Seq, scopeReadGlobalSeq)

	peerUser := model.User{ID: peer.UserID, Name: "mixed_legacy_local_read_state_peer", DisplayName: "mixed_legacy_local_read_state_peer"}
	thirdMessage, _, err := store.SendMessageForUser(ctx, peerUser, fixture.WorkspaceID, fixture.AllChannelID, "fresh local-seq message", nil)
	require.NoError(t, err)
	require.Equal(t, newMessage.Seq+1, thirdMessage.Seq)

	unreadCounts, err = store.UnreadCountsForUser(ctx, fixture.UserID, fixture.WorkspaceID)
	require.NoError(t, err)
	require.Equal(t, int64(1), unreadCounts[fixture.AllChannelID])

	unreadCount, err := store.MarkChannelUnreadForUser(ctx, fixture.UserID, fixture.WorkspaceID, fixture.AllChannelID)
	require.NoError(t, err)
	require.Equal(t, int64(1), unreadCount)
	require.NoError(t, pool.QueryRow(ctx, `
SELECT last_read_seq, last_read_global_seq
FROM scope_memberships
WHERE member_id = $1 AND scope_id = $2
`, fixture.MemberID, fixture.AllChannelID).Scan(&scopeReadSeq, &scopeReadGlobalSeq))
	require.Equal(t, newMessage.Seq, scopeReadSeq)
	require.Equal(t, newMessage.Seq, scopeReadGlobalSeq)

	var manualUnreadSeq int64
	var manualUnread bool
	require.NoError(t, pool.QueryRow(ctx, `
SELECT manual_unread_seq, manual_unread_at IS NOT NULL
FROM conversation_member_states
WHERE member_id = $1 AND scope_id = $2
`, fixture.MemberID, fixture.AllChannelID).Scan(&manualUnreadSeq, &manualUnread))
	require.True(t, manualUnread)
	require.Equal(t, thirdMessage.Seq, manualUnreadSeq)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 15, 11, 40, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "mixed_legacy_local_dismiss", now)
	peer := seedWorkspaceMemberFixture(t, pool, fixture.WorkspaceID, "mixed_legacy_local_dismiss_peer", now)
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, fixture.AllChannelID, "all", "", "public", fixture.MemberID, peer.MemberID, now)

	_, _, newMessage := seedMixedLegacyAndLocalMessagesForAppPanelTest(t, pool, store, fixture, peer, now)

	err := store.DismissPendingOwnerContextForUser(ctx, fixture.UserID, fixture.WorkspaceID, fixture.AllChannelID, newMessage.Seq+48)
	require.Error(t, err)
	require.ErrorContains(t, err, "invalid dismissedThroughGlobalSeq")

	require.NoError(t, store.DismissPendingOwnerContextForUser(ctx, fixture.UserID, fixture.WorkspaceID, fixture.AllChannelID, newMessage.Seq))
	var dismissedThroughSeq int64
	require.NoError(t, pool.QueryRow(ctx, `
SELECT owner_context_dismissed_through_global_seq
FROM scope_memberships
WHERE member_id = $1 AND scope_id = $2
`, fixture.MemberID, fixture.AllChannelID).Scan(&dismissedThroughSeq))
	require.Equal(t, newMessage.Seq, dismissedThroughSeq)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 12, 19, 35, 0, 0, time.UTC)

	fixture := seedAppAdminFixture(t, pool, "channel_public_auto_wake_scope", now)
	const (
		autoMachineID       = "mach_chan_pub_auto_scope"
		autoAgentID         = "agt_chan_pub_auto_scope"
		autoAgentMemberID   = "mem_chan_pub_auto_scope"
		noWakeMachineID     = "mach_chan_pub_no_wake_scope"
		noWakeAgentID       = "agt_chan_pub_no_wake_scope"
		noWakeAgentMemberID = "mem_chan_pub_no_wake_scope"
		privateMachineID    = "mach_chan_pub_private_peer_scope"
		privateAgentID      = "agt_chan_pub_private_peer_scope"
		privateAgentMember  = "mem_chan_pub_private_peer_scope"
	)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, autoMachineID, autoAgentID, autoAgentMemberID, "channel-public-auto-wake-scope", now)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, noWakeMachineID, noWakeAgentID, noWakeAgentMemberID, "channel-public-no-wake-scope", now)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, privateMachineID, privateAgentID, privateAgentMember, "channel-public-private-peer-scope", now)
	upsertScopeSubscriptionForTests(t, pool, fixture.AllChannelID, noWakeAgentMemberID, "no-wake", now)

	_, delivery, err := store.SendMessageForUser(
		ctx,
		model.User{ID: fixture.UserID, Name: "channel_public_auto_wake_scope", DisplayName: "channel_public_auto_wake_scope"},
		fixture.WorkspaceID,
		fixture.AllChannelID,
		"ordinary channel default delivery",
		nil,
	)
	require.NoError(t, err)
	require.Equal(t, map[string]string{
		autoAgentMemberID:   message.AgentDeliveryReasonSpaceDefault,
		noWakeAgentMemberID: message.AgentDeliveryReasonSpaceDefault,
	}, agentDeliveryReasonsByMember(delivery.Recipients))
	require.Equal(t, map[string]string{
		autoAgentMemberID: message.AgentDeliveryReasonSpaceDefault,
	}, agentDeliveryReasonsByMember(delivery.WakeRecipients))

	requireScopeMessageAndSingleFanout(t, pool, delivery.MessageID, fixture.WorkspaceID, fixture.AllChannelID)

	autoMessages, err := store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: autoAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: autoAgentMemberID}, 10)
	require.NoError(t, err)
	require.Len(t, autoMessages, 1)
	require.Equal(t, delivery.MessageID, autoMessages[0].MessageID)
	noWakeMessages, err := store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: noWakeAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: noWakeAgentMemberID}, 10)
	require.NoError(t, err)
	require.Len(t, noWakeMessages, 1)
	require.Equal(t, delivery.MessageID, noWakeMessages[0].MessageID)
	privateMessages, err := store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: privateAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: privateAgentMember}, 10)
	require.NoError(t, err)
	require.Empty(t, privateMessages)
}

func seedMixedLegacyAndLocalMessagesForAppPanelTest(t *testing.T, pool coredb.QueryExecer, store *Store, fixture appAdminFixture, peer appAdminFixture, now time.Time) (string, int64, message.AppMessage) {
	t.Helper()

	legacyMessageID := "msg_" + fixture.WorkspaceID + "_legacy"
	legacyCreatedAt := now.Add(time.Minute)
	mustExec(t, pool, `
INSERT INTO scope_messages (
  id, workspace_id, scope_id, seq, workspace_global_seq, author_member_id,
  author_type, message_type, content, metadata, created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, 'human', '', 'legacy mixed history row', '{}', $7, $7)
`, legacyMessageID, fixture.WorkspaceID, fixture.AllChannelID, int64(1), int64(100), peer.MemberID, legacyCreatedAt)
	mustExec(t, pool, `
UPDATE scopes
SET latest_seq = 1,
    latest_global_seq = 100,
    latest_message_at = $2,
    visible_message_count = 1,
    visible_last_message_at = $2,
    updated_at = $2
WHERE id = $1
`, fixture.AllChannelID, legacyCreatedAt)

	peerUser := model.User{ID: peer.UserID, Name: "mixed_legacy_peer", DisplayName: "mixed_legacy_peer"}
	newMessage, _, err := store.SendMessageForUser(context.Background(), peerUser, fixture.WorkspaceID, fixture.AllChannelID, "fresh local-seq message", nil)
	require.NoError(t, err)
	require.Equal(t, int64(2), newMessage.Seq)

	var storedLegacySeq int64
	require.NoError(t, pool.QueryRow(context.Background(), `
SELECT seq
FROM scope_messages
WHERE id = $1
`, legacyMessageID).Scan(&storedLegacySeq))
	require.Equal(t, int64(1), storedLegacySeq)

	return legacyMessageID, storedLegacySeq, newMessage
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 14, 9, 0, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "attachment_agent_entry", now)
	const (
		machineID      = "mac_attachment_agent_entry"
		agentID        = "agt_attachment_agent_entry"
		agentMemberID  = "mem_agent_attachment_entry"
		attachmentID   = "att_attachment_agent_entry"
		messageID      = "msg_attachment_agent_entry"
		attachmentBody = "agent-visible payload"
	)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, machineID, agentID, agentMemberID, "attachment-agent-entry", now)
	mustExec(t, pool, `
UPDATE scope_memberships
SET visibility = 'public', owner_member_id = NULL, updated_at = $4
WHERE scope_id = $2 AND member_id = $3
`, fixture.WorkspaceID, fixture.AllChannelID, agentMemberID, now)
	mustExec(t, pool, `
INSERT INTO scope_messages (
  id, workspace_id, scope_id, seq, workspace_global_seq, author_member_id,
  author_type, message_type, content, metadata, created_at, updated_at
) VALUES ($1, $2, $3, 1, 1, $4, 'human', 'message', 'with attachment', '{}', $5, $5)
`, messageID, fixture.WorkspaceID, fixture.AllChannelID, fixture.MemberID, now)
	mustExec(t, pool, `
INSERT INTO attachments (
  id, workspace_id, scope_id, uploaded_by_member_id, message_id,
  filename, content_type, size_bytes, payload, created_at
) VALUES ($1, $2, $3, $4, $5, 'screenshot.png', 'image/png', $6, $7, $8)
`, attachmentID, fixture.WorkspaceID, fixture.AllChannelID, fixture.MemberID, messageID, len(attachmentBody), []byte(attachmentBody), now)

	require.NoError(t, store.AttachmentOptions().AuthorizeAttachmentMessageReadForMember(ctx, pool, agentMemberID, fixture.WorkspaceID, messageID))

	attachmentStore := store.attachmentStore()
	loaded, err := attachmentStore.LoadAttachmentForMember(ctx, agentMemberID, fixture.WorkspaceID, attachmentID)
	require.NoError(t, err)
	require.Equal(t, []byte(attachmentBody), loaded.Blob)

	legacyLoaded, err := attachmentStore.LoadAttachmentForMember(ctx, agentMemberID, "", attachmentID)
	require.NoError(t, err)
	require.Equal(t, []byte(attachmentBody), legacyLoaded.Blob)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 25, 1, 30, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "attachment_parent_thread_read", now)
	viewer := seedWorkspaceMemberFixture(t, pool, fixture.WorkspaceID, "attachment_parent_thread_viewer", now)
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, fixture.AllChannelID, "all", "", "public", fixture.MemberID, viewer.MemberID, now)

	ownerUser := model.User{ID: fixture.UserID, Name: "attachment_parent_thread_read", DisplayName: "attachment_parent_thread_read"}
	parent, _, err := store.SendMessageForUser(ctx, ownerUser, fixture.WorkspaceID, fixture.AllChannelID, "parent for reply-thread sidecar", nil)
	require.NoError(t, err)
	replyThread, err := message.OpenReplyThreadForMember(ctx, pool, fixture.MemberID, fixture.WorkspaceID, fixture.AllChannelID, parent.ID)
	require.NoError(t, err)
	reply, _, err := store.SendMessageForUser(ctx, ownerUser, fixture.WorkspaceID, replyThread.ThreadChannelID, "reply thread message with attachment", nil)
	require.NoError(t, err)

	const (
		attachmentID   = "att_attachment_parent_thread_read"
		attachmentBody = "parent readable reply-thread attachment"
	)
	mustExec(t, pool, `
INSERT INTO attachments (
  id, workspace_id, scope_id, uploaded_by_member_id, message_id,
  filename, content_type, size_bytes, payload, created_at
) VALUES ($1, $2, $3, $4, $5, 'parent-thread.txt', 'text/plain', $6, $7, $8)
`, attachmentID, fixture.WorkspaceID, replyThread.ThreadChannelID, fixture.MemberID, reply.ID, len(attachmentBody), []byte(attachmentBody), now.Add(time.Second))

	var directThreadMemberships int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_memberships
WHERE scope_id = $1
  AND member_id = $2
  AND removed_at IS NULL
`, replyThread.ThreadChannelID, viewer.MemberID).Scan(&directThreadMemberships))
	require.Zero(t, directThreadMemberships)

	loaded, err := store.attachmentStore().LoadAttachmentForUser(ctx, viewer.UserID, fixture.WorkspaceID, attachmentID)
	require.NoError(t, err)
	require.Equal(t, []byte(attachmentBody), loaded.Blob)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 24, 23, 10, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "attachment_sidecar_read", now)
	stranger := seedWorkspaceMemberFixture(t, pool, fixture.WorkspaceID, "attachment_sidecar_stranger", now)
	require.NoError(t, message.EnsureScopeMembership(ctx, pool, fixture.AllChannelID, stranger.MemberID, "public", "", "no-wake", 0, 0, now))

	const (
		machineID      = "mac_attachment_sidecar_private"
		agentID        = "agt_attachment_sidecar_private"
		agentMemberID  = "mem_attachment_sidecar_private"
		attachmentID   = "att_attachment_sidecar_private"
		messageID      = "msg_attachment_sidecar_private"
		attachmentBody = "hidden attachment payload"
	)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, machineID, agentID, agentMemberID, "attachment-sidecar-private", now)
	mustExec(t, pool, `
INSERT INTO scope_messages (
  id, workspace_id, scope_id, seq, workspace_global_seq, author_member_id,
  author_type, message_type, content, metadata, created_at, updated_at
) VALUES ($1, $2, $3, 1, 1, $4, 'human', 'message', 'stranger hidden attachment', '{}', $5, $5)
`, messageID, fixture.WorkspaceID, fixture.AllChannelID, stranger.MemberID, now)
	mustExec(t, pool, `
INSERT INTO attachments (
  id, workspace_id, scope_id, uploaded_by_member_id, message_id,
  filename, content_type, size_bytes, payload, created_at
) VALUES ($1, $2, $3, $4, $5, 'hidden.txt', 'text/plain', $6, $7, $8)
`, attachmentID, fixture.WorkspaceID, fixture.AllChannelID, stranger.MemberID, messageID, len(attachmentBody), []byte(attachmentBody), now)

	_, err := store.attachmentStore().LoadAttachmentForMember(ctx, agentMemberID, fixture.WorkspaceID, attachmentID)
	require.ErrorIs(t, err, ErrForbidden)

	ownerVisibleMessageID := "msg_attachment_sidecar_owner"
	ownerVisibleAttachmentID := "att_attachment_sidecar_owner"
	mustExec(t, pool, `
INSERT INTO scope_messages (
  id, workspace_id, scope_id, seq, workspace_global_seq, author_member_id,
  author_type, message_type, content, metadata, created_at, updated_at
) VALUES ($1, $2, $3, 2, 2, $4, 'human', 'message', 'owner visible attachment', '{}', $5, $5)
`, ownerVisibleMessageID, fixture.WorkspaceID, fixture.AllChannelID, fixture.MemberID, now.Add(time.Second))
	mustExec(t, pool, `
INSERT INTO attachments (
  id, workspace_id, scope_id, uploaded_by_member_id, message_id,
  filename, content_type, size_bytes, payload, created_at
) VALUES ($1, $2, $3, $4, $5, 'owner.txt', 'text/plain', $6, $7, $8)
`, ownerVisibleAttachmentID, fixture.WorkspaceID, fixture.AllChannelID, fixture.MemberID, ownerVisibleMessageID, len(attachmentBody), []byte(attachmentBody), now.Add(time.Second))

	loaded, err := store.attachmentStore().LoadAttachmentForMember(ctx, agentMemberID, fixture.WorkspaceID, ownerVisibleAttachmentID)
	require.NoError(t, err)
	require.Equal(t, []byte(attachmentBody), loaded.Blob)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 14, 13, 45, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "attachment_private_agent_upload", now)
	const (
		machineID       = "mac_attachment_private_upload"
		agentID         = "agt_attachment_private_upload"
		agentMemberID   = "mem_attachment_private_upload"
		attachmentBytes = "private agent upload payload"
	)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, machineID, agentID, agentMemberID, "attachment-private-upload", now)

	created, err := store.attachmentStore().UploadAttachmentsForMember(ctx, agentMemberID, fixture.WorkspaceID, fixture.AllChannelID, []attachment.CreateAttachmentParams{{
		Filename:    "private-agent.txt",
		ContentType: "text/plain",
		Blob:        []byte(attachmentBytes),
	}})
	require.NoError(t, err)
	require.Len(t, created, 1)
	require.NotEmpty(t, created[0].ID)

	var scopeID string
	var uploadedByMemberID string
	var publicMemberships int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT scope_id, uploaded_by_member_id
FROM attachments
WHERE id = $1
`, created[0].ID).Scan(&scopeID, &uploadedByMemberID))
	require.Equal(t, fixture.AllChannelID, scopeID)
	require.Equal(t, agentMemberID, uploadedByMemberID)
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_memberships
WHERE scope_id = $1
  AND member_id = $2
  AND visibility = 'public'
  AND removed_at IS NULL
`, fixture.AllChannelID, agentMemberID).Scan(&publicMemberships))
	require.Equal(t, 0, publicMemberships)

	// Channel archive closes the Human owner membership. A private Agent
	// target owned by that Human must not remain writable through a stale
	// private roster row.
	mustExec(t, pool, `
UPDATE scope_memberships
SET archived_at = $3, updated_at = $3
WHERE scope_id = $1 AND member_id = $2
`, fixture.AllChannelID, fixture.MemberID, now.Add(time.Minute))
	_, err = store.ResolveWritableAgentTargetScopeID(ctx, agent.AuthenticatedAgent{
		AgentID: agentID, WorkspaceID: fixture.WorkspaceID, MemberID: agentMemberID,
	}, "#all")
	require.ErrorIs(t, err, ErrForbidden)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 22, 3, 15, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "attachment_private_agent_thread_upload", now)
	ownerUser := model.User{ID: fixture.UserID, Name: "attachment_private_agent_thread_upload", DisplayName: "attachment_private_agent_thread_upload"}
	parentMessage, _, err := store.SendMessageForUser(ctx, ownerUser, fixture.WorkspaceID, fixture.AllChannelID, "root for private agent reply-thread upload", nil)
	require.NoError(t, err)
	replyThread, err := message.OpenReplyThreadForMember(ctx, pool, fixture.MemberID, fixture.WorkspaceID, fixture.AllChannelID, parentMessage.ID)
	require.NoError(t, err)
	threadChannelID := replyThread.ThreadChannelID
	threadShortID := parentMessage.ID[:8]

	const (
		allowedMachineID     = "mac_attachment_private_thread_allowed"
		allowedAgentID       = "agt_attachment_private_thread_allowed"
		allowedAgentMemberID = "mem_attachment_private_thread_allowed"
		deniedMachineID      = "mac_attachment_private_thread_denied"
		deniedAgentID        = "agt_attachment_private_thread_denied"
		deniedAgentMemberID  = "mem_attachment_private_thread_denied"
	)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, allowedMachineID, allowedAgentID, allowedAgentMemberID, "attachment-private-thread-allowed", now)
	otherChannelID := "chn_attachment_private_thread_other"
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, otherChannelID, "other-private-thread-upload", "", "private", fixture.MemberID, fixture.MemberID, now)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, otherChannelID, deniedMachineID, deniedAgentID, deniedAgentMemberID, "attachment-private-thread-denied", now)

	resolvedThreadScopeID, err := store.ResolveWritableAgentTargetScopeID(ctx, agent.AuthenticatedAgent{
		AgentID:     allowedAgentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    allowedAgentMemberID,
	}, "#all:"+threadShortID)
	require.NoError(t, err)
	require.Equal(t, threadChannelID, resolvedThreadScopeID)
	resolvedTarget, err := store.ResolveWritableAgentMessageTarget(ctx, agent.AuthenticatedAgent{
		AgentID:     allowedAgentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    allowedAgentMemberID,
	}, "#all:"+threadShortID)
	require.NoError(t, err)
	require.Equal(t, access.ScopeTargetOperationMessageSend, resolvedTarget.Operation)
	require.Equal(t, access.ScopeTargetKindThread, resolvedTarget.Requested.Kind)
	require.Equal(t, threadChannelID, resolvedTarget.StorageScope.ScopeID)
	require.Equal(t, threadChannelID, resolvedTarget.DeliveryScope.ScopeID)
	require.Equal(t, fixture.AllChannelID, resolvedTarget.ParentScope.ScopeID)
	require.Equal(t, access.ScopePermissionBasisOwnerPrivateMembership, resolvedTarget.PermissionBasis[0].Kind)

	rootCreated, err := store.attachmentStore().UploadAttachmentsForMember(ctx, allowedAgentMemberID, fixture.WorkspaceID, fixture.AllChannelID, []attachment.CreateAttachmentParams{{
		Filename:    "private-agent-root.txt",
		ContentType: "text/plain",
		Blob:        []byte("root upload payload"),
	}})
	require.NoError(t, err)
	require.Len(t, rootCreated, 1)

	threadCreated, err := store.attachmentStore().UploadAttachmentsForMember(ctx, allowedAgentMemberID, fixture.WorkspaceID, threadChannelID, []attachment.CreateAttachmentParams{{
		Filename:    "private-agent-thread.txt",
		ContentType: "text/plain",
		Blob:        []byte("thread upload payload"),
	}})
	require.NoError(t, err)
	require.Len(t, threadCreated, 1)

	attachmentsByID := map[string]string{}
	rows, err := pool.Query(ctx, `
SELECT id, scope_id
FROM attachments
WHERE id IN ($1, $2)
`, rootCreated[0].ID, threadCreated[0].ID)
	require.NoError(t, err)
	defer rows.Close()
	for rows.Next() {
		var attachmentID string
		var scopeID string
		require.NoError(t, rows.Scan(&attachmentID, &scopeID))
		attachmentsByID[attachmentID] = scopeID
	}
	require.NoError(t, rows.Err())
	require.Equal(t, fixture.AllChannelID, attachmentsByID[rootCreated[0].ID])
	require.Equal(t, threadChannelID, attachmentsByID[threadCreated[0].ID])

	threadDelivery, err := store.SendAgentThreadMessage(ctx, agent.AuthenticatedAgent{
		AgentID:     allowedAgentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    allowedAgentMemberID,
	}, message.SendMessageParams{
		TargetRef:     "#all:" + threadShortID,
		Content:       "claim private agent reply-thread attachment",
		AttachmentIDs: []string{threadCreated[0].ID},
	})
	require.NoError(t, err)
	require.NotEmpty(t, threadDelivery.MessageID)

	loadedThreadAttachment, err := store.attachmentStore().LoadAttachmentForMember(ctx, allowedAgentMemberID, fixture.WorkspaceID, threadCreated[0].ID)
	require.NoError(t, err)
	require.Equal(t, []byte("thread upload payload"), loadedThreadAttachment.Blob)

	mustExec(t, pool, `
UPDATE scope_memberships
SET removed_at = $3, updated_at = $3
WHERE scope_id = $1 AND member_id = $2
`, threadChannelID, allowedAgentMemberID, now.Add(time.Minute))
	_, err = store.attachmentStore().LoadAttachmentForMember(ctx, allowedAgentMemberID, fixture.WorkspaceID, threadCreated[0].ID)
	require.ErrorIs(t, err, ErrForbidden)

	_, err = store.attachmentStore().LoadAttachmentForMember(ctx, deniedAgentMemberID, fixture.WorkspaceID, threadCreated[0].ID)
	require.ErrorIs(t, err, ErrForbidden)

	_, err = store.attachmentStore().UploadAttachmentsForMember(ctx, deniedAgentMemberID, fixture.WorkspaceID, threadChannelID, []attachment.CreateAttachmentParams{{
		Filename:    "denied-thread.txt",
		ContentType: "text/plain",
		Blob:        []byte("denied payload"),
	}})
	require.ErrorIs(t, err, ErrForbidden)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 7, 12, 0, 0, 0, time.UTC)

	fixture := seedAppAdminFixture(t, pool, "mention_default_delivery", now)
	mentionedHuman := seedWorkspaceMemberFixture(t, pool, fixture.WorkspaceID, "mentioned_human", now)
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, fixture.AllChannelID, "all", "", "public", fixture.MemberID, mentionedHuman.MemberID, now)

	const (
		machineID            = "mach_mention_default_delivery"
		mentionedAgentID     = "agt_mention_default_delivery"
		mentionedAgentMember = "mem_agent_mention_default_delivery"
		ambientAgentID       = "agt_ambient_default_delivery"
		ambientAgentMember   = "mem_ambient_default_delivery"
	)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, machineID, mentionedAgentID, mentionedAgentMember, "mentioned-agent", now)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, machineID, ambientAgentID, ambientAgentMember, "ambient-agent", now)

	_, delivery, err := store.SendMessageForUser(
		ctx,
		model.User{ID: fixture.UserID, Name: "mention_default_delivery", DisplayName: "mention_default_delivery"},
		fixture.WorkspaceID,
		fixture.AllChannelID,
		"@mentioned-agent please coordinate with @mentioned_human",
		nil,
	)
	require.NoError(t, err)

	require.Equal(t, map[string]string{
		mentionedAgentMember: message.AgentDeliveryReasonDirectMention,
		ambientAgentMember:   message.AgentDeliveryReasonSpaceDefault,
	}, agentDeliveryReasonsByMember(delivery.Recipients))
	require.Equal(t, map[string]string{
		mentionedAgentMember: message.AgentDeliveryReasonDirectMention,
		ambientAgentMember:   message.AgentDeliveryReasonSpaceDefault,
	}, agentDeliveryReasonsByMember(delivery.WakeRecipients))

	requireScopeMessageAndSingleFanout(t, pool, delivery.MessageID, fixture.WorkspaceID, fixture.AllChannelID)

	ambientMessages, err := store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{
		AgentID:     ambientAgentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    ambientAgentMember,
	}, 10)
	require.NoError(t, err)
	require.Len(t, ambientMessages, 1)
	require.Equal(t, delivery.Message.Content, ambientMessages[0].Content)

	mentionedMessages, err := store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{
		AgentID:     mentionedAgentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    mentionedAgentMember,
	}, 10)
	require.NoError(t, err)
	require.Len(t, mentionedMessages, 1)
	require.Equal(t, delivery.Message.Content, mentionedMessages[0].Content)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 7, 12, 0, 0, 0, time.UTC)

	fixture := seedAppAdminFixture(t, pool, "thread_human_hint", now)
	mentionedHuman := seedWorkspaceMemberFixture(t, pool, fixture.WorkspaceID, "thread_human_hint_peer", now)
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, fixture.AllChannelID, "all", "", "public", fixture.MemberID, mentionedHuman.MemberID, now)
	mustExec(t, pool, `
UPDATE users
SET name = $2, display_name = $3, updated_at = $4
WHERE id = $1
`, mentionedHuman.UserID, "raw_thread_human_hint_peer", "赵梦梦", now)
	mustExec(t, pool, `
UPDATE members
SET display_name = $2, updated_at = $3
WHERE id = $1
`, mentionedHuman.MemberID, "赵梦梦", now)

	user := model.User{ID: fixture.UserID, Name: "thread_human_hint", DisplayName: "thread_human_hint"}
	parent, _, err := store.SendMessageForUser(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "parent message", nil)
	require.NoError(t, err)
	replyThread, err := message.OpenReplyThreadForMember(ctx, pool, fixture.MemberID, fixture.WorkspaceID, fixture.AllChannelID, parent.ID)
	require.NoError(t, err)

	_, delivery, err := store.SendAppMessageForUserWithOptions(
		ctx,
		user,
		fixture.WorkspaceID,
		replyThread.ThreadChannelID,
		"@赵梦梦 请看一下",
		nil,
		SendAppMessageOptions{
			MentionHints: []message.MessageMentionHint{{
				Type:  "human",
				ID:    mentionedHuman.UserID,
				Token: "赵梦梦",
			}},
		},
	)
	require.NoError(t, err)
	require.Equal(t, []message.HumanMentionRecipient{{
		UserID:   mentionedHuman.UserID,
		MemberID: mentionedHuman.MemberID,
	}}, delivery.MentionedHumans)

	subscribedThreads, err := store.messageStore().ListSubscribedThreadsForUserWithLimit(ctx, mentionedHuman.UserID, fixture.WorkspaceID, 0)
	require.NoError(t, err)
	require.Len(t, subscribedThreads, 1)
	require.Equal(t, replyThread.ThreadChannelID, subscribedThreads[0].ThreadChannelID)
	require.Equal(t, "active", subscribedThreads[0].SubscriptionState)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 20, 9, 15, 0, 0, time.UTC)

	fixture := seedAppAdminFixture(t, pool, "agent_mention_display_name", now)
	const (
		machineID        = "mach_agent_mention_display_name"
		authorAgentID    = "agt_agent_mention_display_sender"
		authorMemberID   = "mem_agent_mention_display_sender"
		targetAgentID    = "agt_agent_mention_display_target"
		targetMemberID   = "mem_agent_mention_display_target"
		otherAgentID     = "agt_agent_mention_display_other"
		otherMemberID    = "mem_agent_mention_display_other"
		sharedDisplay    = "shared-agent-label"
		targetPublicName = "target-public-agent"
		otherPublicName  = "other-public-agent"
	)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, machineID, authorAgentID, authorMemberID, "author-public-agent", now)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, machineID, targetAgentID, targetMemberID, targetPublicName, now)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, machineID, otherAgentID, otherMemberID, otherPublicName, now)
	mustExec(t, pool, `
UPDATE members
SET display_name = $3
WHERE workspace_id = $1
  AND id IN ($2, $4)
`, fixture.WorkspaceID, targetMemberID, sharedDisplay, otherMemberID)

	authenticatedAgent := agent.AuthenticatedAgent{
		AgentID:     authorAgentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    authorMemberID,
	}
	recipient, err := store.ResolveChannelAgentMentionForAgent(ctx, authenticatedAgent, "#all", targetPublicName)
	require.NoError(t, err)
	require.Equal(t, targetAgentID, recipient.AgentID)

	recipient, err = store.ResolveChannelAgentMentionForAgent(ctx, authenticatedAgent, "#all", targetMemberID)
	require.NoError(t, err)
	require.Equal(t, targetAgentID, recipient.AgentID)

	_, err = store.ResolveChannelAgentMentionForAgent(ctx, authenticatedAgent, "#all", sharedDisplay)
	require.Error(t, err)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 7, 12, 0, 0, 0, time.UTC)

	fixture := seedAppAdminFixture(t, pool, "removed_channel_agent_mention", now)
	user := model.User{ID: fixture.UserID, Name: "removed_channel_agent_mention", DisplayName: "removed_channel_agent_mention"}
	channelID := "chn_removed_channel_agent_mention"
	agentID := "agt_removed_channel_agent_mention"
	agentMemberID := "mem_removed_channel_agent_mention_agent"

	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, channelID, "removed-channel-agent", "", "private", fixture.MemberID, fixture.MemberID, now)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, "mach_removed_channel_agent_mention", agentID, agentMemberID, "removed-agent", now)
	require.NoError(t, store.AddChannelAgentForUser(ctx, fixture.UserID, fixture.WorkspaceID, channelID, agentID))

	_, joinedDelivery, err := store.SendMessageForUser(
		ctx,
		user,
		fixture.WorkspaceID,
		channelID,
		"@removed-agent should receive while joined",
		nil,
	)
	require.NoError(t, err)
	require.Equal(t, message.AgentDeliveryReasonDirectMention, agentDeliveryReasonsByMember(joinedDelivery.Recipients)[agentMemberID])
	require.Equal(t, message.AgentDeliveryReasonDirectMention, agentDeliveryReasonsByMember(joinedDelivery.WakeRecipients)[agentMemberID])

	require.NoError(t, store.RemoveChannelAgentForUser(ctx, fixture.UserID, fixture.WorkspaceID, channelID, agentID))

	_, removedDelivery, err := store.SendMessageForUser(
		ctx,
		user,
		fixture.WorkspaceID,
		channelID,
		"@removed-agent should not receive after removal",
		nil,
	)
	require.NoError(t, err)
	require.NotContains(t, agentDeliveryReasonsByMember(removedDelivery.Recipients), agentMemberID)
	require.NotContains(t, agentDeliveryReasonsByMember(removedDelivery.WakeRecipients), agentMemberID)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 6, 23, 10, 15, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "start_thread_normal_message", now)
	user := model.User{ID: fixture.UserID, Name: "start_thread_normal_message", DisplayName: "start_thread_normal_message"}

	sentMessage, delivery, err := store.SendAppMessageForUserWithOptions(
		ctx,
		user,
		fixture.WorkspaceID,
		fixture.AllChannelID,
		"ordinary channel message should also open a reply thread",
		nil,
		SendAppMessageOptions{StartAgentThread: true},
	)

	require.NoError(t, err)
	require.Equal(t, sentMessage.ID, delivery.MessageID)
	require.NotNil(t, delivery.StartedReplyThread)
	require.NotEmpty(t, delivery.StartedReplyThread.ThreadChannelID)
	require.Equal(t, "channel", delivery.Message.ChannelType)
	require.Equal(t, fixture.AllChannelID, delivery.Message.TargetKey)
	require.NotZero(t, delivery.Message.TargetSeq)
	require.Empty(t, delivery.Recipients)
	require.Empty(t, delivery.WakeRecipients)
	requireScopeMessageAndSingleFanout(t, pool, delivery.MessageID, fixture.WorkspaceID, fixture.AllChannelID)

	var threadMessageCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_messages
WHERE workspace_id = $1
  AND scope_id = $2
`, fixture.WorkspaceID, delivery.StartedReplyThread.ThreadChannelID).Scan(&threadMessageCount))
	require.Zero(t, threadMessageCount)

	subscribedThreads, err := store.messageStore().ListSubscribedThreadsForUserWithLimit(ctx, fixture.UserID, fixture.WorkspaceID, 0)
	require.NoError(t, err)
	require.Len(t, subscribedThreads, 1)
	require.Equal(t, delivery.StartedReplyThread.ThreadChannelID, subscribedThreads[0].ThreadChannelID)
	require.Equal(t, "active", subscribedThreads[0].SubscriptionState)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 6, 23, 10, 30, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "start_thread_public_mention", now)
	user := model.User{ID: fixture.UserID, Name: "start_thread_public_mention", DisplayName: "start_thread_public_mention"}
	const (
		machineID            = "mach_start_thread_public_mention"
		agentID              = "agt_start_thread_public_mention"
		agentMemberID        = "mem_agent_start_thread_public_mention"
		agentName            = "thread-public-agent"
		ambientAgentID       = "agt_start_thread_public_ambient"
		ambientAgentMemberID = "mem_agent_start_thread_public_ambient"
	)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, machineID, agentID, agentMemberID, agentName, now)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, "mach_start_thread_public_ambient", ambientAgentID, ambientAgentMemberID, "thread-public-ambient", now)

	_, delivery, err := store.SendAppMessageForUserWithOptions(
		ctx,
		user,
		fixture.WorkspaceID,
		fixture.AllChannelID,
		"@thread-public-agent please look, and also start a thread",
		nil,
		SendAppMessageOptions{StartAgentThread: true},
	)

	require.NoError(t, err)
	require.NotNil(t, delivery.StartedReplyThread)
	require.Equal(t, "thread", delivery.Message.ChannelType)
	require.Equal(t, delivery.StartedReplyThread.ThreadChannelID, delivery.Message.TargetKey)
	require.Zero(t, delivery.Message.TargetSeq)
	require.Equal(t, map[string]string{
		agentMemberID: message.AgentDeliveryReasonDirectMention,
	}, agentDeliveryReasonsByMember(delivery.Recipients))
	require.Equal(t, map[string]string{
		agentMemberID: message.AgentDeliveryReasonDirectMention,
	}, agentDeliveryReasonsByMember(delivery.WakeRecipients))
	require.NotContains(t, agentDeliveryReasonsByMember(delivery.WakeRecipients), ambientAgentMemberID)

	messages, err := store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{
		AgentID:     agentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    agentMemberID,
	}, 10)
	require.NoError(t, err)
	require.Len(t, messages, 1)
	require.Equal(t, delivery.MessageID, messages[0].MessageID)
	require.Equal(t, delivery.StartedReplyThread.ThreadChannelID, messages[0].TargetKey)
	require.Zero(t, messages[0].TargetSeq)
	require.Equal(t, "thread", messages[0].ChannelType)
	ambientMessages, err := store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{
		AgentID:     ambientAgentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    ambientAgentMemberID,
	}, 10)
	require.NoError(t, err)
	require.Empty(t, ambientMessages)

	var agentThreadMembershipCount int
	require.NoError(t, 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
`, delivery.StartedReplyThread.ThreadChannelID, agentMemberID).Scan(&agentThreadMembershipCount))
	require.Equal(t, 1, agentThreadMembershipCount)

	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_notification_settings
WHERE scope_id = $1
  AND member_id = $2
  AND state IN ('active', 'quiet', 'mute', 'auto-wake', 'no-wake')
`, delivery.StartedReplyThread.ThreadChannelID, agentMemberID).Scan(&agentThreadMembershipCount))
	require.Equal(t, 1, agentThreadMembershipCount)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	teamStore := team.New(pool)
	now := time.Date(2026, 6, 25, 12, 10, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "start_thread_team_mention", now)
	user := model.User{ID: fixture.UserID, Name: "start_thread_team_mention", DisplayName: "start_thread_team_mention"}

	const (
		leadMachineID        = "mach_start_thread_team_lead"
		leadAgentID          = "agt_start_thread_team_lead"
		leadAgentMemberID    = "mem_start_thread_team_lead"
		leadAgentName        = "start-thread-team-lead"
		builderMachineID     = "mach_start_thread_team_builder"
		builderAgentID       = "agt_start_thread_team_builder"
		builderAgentMemberID = "mem_start_thread_team_builder"
		builderAgentName     = "start-thread-team-builder"
	)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, leadMachineID, leadAgentID, leadAgentMemberID, leadAgentName, now)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, builderMachineID, builderAgentID, builderAgentMemberID, builderAgentName, now)
	upsertScopeSubscriptionForTests(t, pool, fixture.AllChannelID, builderAgentMemberID, "no-wake", now)

	wakeOnMention := false
	team, err := teamStore.CreateTeam(ctx, team.CreateTeamParams{
		ID:          "team_start_thread_team_mention",
		WorkspaceID: fixture.WorkspaceID,
		Handle:      "start-thread-team",
		DisplayName: "Start Thread Team",
		InitialMembers: []team.AddTeamMemberParams{
			{
				ID:        "wtm_start_thread_team_lead",
				ActorKind: team.ActorKindAgent,
				ActorID:   leadAgentMemberID,
				Role:      team.MemberRoleLeader,
				RoleLabel: "Lead",
			},
			{
				ID:                "wtm_start_thread_team_builder",
				ActorKind:         team.ActorKindAgent,
				ActorID:           builderAgentMemberID,
				Role:              team.MemberRoleMember,
				RoleLabel:         "Builder",
				WakeOnTeamMention: &wakeOnMention,
			},
		},
		CreatedByMemberID: fixture.MemberID,
		Now:               now.Add(time.Minute),
	})
	require.NoError(t, err)

	_, delivery, err := store.SendAppMessageForUserWithOptions(
		ctx,
		user,
		fixture.WorkspaceID,
		fixture.AllChannelID,
		"@start-thread-team please build this, and also start a topic",
		nil,
		SendAppMessageOptions{
			StartAgentThread: true,
			MentionHints: []message.MessageMentionHint{{
				Type:  "team",
				ID:    team.ID,
				Token: "start-thread-team",
			}},
		},
	)
	require.NoError(t, err)
	require.NotNil(t, delivery.StartedReplyThread)
	require.Equal(t, "thread", delivery.Message.ChannelType)
	require.Equal(t, delivery.StartedReplyThread.ThreadChannelID, delivery.Message.TargetKey)
	require.Zero(t, delivery.Message.TargetSeq)
	require.Equal(t, message.AgentDeliveryReasonTeamMention, agentDeliveryReasonsByMember(delivery.WakeRecipients)[leadAgentMemberID])

	messages, err := store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{
		AgentID:     leadAgentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    leadAgentMemberID,
	}, 10)
	require.NoError(t, err)
	require.Len(t, messages, 1)
	require.Equal(t, delivery.MessageID, messages[0].MessageID)
	require.Equal(t, delivery.StartedReplyThread.ThreadChannelID, messages[0].TargetKey)
	require.Zero(t, messages[0].TargetSeq)
	require.Equal(t, "thread", messages[0].ChannelType)

	var subscribedCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_notification_settings
WHERE scope_id = $1
  AND member_id = $2
  AND state IN ('active', 'quiet', 'mute', 'auto-wake', 'no-wake')
`, delivery.StartedReplyThread.ThreadChannelID, leadAgentMemberID).Scan(&subscribedCount))
	require.Equal(t, 1, subscribedCount)
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_memberships
WHERE scope_id = $1
  AND member_id = $2
  AND removed_at IS NULL
  AND effective_to_seq IS NULL
`, delivery.StartedReplyThread.ThreadChannelID, leadAgentMemberID).Scan(&subscribedCount))
	require.Equal(t, 1, subscribedCount)
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_notification_settings
WHERE scope_id = $1
  AND member_id = $2
  AND state IN ('active', 'quiet', 'mute', 'auto-wake', 'no-wake')
`, delivery.StartedReplyThread.ThreadChannelID, builderAgentMemberID).Scan(&subscribedCount))
	require.Zero(t, subscribedCount)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 12, 3, 5, 0, 0, time.UTC)

	fixture := seedAppAdminFixture(t, pool, "start_private_thread_delivery", now)
	const (
		machineID       = "mach_start_private_thread_delivery"
		agentID         = "agt_start_private_thread_delivery"
		agentMemberID   = "mem_agent_start_private_thread_delivery"
		agentPublicName = "thread-private-agent"
	)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, machineID, agentID, agentMemberID, agentPublicName, now)

	_, delivery, err := store.SendAppMessageForUserWithOptions(
		ctx,
		model.User{ID: fixture.UserID, Name: "start_private_thread_delivery", DisplayName: "start_private_thread_delivery"},
		fixture.WorkspaceID,
		fixture.AllChannelID,
		"@thread-private-agent please handle this in a thread",
		nil,
		SendAppMessageOptions{StartAgentThread: true},
	)
	require.NoError(t, err)
	require.NotNil(t, delivery.StartedReplyThread)
	require.Equal(t, "thread", delivery.Message.ChannelType)
	require.Equal(t, "thread-"+delivery.MessageID[:8], delivery.Message.ChannelName)
	require.Equal(t, "channel", delivery.Message.ParentChannelType)
	require.Equal(t, "all", delivery.Message.ParentChannelName)
	require.Equal(t, map[string]string{
		agentMemberID: message.AgentDeliveryReasonDirectMention,
	}, agentDeliveryReasonsByMember(delivery.Recipients))
	require.Equal(t, map[string]string{
		agentMemberID: message.AgentDeliveryReasonDirectMention,
	}, agentDeliveryReasonsByMember(delivery.WakeRecipients))

	requireScopeMessageAndSingleFanout(t, pool, delivery.MessageID, fixture.WorkspaceID, fixture.AllChannelID)

	subscribedThreads, err := store.messageStore().ListSubscribedThreadsForUserWithLimit(ctx, fixture.UserID, fixture.WorkspaceID, 0)
	require.NoError(t, err)
	require.Len(t, subscribedThreads, 1)
	require.Equal(t, delivery.StartedReplyThread.ThreadChannelID, subscribedThreads[0].ThreadChannelID)
	require.Equal(t, "active", subscribedThreads[0].SubscriptionState)
	require.NotNil(t, subscribedThreads[0].LastActivityAt)

	var authorThreadMembershipCount int
	require.NoError(t, 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
`, delivery.StartedReplyThread.ThreadChannelID, fixture.MemberID).Scan(&authorThreadMembershipCount))
	require.Equal(t, 1, authorThreadMembershipCount)

	var threadMessageCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_messages
WHERE workspace_id = $1
  AND scope_id = $2
`, fixture.WorkspaceID, delivery.StartedReplyThread.ThreadChannelID).Scan(&threadMessageCount))
	require.Zero(t, threadMessageCount)

	hasUnread, err := store.AgentHasUnreadMessages(ctx, agent.AuthenticatedAgent{
		AgentID:     agentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    agentMemberID,
	})
	require.NoError(t, err)
	require.True(t, hasUnread)

	messages, err := store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{
		AgentID:     agentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    agentMemberID,
	}, 10)
	require.NoError(t, err)
	require.Len(t, messages, 1)
	require.Equal(t, delivery.MessageID, messages[0].MessageID)
	require.Equal(t, delivery.StartedReplyThread.ThreadChannelID, messages[0].TargetKey)
	require.Zero(t, messages[0].TargetSeq)
	require.Equal(t, "thread", messages[0].ChannelType)
	require.Equal(t, "thread-"+delivery.MessageID[:8], messages[0].ChannelName)
	require.Equal(t, "channel", messages[0].ParentChannelType)
	require.Equal(t, "all", messages[0].ParentChannelName)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 20, 12, 10, 0, 0, time.UTC)

	fixture := seedAppAdminFixture(t, pool, "start_thread_private_hint", now)
	const (
		machineID     = "mach_start_thread_private_hint"
		agentID       = "agt_start_thread_private_hint"
		agentMemberID = "mem_agent_start_thread_private_hint"
		agentName     = "thread-hint-private"
	)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, machineID, agentID, agentMemberID, agentName, now)

	sentMessage, delivery, err := store.SendAppMessageForUserWithOptions(
		ctx,
		model.User{ID: fixture.UserID, Name: "start_thread_private_hint", DisplayName: "start_thread_private_hint"},
		fixture.WorkspaceID,
		fixture.AllChannelID,
		"@thread-hint-private please handle this in a thread",
		nil,
		SendAppMessageOptions{
			StartAgentThread: true,
			MentionHints: []message.MessageMentionHint{{
				Type:  "agent",
				ID:    agentID,
				Token: agentName,
			}},
		},
	)
	require.NoError(t, err)
	require.NotNil(t, delivery.StartedReplyThread)
	require.Equal(t, []any{
		map[string]any{
			"type":  "agent",
			"id":    agentID,
			"token": agentName,
		},
	}, sentMessage.Metadata[message.MentionMetadataKey])
	require.Equal(t, map[string]string{
		agentMemberID: message.AgentDeliveryReasonDirectMention,
	}, agentDeliveryReasonsByMember(delivery.Recipients))
	require.Equal(t, map[string]string{
		agentMemberID: message.AgentDeliveryReasonDirectMention,
	}, agentDeliveryReasonsByMember(delivery.WakeRecipients))
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	board := taskboard.New(pool)
	now := time.Date(2026, 5, 20, 12, 30, 0, 0, time.UTC)

	fixture := seedAppAdminFixture(t, pool, "task_discussion_hint_wake", now)
	user := model.User{ID: fixture.UserID, Name: "task_discussion_hint_wake", DisplayName: "task_discussion_hint_wake"}
	const (
		machineID       = "mach_task_discussion_hint_wake"
		agentID         = "agt_task_discussion_hint_wake"
		agentMemberID   = "mem_agent_task_discussion_hint_wake"
		originalName    = "task-hint-before"
		replacementName = "task-hint-after"
		agentLabel      = "Task Hint Agent"
	)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, machineID, agentID, agentMemberID, originalName, now)

	created, err := board.CreateTasks(ctx, taskboard.CreateTasksRequest{
		Context: taskboard.NewUserServiceContext(fixture.WorkspaceID, fixture.UserID, user.DisplayName),
		Board:   taskboard.BoardRef{ChannelID: fixture.AllChannelID},
		Titles:  []string{"Persisted mention hint wakes task discussion agent"},
	})
	require.NoError(t, err)
	require.Len(t, created.Tasks, 1)

	mustExec(t, pool, `
UPDATE agents
SET name = $2
WHERE id = $1
`, agentID, replacementName)
	mustExec(t, pool, `
UPDATE members
SET display_name = $2
WHERE id = $1
`, agentMemberID, agentLabel)

	appMessage, delivery, err := store.SendMessageForUserWithOptions(
		ctx,
		user,
		fixture.WorkspaceID,
		created.Tasks[0].TaskScopeID,
		"@task-hint-before please inspect this task discussion",
		nil,
		message.HumanMessageCreateOptions{
			MentionHints: []message.MessageMentionHint{{
				Type:  "agent",
				ID:    agentID,
				Token: originalName,
			}},
		},
	)
	require.NoError(t, err)
	require.Equal(t, map[string]string{
		agentMemberID: message.AgentDeliveryReasonDirectMention,
	}, agentDeliveryReasonsByMember(delivery.WakeRecipients))
	expected := []message.ResolvedMention{{
		Token:         originalName,
		Type:          "agent",
		ID:            agentID,
		DisplayName:   agentLabel,
		ResolvedToken: replacementName,
	}}
	require.Equal(t, expected, appMessage.ResolvedMentions)
	require.NotNil(t, delivery.AppMessage)
	require.Equal(t, expected, delivery.AppMessage.ResolvedMentions)
	fanoutPayload := requireScopeFanoutPayloadForMessage(t, pool, delivery.MessageID)
	require.Equal(t, expected, fanoutPayload.AppMessage.ResolvedMentions)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 12, 10, 20, 0, 0, time.UTC)

	fixture := seedAppAdminFixture(t, pool, "channel_private_auto_wake", now)
	const (
		wakeMachineID     = "mach_channel_private_wake"
		wakeAgentID       = "agt_channel_private_wake"
		wakeAgentMemberID = "mem_channel_private_wake"
		idleMachineID     = "mach_channel_private_idle"
		idleAgentID       = "agt_channel_private_idle"
		idleAgentMemberID = "mem_channel_private_idle"
	)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, wakeMachineID, wakeAgentID, wakeAgentMemberID, "channel-private-wake", now)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, idleMachineID, idleAgentID, idleAgentMemberID, "channel-private-idle", now)

	_, err := store.privateAgentProjectionStore().SetChannelNotificationAgentWakeStateForUser(ctx, fixture.UserID, fixture.WorkspaceID, fixture.AllChannelID, wakeAgentID, "auto-wake")
	require.NoError(t, err)

	_, delivery, err := store.SendMessageForUser(
		ctx,
		model.User{ID: fixture.UserID, Name: "channel_private_auto_wake", DisplayName: "channel_private_auto_wake"},
		fixture.WorkspaceID,
		fixture.AllChannelID,
		"ordinary channel update",
		nil,
	)
	require.NoError(t, err)
	require.Equal(t, map[string]string{
		wakeAgentMemberID: message.AgentDeliveryReasonOwnerPrivateContext,
	}, agentDeliveryReasonsByMember(delivery.Recipients))
	require.Equal(t, map[string]string{
		wakeAgentMemberID: message.AgentDeliveryReasonOwnerPrivateContext,
	}, agentDeliveryReasonsByMember(delivery.WakeRecipients))

	requireScopeMessageAndSingleFanout(t, pool, delivery.MessageID, fixture.WorkspaceID, fixture.AllChannelID)

	var participantCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_memberships
WHERE scope_id = $1
  AND member_id IN ($2, $3)
  AND removed_at IS NULL
  AND effective_to_seq IS NULL
`, fixture.AllChannelID, wakeAgentMemberID, idleAgentMemberID).Scan(&participantCount))
	require.Equal(t, 2, participantCount)

	messages, err := store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{
		AgentID:     wakeAgentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    wakeAgentMemberID,
	}, 10)
	require.NoError(t, err)
	require.Len(t, messages, 1)
	require.Equal(t, delivery.MessageID, messages[0].MessageID)
	require.Equal(t, "channel", messages[0].ChannelType)
	require.Equal(t, "all", messages[0].ChannelName)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 12, 10, 25, 0, 0, time.UTC)

	fixture := seedAppAdminFixture(t, pool, "thread_private_followup", now)
	const (
		machineID           = "mach_thread_private_followup"
		agentID             = "agt_thread_private_followup"
		agentMemberID       = "mem_agent_thread_private_followup"
		publicMachineID     = "mach_thread_public_followup"
		publicAgentID       = "agt_thread_public_followup"
		publicAgentMemberID = "mem_agent_thread_public_followup"
	)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, machineID, agentID, agentMemberID, "thread-private-followup", now)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, publicMachineID, publicAgentID, publicAgentMemberID, "thread-public-followup", now)
	ordinaryMember := seedWorkspaceMemberFixture(t, pool, fixture.WorkspaceID, "thread_private_ordinary_member", now)
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, fixture.AllChannelID, "all", "", "public", fixture.MemberID, ordinaryMember.MemberID, now)
	collaborator := seedWorkspaceMemberFixture(t, pool, fixture.WorkspaceID, "thread_private_collaborator", now)
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, fixture.AllChannelID, "all", "", "public", fixture.MemberID, collaborator.MemberID, now)

	_, initialDelivery, err := store.SendAppMessageForUserWithOptions(
		ctx,
		model.User{ID: fixture.UserID, Name: "thread_private_followup", DisplayName: "thread_private_followup"},
		fixture.WorkspaceID,
		fixture.AllChannelID,
		"@thread-private-followup starting thread context",
		nil,
		SendAppMessageOptions{StartAgentThread: true},
	)
	require.NoError(t, err)
	require.NotNil(t, initialDelivery.StartedReplyThread)

	initialMessages, err := store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{
		AgentID:     agentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    agentMemberID,
	}, 10)
	require.NoError(t, err)
	require.Len(t, initialMessages, 1)
	require.Equal(t, initialDelivery.MessageID, initialMessages[0].MessageID)

	_, replyDelivery, err := store.SendMessageForUser(
		ctx,
		model.User{ID: fixture.UserID, Name: "thread_private_followup", DisplayName: "thread_private_followup"},
		fixture.WorkspaceID,
		initialDelivery.StartedReplyThread.ThreadChannelID,
		"plain thread follow-up",
		nil,
	)
	require.NoError(t, err)
	require.Equal(t, map[string]string{
		agentMemberID: message.AgentDeliveryReasonOwnerPrivateContext,
	}, agentDeliveryReasonsByMember(replyDelivery.Recipients))
	require.Equal(t, map[string]string{
		agentMemberID: message.AgentDeliveryReasonOwnerPrivateContext,
	}, agentDeliveryReasonsByMember(replyDelivery.WakeRecipients))

	requireScopeMessageAndSingleFanout(t, pool, replyDelivery.MessageID, fixture.WorkspaceID, initialDelivery.StartedReplyThread.ThreadChannelID)

	var participantCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_memberships
WHERE scope_id IN ($1, $2)
  AND member_id = $3
  AND removed_at IS NULL
  AND effective_to_seq IS NULL
`, fixture.AllChannelID, initialDelivery.StartedReplyThread.ThreadChannelID, agentMemberID).Scan(&participantCount))
	require.Equal(t, 2, participantCount)

	followupMessages, err := store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{
		AgentID:     agentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    agentMemberID,
	}, 10)
	require.NoError(t, err)
	require.Len(t, followupMessages, 1)
	require.Equal(t, replyDelivery.MessageID, followupMessages[0].MessageID)
	require.Equal(t, "thread", followupMessages[0].ChannelType)
	require.Equal(t, replyDelivery.Message.ChannelName, followupMessages[0].ChannelName)

	publicAgentReply, err := store.SendAgentThreadMessage(ctx, agent.AuthenticatedAgent{
		AgentID:     publicAgentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    publicAgentMemberID,
	}, message.SendMessageParams{
		TargetRef: "#all:" + initialDelivery.MessageID[:8],
		Content:   "@thread-private-followup public agent follow-up",
	})
	require.NoError(t, err)
	require.NotEqual(t, message.AgentDeliveryReasonOwnerPrivateContext, agentDeliveryReasonsByMember(publicAgentReply.Recipients)[agentMemberID])
	require.NotEqual(t, message.AgentDeliveryReasonDirectMention, agentDeliveryReasonsByMember(publicAgentReply.Recipients)[agentMemberID])

	followupMessages, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{
		AgentID:     agentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    agentMemberID,
	}, 10)
	require.NoError(t, err)
	require.Empty(t, followupMessages)

	_, ordinaryReply, err := store.SendMessageForUser(
		ctx,
		model.User{ID: ordinaryMember.UserID, Name: "thread_private_ordinary_member", DisplayName: "thread_private_ordinary_member"},
		fixture.WorkspaceID,
		initialDelivery.StartedReplyThread.ThreadChannelID,
		"ordinary member follow-up",
		nil,
	)
	require.NoError(t, err)

	requireScopeMessageAndSingleFanout(t, pool, ordinaryReply.MessageID, fixture.WorkspaceID, initialDelivery.StartedReplyThread.ThreadChannelID)

	followupMessages, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{
		AgentID:     agentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    agentMemberID,
	}, 10)
	require.NoError(t, err)
	require.Empty(t, followupMessages)

	seedThreadScopeCollaboratorGrantForTests(t, pool, fixture.WorkspaceID, fixture.MemberID, initialDelivery.StartedReplyThread.ThreadChannelID, collaborator.MemberID, fixture.MemberID, "thread_private_collaborator_grant", time.Now().UTC().Add(-time.Minute))

	_, collaboratorReply, err := store.SendMessageForUser(
		ctx,
		model.User{ID: collaborator.UserID, Name: "thread_private_collaborator", DisplayName: "thread_private_collaborator"},
		fixture.WorkspaceID,
		initialDelivery.StartedReplyThread.ThreadChannelID,
		"collaborator member follow-up",
		nil,
	)
	require.NoError(t, err)
	require.Equal(t, message.AgentDeliveryReasonOwnerPrivateContext, agentDeliveryReasonsByMember(collaboratorReply.Recipients)[agentMemberID])

	followupMessages, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{
		AgentID:     agentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    agentMemberID,
	}, 10)
	require.NoError(t, err)
	require.Len(t, followupMessages, 1)
	require.Equal(t, collaboratorReply.MessageID, followupMessages[0].MessageID)

	mustExec(t, pool, `
UPDATE task_scope_participant_metadata
SET removed_at = $2, updated_at = $2
WHERE id = $1
`, "tspm_thread_private_collaborator_grant", time.Now().UTC())
	_, expiredCollaboratorReply, err := store.SendMessageForUser(
		ctx,
		model.User{ID: collaborator.UserID, Name: "thread_private_collaborator", DisplayName: "thread_private_collaborator"},
		fixture.WorkspaceID,
		initialDelivery.StartedReplyThread.ThreadChannelID,
		"collaborator member follow-up after grant removal",
		nil,
	)
	require.NoError(t, err)
	require.NotEqual(t, message.AgentDeliveryReasonOwnerPrivateContext, agentDeliveryReasonsByMember(expiredCollaboratorReply.Recipients)[agentMemberID])
	followupMessages, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{
		AgentID:     agentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    agentMemberID,
	}, 10)
	require.NoError(t, err)
	require.Empty(t, followupMessages)

	mustExec(t, pool, `
UPDATE scope_memberships
SET removed_at = $4, updated_at = $4
WHERE scope_id IN ($1, $2)
  AND member_id = $3
  AND visibility = 'private'
`, fixture.AllChannelID, initialDelivery.StartedReplyThread.ThreadChannelID, agentMemberID, time.Now().UTC())
	_, removedEntryReply, err := store.SendMessageForUser(
		ctx,
		model.User{ID: fixture.UserID, Name: "thread_private_followup", DisplayName: "thread_private_followup"},
		fixture.WorkspaceID,
		initialDelivery.StartedReplyThread.ThreadChannelID,
		"owner follow-up after private agent removal",
		nil,
	)
	require.NoError(t, err)
	require.NotEqual(t, message.AgentDeliveryReasonOwnerPrivateContext, agentDeliveryReasonsByMember(removedEntryReply.Recipients)[agentMemberID])
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 12, 12, 5, 0, 0, time.UTC)

	fixture := seedAppAdminFixture(t, pool, "thread_private_direct_mention", now)
	const (
		contextMachineID     = "mach_thread_direct_context"
		contextAgentID       = "agt_thread_direct_context"
		contextAgentMemberID = "mem_thread_direct_context"
		peerMachineID        = "mach_thread_direct_peer"
		peerAgentID          = "agt_thread_direct_peer"
		peerAgentMemberID    = "mem_thread_direct_peer"
		outsideMachineID     = "mach_thread_direct_outside"
		outsideAgentID       = "agt_thread_direct_outside"
		outsideAgentMemberID = "mem_thread_direct_outside"
	)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, contextMachineID, contextAgentID, contextAgentMemberID, "thread-direct-context", now)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, peerMachineID, peerAgentID, peerAgentMemberID, "thread-direct-peer", now)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, outsideMachineID, outsideAgentID, outsideAgentMemberID, "thread-direct-outside", now)

	ownerUser := model.User{ID: fixture.UserID, Name: "thread_private_direct_mention", DisplayName: "thread_private_direct_mention"}
	_, initialDelivery, err := store.SendAppMessageForUserWithOptions(ctx, ownerUser, fixture.WorkspaceID, fixture.AllChannelID, "@thread-direct-context start thread", nil, SendAppMessageOptions{StartAgentThread: true})
	require.NoError(t, err)
	require.NotNil(t, initialDelivery.StartedReplyThread)

	contextMessages, err := store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: contextAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: contextAgentMemberID}, 10)
	require.NoError(t, err)
	require.Len(t, contextMessages, 1)
	require.Equal(t, initialDelivery.MessageID, contextMessages[0].MessageID)

	_, peerMentionDelivery, err := store.SendMessageForUser(ctx, ownerUser, fixture.WorkspaceID, initialDelivery.StartedReplyThread.ThreadChannelID, "@thread-direct-peer add only peer", nil)
	require.NoError(t, err)
	reasons := agentDeliveryReasonsByMember(peerMentionDelivery.Recipients)
	require.Equal(t, message.AgentDeliveryReasonDirectMention, reasons[peerAgentMemberID])
	require.Equal(t, message.AgentDeliveryReasonOwnerPrivateContext, reasons[contextAgentMemberID])
	require.NotContains(t, reasons, outsideAgentMemberID)
	requireScopeMessageAndSingleFanout(t, pool, peerMentionDelivery.MessageID, fixture.WorkspaceID, initialDelivery.StartedReplyThread.ThreadChannelID)

	peerMessages, err := store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: peerAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: peerAgentMemberID}, 10)
	require.NoError(t, err)
	require.Len(t, peerMessages, 1)
	require.Equal(t, peerMentionDelivery.MessageID, peerMessages[0].MessageID)

	contextMessages, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: contextAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: contextAgentMemberID}, 10)
	require.NoError(t, err)
	require.Len(t, contextMessages, 1)
	require.Equal(t, peerMentionDelivery.MessageID, contextMessages[0].MessageID)
	outsideMessages, err := store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: outsideAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: outsideAgentMemberID}, 10)
	require.NoError(t, err)
	require.Empty(t, outsideMessages)

	_, contextMentionDelivery, err := store.SendMessageForUser(ctx, ownerUser, fixture.WorkspaceID, initialDelivery.StartedReplyThread.ThreadChannelID, "@thread-direct-context mention only original", nil)
	require.NoError(t, err)
	reasons = agentDeliveryReasonsByMember(contextMentionDelivery.Recipients)
	require.Equal(t, message.AgentDeliveryReasonDirectMention, reasons[contextAgentMemberID])
	require.Equal(t, message.AgentDeliveryReasonOwnerPrivateContext, reasons[peerAgentMemberID])
	require.NotContains(t, reasons, outsideAgentMemberID)
	requireScopeMessageAndSingleFanout(t, pool, contextMentionDelivery.MessageID, fixture.WorkspaceID, initialDelivery.StartedReplyThread.ThreadChannelID)

	contextMessages, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: contextAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: contextAgentMemberID}, 10)
	require.NoError(t, err)
	require.Len(t, contextMessages, 1)
	require.Equal(t, contextMentionDelivery.MessageID, contextMessages[0].MessageID)

	peerMessages, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: peerAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: peerAgentMemberID}, 10)
	require.NoError(t, err)
	require.Len(t, peerMessages, 1)
	require.Equal(t, contextMentionDelivery.MessageID, peerMessages[0].MessageID)
	outsideMessages, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: outsideAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: outsideAgentMemberID}, 10)
	require.NoError(t, err)
	require.Empty(t, outsideMessages)

	var participantCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_memberships
WHERE scope_id IN ($1, $2)
  AND member_id IN ($3, $4, $5)
  AND removed_at IS NULL
  AND effective_to_seq IS NULL
`, fixture.AllChannelID, initialDelivery.StartedReplyThread.ThreadChannelID, contextAgentMemberID, peerAgentMemberID, outsideAgentMemberID).Scan(&participantCount))
	require.Equal(t, 5, participantCount)

	var subscriptionCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_notification_settings
WHERE scope_id = $1
  AND member_id IN ($2, $3)
  AND state IN ('active', 'quiet', 'mute', 'auto-wake', 'no-wake')
`, initialDelivery.StartedReplyThread.ThreadChannelID, contextAgentMemberID, peerAgentMemberID).Scan(&subscriptionCount))
	require.Equal(t, 2, subscriptionCount)
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_notification_settings
WHERE scope_id = $1
  AND member_id = $2
  AND state IN ('active', 'quiet', 'mute', 'auto-wake', 'no-wake')
`, initialDelivery.StartedReplyThread.ThreadChannelID, outsideAgentMemberID).Scan(&subscriptionCount))
	require.Zero(t, subscriptionCount)
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_memberships
WHERE scope_id = $1
  AND member_id IN ($2, $3)
  AND removed_at IS NULL
  AND effective_to_seq IS NULL
`, initialDelivery.StartedReplyThread.ThreadChannelID, contextAgentMemberID, peerAgentMemberID).Scan(&subscriptionCount))
	require.Equal(t, 2, subscriptionCount)
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_memberships
WHERE scope_id = $1
  AND member_id = $2
  AND removed_at IS NULL
  AND effective_to_seq IS NULL
`, initialDelivery.StartedReplyThread.ThreadChannelID, outsideAgentMemberID).Scan(&subscriptionCount))
	require.Zero(t, subscriptionCount)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 12, 12, 15, 0, 0, time.UTC)

	fixture := seedAppAdminFixture(t, pool, "thread_private_empty_context", now)
	threadChannelID := "chn_thread_private_empty_context"
	seedPrivateWakeReplyThread(t, pool, fixture, threadChannelID, "emptyctx", now)

	const (
		targetMachineID     = "mach_thread_empty_target"
		targetAgentID       = "agt_thread_empty_target"
		targetAgentMemberID = "mem_thread_empty_target"
		peerMachineID       = "mach_thread_empty_peer"
		peerAgentID         = "agt_thread_empty_peer"
		peerAgentMemberID   = "mem_thread_empty_peer"
	)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, targetMachineID, targetAgentID, targetAgentMemberID, "thread-empty-target", now)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, peerMachineID, peerAgentID, peerAgentMemberID, "thread-empty-peer", now)
	mustExec(t, pool, `
UPDATE members
SET display_name = $3
WHERE workspace_id = $1
  AND id IN ($2, $4)
`, fixture.WorkspaceID, targetAgentMemberID, "shared-private-label", peerAgentMemberID)

	ownerUser := model.User{ID: fixture.UserID, Name: "thread_private_empty_context", DisplayName: "thread_private_empty_context"}
	_, displayNameMention, err := store.SendMessageForUser(ctx, ownerUser, fixture.WorkspaceID, threadChannelID, "@shared-private-label should not route by display name", nil)
	require.NoError(t, err)
	reasons := agentDeliveryReasonsByMember(displayNameMention.Recipients)
	require.NotContains(t, reasons, targetAgentMemberID)
	require.NotContains(t, reasons, peerAgentMemberID)
	targetMessages, err := store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: targetAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: targetAgentMemberID}, 10)
	require.NoError(t, err)
	require.Empty(t, targetMessages)
	peerMessages, err := store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: peerAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: peerAgentMemberID}, 10)
	require.NoError(t, err)
	require.Empty(t, peerMessages)

	_, delivery, err := store.SendMessageForUser(ctx, ownerUser, fixture.WorkspaceID, threadChannelID, "@thread-empty-target join this empty private context", nil)
	require.NoError(t, err)
	reasons = agentDeliveryReasonsByMember(delivery.Recipients)
	require.Equal(t, message.AgentDeliveryReasonDirectMention, reasons[targetAgentMemberID])
	require.NotContains(t, reasons, peerAgentMemberID)
	requireScopeMessageAndSingleFanout(t, pool, delivery.MessageID, fixture.WorkspaceID, threadChannelID)

	targetMessages, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: targetAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: targetAgentMemberID}, 10)
	require.NoError(t, err)
	require.Len(t, targetMessages, 1)
	require.Equal(t, delivery.MessageID, targetMessages[0].MessageID)
	peerMessages, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: peerAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: peerAgentMemberID}, 10)
	require.NoError(t, err)
	require.Empty(t, peerMessages)

	var peerProjectionCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_memberships
WHERE scope_id IN ($1, $2)
  AND member_id = $3
  AND removed_at IS NULL
  AND effective_to_seq IS NULL
`, fixture.AllChannelID, threadChannelID, peerAgentMemberID).Scan(&peerProjectionCount))
	require.Equal(t, 1, peerProjectionCount)

	var subscriptionCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_notification_settings
WHERE scope_id = $1
  AND member_id = $2
  AND state IN ('active', 'quiet', 'mute', 'auto-wake', 'no-wake')
`, threadChannelID, targetAgentMemberID).Scan(&subscriptionCount))
	require.Equal(t, 1, subscriptionCount)
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_memberships
WHERE scope_id = $1
  AND member_id = $2
  AND removed_at IS NULL
  AND effective_to_seq IS NULL
`, threadChannelID, targetAgentMemberID).Scan(&subscriptionCount))
	require.Equal(t, 1, subscriptionCount)
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_notification_settings
WHERE scope_id = $1
  AND member_id = $2
  AND state IN ('active', 'quiet', 'mute', 'auto-wake', 'no-wake')
`, threadChannelID, peerAgentMemberID).Scan(&subscriptionCount))
	require.Zero(t, subscriptionCount)
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_memberships
WHERE scope_id = $1
  AND member_id = $2
`, threadChannelID, peerAgentMemberID).Scan(&subscriptionCount))
	require.Zero(t, subscriptionCount)

	_, followup, err := store.SendMessageForUser(ctx, ownerUser, fixture.WorkspaceID, threadChannelID, "plain follow-up after private mention", nil)
	require.NoError(t, err)
	reasons = agentDeliveryReasonsByMember(followup.Recipients)
	require.Equal(t, message.AgentDeliveryReasonOwnerPrivateContext, reasons[targetAgentMemberID])
	require.NotContains(t, reasons, peerAgentMemberID)
	targetMessages, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: targetAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: targetAgentMemberID}, 10)
	require.NoError(t, err)
	require.Len(t, targetMessages, 1)
	require.Equal(t, followup.MessageID, targetMessages[0].MessageID)
	peerMessages, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: peerAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: peerAgentMemberID}, 10)
	require.NoError(t, err)
	require.Empty(t, peerMessages)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	notifications := store.privateAgentProjectionStore()
	now := time.Date(2026, 5, 12, 12, 18, 0, 0, time.UTC)

	fixture := seedAppAdminFixture(t, pool, "thread_private_root_mention", now)
	threadChannelID := "chn_thread_private_root_mention"
	seedPrivateWakeReplyThread(t, pool, fixture, threadChannelID, "rootmention", now)

	const (
		targetMachineID     = "mach_thread_root_target"
		targetAgentID       = "agt_thread_root_target"
		targetAgentMemberID = "mem_thread_root_target"
		peerMachineID       = "mach_thread_root_peer"
		peerAgentID         = "agt_thread_root_peer"
		peerAgentMemberID   = "mem_thread_root_peer"
	)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, targetMachineID, targetAgentID, targetAgentMemberID, "thread-root-target", now)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, peerMachineID, peerAgentID, peerAgentMemberID, "thread-root-peer", now)
	mustExec(t, pool, `
UPDATE scope_messages
SET content = $2, updated_at = $3
WHERE id = $1
`, "msg_"+threadChannelID, "@thread-root-target private wake reply root", now)

	var threadScopeCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scopes
WHERE id = $1
  AND type = 'thread'
  AND status = 'active'
`, threadChannelID).Scan(&threadScopeCount))
	require.Equal(t, 1, threadScopeCount)

	ownerUser := model.User{ID: fixture.UserID, Name: "thread_private_root_mention", DisplayName: "thread_private_root_mention"}
	_, delivery, err := store.SendMessageForUser(ctx, ownerUser, fixture.WorkspaceID, threadChannelID, "plain reply after root mention", nil)
	require.NoError(t, err)
	reasons := agentDeliveryReasonsByMember(delivery.Recipients)
	require.Equal(t, message.AgentDeliveryReasonOwnerPrivateContext, reasons[targetAgentMemberID])
	require.NotContains(t, reasons, peerAgentMemberID)
	requireScopeMessageAndSingleFanout(t, pool, delivery.MessageID, fixture.WorkspaceID, threadChannelID)
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scopes
WHERE id = $1
  AND type = 'thread'
  AND status = 'active'
`, threadChannelID).Scan(&threadScopeCount))
	require.Equal(t, 1, threadScopeCount)

	targetMessages, err := store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: targetAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: targetAgentMemberID}, 10)
	require.NoError(t, err)
	require.Len(t, targetMessages, 1)
	require.Equal(t, delivery.MessageID, targetMessages[0].MessageID)
	peerMessages, err := store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: peerAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: peerAgentMemberID}, 10)
	require.NoError(t, err)
	require.Empty(t, peerMessages)

	panel, err := notifications.GetThreadNotificationPanelForUser(ctx, fixture.UserID, fixture.WorkspaceID, threadChannelID, "", 0)
	require.NoError(t, err)
	require.Len(t, panel.Agents, 1)
	require.Equal(t, targetAgentID, panel.Agents[0].AgentID)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 12, 12, 25, 0, 0, time.UTC)

	fixture := seedAppAdminFixture(t, pool, "thread_private_parent_author", now)
	ordinaryMember := seedWorkspaceMemberFixture(t, pool, fixture.WorkspaceID, "thread_private_parent_ordinary", now)
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, fixture.AllChannelID, "all", "", "public", fixture.MemberID, ordinaryMember.MemberID, now)

	const (
		parentMachineID     = "mach_thread_parent_private_sender"
		parentAgentID       = "agt_thread_parent_private_sender"
		parentAgentMemberID = "mem_thread_parent_private_sender"
		peerMachineID       = "mach_thread_parent_private_peer"
		peerAgentID         = "agt_thread_parent_private_peer"
		peerAgentMemberID   = "mem_thread_parent_private_peer"
	)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, parentMachineID, parentAgentID, parentAgentMemberID, "thread-parent-private-author", now)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, peerMachineID, peerAgentID, peerAgentMemberID, "thread-parent-private-peer", now)

	threadChannelID := "chn_thread_private_parent_author"
	seedPrivateWakeReplyThread(t, pool, fixture, threadChannelID, "privroot", now)
	parentMessageID := "msg_" + threadChannelID
	mustExec(t, pool, `
UPDATE scope_messages
SET author_member_id = $2,
    author_type = 'agent',
    updated_at = $3
WHERE id = $1
`, parentMessageID, parentAgentMemberID, now)

	ordinaryUser := model.User{ID: ordinaryMember.UserID, Name: "thread_private_parent_ordinary", DisplayName: "thread_private_parent_ordinary"}
	_, ordinaryReply, err := store.SendMessageForUser(ctx, ordinaryUser, fixture.WorkspaceID, threadChannelID, "ordinary member should not enter private parent thread", nil)
	require.NoError(t, err)
	require.NotContains(t, agentDeliveryReasonsByMember(ordinaryReply.Recipients), parentAgentMemberID)
	requireScopeMessageAndSingleFanout(t, pool, ordinaryReply.MessageID, fixture.WorkspaceID, threadChannelID)

	ownerUser := model.User{ID: fixture.UserID, Name: "thread_private_parent_author", DisplayName: "thread_private_parent_author"}
	_, ownerReply, err := store.SendMessageForUser(ctx, ownerUser, fixture.WorkspaceID, threadChannelID, "owner replies to private agent root", nil)
	require.NoError(t, err)
	reasons := agentDeliveryReasonsByMember(ownerReply.Recipients)
	require.Equal(t, message.AgentDeliveryReasonOwnerPrivateContext, reasons[parentAgentMemberID])
	require.NotContains(t, reasons, peerAgentMemberID)
	requireScopeMessageAndSingleFanout(t, pool, ownerReply.MessageID, fixture.WorkspaceID, threadChannelID)

	parentMessages, err := store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: parentAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: parentAgentMemberID}, 10)
	require.NoError(t, err)
	require.Len(t, parentMessages, 1)
	require.Equal(t, ownerReply.MessageID, parentMessages[0].MessageID)
	peerMessages, err := store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: peerAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: peerAgentMemberID}, 10)
	require.NoError(t, err)
	require.Empty(t, peerMessages)

	_, followup, err := store.SendMessageForUser(ctx, ownerUser, fixture.WorkspaceID, threadChannelID, "owner follow-up after private parent author entered", nil)
	require.NoError(t, err)
	reasons = agentDeliveryReasonsByMember(followup.Recipients)
	require.Equal(t, message.AgentDeliveryReasonOwnerPrivateContext, reasons[parentAgentMemberID])
	require.NotContains(t, reasons, peerAgentMemberID)
	parentMessages, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: parentAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: parentAgentMemberID}, 10)
	require.NoError(t, err)
	require.Len(t, parentMessages, 1)
	require.Equal(t, followup.MessageID, parentMessages[0].MessageID)

	var privateProjectionCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_memberships
WHERE scope_id IN ($1, $2)
  AND member_id IN ($3, $4)
  AND removed_at IS NULL
  AND effective_to_seq IS NULL
`, fixture.AllChannelID, threadChannelID, parentAgentMemberID, peerAgentMemberID).Scan(&privateProjectionCount))
	require.Equal(t, 3, privateProjectionCount)
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_notification_settings
WHERE scope_id = $1
  AND member_id = $2
  AND state IN ('active', 'quiet', 'mute', 'auto-wake', 'no-wake')
`, threadChannelID, parentAgentMemberID).Scan(&privateProjectionCount))
	require.Equal(t, 1, privateProjectionCount)
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_notification_settings
WHERE scope_id = $1
  AND member_id = $2
  AND state IN ('active', 'quiet', 'mute', 'auto-wake', 'no-wake')
`, threadChannelID, peerAgentMemberID).Scan(&privateProjectionCount))
	require.Zero(t, privateProjectionCount)
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_memberships
WHERE scope_id = $1
  AND member_id = $2
  AND removed_at IS NULL
  AND effective_to_seq IS NULL
`, threadChannelID, parentAgentMemberID).Scan(&privateProjectionCount))
	require.Equal(t, 1, privateProjectionCount)
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_memberships
WHERE scope_id = $1
  AND member_id = $2
`, threadChannelID, peerAgentMemberID).Scan(&privateProjectionCount))
	require.Zero(t, privateProjectionCount)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 12, 11, 20, 0, 0, time.UTC)

	fixture := seedAppAdminFixture(t, pool, "owner_quote_private_routing", now)
	ordinaryMember := seedWorkspaceMemberFixture(t, pool, fixture.WorkspaceID, "owner_quote_private_member", now)
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, fixture.AllChannelID, "all", "", "public", fixture.MemberID, ordinaryMember.MemberID, now)

	const (
		wakeMachineID       = "mach_owner_quote_wake"
		wakeAgentID         = "agt_owner_quote_wake"
		wakeAgentMemberID   = "mem_owner_quote_wake"
		idleMachineID       = "mach_owner_quote_idle"
		idleAgentID         = "agt_owner_quote_idle"
		idleAgentMemberID   = "mem_owner_quote_idle"
		publicMachineID     = "mach_owner_quote_public"
		publicAgentID       = "agt_owner_quote_public"
		publicAgentMemberID = "mem_owner_quote_public"
	)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, wakeMachineID, wakeAgentID, wakeAgentMemberID, "owner-quote-wake", now)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, idleMachineID, idleAgentID, idleAgentMemberID, "owner-quote-idle", now)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, publicMachineID, publicAgentID, publicAgentMemberID, "owner-quote-public", now)
	require.NoError(t, message.UpsertScopeNotificationState(context.Background(), pool, wakeAgentMemberID, fixture.AllChannelID, "auto-wake", now))
	require.NoError(t, message.UpsertScopeNotificationState(context.Background(), pool, idleAgentMemberID, fixture.AllChannelID, "active", now))

	ordinaryUser := model.User{ID: ordinaryMember.UserID, Name: "owner_quote_private_member", DisplayName: "owner_quote_private_member"}
	source, _, err := store.SendMessageForUser(ctx, ordinaryUser, fixture.WorkspaceID, fixture.AllChannelID, "source for owner quote channel", nil)
	require.NoError(t, err)
	secondSource, _, err := store.SendMessageForUser(ctx, ordinaryUser, fixture.WorkspaceID, fixture.AllChannelID, "second source for owner quote channel", nil)
	require.NoError(t, err)
	publicMessages, err := store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: publicAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: publicAgentMemberID}, 10)
	require.NoError(t, err)
	require.Len(t, publicMessages, 2)

	ownerUser := model.User{ID: fixture.UserID, Name: "owner_quote_private_routing", DisplayName: "owner_quote_private_routing"}
	_, channelQuote, err := store.SendMessageForUserWithType(
		ctx,
		ownerUser,
		fixture.WorkspaceID,
		fixture.AllChannelID,
		ownerQuoteTestContentForSources("forward selected channel context @owner-quote-public @owner-quote-idle", "source for owner quote channel @owner-quote-idle", source.ID, secondSource.ID),
		nil,
		MessageTypeOwnerQuote,
	)
	require.NoError(t, err)
	channelQuoteReasons := agentDeliveryReasonsByMember(channelQuote.Recipients)
	channelQuoteWakeReasons := agentDeliveryReasonsByMember(channelQuote.WakeRecipients)
	require.Equal(t, message.AgentDeliveryReasonOwnerPrivateContext, channelQuoteReasons[wakeAgentMemberID])
	require.Equal(t, message.AgentDeliveryReasonDirectMention, channelQuoteReasons[publicAgentMemberID])
	require.Equal(t, message.AgentDeliveryReasonDirectMention, channelQuoteReasons[idleAgentMemberID])
	require.Equal(t, message.AgentDeliveryReasonOwnerPrivateContext, channelQuoteWakeReasons[wakeAgentMemberID])
	require.Equal(t, message.AgentDeliveryReasonDirectMention, channelQuoteWakeReasons[publicAgentMemberID])
	require.Equal(t, message.AgentDeliveryReasonDirectMention, channelQuoteWakeReasons[idleAgentMemberID])

	wakeMessages, err := store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: wakeAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: wakeAgentMemberID}, 10)
	require.NoError(t, err)
	require.Len(t, wakeMessages, 1)
	require.Equal(t, channelQuote.MessageID, wakeMessages[0].MessageID)
	publicMessages, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: publicAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: publicAgentMemberID}, 10)
	require.NoError(t, err)
	require.Len(t, publicMessages, 1)
	require.Equal(t, channelQuote.MessageID, publicMessages[0].MessageID)
	idleMessages, err := store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: idleAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: idleAgentMemberID}, 10)
	require.NoError(t, err)
	require.Len(t, idleMessages, 1)
	require.Equal(t, channelQuote.MessageID, idleMessages[0].MessageID)

	_, singleForwardQuote, err := store.SendMessageForUserWithType(
		ctx,
		ownerUser,
		fixture.WorkspaceID,
		fixture.AllChannelID,
		ownerQuoteTestContent(secondSource.ID, "@owner-quote-public @owner-quote-idle single forward acts like owner text", "single forwarded body"),
		nil,
		MessageTypeOwnerQuote,
	)
	require.NoError(t, err)
	singleForwardReasons := agentDeliveryReasonsByMember(singleForwardQuote.Recipients)
	require.Equal(t, message.AgentDeliveryReasonDirectMention, singleForwardReasons[publicAgentMemberID])
	require.Equal(t, message.AgentDeliveryReasonDirectMention, singleForwardReasons[idleAgentMemberID])
	require.Equal(t, message.AgentDeliveryReasonOwnerPrivateContext, singleForwardReasons[wakeAgentMemberID])
	idleMessages, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: idleAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: idleAgentMemberID}, 10)
	require.NoError(t, err)
	require.Len(t, idleMessages, 1)
	require.Equal(t, singleForwardQuote.MessageID, idleMessages[0].MessageID)
	publicMessages, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: publicAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: publicAgentMemberID}, 10)
	require.NoError(t, err)
	require.Len(t, publicMessages, 1)
	require.Equal(t, singleForwardQuote.MessageID, publicMessages[0].MessageID)
	wakeMessages, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: wakeAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: wakeAgentMemberID}, 10)
	require.NoError(t, err)
	require.Len(t, wakeMessages, 1)
	require.Equal(t, singleForwardQuote.MessageID, wakeMessages[0].MessageID)

	_, broadcastDelivery, err := store.SendMessageForUser(ctx, ownerUser, fixture.WorkspaceID, fixture.AllChannelID, "@all broadcast selected private context", nil)
	require.NoError(t, err)
	broadcastReasons := agentDeliveryReasonsByMember(broadcastDelivery.Recipients)
	require.Equal(t, message.AgentDeliveryReasonChannelBroadcastAll, broadcastReasons[wakeAgentMemberID])
	require.Equal(t, message.AgentDeliveryReasonChannelBroadcastAll, broadcastReasons[idleAgentMemberID])
	require.Equal(t, message.AgentDeliveryReasonChannelBroadcastAll, broadcastReasons[publicAgentMemberID])
	wakeMessages, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: wakeAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: wakeAgentMemberID}, 10)
	require.NoError(t, err)
	require.Len(t, wakeMessages, 1)
	require.Equal(t, broadcastDelivery.MessageID, wakeMessages[0].MessageID)
	idleMessages, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: idleAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: idleAgentMemberID}, 10)
	require.NoError(t, err)
	require.Len(t, idleMessages, 1)
	require.Equal(t, broadcastDelivery.MessageID, idleMessages[0].MessageID)

	quotedBroadcastSource, _, err := store.SendMessageForUser(ctx, ordinaryUser, fixture.WorkspaceID, fixture.AllChannelID, "quoted source says @all", nil)
	require.NoError(t, err)
	_, inertBroadcastQuote, err := store.SendMessageForUserWithType(
		ctx,
		ownerUser,
		fixture.WorkspaceID,
		fixture.AllChannelID,
		ownerQuoteTestContent(quotedBroadcastSource.ID, "forward quoted broadcast token only", "quoted source says @all"),
		nil,
		MessageTypeOwnerQuote,
	)
	require.NoError(t, err)
	inertBroadcastReasons := agentDeliveryReasonsByMember(inertBroadcastQuote.Recipients)
	require.NotEqual(t, message.AgentDeliveryReasonChannelBroadcastAll, inertBroadcastReasons[wakeAgentMemberID])
	require.NotEqual(t, message.AgentDeliveryReasonChannelBroadcastAll, inertBroadcastReasons[idleAgentMemberID])
	require.NotEqual(t, message.AgentDeliveryReasonChannelBroadcastAll, inertBroadcastReasons[publicAgentMemberID])
	_, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: wakeAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: wakeAgentMemberID}, 10)
	require.NoError(t, err)
	_, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: idleAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: idleAgentMemberID}, 10)
	require.NoError(t, err)
	_, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: publicAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: publicAgentMemberID}, 10)
	require.NoError(t, err)

	_, directMentionIdleDelivery, err := store.SendMessageForUser(ctx, ownerUser, fixture.WorkspaceID, fixture.AllChannelID, "@owner-quote-idle explicit private mention", nil)
	require.NoError(t, err)
	require.Equal(t, message.AgentDeliveryReasonDirectMention, agentDeliveryReasonsByMember(directMentionIdleDelivery.Recipients)[idleAgentMemberID])
	require.Equal(t, message.AgentDeliveryReasonDirectMention, agentDeliveryReasonsByMember(directMentionIdleDelivery.WakeRecipients)[idleAgentMemberID])
	idleMessages, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: idleAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: idleAgentMemberID}, 10)
	require.NoError(t, err)
	require.Len(t, idleMessages, 1)
	require.Equal(t, directMentionIdleDelivery.MessageID, idleMessages[0].MessageID)

	_, memberBroadcastDelivery, err := store.SendMessageForUser(ctx, ordinaryUser, fixture.WorkspaceID, fixture.AllChannelID, "@all ordinary member broadcast", nil)
	require.NoError(t, err)
	require.NotContains(t, agentDeliveryReasonsByMember(memberBroadcastDelivery.Recipients), wakeAgentMemberID)
	require.NotContains(t, agentDeliveryReasonsByMember(memberBroadcastDelivery.Recipients), idleAgentMemberID)

	publicAgentBroadcast, err := store.SendAgentThreadMessage(ctx, agent.AuthenticatedAgent{AgentID: publicAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: publicAgentMemberID}, message.SendMessageParams{
		TargetRef: "#all",
		Content:   "@online @owner-quote-wake owner public agent cannot enter private context",
	})
	require.NoError(t, err)
	require.NotContains(t, agentDeliveryReasonsByMember(publicAgentBroadcast.Recipients), wakeAgentMemberID)
	require.NotContains(t, agentDeliveryReasonsByMember(publicAgentBroadcast.Recipients), idleAgentMemberID)

	seedChannelScopeCollaboratorGrantForTests(t, pool, fixture.WorkspaceID, fixture.MemberID, fixture.AllChannelID, ordinaryMember.MemberID, fixture.MemberID, "owner_quote_channel_grant", time.Now().UTC().Add(-time.Minute))
	_, collaboratorBroadcastDelivery, err := store.SendMessageForUser(ctx, ordinaryUser, fixture.WorkspaceID, fixture.AllChannelID, "@online collaborator broadcast", nil)
	require.NoError(t, err)
	require.NotContains(t, agentDeliveryReasonsByMember(collaboratorBroadcastDelivery.Recipients), wakeAgentMemberID)
	require.NotContains(t, agentDeliveryReasonsByMember(collaboratorBroadcastDelivery.Recipients), idleAgentMemberID)
	wakeMessages, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: wakeAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: wakeAgentMemberID}, 10)
	require.NoError(t, err)
	require.Len(t, wakeMessages, 1)
	require.Equal(t, directMentionIdleDelivery.MessageID, wakeMessages[0].MessageID)
	ownerSource, _, err := store.SendMessageForUser(ctx, ownerUser, fixture.WorkspaceID, fixture.AllChannelID, "owner source for collaborator quote", nil)
	require.NoError(t, err)
	wakeMessages, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: wakeAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: wakeAgentMemberID}, 10)
	require.NoError(t, err)
	require.Len(t, wakeMessages, 1)
	require.Equal(t, ownerSource.ID, wakeMessages[0].MessageID)
	_, memberQuote, err := store.SendMessageForUserWithType(
		ctx,
		ordinaryUser,
		fixture.WorkspaceID,
		fixture.AllChannelID,
		ownerQuoteTestContent(ownerSource.ID, "collaborator forwarded context", "owner source for collaborator quote"),
		nil,
		MessageTypeOwnerQuote,
	)
	require.NoError(t, err)
	require.Equal(t, message.AgentDeliveryReasonOwnerPrivateContext, agentDeliveryReasonsByMember(memberQuote.Recipients)[wakeAgentMemberID])
	wakeMessages, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: wakeAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: wakeAgentMemberID}, 10)
	require.NoError(t, err)
	require.Len(t, wakeMessages, 1)
	require.Equal(t, memberQuote.MessageID, wakeMessages[0].MessageID)

	outsider := seedWorkspaceMemberFixture(t, pool, fixture.WorkspaceID, "owner_quote_private_outsider", now)
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, fixture.AllChannelID, "all", "", "public", fixture.MemberID, outsider.MemberID, now)
	outsiderUser := model.User{ID: outsider.UserID, Name: "owner_quote_private_outsider", DisplayName: "owner_quote_private_outsider"}
	outsiderSource, _, err := store.SendMessageForUser(ctx, ownerUser, fixture.WorkspaceID, fixture.AllChannelID, "owner source for outsider quote", nil)
	require.NoError(t, err)
	wakeMessages, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: wakeAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: wakeAgentMemberID}, 10)
	require.NoError(t, err)
	require.Len(t, wakeMessages, 1)
	require.Equal(t, outsiderSource.ID, wakeMessages[0].MessageID)
	_, outsiderQuote, err := store.SendMessageForUserWithType(
		ctx,
		outsiderUser,
		fixture.WorkspaceID,
		fixture.AllChannelID,
		ownerQuoteTestContent(outsiderSource.ID, "outsider forwarded context", "owner source for outsider quote"),
		nil,
		MessageTypeOwnerQuote,
	)
	require.NoError(t, err)
	require.NotContains(t, agentDeliveryReasonsByMember(outsiderQuote.Recipients), wakeAgentMemberID)
	wakeMessages, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: wakeAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: wakeAgentMemberID}, 10)
	require.NoError(t, err)
	require.Empty(t, wakeMessages)

	_, initialDelivery, err := store.SendAppMessageForUserWithOptions(
		ctx,
		ownerUser,
		fixture.WorkspaceID,
		fixture.AllChannelID,
		"@owner-quote-wake start private thread",
		nil,
		SendAppMessageOptions{StartAgentThread: true},
	)
	require.NoError(t, err)
	require.NotNil(t, initialDelivery.StartedReplyThread)
	_, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: wakeAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: wakeAgentMemberID}, 10)
	require.NoError(t, err)

	threadSource, _, err := store.SendMessageForUser(ctx, outsiderUser, fixture.WorkspaceID, initialDelivery.StartedReplyThread.ThreadChannelID, "source for owner quote thread", nil)
	require.NoError(t, err)
	wakeMessages, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: wakeAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: wakeAgentMemberID}, 10)
	require.NoError(t, err)
	require.Empty(t, wakeMessages)

	_, threadQuote, err := store.SendMessageForUserWithType(
		ctx,
		ownerUser,
		fixture.WorkspaceID,
		initialDelivery.StartedReplyThread.ThreadChannelID,
		ownerQuoteTestContent(threadSource.ID, "forward selected thread context", "source for owner quote thread @all"),
		nil,
		MessageTypeOwnerQuote,
	)
	require.NoError(t, err)
	require.Equal(t, message.AgentDeliveryReasonOwnerPrivateContext, agentDeliveryReasonsByMember(threadQuote.Recipients)[wakeAgentMemberID])
	wakeMessages, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: wakeAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: wakeAgentMemberID}, 10)
	require.NoError(t, err)
	require.Len(t, wakeMessages, 1)
	require.Equal(t, threadQuote.MessageID, wakeMessages[0].MessageID)

}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 12, 11, 45, 0, 0, time.UTC)

	fixture := seedAppAdminFixture(t, pool, "owner_private_history_filter", now)
	ordinaryMember := seedWorkspaceMemberFixture(t, pool, fixture.WorkspaceID, "owner_private_history_member", now)
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, fixture.AllChannelID, "all", "", "public", fixture.MemberID, ordinaryMember.MemberID, now)
	collaborator := seedWorkspaceMemberFixture(t, pool, fixture.WorkspaceID, "owner_private_history_collab", now)
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, fixture.AllChannelID, "all", "", "public", fixture.MemberID, collaborator.MemberID, now)

	const (
		targetMachineID     = "mach_history_private_target"
		targetAgentID       = "agt_history_private_target"
		targetAgentMemberID = "mem_history_private_target"
		peerMachineID       = "mach_history_private_peer"
		peerAgentID         = "agt_history_private_peer"
		peerAgentMemberID   = "mem_history_private_peer"
		publicMachineID     = "mach_history_public_agent"
		publicAgentID       = "agt_history_public_agent"
		publicAgentMemberID = "mem_history_public_agent"
	)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, targetMachineID, targetAgentID, targetAgentMemberID, "history-private-target", now)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, peerMachineID, peerAgentID, peerAgentMemberID, "history-private-peer", now)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, publicMachineID, publicAgentID, publicAgentMemberID, "history-public-agent", now)

	ownerUser := model.User{ID: fixture.UserID, Name: "owner_private_history_filter", DisplayName: "owner_private_history_filter"}
	_, initialDelivery, err := store.SendAppMessageForUserWithOptions(ctx, ownerUser, fixture.WorkspaceID, fixture.AllChannelID, "@history-private-target start history thread", nil, SendAppMessageOptions{StartAgentThread: true})
	require.NoError(t, err)
	require.NotNil(t, initialDelivery.StartedReplyThread)
	_, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: targetAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: targetAgentMemberID}, 10)
	require.NoError(t, err)

	publicReply, err := store.SendAgentThreadMessage(ctx, agent.AuthenticatedAgent{AgentID: publicAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: publicAgentMemberID}, message.SendMessageParams{
		TargetRef: "#all:" + initialDelivery.MessageID[:8],
		Content:   "owner public agent should stay outside private history",
	})
	require.NoError(t, err)

	ordinaryUser := model.User{ID: ordinaryMember.UserID, Name: "owner_private_history_member", DisplayName: "owner_private_history_member"}
	_, ordinaryReply, err := store.SendMessageForUser(ctx, ordinaryUser, fixture.WorkspaceID, initialDelivery.StartedReplyThread.ThreadChannelID, "ordinary member should stay outside private history", nil)
	require.NoError(t, err)

	_, mentionPeerDelivery, err := store.SendMessageForUser(ctx, ownerUser, fixture.WorkspaceID, initialDelivery.StartedReplyThread.ThreadChannelID, "@history-private-peer add peer to private context", nil)
	require.NoError(t, err)
	require.Equal(t, message.AgentDeliveryReasonDirectMention, agentDeliveryReasonsByMember(mentionPeerDelivery.Recipients)[peerAgentMemberID])
	_, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: peerAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: peerAgentMemberID}, 10)
	require.NoError(t, err)

	peerReply, err := store.SendAgentThreadMessage(ctx, agent.AuthenticatedAgent{AgentID: peerAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: peerAgentMemberID}, message.SendMessageParams{
		TargetRef: "#all:" + initialDelivery.MessageID[:8],
		Content:   "same context private peer should be visible",
	})
	require.NoError(t, err)

	seedThreadScopeCollaboratorGrantForTests(t, pool, fixture.WorkspaceID, fixture.MemberID, initialDelivery.StartedReplyThread.ThreadChannelID, collaborator.MemberID, fixture.MemberID, "history_collaborator_grant", time.Now().UTC().Add(-time.Minute))
	_, collaboratorReply, err := store.SendMessageForUser(ctx, model.User{ID: collaborator.UserID, Name: "owner_private_history_collab", DisplayName: "owner_private_history_collab"}, fixture.WorkspaceID, initialDelivery.StartedReplyThread.ThreadChannelID, "collaborator should be visible in grant window", nil)
	require.NoError(t, err)

	history, err := message.NewScopeAPIService(store.messageStore()).ListHistory(ctx, message.ScopeAPIHistoryRequest{
		WorkspaceID:    fixture.WorkspaceID,
		ScopeID:        initialDelivery.StartedReplyThread.ThreadChannelID,
		ViewerMemberID: targetAgentMemberID,
		ViewerType:     message.ScopeActorPrivateAgent,
		RequestKind:    message.ScopeVisibilityRequestHistory,
		Limit:          50,
	})
	require.NoError(t, err)
	historyIDs := historyMessageIDs(history.Messages)
	require.Contains(t, historyIDs, mentionPeerDelivery.MessageID)
	require.Contains(t, historyIDs, peerReply.MessageID)
	require.Contains(t, historyIDs, collaboratorReply.MessageID)
	require.NotContains(t, historyIDs, publicReply.MessageID)
	require.NotContains(t, historyIDs, ordinaryReply.MessageID)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 17, 14, 45, 0, 0, time.UTC)

	fixture := seedAppAdminFixture(t, pool, "read_resolver_no_thread_create", now)
	const (
		machineID     = "mach_read_resolver_no_thread_create"
		agentID       = "agt_read_resolver_no_thread_create"
		agentMemberID = "mem_agent_read_resolver_no_thread_create"
		agentName     = "read-resolver-private"
	)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, machineID, agentID, agentMemberID, agentName, now)
	ownerUser := model.User{ID: fixture.UserID, Name: "read_resolver_no_thread_create", DisplayName: "read_resolver_no_thread_create"}
	authenticatedAgent := agent.AuthenticatedAgent{
		AgentID:     agentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    agentMemberID,
		MachineID:   machineID,
	}

	readScopeID, err := store.ResolveReadableAgentTargetScopeID(ctx, authenticatedAgent, "#all")
	require.NoError(t, err)
	require.Equal(t, fixture.AllChannelID, readScopeID)
	channelTarget, err := store.ResolveReadableAgentMessageTarget(ctx, authenticatedAgent, "#all")
	require.NoError(t, err)
	require.Equal(t, access.ScopeTargetOperationMessageRead, channelTarget.Operation)
	require.Equal(t, access.ScopeTargetKindChannel, channelTarget.Requested.Kind)
	require.Equal(t, fixture.AllChannelID, channelTarget.ReadScope.ScopeID)

	parent, parentDelivery, err := store.SendAppMessageForUserWithOptions(
		ctx,
		ownerUser,
		fixture.WorkspaceID,
		fixture.AllChannelID,
		"@read-resolver-private visible parent without a topic",
		nil,
		SendAppMessageOptions{},
	)
	require.NoError(t, err)
	require.Nil(t, parentDelivery.StartedReplyThread)

	_, err = store.ResolveReadableAgentTargetScopeID(ctx, authenticatedAgent, "#all:"+parent.ID[:8])
	require.Error(t, err)

	var createdThreads int
	require.NoError(t, pool.QueryRow(ctx, `
	SELECT COUNT(*)
	FROM scopes
	WHERE workspace_id = $1
	  AND type = 'thread'
	  AND parent_scope_id = $2
	  AND root_message_id = $3
	`, fixture.WorkspaceID, fixture.AllChannelID, parent.ID).Scan(&createdThreads))
	require.Equal(t, 0, createdThreads)

	threadRoot, threadDelivery, err := store.SendAppMessageForUserWithOptions(
		ctx,
		ownerUser,
		fixture.WorkspaceID,
		fixture.AllChannelID,
		"@read-resolver-private visible parent with a topic",
		nil,
		SendAppMessageOptions{StartAgentThread: true},
	)
	require.NoError(t, err)
	require.NotNil(t, threadDelivery.StartedReplyThread)

	threadScopeID, err := store.ResolveReadableAgentTargetScopeID(ctx, authenticatedAgent, "#all:"+threadRoot.ID[:8])
	require.NoError(t, err)
	require.Equal(t, threadDelivery.StartedReplyThread.ThreadChannelID, threadScopeID)
	threadTarget, err := store.ResolveReadableAgentMessageTarget(ctx, authenticatedAgent, "#all:"+threadRoot.ID[:8])
	require.NoError(t, err)
	require.Equal(t, access.ScopeTargetKindThread, threadTarget.Requested.Kind)
	require.Equal(t, threadDelivery.StartedReplyThread.ThreadChannelID, threadTarget.ReadScope.ScopeID)
	require.Equal(t, fixture.AllChannelID, threadTarget.ParentScope.ScopeID)
	require.Equal(t, access.ScopePermissionBasisOwnerPrivateMembership, threadTarget.PermissionBasis[0].Kind)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 21, 16, 20, 0, 0, time.UTC)

	fixture := seedAppAdminFixture(t, pool, "read_resolver_dm_underscore", now)
	const (
		machineID      = "mach_read_resolver_dm_underscore"
		agentID        = "agt_read_resolver_dm_underscore"
		agentMemberID  = "mem_agent_read_resolver_dm_underscore"
		agentPublicRef = "dm-history-agent"
	)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, machineID, agentID, agentMemberID, agentPublicRef, now)
	authenticatedAgent := agent.AuthenticatedAgent{
		AgentID:     agentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    agentMemberID,
		MachineID:   machineID,
	}

	dmTarget, err := message.EnsureDMThread(ctx, pool, message.MemberActor{
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    fixture.MemberID,
	}, agentPublicRef)
	require.NoError(t, err)
	require.Equal(t, domain.ThreadTypeDM, dmTarget.ThreadType)
	require.Equal(t, agentMemberID, dmTarget.PeerMemberID)

	ownerUser := model.User{ID: fixture.UserID, Name: "read_resolver_dm_underscore", DisplayName: "read_resolver_dm_underscore"}
	appMessage, _, err := store.SendMessageForUser(ctx, ownerUser, fixture.WorkspaceID, dmTarget.DMThreadID, "history should be readable through exact DM target", nil)
	require.NoError(t, err)

	exactTarget := "dm:@" + fixture.MemberID
	scopeID, err := store.ResolveReadableAgentTargetScopeID(ctx, authenticatedAgent, exactTarget)
	require.NoError(t, err)
	require.Equal(t, dmTarget.DMThreadID, scopeID)

	history, err := message.NewScopeAPIService(store.messageStore()).ListHistory(ctx, message.ScopeAPIHistoryRequest{
		WorkspaceID:    fixture.WorkspaceID,
		ScopeID:        scopeID,
		ViewerMemberID: agentMemberID,
		ViewerType:     message.ScopeActorPrivateAgent,
		RequestKind:    message.ScopeVisibilityRequestHistory,
		Limit:          10,
	})
	require.NoError(t, err)
	require.Contains(t, historyMessageIDs(history.Messages), appMessage.ID)

	replyThread, err := message.OpenReplyThreadForMember(ctx, pool, fixture.MemberID, fixture.WorkspaceID, dmTarget.DMThreadID, appMessage.ID)
	require.NoError(t, err)
	require.NotEmpty(t, replyThread.ThreadChannelID)

	dmReplyTarget := exactTarget + ":" + appMessage.ID[:8]
	writableScopeID, err := store.ResolveWritableAgentTargetScopeID(ctx, authenticatedAgent, dmReplyTarget)
	require.NoError(t, err)
	require.Equal(t, replyThread.ThreadChannelID, writableScopeID)
	writableTarget, err := store.ResolveWritableAgentMessageTarget(ctx, authenticatedAgent, dmReplyTarget)
	require.NoError(t, err)
	require.Equal(t, access.ScopeTargetOperationMessageSend, writableTarget.Operation)
	require.Equal(t, access.ScopeTargetKindThread, writableTarget.Requested.Kind)
	require.Equal(t, replyThread.ThreadChannelID, writableTarget.StorageScope.ScopeID)
	require.Equal(t, replyThread.ThreadChannelID, writableTarget.DeliveryScope.ScopeID)
	require.Equal(t, dmTarget.DMThreadID, writableTarget.ParentScope.ScopeID)
	require.Equal(t, access.ScopeTargetKindDM, writableTarget.ParentScope.ScopeType)
	require.Equal(t, appMessage.ID, writableTarget.RootMessageID)
	require.Equal(t, access.ScopePermissionBasisParentDMParticipant, writableTarget.PermissionBasis[0].Kind)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	teamStore := team.New(pool)
	now := time.Date(2026, 7, 3, 19, 35, 0, 0, time.UTC)

	fixture := seedAppAdminFixture(t, pool, "agent_team_target_resolve", now)
	const (
		machineID     = "mach_agent_team_target_resolve_v2"
		agentID       = "agt_agent_team_target_resolve_v2"
		agentMemberID = "mem_agent_team_target_resolve_v2"
		agentName     = "agent-team-target-resolve-v2"
		teamID        = "team_abcteamres01xy"
	)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, machineID, agentID, agentMemberID, agentName, now)

	team, err := teamStore.CreateTeam(ctx, team.CreateTeamParams{
		ID:          teamID,
		WorkspaceID: fixture.WorkspaceID,
		Handle:      "agent-team-target-resolve",
		DisplayName: "Agent Team Target Resolve",
		InitialMembers: []team.AddTeamMemberParams{{
			ID:        "wtm_agent_team_target_resolve_v2",
			ActorKind: team.ActorKindAgent,
			ActorID:   agentMemberID,
			Role:      team.MemberRoleLeader,
			RoleLabel: "Lead",
		}},
		CreatedByMemberID: fixture.MemberID,
		Now:               now.Add(time.Minute),
	})
	require.NoError(t, err)
	team, err = teamStore.EnsureTeamChannel(ctx, fixture.WorkspaceID, team.ID)
	require.NoError(t, err)
	require.NotEmpty(t, team.ChannelID)

	authenticatedAgent := agent.AuthenticatedAgent{
		AgentID:     agentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    agentMemberID,
		MachineID:   machineID,
	}
	teamTarget := "#" + team.ID

	writableScopeID, err := store.ResolveWritableAgentTargetScopeID(ctx, authenticatedAgent, teamTarget)
	require.NoError(t, err)
	require.Equal(t, team.ChannelID, writableScopeID)
	readableScopeID, err := store.ResolveReadableAgentTargetScopeID(ctx, authenticatedAgent, teamTarget)
	require.NoError(t, err)
	require.Equal(t, team.ChannelID, readableScopeID)

	resolved, err := store.ResolveWritableAgentMessageTarget(ctx, authenticatedAgent, teamTarget)
	require.NoError(t, err)
	require.Equal(t, access.ScopeTargetKindChannel, resolved.Requested.Kind)
	require.Equal(t, teamTarget, resolved.Requested.Ref)
	require.Equal(t, team.ChannelID, resolved.StorageScope.ScopeID)
	require.Equal(t, team.ChannelID, resolved.DeliveryScope.ScopeID)
	require.Equal(t, access.ScopePermissionBasisDirectMembership, resolved.PermissionBasis[0].Kind)

	delivery, err := store.SendAgentThreadMessage(ctx, authenticatedAgent, message.SendMessageParams{
		TargetRef: teamTarget,
		Content:   "team target reply should land in Team channel",
	})
	require.NoError(t, err)
	require.Equal(t, team.ChannelID, delivery.ThreadID)
	require.NotNil(t, delivery.AppMessage)
	require.Equal(t, team.ChannelID, delivery.AppMessage.ChannelID)

	history, err := message.NewScopeAPIService(store.messageStore()).ListHistory(ctx, message.ScopeAPIHistoryRequest{
		WorkspaceID:    fixture.WorkspaceID,
		ScopeID:        readableScopeID,
		ViewerMemberID: agentMemberID,
		ViewerType:     message.ScopeActorAgent,
		RequestKind:    message.ScopeVisibilityRequestHistory,
		Limit:          10,
	})
	require.NoError(t, err)
	require.Contains(t, historyMessageIDs(history.Messages), delivery.MessageID)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 21, 16, 45, 0, 0, time.UTC)

	fixture := seedAppAdminFixture(t, pool, "dm_thread_auto_wake", now)
	const (
		machineID      = "mach_dm_thread_auto_wake"
		agentID        = "agt_dm_thread_auto_wake"
		agentMemberID  = "mem_agent_dm_thread_auto_wake"
		agentPublicRef = "dm-thread-auto-wake-agent"
	)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, machineID, agentID, agentMemberID, agentPublicRef, now)
	targetAuth := agent.AuthenticatedAgent{
		AgentID:     agentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    agentMemberID,
	}

	dmTarget, err := message.EnsureDMThread(ctx, pool, message.MemberActor{
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    fixture.MemberID,
	}, agentPublicRef)
	require.NoError(t, err)
	require.Equal(t, domain.ThreadTypeDM, dmTarget.ThreadType)
	require.Equal(t, agentMemberID, dmTarget.PeerMemberID)

	ownerUser := model.User{ID: fixture.UserID, Name: "dm_thread_auto_wake", DisplayName: "dm_thread_auto_wake"}
	rootMessage, _, err := store.SendMessageForUser(ctx, ownerUser, fixture.WorkspaceID, dmTarget.DMThreadID, "dm root for auto-wake thread", nil)
	require.NoError(t, err)

	initialMessages, err := store.ReceiveAgentMessages(ctx, targetAuth, 10)
	require.NoError(t, err)
	require.Len(t, initialMessages, 1)
	require.Equal(t, rootMessage.ID, initialMessages[0].MessageID)

	replyThread, err := message.OpenReplyThreadForMember(ctx, pool, fixture.MemberID, fixture.WorkspaceID, dmTarget.DMThreadID, rootMessage.ID)
	require.NoError(t, err)
	require.NotEmpty(t, replyThread.ThreadChannelID)

	threadReply, delivery, err := store.SendMessageForUser(ctx, ownerUser, fixture.WorkspaceID, replyThread.ThreadChannelID, "first dm thread reply should auto-wake peer agent", nil)
	require.NoError(t, err)
	require.Equal(t, message.AgentDeliveryReasonDMPeer, agentDeliveryReasonsByMember(delivery.Recipients)[agentMemberID])

	threadMessages, err := store.ReceiveAgentMessages(ctx, targetAuth, 10)
	require.NoError(t, err)
	require.Len(t, threadMessages, 1)
	require.Equal(t, threadReply.ID, threadMessages[0].MessageID)
	rootMetadata, ok := threadMessages[0].Metadata[message.ReceiveMetadataReplyThreadRootKey].(map[string]any)
	require.True(t, ok)
	require.Equal(t, rootMessage.ID, rootMetadata["messageId"])
	require.Equal(t, "dm root for auto-wake thread", rootMetadata["content"])

	var membershipCount int64
	require.NoError(t, 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
`, replyThread.ThreadChannelID, agentMemberID).Scan(&membershipCount))
	require.EqualValues(t, 1, membershipCount)

	var notificationState string
	require.NoError(t, pool.QueryRow(ctx, `
SELECT state
FROM scope_notification_settings
WHERE scope_id = $1
  AND member_id = $2
`, replyThread.ThreadChannelID, agentMemberID).Scan(&notificationState))
	require.Equal(t, "auto-wake", notificationState)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 12, 10, 40, 0, 0, time.UTC)

	fixture := seedAppAdminFixture(t, pool, "agent_private_mentions", now)
	ownerUser := model.User{ID: fixture.UserID, Name: "agent_private_mentions", DisplayName: "agent_private_mentions"}
	for i := 0; i < 3; i++ {
		_, _, err := store.SendMessageForUser(ctx, ownerUser, fixture.WorkspaceID, fixture.AllChannelID, fmt.Sprintf("parent channel seq padding %d", i), nil)
		require.NoError(t, err)
	}
	const (
		authorMachineID     = "mach_agent_private_sender"
		authorAgentID       = "agt_agent_private_sender"
		authorAgentMemberID = "mem_agent_private_sender"
		targetMachineID     = "mach_agent_private_target"
		targetAgentID       = "agt_agent_private_target"
		targetAgentMemberID = "mem_agent_private_target"
		crossMachineID      = "mach_agent_private_cross"
		crossAgentID        = "agt_agent_private_cross"
		crossAgentMemberID  = "mem_agent_private_cross"
	)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, authorMachineID, authorAgentID, authorAgentMemberID, "agent-private-sender", now)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, targetMachineID, targetAgentID, targetAgentMemberID, "agent-private-target", now)
	crossOwner := seedWorkspaceMemberFixture(t, pool, fixture.WorkspaceID, "agent_private_cross_owner", now)
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, fixture.AllChannelID, "all", "", "public", fixture.MemberID, crossOwner.MemberID, now)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, crossOwner.UserID, crossOwner.MemberID, fixture.AllChannelID, crossMachineID, crossAgentID, crossAgentMemberID, "agent-private-cross", now)

	_, initialDelivery, err := store.SendAppMessageForUserWithOptions(
		ctx,
		ownerUser,
		fixture.WorkspaceID,
		fixture.AllChannelID,
		"@agent-private-sender starting private agent context",
		nil,
		SendAppMessageOptions{StartAgentThread: true},
	)
	require.NoError(t, err)
	require.NotNil(t, initialDelivery.StartedReplyThread)
	_, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{
		AgentID:     authorAgentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    authorAgentMemberID,
	}, 10)
	require.NoError(t, err)

	directMentionReply, err := store.SendAgentThreadMessage(ctx, agent.AuthenticatedAgent{
		AgentID:     authorAgentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    authorAgentMemberID,
	}, message.SendMessageParams{
		TargetRef: "#all:" + initialDelivery.MessageID[:8],
		Content:   "@agent-private-target please join",
	})
	require.NoError(t, err)
	reasons := agentDeliveryReasonsByMember(directMentionReply.Recipients)
	require.Equal(t, message.AgentDeliveryReasonDirectMention, reasons[targetAgentMemberID])
	require.NotContains(t, reasons, authorAgentMemberID)
	require.NotNil(t, directMentionReply.AppMessage)
	require.Equal(t, []any{map[string]any{
		"type":  "agent",
		"id":    targetAgentID,
		"token": "agent-private-target",
	}}, directMentionReply.AppMessage.Metadata[message.MentionMetadataKey])

	ownerThreadPage, err := store.ListMessagesForUser(ctx, fixture.UserID, fixture.WorkspaceID, initialDelivery.StartedReplyThread.ThreadChannelID, 20, nil, nil)
	require.NoError(t, err)
	listedDirectMention := requireListedAppMessage(t, ownerThreadPage.Messages, directMentionReply.MessageID)
	require.Equal(t, []message.ResolvedMention{{
		Token:         "agent-private-target",
		Type:          "agent",
		ID:            targetAgentID,
		DisplayName:   "agent-private-target",
		ResolvedToken: "agent-private-target",
	}}, listedDirectMention.ResolvedMentions)

	targetAuth := agent.AuthenticatedAgent{AgentID: targetAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: targetAgentMemberID}
	targetMessages, err := store.ReceiveAgentMessages(ctx, targetAuth, 10)
	require.NoError(t, err)
	require.Len(t, targetMessages, 1)
	require.Equal(t, directMentionReply.MessageID, targetMessages[0].MessageID)
	rootMetadata, ok := targetMessages[0].Metadata[message.ReceiveMetadataReplyThreadRootKey].(map[string]any)
	require.True(t, ok)
	require.Equal(t, initialDelivery.MessageID, rootMetadata["messageId"])
	require.Equal(t, "@agent-private-sender starting private agent context", rootMetadata["content"])

	targetHistory, err := message.NewScopeAPIService(store.messageStore()).ListHistory(ctx, message.ScopeAPIHistoryRequest{
		WorkspaceID:            fixture.WorkspaceID,
		ScopeID:                initialDelivery.StartedReplyThread.ThreadChannelID,
		ViewerMemberID:         targetAuth.MemberID,
		ViewerType:             message.ScopeActorPrivateAgent,
		RequestKind:            message.ScopeVisibilityRequestHistory,
		Limit:                  10,
		IncludeReplyThreadRoot: true,
		AdvanceReadState:       true,
	})
	require.NoError(t, err)
	targetHistoryIDs := historyMessageIDs(targetHistory.Messages)
	require.Contains(t, targetHistoryIDs, initialDelivery.MessageID)
	require.Contains(t, targetHistoryIDs, directMentionReply.MessageID)

	var rootCreatedAt time.Time
	require.NoError(t, pool.QueryRow(ctx, `SELECT created_at FROM scope_messages WHERE id = $1`, initialDelivery.MessageID).Scan(&rootCreatedAt))
	var directMentionThreadSeq int64
	var directMentionCreatedAt time.Time
	require.NoError(t, pool.QueryRow(ctx, `SELECT seq, created_at FROM scope_messages WHERE id = $1`, directMentionReply.MessageID).Scan(&directMentionThreadSeq, &directMentionCreatedAt))
	require.True(t, rootCreatedAt.Before(directMentionCreatedAt) || rootCreatedAt.Equal(directMentionCreatedAt))
	var targetLastReadSeq int64
	require.NoError(t, pool.QueryRow(ctx, `
SELECT last_read_seq
FROM scope_memberships
WHERE member_id = $1 AND scope_id = $2
`, targetAgentMemberID, initialDelivery.StartedReplyThread.ThreadChannelID).Scan(&targetLastReadSeq))
	require.Equal(t, directMentionThreadSeq, targetLastReadSeq)

	targetReply, err := store.SendAgentThreadMessage(ctx, targetAuth, message.SendMessageParams{
		TargetRef: "#all:" + initialDelivery.MessageID[:8],
		Content:   "target can read and reply after private mention subscription",
	})
	require.NoError(t, err)
	require.NotEmpty(t, targetReply.MessageID)

	mentionThreadChannelID := "chn_agent_private_mentions_public_root"
	mentionThreadShortID := "a1b2c3d4"
	seedPrivateWakeReplyThread(t, pool, fixture, mentionThreadChannelID, mentionThreadShortID, now)
	mustExec(t, pool, `
UPDATE scope_messages
SET author_member_id = $2,
    author_type = 'human',
    updated_at = $3
WHERE id = $1
`, "msg_"+mentionThreadChannelID, crossOwner.MemberID, now)
	_, ownerThreadMention, err := store.SendMessageForUser(ctx, ownerUser, fixture.WorkspaceID, mentionThreadChannelID, "@agent-private-target owner mentions private agent in someone else's thread", nil)
	require.NoError(t, err)
	require.Equal(t, message.AgentDeliveryReasonDirectMention, agentDeliveryReasonsByMember(ownerThreadMention.Recipients)[targetAgentMemberID])
	targetMessages, err = store.ReceiveAgentMessages(ctx, targetAuth, 10)
	require.NoError(t, err)
	require.Len(t, targetMessages, 1)
	require.Equal(t, ownerThreadMention.MessageID, targetMessages[0].MessageID)
	require.NotContains(t, targetMessages[0].Metadata, message.ReceiveMetadataReplyThreadRootKey)

	targetMentionThreadHistory, err := message.NewScopeAPIService(store.messageStore()).ListHistory(ctx, message.ScopeAPIHistoryRequest{
		WorkspaceID:    fixture.WorkspaceID,
		ScopeID:        mentionThreadChannelID,
		ViewerMemberID: targetAuth.MemberID,
		ViewerType:     message.ScopeActorPrivateAgent,
		RequestKind:    message.ScopeVisibilityRequestHistory,
		Limit:          10,
	})
	require.NoError(t, err)
	targetMentionThreadHistoryIDs := historyMessageIDs(targetMentionThreadHistory.Messages)
	require.NotContains(t, targetMentionThreadHistoryIDs, "msg_"+mentionThreadChannelID)
	require.Contains(t, targetMentionThreadHistoryIDs, ownerThreadMention.MessageID)

	targetMentionThreadReply, err := store.SendAgentThreadMessage(ctx, targetAuth, message.SendMessageParams{
		TargetRef: "#all:" + mentionThreadShortID,
		Content:   "private agent can reply to thread after private mention without reading the root",
	})
	require.NoError(t, err)
	require.NotEmpty(t, targetMentionThreadReply.MessageID)

	contextFollowup, err := store.SendAgentThreadMessage(ctx, agent.AuthenticatedAgent{
		AgentID:     authorAgentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    authorAgentMemberID,
	}, message.SendMessageParams{
		TargetRef: "#all:" + initialDelivery.MessageID[:8],
		Content:   "same-owner private context follow-up",
	})
	require.NoError(t, err)
	reasons = agentDeliveryReasonsByMember(contextFollowup.Recipients)
	require.Equal(t, message.AgentDeliveryReasonOwnerPrivateContext, reasons[targetAgentMemberID])
	require.NotContains(t, reasons, authorAgentMemberID)
	targetMessages, err = store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{
		AgentID:     targetAgentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    targetAgentMemberID,
	}, 10)
	require.NoError(t, err)
	require.Len(t, targetMessages, 1)
	require.Equal(t, contextFollowup.MessageID, targetMessages[0].MessageID)
	require.NotContains(t, targetMessages[0].Metadata, message.ReceiveMetadataReplyThreadRootKey)

	crossOwnerMention, err := store.SendAgentThreadMessage(ctx, agent.AuthenticatedAgent{
		AgentID:     authorAgentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    authorAgentMemberID,
	}, message.SendMessageParams{
		TargetRef: "#all:" + initialDelivery.MessageID[:8],
		Content:   "@agent-private-cross should not cross owner boundary",
	})
	require.NoError(t, err)
	reasons = agentDeliveryReasonsByMember(crossOwnerMention.Recipients)
	require.NotContains(t, reasons, crossAgentMemberID)

	crossMessages, err := store.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{
		AgentID:     crossAgentID,
		WorkspaceID: fixture.WorkspaceID,
		MemberID:    crossAgentMemberID,
	}, 10)
	require.NoError(t, err)
	require.Empty(t, crossMessages)
}

func ownerQuoteTestContent(sourceMessageID string, instruction string, quoted string) string {
	return ownerQuoteTestContentForSources(instruction, quoted, sourceMessageID)
}

func ownerQuoteTestContentForSources(instruction string, quoted string, sourceMessageIDs ...string) string {
	sourceRefs := ""
	quotedHeaders := ""
	for i, sourceMessageID := range sourceMessageIDs {
		sourceRefs += fmt.Sprintf("- source_message_id=%s\n", sourceMessageID)
		quotedHeaders += fmt.Sprintf("[%d] author=@source message_id=%s created_at=2026-05-12T00:00:00Z\n", i+1, sourceMessageID)
	}
	return fmt.Sprintf(`Owner quote

Instruction:
%s

Source message refs:
%s
Quoted messages:
%s%s
`, instruction, sourceRefs, quotedHeaders, quoted)
}

func historyMessageIDs(messages []message.HistoryMessage) []string {
	ids := make([]string, 0, len(messages))
	for _, item := range messages {
		ids = append(ids, item.ID)
	}
	return ids
}

func requireListedAppMessage(t *testing.T, messages []message.AppMessage, messageID string) message.AppMessage {
	t.Helper()

	for _, appMessage := range messages {
		if appMessage.ID == messageID {
			return appMessage
		}
	}
	require.Failf(t, "message not listed", "message %s was not listed", messageID)
	return message.AppMessage{}
}

func seedAppAgentParticipant(t *testing.T, pool coredb.QueryExecer, workspaceID string, userID string, ownerMemberID string, channelID string, machineID string, agentID string, agentMemberID string, agentName string, now time.Time) {
	t.Helper()

	mustExec(t, pool, `
INSERT IGNORE INTO machines (id, workspace_id, created_by_user_id, name, api_key, created_at, updated_at)
VALUES ($1, $2, $3, 'test-machine', $4, $5, $5)
`, machineID, workspaceID, userID, machineID+"-key", now)
	mustExec(t, pool, `
INSERT INTO members (id, workspace_id, type, display_name, created_at, updated_at)
VALUES ($1, $2, 'agent', $3, $4, $4)
`, agentMemberID, workspaceID, agentName, now)
	mustExec(t, pool, `
INSERT INTO agents (
  id, workspace_id, machine_id, member_id, name, runtime, model, description,
  auth_token_hash, auto_wake_enabled, status, activity, activity_detail,
  created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, 'codex', 'gpt-5-codex', '', NULL, TRUE, 'active', 'sleeping', '', $6, $6)
`, agentID, workspaceID, machineID, agentMemberID, agentName, now)
	seedChannelParticipantProjectionForTests(t, pool, workspaceID, channelID, "all", "", "public", ownerMemberID, agentMemberID, now)
	require.NoError(t, message.EnsureScopeMembership(context.Background(), pool, channelID, agentMemberID, "public", "", "auto-wake", 0, 0, now))
}

func seedAppPrivateAgentEntry(t *testing.T, pool coredb.QueryExecer, workspaceID string, userID string, ownerMemberID string, channelID string, machineID string, agentID string, agentMemberID string, agentName string, now time.Time) {
	t.Helper()

	mustExec(t, pool, `
INSERT IGNORE INTO machines (id, workspace_id, created_by_user_id, name, api_key, created_at, updated_at)
VALUES ($1, $2, $3, 'test-private-machine', $4, $5, $5)
`, machineID, workspaceID, userID, machineID+"-key", now)
	mustExec(t, pool, `
INSERT INTO members (id, workspace_id, type, display_name, created_at, updated_at)
VALUES ($1, $2, 'agent', $3, $4, $4)
`, agentMemberID, workspaceID, agentName, now)
	mustExec(t, pool, `
INSERT INTO agents (
  id, workspace_id, machine_id, member_id, name, runtime, model, description,
  auth_token_hash, auto_wake_enabled, status, activity, activity_detail,
  created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, 'codex', 'gpt-5-codex', '', NULL, TRUE, 'active', 'sleeping', '', $6, $6)
`, agentID, workspaceID, machineID, agentMemberID, agentName, now)
	require.NoError(t, message.EnsureScopeMembership(context.Background(), pool, channelID, agentMemberID, "private", ownerMemberID, "", 0, 0, now))
}

func seedChannelScopeCollaboratorGrantForTests(t *testing.T, pool coredb.QueryExecer, workspaceID string, ownerMemberID string, channelID string, collaboratorMemberID string, addedByMemberID string, suffix string, addedAt time.Time) {
	t.Helper()

	mustExec(t, pool, `
INSERT INTO task_scope_executions (
  id, workspace_id, owner_member_id, scope_type, scope_id,
  created_at, updated_at
) VALUES ($1, $2, $3, 'channel', $4, $5, $5)
`, "tse_"+suffix, workspaceID, ownerMemberID, channelID, addedAt)
	mustExec(t, pool, `
INSERT INTO task_scope_participant_metadata (
  id, owner_scope_execution_id, member_id, active_key, role, added_by_member_id,
  added_at, add_notification_state, created_at, updated_at
) VALUES ($1, $2, $3, $4, 'contributor', $5, $6, 'none', $6, $6)
`, "tspm_"+suffix, "tse_"+suffix, collaboratorMemberID, "tspm_active_"+suffix, addedByMemberID, addedAt)
}

func seedThreadScopeCollaboratorGrantForTests(t *testing.T, pool coredb.QueryExecer, workspaceID string, ownerMemberID string, threadChannelID string, collaboratorMemberID string, addedByMemberID string, suffix string, addedAt time.Time) {
	t.Helper()

	mustExec(t, pool, `
INSERT INTO task_scope_executions (
  id, workspace_id, owner_member_id, scope_type, scope_id,
  created_at, updated_at
) VALUES ($1, $2, $3, 'thread', $4, $5, $5)
`, "tse_"+suffix, workspaceID, ownerMemberID, threadChannelID, addedAt)
	mustExec(t, pool, `
INSERT INTO task_scope_participant_metadata (
  id, owner_scope_execution_id, member_id, active_key, role, added_by_member_id,
  added_at, add_notification_state, created_at, updated_at
) VALUES ($1, $2, $3, $4, 'contributor', $5, $6, 'none', $6, $6)
`, "tspm_"+suffix, "tse_"+suffix, collaboratorMemberID, "tspm_active_"+suffix, addedByMemberID, addedAt)
}

func requireScopeMessageAndSingleFanout(t *testing.T, pool coredb.QueryExecer, messageID string, workspaceID string, scopeID string) {
	t.Helper()

	var messageCount int
	require.NoError(t, pool.QueryRow(context.Background(), `
SELECT COUNT(*)
FROM scope_messages
WHERE id = $1
  AND workspace_id = $2
  AND scope_id = $3
`, messageID, workspaceID, scopeID).Scan(&messageCount))
	require.Equal(t, 1, messageCount)

	var outboxCount int
	require.NoError(t, pool.QueryRow(context.Background(), `
SELECT COUNT(*)
FROM scope_fanout_outbox
WHERE message_id = $1
  AND event_ref_type = 'message'
  AND event_ref_id = $1
  AND fanout_type = 'scope_fanout'
	`, messageID).Scan(&outboxCount))
	require.Equal(t, 1, outboxCount)
	var payload []byte
	require.NoError(t, pool.QueryRow(context.Background(), `
SELECT payload
FROM scope_fanout_outbox
WHERE message_id = $1
  AND event_ref_type = 'message'
  AND event_ref_id = $1
  AND fanout_type = 'scope_fanout'
LIMIT 1
	`, messageID).Scan(&payload))
	fanoutPayload, err := message.UnmarshalScopeFanoutPayload(payload)
	require.NoError(t, err)
	require.Equal(t, messageID, fanoutPayload.AppMessage.ID)
	require.Equal(t, messageID, fanoutPayload.Delivery.MessageID)
}

func requireScopeFanoutPayloadSource(t *testing.T, pool coredb.QueryExecer, messageID string, source message.ContributionSource) {
	t.Helper()

	fanoutPayload := requireScopeFanoutPayloadForMessage(t, pool, messageID)
	require.Equal(t, source.Normalize(), fanoutPayload.AppMessage.Source)
}

func requireScopeFanoutPayloadForMessage(t *testing.T, pool coredb.QueryExecer, messageID string) message.ScopeFanoutPayload {
	t.Helper()

	var payload []byte
	require.NoError(t, pool.QueryRow(context.Background(), `
SELECT payload
FROM scope_fanout_outbox
WHERE message_id = $1
  AND fanout_type = 'scope_fanout'
LIMIT 1
`, messageID).Scan(&payload))
	fanoutPayload, err := message.UnmarshalScopeFanoutPayload(payload)
	require.NoError(t, err)
	return fanoutPayload
}

// seedLegacyTaskDispatchMessageForTest bypasses the active message append
// boundary intentionally. It models a hidden row already committed by an old
// node so read-compatibility tests do not reopen a production writer.
func seedLegacyTaskDispatchMessageForTest(t *testing.T, ctx context.Context, db coredb.QueryExecer, workspaceID string, scopeID string, authorMemberID string, content string) message.ScopeMessageRecord {
	t.Helper()

	var latestSeq int64
	var latestGlobalSeq int64
	require.NoError(t, db.QueryRow(ctx, `
SELECT latest_seq, latest_global_seq
FROM scopes
WHERE id = $1
  AND workspace_id = $2
`, scopeID, workspaceID).Scan(&latestSeq, &latestGlobalSeq))

	seq := latestSeq + 1
	globalSeq := latestGlobalSeq + 1
	if globalSeq < seq {
		globalSeq = seq
	}
	now := time.Now().UTC()
	record := message.ScopeMessageRecord{
		ID:                 fmt.Sprintf("msg_legacy_task_dispatch_%s_%d", scopeID, seq),
		WorkspaceID:        workspaceID,
		ScopeID:            scopeID,
		Seq:                seq,
		WorkspaceGlobalSeq: globalSeq,
		AuthorMemberID:     authorMemberID,
		AuthorType:         message.ScopeActorHuman,
		MessageType:        message.MessageTypeTaskDispatch,
		Content:            content,
		CreatedAt:          now,
		UpdatedAt:          now,
	}
	repos := message.NewScopeRepositories(db)
	require.NoError(t, repos.Messages.Insert(ctx, record))
	require.NoError(t, repos.Scopes.AdvanceMessageStats(ctx, scopeID, seq, globalSeq, now, false, ""))
	return record
}

func agentDeliveryReasonsByMember(recipients []message.AgentRecipient) map[string]string {
	reasons := make(map[string]string, len(recipients))
	for _, recipient := range recipients {
		reasons[recipient.AgentMemberID] = recipient.DeliveryReason
	}
	return reasons
}
