package app

import (
	"context"
	"database/sql"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

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

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	var cleanerCalls []string
	store := New(pool, Options{
		DeleteAttachmentDerivatives: func(_ context.Context, _ string, attachmentID string) {
			cleanerCalls = append(cleanerCalls, attachmentID)
		},
	})
	require.Empty(t, cleanerCalls, "cleaner must not fire before recall")
	now := time.Date(2026, 6, 26, 9, 0, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "recall_projection_scrub", now)
	user := model.User{ID: fixture.UserID, Name: "recall_projection_scrub", DisplayName: "recall_projection_scrub"}

	sent, _, err := store.SendAppMessageForUserWithOptions(
		ctx,
		user,
		fixture.WorkspaceID,
		fixture.AllChannelID,
		"durable original needle",
		nil,
		SendAppMessageOptions{},
	)
	require.NoError(t, err)
	mustExec(t, pool, `
UPDATE scope_messages
SET message_type = $4
WHERE workspace_id = $1
  AND scope_id = $2
  AND id = $3
`, fixture.WorkspaceID, fixture.AllChannelID, sent.ID, message.MessageTypeSticker)
	mustExec(t, pool, `
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 (
  'att_recall_projection_scrub', $1, $2, $3, $4,
  'sticker.png', 'image/png', 8, 'inline', 'sticker', $5
)
`, fixture.WorkspaceID, fixture.AllChannelID, fixture.MemberID, sent.ID, now)

	mustExec(t, pool, `
INSERT INTO im_stream_events (
  workspace_id, member_id, event_seq, event_id, type, scope_id,
  conversation_id, entity_id, entity_seq, payload_json, patches_json,
  cursor_generation, created_at, expires_at
) VALUES (
  $1, $2, 1, 'evt_recall_projection_scrub_created', 'message.created', $3,
  $3, $4, $5, $6, NULL,
  '', $7, NULL
)
`, fixture.WorkspaceID, fixture.MemberID, fixture.AllChannelID, sent.ID, sent.Seq, `{
  "content": "durable original needle",
  "mention": {"preview": "durable original needle"},
  "attachments": [{"id": "att_recall_projection_scrub", "filename": "original.txt"}],
  "message": {
    "id": "`+sent.ID+`",
    "content": "durable original needle",
    "metadata": {"mentions": [{"id": "agt"}], "agentDelivery": {"status": "delivered"}},
    "resolvedMentions": [{"id": "agt"}],
    "attachments": [{"id": "att_recall_projection_scrub", "filename": "original.txt"}],
    "source": {"snippet": "durable original needle"},
    "deliverySummaryPreview": {"targetedAgentCount": 1}
  }
}`, now)
	mustExec(t, pool, `
INSERT INTO im_conversation_index (
  workspace_id, member_id, conversation_id, scope_id, kind, sort_at,
  last_event_seq, pinned_at, archived_at, projection_json, created_at, updated_at
) VALUES (
  $1, $2, $3, $3, 'channel', $4,
  1, NULL, NULL, $5, $4, $4
)
`, fixture.WorkspaceID, fixture.MemberID, fixture.AllChannelID, now, `{
  "content": "durable original needle",
  "attachments": [{"id": "att_recall_projection_scrub", "filename": "original.txt"}],
  "lastMessage": {
    "id": "`+sent.ID+`",
    "content": "durable original needle",
    "metadata": {"mentions": [{"id": "agt"}], "agentDelivery": {"status": "delivered"}},
    "resolvedMentions": [{"id": "agt"}],
    "attachments": [{"id": "att_recall_projection_scrub", "filename": "original.txt"}],
    "source": {"snippet": "durable original needle"},
    "deliverySummaryPreview": {"targetedAgentCount": 1}
  }
}`)

	result, err := store.RecallMessageForUser(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, sent.ID, message.HumanMessageRecallOptions{})
	require.NoError(t, err)
	require.True(t, result.Changed)
	require.Equal(t, "recall_projection_scrub 撤回了一条消息", result.Message.Content)
	require.Equal(t, message.MessageTypeSticker, result.Message.MessageType)
	require.True(t, message.MessageMetadataIsRecalled(result.Message.Metadata))
	require.Empty(t, result.Message.Attachments)

	// Verify DeletedAttachmentDerivatives was populated (proves the collect-in-tx
	// + return-in-result pipeline works for recall). The post-commit cleanup
	// calls messageStore().CleanupAttachmentDerivatives → deleteAttachmentDerivative.
	require.Len(t, result.DeletedAttachmentDerivatives, 1)
	require.Equal(t, fixture.WorkspaceID, result.DeletedAttachmentDerivatives[0].WorkspaceID)
	require.Equal(t, "att_recall_projection_scrub", result.DeletedAttachmentDerivatives[0].AttachmentID)
	// Recording callback proves the post-commit cleanup actually fired.
	require.Equal(t, []string{"att_recall_projection_scrub"}, cleanerCalls,
		"derivative cleaner must fire post-commit with the recalled attachment's ID")

	var attachmentCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM attachments
WHERE message_id = $1
`, sent.ID).Scan(&attachmentCount))
	require.Zero(t, attachmentCount)

	var streamPayload, streamMessageContent, streamAttachmentJSON string
	var streamMention sql.NullString
	require.NoError(t, pool.QueryRow(ctx, `
SELECT CAST(payload_json AS CHAR),
       COALESCE(JSON_UNQUOTE(JSON_EXTRACT(payload_json, '$.message.content')), ''),
       COALESCE(CAST(JSON_EXTRACT(payload_json, '$.message.attachments') AS CHAR), ''),
       JSON_UNQUOTE(JSON_EXTRACT(payload_json, '$.mention'))
FROM im_stream_events
WHERE workspace_id = $1
  AND member_id = $2
  AND event_id = 'evt_recall_projection_scrub_created'
`, fixture.WorkspaceID, fixture.MemberID).Scan(&streamPayload, &streamMessageContent, &streamAttachmentJSON, &streamMention))
	require.NotContains(t, streamPayload, "durable original needle")
	require.Equal(t, "recall_projection_scrub 撤回了一条消息", streamMessageContent)
	require.Equal(t, "[]", streamAttachmentJSON)
	require.False(t, streamMention.Valid)

	var projectionJSON, projectionLastContent, projectionAttachmentJSON, projectionRecallStatus string
	require.NoError(t, pool.QueryRow(ctx, `
SELECT CAST(projection_json AS CHAR),
       COALESCE(JSON_UNQUOTE(JSON_EXTRACT(projection_json, '$.lastMessage.content')), ''),
       COALESCE(CAST(JSON_EXTRACT(projection_json, '$.lastMessage.attachments') AS CHAR), ''),
       COALESCE(JSON_UNQUOTE(JSON_EXTRACT(projection_json, '$.lastMessage.metadata.messageRecall.status')), '')
FROM im_conversation_index
WHERE workspace_id = $1
  AND member_id = $2
  AND conversation_id = $3
`, fixture.WorkspaceID, fixture.MemberID, fixture.AllChannelID).Scan(&projectionJSON, &projectionLastContent, &projectionAttachmentJSON, &projectionRecallStatus))
	require.NotContains(t, projectionJSON, "durable original needle")
	require.Equal(t, "recall_projection_scrub 撤回了一条消息", projectionLastContent)
	require.Equal(t, "[]", projectionAttachmentJSON)
	require.Equal(t, "recalled", projectionRecallStatus)
}
