package message

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

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

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

	ctx := context.Background()
	db := startMessagingTestPool(t, ctx)
	repos := NewScopeRepositories(db)
	now := time.Now().UTC().Add(-time.Hour).Truncate(time.Second)

	const (
		workspaceID  = "wsp_scope_recall_projection"
		userID       = "usr_scope_recall_projection_author"
		memberID     = "mem_scope_recall_projection_author"
		scopeID      = "scp_scope_recall_projection"
		messageID    = "msg_scope_recall_projection_target"
		laterID      = "msg_scope_recall_projection_later"
		reactionID   = "rea_scope_recall_projection_target"
		attachmentID = "att_scope_recall_projection_target"
	)

	insertMessagingWorkspace(t, ctx, db, workspaceID, now)
	insertMessagingUser(t, ctx, db, userID, "scope-recall-projection@example.com", now)
	insertMessagingHumanMember(t, ctx, db, memberID, workspaceID, userID, now)
	require.NoError(t, repos.Scopes.UpsertScope(ctx, ScopeRecord{
		ID:                scopeID,
		WorkspaceID:       workspaceID,
		Type:              "channel",
		IdentityKey:       "channel:scope-recall-projection",
		LatestSeq:         2,
		LatestGlobalSeq:   20,
		LatestMessageAt:   timePtr(now.Add(2 * time.Second)),
		Status:            "active",
		CreatedByMemberID: memberID,
		CreatedAt:         now,
		UpdatedAt:         now,
	}))
	require.NoError(t, repos.Memberships.Upsert(ctx, ScopeMembershipRecord{
		ScopeID:           scopeID,
		MemberID:          memberID,
		Visibility:        "public",
		LastReadSeq:       0,
		LastReadGlobalSeq: 0,
		EffectiveFromSeq:  1,
		CreatedAt:         now,
		UpdatedAt:         now,
	}))

	target := ScopeMessageRecord{
		ID:                 messageID,
		WorkspaceID:        workspaceID,
		ScopeID:            scopeID,
		Seq:                1,
		WorkspaceGlobalSeq: 10,
		AuthorMemberID:     memberID,
		AuthorType:         ScopeActorHuman,
		MessageType:        MessageTypeSticker,
		Content:            "recall original content needle",
		Metadata:           []byte(`{"mentions":[{"id":"agent"}],"agentDelivery":{"status":"delivered"}}`),
		CreatedAt:          now.Add(time.Second),
		UpdatedAt:          now.Add(time.Second),
	}
	later := ScopeMessageRecord{
		ID:                 laterID,
		WorkspaceID:        workspaceID,
		ScopeID:            scopeID,
		Seq:                2,
		WorkspaceGlobalSeq: 20,
		AuthorMemberID:     memberID,
		AuthorType:         ScopeActorHuman,
		Content:            "recall later survivor needle",
		Metadata:           []byte(`{}`),
		CreatedAt:          now.Add(2 * time.Second),
		UpdatedAt:          now.Add(2 * time.Second),
	}
	for _, record := range []ScopeMessageRecord{target, later} {
		require.NoError(t, repos.Messages.Insert(ctx, record))
		require.NoError(t, repos.Search.InsertDoc(ctx, ScopeSearchDocRecord{
			MessageID:   record.ID,
			WorkspaceID: record.WorkspaceID,
			ScopeID:     record.ScopeID,
			GlobalSeq:   record.WorkspaceGlobalSeq,
			Content:     record.Content,
			CreatedAt:   record.CreatedAt,
		}))
	}
	require.NoError(t, repos.Reactions.Insert(ctx, ScopeMessageReactionRecord{
		ID:              reactionID,
		WorkspaceID:     workspaceID,
		ScopeID:         scopeID,
		MessageID:       messageID,
		Emoji:           "👌",
		ReactorMemberID: memberID,
		CreatedAt:       now,
	}))
	require.NoError(t, repos.ActivityEvents.Insert(ctx, ScopeActivityEventRecord{
		ID:              "act_scope_recall_projection_reaction",
		WorkspaceID:     workspaceID,
		ScopeID:         scopeID,
		Seq:             3,
		ActivityType:    "reaction_added",
		ActivityRefType: "reaction",
		ActivityRefID:   reactionID,
		SourceMessageID: messageID,
		ActorMemberID:   memberID,
		CreatedAt:       now,
	}))
	require.NoError(t, repos.Sources.Insert(ctx, ScopeMessageSourceRecord{
		TargetScopeID:   scopeID,
		MessageID:       messageID,
		Ordinal:         0,
		SourceScopeID:   scopeID,
		SourceSeq:       2,
		SourceMessageID: laterID,
		SourceKind:      "quote",
		SourceDedupeKey: "source:scope-recall-projection-target",
		CreatedAt:       now,
	}))
	require.NoError(t, repos.Sources.Insert(ctx, ScopeMessageSourceRecord{
		TargetScopeID:   scopeID,
		MessageID:       laterID,
		Ordinal:         0,
		SourceScopeID:   scopeID,
		SourceSeq:       1,
		SourceMessageID: messageID,
		SourceKind:      "quote",
		SourceDedupeKey: "source:scope-recall-projection-source",
		CreatedAt:       now,
	}))
	require.NoError(t, repos.ShareCards.Insert(ctx, ScopeShareCardRecord{
		ID:                "shr_scope_recall_projection_target",
		WorkspaceID:       workspaceID,
		ScopeID:           scopeID,
		MessageID:         messageID,
		SourceScopeID:     scopeID,
		SourceMessageID:   laterID,
		TargetScopeID:     scopeID,
		CreatedByMemberID: memberID,
		CreatedAt:         now,
		UpdatedAt:         now,
	}))
	mustExecMessaging(t, ctx, db, `
INSERT INTO attachments (
  id, workspace_id, scope_id, uploaded_by_member_id, message_id,
  filename, content_type, size_bytes, storage_driver, payload, created_at
) VALUES (
  $1, $2, $3, $4, $5,
  'recalled.txt', 'text/plain', 8, 'inline', 'recalled', $6
)
`, attachmentID, workspaceID, scopeID, memberID, messageID, now)
	mustExecMessaging(t, ctx, db, `
INSERT INTO scope_message_pins (
  id, workspace_id, scope_id, message_id, pinned_by_member_id, pinned_at, created_at, updated_at
) VALUES (
  'pin_scope_recall_projection_target', $1, $2, $3, $4, $5, $5, $5
)
`, workspaceID, scopeID, messageID, memberID, now)
	mustExecMessaging(t, ctx, db, `
INSERT INTO scope_message_top_notices (
  id, workspace_id, scope_id, message_id, set_by_member_id, set_at, created_at, updated_at
) VALUES (
  'top_scope_recall_projection_target', $1, $2, $3, $4, $5, $5, $5
)
`, workspaceID, scopeID, messageID, memberID, now)
	mustExecMessaging(t, ctx, db, `
INSERT INTO scope_fanout_outbox (
  id, workspace_id, scope_id, message_id, event_ref_type, event_ref_id,
  fanout_type, dedupe_key, payload, status, next_attempt_at, created_at, last_error
) VALUES (
  'out_scope_recall_projection_target', $1, $2, $3, 'message', $3,
  'agent_delivery', 'dedupe:scope-recall-projection-target', '{}', 'pending', $4, $4, ''
)
`, workspaceID, scopeID, messageID, now)

	tx, err := db.BeginTx(ctx, coredb.TxOptions{})
	require.NoError(t, err)
	result, err := RecallScopeMessageTx(ctx, tx, ScopeRecallMessageRequest{
		WorkspaceID:        workspaceID,
		ScopeID:            scopeID,
		MessageID:          messageID,
		RecalledByMemberID: memberID,
		PlaceholderContent: "作者 撤回了一条消息",
		Metadata:           []byte(`{"messageRecall":{"status":"recalled","recalledByMemberId":"mem_scope_recall_projection_author","placeholder":"作者 撤回了一条消息"}}`),
		RecalledAt:         now.Add(time.Hour),
	})
	require.NoError(t, err)
	require.NoError(t, tx.Commit(ctx))

	require.Equal(t, messageID, result.Message.ID)
	require.Equal(t, int64(1), result.Message.Seq)
	require.False(t, result.IsLatestVisibleMessage)
	require.Equal(t, "作者 撤回了一条消息", result.Message.Content)

	loaded, err := repos.Messages.LoadByID(ctx, workspaceID, scopeID, messageID)
	require.NoError(t, err)
	require.Equal(t, "作者 撤回了一条消息", loaded.Content)
	metadata, err := DecodeMessageMetadata(loaded.Metadata)
	require.NoError(t, err)
	require.True(t, MessageMetadataIsRecalled(metadata))
	require.NotContains(t, metadata, MentionMetadataKey)
	require.NotContains(t, metadata, AgentDeliveryMetadataKey)

	require.Equal(t, int64(1), countScopeRows(t, ctx, db, "scope_messages", "id = $1", messageID))
	require.Equal(t, int64(0), countScopeRows(t, ctx, db, "scope_message_search_docs", "message_id = $1", messageID))
	require.Equal(t, int64(0), countScopeRows(t, ctx, db, "attachments", "message_id = $1", messageID))
	require.Equal(t, int64(0), countScopeRows(t, ctx, db, "scope_message_reactions", "message_id = $1", messageID))
	require.Equal(t, int64(0), countScopeRows(t, ctx, db, "scope_message_pins", "message_id = $1", messageID))
	require.Equal(t, int64(0), countScopeRows(t, ctx, db, "scope_message_top_notices", "message_id = $1", messageID))
	require.Equal(t, int64(0), countScopeRows(t, ctx, db, "scope_message_sources", "message_id = $1 OR source_message_id = $1", messageID))
	require.Equal(t, int64(0), countScopeRows(t, ctx, db, "scope_share_cards", "message_id = $1", messageID))

	var outboxStatus, suppressionReason string
	require.NoError(t, db.QueryRow(ctx, `
SELECT status, suppression_reason
FROM scope_fanout_outbox
WHERE id = 'out_scope_recall_projection_target'
`).Scan(&outboxStatus, &suppressionReason))
	require.Equal(t, "suppressed", outboxStatus)
	require.Equal(t, ScopeRecallSuppressionReason, suppressionReason)

	var suppressedAt time.Time
	require.NoError(t, db.QueryRow(ctx, `
SELECT suppressed_at
FROM scope_activity_events
WHERE id = 'act_scope_recall_projection_reaction'
`).Scan(&suppressedAt))
	require.False(t, suppressedAt.IsZero())

	api := NewScopeAPIService(New(db, nil, 0, Options{}))
	page, err := api.ListHistory(ctx, ScopeAPIHistoryRequest{
		WorkspaceID:    workspaceID,
		ScopeID:        scopeID,
		ViewerMemberID: memberID,
		ViewerType:     ScopeActorHuman,
		Limit:          10,
	})
	require.NoError(t, err)
	require.Equal(t, []string{laterID, messageID}, historyMessageIDs(page.Messages))
	require.Equal(t, "作者 撤回了一条消息", page.Messages[1].Content)
	require.Empty(t, page.Messages[1].Attachments)

	attachmentDecision, err := api.AuthorizeSidecarRead(ctx, SidecarReadRequest{
		WorkspaceID:    workspaceID,
		MessageID:      messageID,
		ViewerMemberID: memberID,
		ViewerType:     ScopeActorHuman,
		Kind:           SidecarKindAttachment,
	})
	require.NoError(t, err)
	require.False(t, attachmentDecision.Allowed)
	require.Equal(t, "message_recalled", attachmentDecision.Reason)

	reactionDecision, err := api.AuthorizeSidecarRead(ctx, SidecarReadRequest{
		WorkspaceID:    workspaceID,
		MessageID:      messageID,
		ViewerMemberID: memberID,
		ViewerType:     ScopeActorHuman,
		Kind:           SidecarKindReaction,
	})
	require.NoError(t, err)
	require.False(t, reactionDecision.Allowed)
	require.Equal(t, "message_recalled", reactionDecision.Reason)

	pinDecisions, err := api.AuthorizeSidecarReadBatch(ctx, SidecarReadBatchRequest{
		WorkspaceID: workspaceID,
		Kind:        SidecarKindPin,
		Items: []SidecarReadBatchItem{{
			RequestID:      "pin-surface-target",
			MessageID:      messageID,
			ViewerMemberID: memberID,
		}},
	})
	require.NoError(t, err)
	require.NotContains(t, pinDecisions, "pin-surface-target")
}
