package app

import (
	"context"
	"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/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
)

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	appStore := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	now := time.Now().UTC()
	fixture := seedAppAdminFixture(t, pool, "app_service_workflow_context_fence", now)

	installationView, err := appStore.installAppForUserForTest(ctx, fixture.UserID, fixture.WorkspaceID, InstallAppParams{
		AppSlug: "gitea",
	})
	require.NoError(t, err)
	installation := model.RuntimeAppInstallation{
		InstallationID: installationView.ID,
		AppID:          installationView.AppID,
		AppSlug:        installationView.AppSlug,
		AppName:        installationView.AppName,
		MemberID:       installationView.MemberID,
		Handle:         installationView.Handle,
		WorkspaceID:    fixture.WorkspaceID,
	}
	target := message.ThreadAccess{
		ThreadID:    fixture.AllChannelID,
		ThreadType:  domain.ThreadTypeChannel,
		ChannelID:   fixture.AllChannelID,
		ChannelName: "all",
	}
	messageCountBefore := countScopeMessagesForTest(t, pool, fixture.WorkspaceID, fixture.AllChannelID)
	outboxCountBefore := countScopeOutboxForTest(t, pool, fixture.WorkspaceID, fixture.AllChannelID)

	tx, err := pool.BeginTx(ctx, coredb.TxOptions{})
	require.NoError(t, err)
	_, _, err = appStore.appendAppServiceMessageWithOptions(
		ctx,
		tx,
		installation,
		target,
		"runtime app forged workflow authority",
		map[string]any{message.WorkflowContextMetadataRunIDKey: "wri_forged"},
		false,
		appendMessageOptions{},
		appServiceMessageProviderEvent,
		nil,
	)
	require.ErrorIs(t, err, ErrBadRequest)
	require.ErrorContains(t, err, message.WorkflowContextMetadataRunIDKey)
	require.NoError(t, tx.Commit(ctx))

	require.Equal(t, messageCountBefore, countScopeMessagesForTest(t, pool, fixture.WorkspaceID, fixture.AllChannelID))
	require.Equal(t, outboxCountBefore, countScopeOutboxForTest(t, pool, fixture.WorkspaceID, fixture.AllChannelID))
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	appStore := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	now := time.Now().UTC()
	fixture := seedAppAdminFixture(t, pool, "service_thread_base_routing", now)

	installationView, err := appStore.installAppForUserForTest(ctx, fixture.UserID, fixture.WorkspaceID, InstallAppParams{
		AppSlug: "gitea",
	})
	require.NoError(t, err)
	require.NotEmpty(t, installationView.MemberID)

	const (
		autoMachineID        = "mach_service_thread_auto_wake"
		autoAgentID          = "agt_service_thread_auto_wake"
		autoAgentMemberID    = "mem_service_thread_auto_wake"
		noWakeMachineID      = "mach_service_thread_no_wake"
		noWakeAgentID        = "agt_service_thread_no_wake"
		noWakeAgentMemberID  = "mem_service_thread_no_wake"
		privateMachineID     = "mach_service_thread_private_peer"
		privateAgentID       = "agt_service_thread_private_peer"
		privateAgentMemberID = "mem_service_thread_private_peer"
	)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, autoMachineID, autoAgentID, autoAgentMemberID, "service-thread-auto", now)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, noWakeMachineID, noWakeAgentID, noWakeAgentMemberID, "service-thread-nowake", now)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, privateMachineID, privateAgentID, privateAgentMemberID, "service-thread-private", now)

	threadChannelID := "chn_service_thread_base_reply"
	threadShortID := "svcbase1"
	seedProviderWatchReplyThread(t, pool, fixture, threadChannelID, threadShortID, now)
	parentMessageID := "msg_" + threadChannelID
	threadID := "thr_" + threadChannelID
	require.NoError(t, message.EnsureScopeMembership(ctx, pool, threadChannelID, autoAgentMemberID, "public", "", "auto-wake", 0, 0, now))
	require.NoError(t, message.EnsureScopeMembership(ctx, pool, threadChannelID, noWakeAgentMemberID, "public", "", "no-wake", 0, 0, now))

	target := message.ThreadAccess{
		ThreadID:   threadID,
		ThreadType: domain.ThreadTypeChannel,
		ChannelID:  threadChannelID,
		ReplyThread: &message.ReplyThreadTarget{
			ParentMessageID:   parentMessageID,
			ShortID:           threadShortID,
			ParentChannelID:   fixture.AllChannelID,
			ParentChannelName: "all",
		},
	}
	installation := model.RuntimeAppInstallation{
		InstallationID: installationView.ID,
		AppID:          installationView.AppID,
		AppSlug:        installationView.AppSlug,
		AppName:        installationView.AppName,
		MemberID:       installationView.MemberID,
		Handle:         installationView.Handle,
		WorkspaceID:    fixture.WorkspaceID,
	}

	tx, err := pool.BeginTx(ctx, coredb.TxOptions{})
	require.NoError(t, err)
	committed := false
	defer func() {
		if !committed {
			_ = tx.Rollback(ctx)
		}
	}()
	delivery, postCommit, err := appStore.appendAppServiceMessageWithOptions(ctx, tx, installation, target, "provider thread event", map[string]any{"kind": "thread"}, false, appendMessageOptions{
		Source: message.ContributionSource{
			Kind: message.ContributionSourceKindProviderEvent, RefType: message.ContributionSourceRefTypeServiceEventDelivery, RefID: "svcedel_service_thread_base",
		},
	}, appServiceMessageProviderEvent, nil)
	require.NoError(t, err)
	require.Empty(t, postCommit.workspaceID)
	require.NoError(t, tx.Commit(ctx))
	committed = true

	require.Equal(t, "thread", delivery.Message.ChannelType)
	require.NotNil(t, delivery.AppMessage)
	require.Equal(t, "thread", delivery.AppMessage.ChannelType)
	require.Equal(t, fixture.AllChannelID, delivery.AppMessage.ParentChannelID)
	require.Equal(t, "channel", delivery.AppMessage.ParentChannelType)
	require.Equal(t, parentMessageID, delivery.AppMessage.ParentMessageID)
	require.Equal(t, "gitea", delivery.AppMessage.AuthorProfile.TargetID)
	require.Equal(t, "gitea", delivery.AppMessage.AuthorProfile.DisplayName)
	require.Equal(t, message.AuthorServiceKindApp, delivery.AppMessage.AuthorProfile.ServiceKind)
	require.Equal(t, "gitea", delivery.AppMessage.AuthorProfile.ServiceSlug)
	require.Equal(t, "https://gitea.com/assets/img/logo.svg", delivery.AppMessage.AuthorProfile.AvatarURL)
	require.Equal(t, map[string]string{
		autoAgentMemberID:   message.AgentDeliveryReasonThreadParticipant,
		noWakeAgentMemberID: message.AgentDeliveryReasonThreadParticipant,
	}, agentDeliveryReasonsByMember(delivery.Recipients))
	require.Equal(t, map[string]string{
		autoAgentMemberID: message.AgentDeliveryReasonThreadParticipant,
	}, agentDeliveryReasonsByMember(delivery.WakeRecipients))
	requireScopeMessageAndSingleFanout(t, pool, delivery.MessageID, fixture.WorkspaceID, threadChannelID)
	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, "https://gitea.com/assets/img/logo.svg", fanoutPayload.AppMessage.AuthorProfile.AvatarURL)

	autoMessages, err := appStore.ReceiveAgentMessages(ctx, agent.AuthenticatedAgent{AgentID: autoAgentID, WorkspaceID: fixture.WorkspaceID, MemberID: autoAgentMemberID}, 10)
	require.NoError(t, err)
	require.Contains(t, receiveMessageIDs(autoMessages), delivery.MessageID)
}

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

func upsertScopeSubscriptionForTests(t *testing.T, pool coredb.QueryExecer, scopeID string, memberID string, state string, now time.Time) {
	t.Helper()

	require.NoError(t, message.EnsureScopeMembership(context.Background(), pool, scopeID, memberID, "public", "", state, 0, 0, now))
}
