package message

import (
	"context"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/coredb/testkit"
)

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

	first, err := ComputeMessageCreatePayloadDigest(MessageCreateDigestInput{
		MessageType: " text ",
		Content:     "hello",
		Metadata: []byte(`{
			"client":{"z":2,"a":1},
			"mentions":[{"type":"agent","id":"agt_before","token":"helper"}]
		}`),
	})
	require.NoError(t, err)
	replayed, err := ComputeMessageCreatePayloadDigest(MessageCreateDigestInput{
		MessageType: "text",
		Content:     "hello",
		Metadata:    []byte(`{"client":{"a":1,"z":2}}`),
	})
	require.NoError(t, err)
	require.Equal(t, first, replayed)

	changed, err := ComputeMessageCreatePayloadDigest(MessageCreateDigestInput{
		MessageType: "text",
		Content:     "hello",
		Metadata:    []byte(`{"client":{"a":1,"z":3}}`),
	})
	require.NoError(t, err)
	require.NotEqual(t, first, changed)
}

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

	base := MessageCreateDigestInput{
		MessageType:   MessageTypeOwnerQuote,
		Content:       "quoted",
		AttachmentIDs: []string{" att_1 ", "att_2"},
		SourceRefs: []SourceRef{
			{SourceMessageID: " msg_1 ", SourceKind: MessageTypeOwnerQuote},
			{SourceMessageID: "msg_2", SourceKind: MessageTypeOwnerQuote},
		},
	}
	first, err := ComputeMessageCreatePayloadDigest(base)
	require.NoError(t, err)

	normalized := base
	normalized.AttachmentIDs = []string{"att_1", "att_2"}
	normalized.SourceRefs = []SourceRef{
		{SourceMessageID: "msg_1", SourceKind: MessageTypeOwnerQuote},
		{SourceMessageID: "msg_2", SourceKind: MessageTypeOwnerQuote},
	}
	replayed, err := ComputeMessageCreatePayloadDigest(normalized)
	require.NoError(t, err)
	require.Equal(t, first, replayed)

	swappedAttachments := normalized
	swappedAttachments.AttachmentIDs = []string{"att_2", "att_1"}
	attachmentDigest, err := ComputeMessageCreatePayloadDigest(swappedAttachments)
	require.NoError(t, err)
	require.NotEqual(t, first, attachmentDigest)

	swappedSources := normalized
	swappedSources.SourceRefs = []SourceRef{
		{SourceMessageID: "msg_2", SourceKind: MessageTypeOwnerQuote},
		{SourceMessageID: "msg_1", SourceKind: MessageTypeOwnerQuote},
	}
	sourceDigest, err := ComputeMessageCreatePayloadDigest(swappedSources)
	require.NoError(t, err)
	require.NotEqual(t, first, sourceDigest)
}

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

	digest := strings.Repeat("a", 64)
	normalized, err := normalizeScopeMessageCreateContract(ScopeMessageRecord{
		AuthorMemberID:           " author ",
		ClientMessageID:          " client ",
		IdempotencyPayloadDigest: strings.ToUpper(digest),
	})
	require.NoError(t, err)
	require.Equal(t, "author", normalized.AuthorMemberID)
	require.Equal(t, "author", normalized.NotifyPrincipalMemberID)
	require.Equal(t, "author", normalized.IdempotencySubjectMemberID)
	require.Equal(t, "client", normalized.ClientMessageID)
	require.Equal(t, digest, normalized.IdempotencyPayloadDigest)

	_, err = normalizeScopeMessageCreateContract(ScopeMessageRecord{
		AuthorMemberID:  "author",
		ClientMessageID: "client",
	})
	require.ErrorIs(t, err, domain.ErrInvalidArgument)

	_, err = normalizeScopeMessageCreateContract(ScopeMessageRecord{
		AuthorMemberID:           "author",
		IdempotencyPayloadDigest: digest,
	})
	require.ErrorIs(t, err, domain.ErrInvalidArgument)
}

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

	digest := strings.Repeat("a", 64)
	for _, test := range []struct {
		name   string
		record ScopeMessageRecord
	}{
		{
			name: "author",
			record: ScopeMessageRecord{
				AuthorMemberID: strings.Repeat("a", scopeMessageIdempotencyKeyMaxBytes+1),
			},
		},
		{
			name: "notification principal",
			record: ScopeMessageRecord{
				AuthorMemberID:          "author",
				NotifyPrincipalMemberID: strings.Repeat("n", scopeMessageIdempotencyKeyMaxBytes+1),
			},
		},
		{
			name: "idempotency subject",
			record: ScopeMessageRecord{
				AuthorMemberID:             "author",
				IdempotencySubjectMemberID: strings.Repeat("s", scopeMessageIdempotencyKeyMaxBytes+1),
			},
		},
		{
			name: "client message id",
			record: ScopeMessageRecord{
				AuthorMemberID:           "author",
				ClientMessageID:          strings.Repeat("c", scopeMessageIdempotencyKeyMaxBytes+1),
				IdempotencyPayloadDigest: digest,
			},
		},
	} {
		test := test
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()
			_, err := normalizeScopeMessageCreateContract(test.record)
			require.ErrorIs(t, err, domain.ErrInvalidArgument)
			require.ErrorContains(t, err, "must be at most 128 bytes")
		})
	}

	normalized, err := normalizeScopeMessageCreateContract(ScopeMessageRecord{
		AuthorMemberID:             strings.Repeat("a", scopeMessageIdempotencyKeyMaxBytes),
		NotifyPrincipalMemberID:    strings.Repeat("n", scopeMessageIdempotencyKeyMaxBytes),
		IdempotencySubjectMemberID: strings.Repeat("s", scopeMessageIdempotencyKeyMaxBytes),
		ClientMessageID:            strings.Repeat("c", scopeMessageIdempotencyKeyMaxBytes),
		IdempotencyPayloadDigest:   digest,
	})
	require.NoError(t, err)
	require.Len(t, normalized.ClientMessageID, scopeMessageIdempotencyKeyMaxBytes)
}

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

	ctx := t.Context()
	db := startMessagingTestPool(t, ctx)
	fixture := seedScopeStoreFixture(t, ctx, db, "legacy_nullable_principals")
	digest := strings.Repeat("b", 64)
	mustExecMessaging(t, ctx, db, `
INSERT INTO scope_messages (
    id, workspace_id, scope_id, seq, workspace_global_seq,
    author_member_id, author_type, notify_principal_member_id,
    idempotency_subject_member_id, client_message_id,
    idempotency_payload_digest, message_type, content, metadata,
    created_at, updated_at
) VALUES (
    $1, $2, $3, 1, 1,
    $4, 'human', NULL,
    NULL, $5,
    $6, '', 'legacy nullable principals', JSON_OBJECT(),
    $7, $7
)
`, "msg_legacy_nullable_principals", fixture.workspaceID, fixture.scopeID,
		fixture.ownerMemberID, "client-legacy-nullable-principals", digest, fixture.now)

	record, err := NewScopeRepositories(db).Messages.LoadByClientMessageID(
		ctx,
		fixture.scopeID,
		fixture.ownerMemberID,
		fixture.ownerMemberID,
		"client-legacy-nullable-principals",
	)
	require.NoError(t, err)
	require.Equal(t, fixture.ownerMemberID, record.NotifyPrincipalMemberID)
	require.Equal(t, fixture.ownerMemberID, record.IdempotencySubjectMemberID)
	require.Equal(t, digest, record.IdempotencyPayloadDigest)

	history, err := NewScopeRepositories(db).Messages.ListBeforeSeq(ctx, fixture.scopeID, 0, 10)
	require.NoError(t, err)
	require.Len(t, history, 1)
	require.Equal(t, fixture.ownerMemberID, history[0].NotifyPrincipalMemberID)
	require.Equal(t, fixture.ownerMemberID, history[0].IdempotencySubjectMemberID)
	require.Equal(t, digest, history[0].IdempotencyPayloadDigest)
}

func TestScopeMessageStoreDuplicateKeyReadsWinnerOutsideStaleSnapshot(t *testing.T) {
	ctx := t.Context()
	rootDSN := messagingTiDB.RootDSN(t)
	dsn := testkit.StartInitializedTiDBWithRootDSN(t, rootDSN)
	sqlDB, err := testkit.OpenTiDBContext(ctx, dsn)
	require.NoError(t, err)
	t.Cleanup(func() { _ = sqlDB.Close() })
	sqlDB.SetMaxOpenConns(4)
	sqlDB.SetMaxIdleConns(4)
	db := coredb.WrapSQLDB(sqlDB)
	fixture := seedScopeStoreFixture(t, ctx, db, "idempotency_stale_snapshot")

	first := ScopeMessageRecord{
		ID:                         "msg_idempotency_stale_snapshot_first",
		WorkspaceID:                fixture.workspaceID,
		ScopeID:                    fixture.scopeID,
		Seq:                        1,
		WorkspaceGlobalSeq:         1,
		AuthorMemberID:             fixture.ownerMemberID,
		AuthorType:                 "human",
		NotifyPrincipalMemberID:    fixture.ownerMemberID,
		IdempotencySubjectMemberID: fixture.ownerMemberID,
		ClientMessageID:            "client-idempotency-stale-snapshot",
		IdempotencyPayloadDigest:   strings.Repeat("a", 64),
		MessageType:                "",
		Content:                    "winner",
		CreatedAt:                  fixture.now,
		UpdatedAt:                  fixture.now,
	}
	conflicting := first
	conflicting.ID = "msg_idempotency_stale_snapshot_conflict"
	conflicting.Seq = 2
	conflicting.WorkspaceGlobalSeq = 2
	conflicting.IdempotencyPayloadDigest = strings.Repeat("b", 64)
	conflicting.Content = "loser"

	winnerTx, err := db.BeginTx(ctx, coredb.TxOptions{})
	require.NoError(t, err)
	winnerCommitted := false
	t.Cleanup(func() {
		if !winnerCommitted {
			_ = winnerTx.Rollback(context.Background())
		}
	})
	loserTx, err := db.BeginTx(ctx, coredb.TxOptions{})
	require.NoError(t, err)
	t.Cleanup(func() { _ = loserTx.Rollback(context.Background()) })

	_, err = NewScopeRepositories(loserTx).Messages.LoadByClientMessageID(
		ctx,
		fixture.scopeID,
		fixture.ownerMemberID,
		fixture.ownerMemberID,
		first.ClientMessageID,
	)
	require.ErrorIs(t, err, ErrNotFound)

	inserted, err := NewScopeRepositories(winnerTx).Messages.InsertIdempotent(ctx, first)
	require.NoError(t, err)
	require.True(t, inserted.Inserted)
	require.NoError(t, winnerTx.Commit(ctx))
	winnerCommitted = true

	_, err = NewScopeRepositories(loserTx).Messages.InsertIdempotent(ctx, conflicting)
	require.ErrorIs(t, err, ErrConflict)
}
