package approvalapp

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"testing"
	"time"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	"github.com/tidbcloud/AgentTeam/internal/core/runeffect"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/service/appruntime"
	"github.com/tidbcloud/AgentTeam/internal/store/app"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
	"github.com/tidbcloud/AgentTeam/internal/workflow/orchestrator"
)

func TestWorkflowRunApprovalPendingCommandUsesRunEffectIdentity(t *testing.T) {
	request := orchestrator.ApprovalRequest{
		WorkspaceID: "workspace-1", PlanID: "plan-1", PlanVersionID: "version-1",
		RunID: "run-1", ActivationID: "activation-1", EffectID: "effect-1",
		NodeID: "approval", AssigneeMemberID: "member-assignee",
		Summary: "Review the release", CausedByMemberID: "member-author",
	}
	content, err := pendingApprovalCardContent(request, "Reviewer")
	require.NoError(t, err)
	require.Equal(t, "workflow-run:run-1:approval:effect-1", workflowRunApprovalClientNotificationID(request.RunID, request.EffectID))

	var card map[string]any
	require.NoError(t, json.Unmarshal(content.CardJSON, &card))
	require.Equal(t, "workflow_run_approval", card["template"])
	require.Equal(t, "pending", card["status"])
	require.NotContains(t, string(content.CardJSON), "approval_instances")
}

func TestWorkflowRunApprovalPendingCardCommentSchemaIsScopeGated(t *testing.T) {
	legacy, err := pendingApprovalCardContent(workflowRunApprovalDecisionRequest(nil), "Reviewer")
	require.NoError(t, err)
	var legacyCard map[string]any
	require.NoError(t, json.Unmarshal(legacy.CardJSON, &legacyCard))
	legacyActions := legacyCard["actions"].([]any)
	for _, raw := range legacyActions {
		action := raw.(map[string]any)
		require.NotContains(t, action, "inputSchema")
	}

	scope := domain.MessageScopeValue{Kind: domain.MessageScopeKindThread, ScopeID: "tch_abcdefghijklmn"}
	request := workflowRunApprovalDecisionRequest(&scope)
	withScope, err := pendingApprovalCardContent(request, "Reviewer")
	require.NoError(t, err)
	var scopedCard map[string]any
	require.NoError(t, json.Unmarshal(withScope.CardJSON, &scopedCard))
	for _, raw := range scopedCard["actions"].([]any) {
		action := raw.(map[string]any)
		schema := action["inputSchema"].(map[string]any)
		require.Equal(t, false, schema["additionalProperties"])
		properties := schema["properties"].(map[string]any)
		comment := properties["comment"].(map[string]any)
		require.Equal(t, "string", comment["type"])
		require.Equal(t, float64(orchestrator.ApprovalDecisionCommentMaxRunes), comment["maxLength"])
		if required, present := schema["required"]; present {
			require.NotContains(t, required, "comment")
		}
	}
}

func TestWorkflowRunApprovalFeedbackKeepsGenericRejectReasonSeparate(t *testing.T) {
	scope := domain.MessageScopeValue{Kind: domain.MessageScopeKindChannel, ScopeID: "chn_abcdefghijklmn"}
	content, err := pendingApprovalCardContent(workflowRunApprovalDecisionRequest(&scope), "Reviewer")
	require.NoError(t, err)
	var card map[string]any
	require.NoError(t, json.Unmarshal(content.CardJSON, &card))
	for _, raw := range card["actions"].([]any) {
		action := raw.(map[string]any)
		schema := action["inputSchema"].(map[string]any)
		properties := schema["properties"].(map[string]any)
		require.Contains(t, properties, "comment")
		require.NotContains(t, properties, "reason")
	}
	comment, provided, err := workflowRunApprovalComment(map[string]any{"comment": "optional feedback"})
	require.NoError(t, err)
	require.Equal(t, "optional feedback", comment)
	require.True(t, provided)
	_, _, err = workflowRunApprovalComment(map[string]any{"reason": "generic rejection"})
	require.Error(t, err, "Workflow input.reason must not substitute feedback comment")
	reason, err := rejectReason(map[string]any{"reason": "generic rejection"})
	require.NoError(t, err)
	require.Equal(t, "generic rejection", reason, "generic Approval continues to persist input.reason")
}

func workflowRunApprovalDecisionRequest(scope *domain.MessageScopeValue) orchestrator.ApprovalRequest {
	return orchestrator.ApprovalRequest{
		WorkspaceID: "workspace-1", PlanID: "plan-1", PlanVersionID: "version-1",
		RunID: "run-1", ActivationID: "activation-1", EffectID: "effect-1",
		NodeID: "approval", AssigneeMemberID: "member-assignee", Summary: "Review the release",
		FeedbackScope: scope, CausedByMemberID: "member-author",
	}
}

func TestWorkflowRunApprovalHandlerPassesTrustedActorAndDecisionToRunSink(t *testing.T) {
	for _, test := range []struct {
		action   string
		decision string
	}{
		{action: ActionApprove, decision: orchestrator.ApprovalDecisionApproved},
		{action: ActionReject, decision: orchestrator.ApprovalDecisionRejected},
	} {
		t.Run(test.action, func(t *testing.T) {
			db, mock := newWorkflowRunApprovalMockDB(t)
			notifications := &workflowRunApprovalNotifications{
				resolved: app.AppNotificationResult{
					NotificationID: "notification-1", MessageID: "message-1",
					Status: app.AppNotificationStatusResolved, NotificationRevision: 2,
				},
			}
			sink := &workflowRunDecisionSink{
				result: workflowRunApprovalDecisionResult(test.decision),
			}
			handler := NewHandler(nil, nil, NewProjector(notifications, NewProvisioner(db)), nil).
				WithWorkflowRunDecisionSink(sink)
			handler.now = func() time.Time { return time.Date(2026, 8, 21, 11, 0, 0, 0, time.UTC) }
			envelope := workflowRunApprovalActionEnvelope(t, test.action)
			mock.ExpectQuery("SELECT COALESCE\\(").WithArgs("workspace-1", "member-assignee").
				WillReturnRows(sqlmock.NewRows([]string{"display_name"}).AddRow("Reviewer"))

			outcome, err := handler.HandleInternalDelivery(context.Background(), envelope)
			require.NoError(t, err)
			require.Equal(t, runeffect.EffectKindNotificationResolved, outcome.Effect.Kind)
			require.Equal(t, "notification-1", outcome.Effect.Ref)
			require.Equal(t, "message-1", outcome.Effect.MessageID)
			require.Equal(t, 1, sink.calls)
			require.Equal(t, "workspace-1", sink.decision.WorkspaceID)
			require.Equal(t, "effect-1", sink.decision.EffectID)
			require.Equal(t, "notification-1", sink.decision.NotificationID)
			require.Equal(t, "member-assignee", sink.decision.ActorMemberID)
			require.Equal(t, test.decision, sink.decision.Decision)
			require.Equal(t, "invocation-1", sink.decision.ClientDecisionID)
			require.Equal(t, IdentityID, notifications.lastIdentityID)
			require.Equal(t, app.AppNotificationMutationResolve, notifications.mutated.Kind)
			require.Equal(t, "member-assignee", notifications.mutated.CausedByActor.ID)
			require.Equal(t, 1, notifications.mutationCalls)
			require.NoError(t, mock.ExpectationsWereMet())
		})
	}
}

func TestWorkflowRunApprovalHandlerCommitsFrozenCommentAndScopeToMessageBoundary(t *testing.T) {
	scope := domain.MessageScopeValue{Kind: domain.MessageScopeKindTask, ScopeID: "tsk_abcdefghijklmn"}
	for _, action := range []string{ActionApprove, ActionReject} {
		t.Run(action, func(t *testing.T) {
			db, mock := newWorkflowRunApprovalMockDB(t)
			notifications := &workflowRunApprovalNotifications{resolved: app.AppNotificationResult{
				NotificationID: "notification-1", MessageID: "message-1", Status: app.AppNotificationStatusResolved, NotificationRevision: 2,
			}}
			sink := &workflowRunDecisionSink{result: workflowRunApprovalDecisionResult(map[string]string{ActionApprove: orchestrator.ApprovalDecisionApproved, ActionReject: orchestrator.ApprovalDecisionRejected}[action])}
			sink.result.FeedbackScope = &scope
			sink.result.Comment = " committed comment "
			handler := NewHandler(nil, nil, NewProjector(notifications, NewProvisioner(db)), nil).WithWorkflowRunDecisionSink(sink)
			handler.now = func() time.Time { return time.Date(2026, 8, 21, 11, 0, 0, 0, time.UTC) }
			envelope := workflowRunApprovalActionEnvelopeWithInput(t, action, map[string]any{"comment": " committed comment "})
			mock.ExpectQuery("SELECT COALESCE\\(").WithArgs("workspace-1", "member-assignee").WillReturnRows(sqlmock.NewRows([]string{"display_name"}).AddRow("Reviewer"))
			outcome, err := handler.HandleInternalDelivery(context.Background(), envelope)
			require.NoError(t, err)
			require.NotEqual(t, runeffect.Effect{}, outcome.Effect)
			require.Equal(t, "committed comment", sink.decision.Comment)
			require.NotNil(t, sink.precommitMutation)
			require.Len(t, notifications.feedback, 1)
			require.Equal(t, "user-assignee", notifications.feedback[0].User.ID)
			require.Equal(t, "member-assignee", notifications.feedback[0].Options.ExpectedAuthorMemberID)
			require.Equal(t, scope.ScopeID, notifications.feedback[0].Scope.ScopeID)
			require.Equal(t, "committed comment", notifications.feedback[0].Content)
			require.Equal(t, workflowApprovalFeedbackClientMessageID(sink.result), notifications.feedback[0].Options.ClientMessageID)
			require.NoError(t, mock.ExpectationsWereMet())
		})
	}
}

func TestWorkflowRunApprovalFeedbackRetryFillsMessageOrCardMutationWithoutDuplicate(t *testing.T) {
	db, _ := newWorkflowRunApprovalMockDB(t)
	scope := domain.MessageScopeValue{Kind: domain.MessageScopeKindChannel, ScopeID: "chn_abcdefghijklmn"}
	notifications := &workflowRunApprovalNotifications{resolved: app.AppNotificationResult{
		NotificationID: "notification-1", MessageID: "message-1", Status: app.AppNotificationStatusResolved, NotificationRevision: 2,
	}}
	sink := &workflowRunDecisionSink{result: workflowRunApprovalDecisionResult(orchestrator.ApprovalDecisionApproved)}
	sink.result.FeedbackScope, sink.result.Comment = &scope, "comment"
	handler := NewHandler(nil, nil, NewProjector(notifications, NewProvisioner(db)), nil).WithWorkflowRunDecisionSink(sink)
	first := workflowRunApprovalActionEnvelopeWithInput(t, ActionApprove, map[string]any{"comment": "comment"})
	notifications.feedbackErr = errors.New("message unavailable")
	_, err := handler.HandleInternalDelivery(context.Background(), first)
	require.Error(t, err)
	require.Zero(t, notifications.mutationCalls, "card mutation must wait for feedback message success")

	notifications.feedbackErr = nil
	notifications.mutationErr = errors.New("card mutation unavailable")
	mockDB, mock := newWorkflowRunApprovalMockDB(t)
	handler = NewHandler(nil, nil, NewProjector(notifications, NewProvisioner(mockDB)), nil).WithWorkflowRunDecisionSink(sink)
	mock.ExpectQuery("SELECT COALESCE\\(").WithArgs("workspace-1", "member-assignee").WillReturnRows(sqlmock.NewRows([]string{"display_name"}).AddRow("Reviewer"))
	_, err = handler.HandleInternalDelivery(context.Background(), first)
	require.Error(t, err)
	require.Len(t, notifications.feedback, 1)
	require.NoError(t, mock.ExpectationsWereMet())

	// The retry sees the same durable decision and the same ClientMessageID. The
	// message writer replays the one committed comment; only the card mutation
	// is attempted again.
	notifications.mutationErr = nil
	mockDB2, mock2 := newWorkflowRunApprovalMockDB(t)
	handler = NewHandler(nil, nil, NewProjector(notifications, NewProvisioner(mockDB2)), nil).WithWorkflowRunDecisionSink(sink)
	mock2.ExpectQuery("SELECT COALESCE\\(").WithArgs("workspace-1", "member-assignee").WillReturnRows(sqlmock.NewRows([]string{"display_name"}).AddRow("Reviewer"))
	_, err = handler.HandleInternalDelivery(context.Background(), first)
	require.NoError(t, err)
	require.Len(t, notifications.feedback, 1, "retries must not duplicate the comment message")
	require.NoError(t, mock2.ExpectationsWereMet())
}

func TestWorkflowRunApprovalHandlerRetriesCardProjectionFromDurableReceipt(t *testing.T) {
	db, mock := newWorkflowRunApprovalMockDB(t)
	scope := domain.MessageScopeValue{Kind: domain.MessageScopeKindChannel, ScopeID: "chn_abcdefghijklmn"}
	notifications := &workflowRunApprovalNotifications{resolved: app.AppNotificationResult{
		NotificationID: "notification-1", MessageID: "message-1", Status: app.AppNotificationStatusActive, NotificationRevision: 1,
	}}
	sink := &workflowRunDecisionSink{result: workflowRunApprovalDecisionResult(orchestrator.ApprovalDecisionApproved), err: orchestrator.ErrApprovalNotPending}
	sink.result.FeedbackScope, sink.result.Comment, sink.result.FeedbackMessageID = &scope, "comment", "msg-feedback-durable"
	handler := NewHandler(nil, nil, NewProjector(notifications, NewProvisioner(db)), nil).WithWorkflowRunDecisionSink(sink)
	notifications.mutationErr = errors.New("card mutation unavailable")
	mock.ExpectQuery("SELECT COALESCE\\(").WithArgs("workspace-1", "member-assignee").WillReturnRows(sqlmock.NewRows([]string{"display_name"}).AddRow("Reviewer"))
	_, err := handler.HandleInternalDelivery(context.Background(), workflowRunApprovalActionEnvelopeWithInput(t, ActionApprove, map[string]any{"comment": "comment"}))
	require.Error(t, err)
	require.Empty(t, notifications.feedback, "a completed receipt retry must not rewrite the ordinary comment")
	require.Equal(t, 1, notifications.mutationCalls)
	require.NoError(t, mock.ExpectationsWereMet())

	notifications.mutationErr = nil
	db2, mock2 := newWorkflowRunApprovalMockDB(t)
	handler = NewHandler(nil, nil, NewProjector(notifications, NewProvisioner(db2)), nil).WithWorkflowRunDecisionSink(sink)
	mock2.ExpectQuery("SELECT COALESCE\\(").WithArgs("workspace-1", "member-assignee").WillReturnRows(sqlmock.NewRows([]string{"display_name"}).AddRow("Reviewer"))
	outcome, err := handler.HandleInternalDelivery(context.Background(), workflowRunApprovalActionEnvelopeWithInput(t, ActionApprove, map[string]any{"comment": "comment"}))
	require.NoError(t, err)
	require.Equal(t, runeffect.EffectKindNotificationResolved, outcome.Effect.Kind)
	require.Empty(t, notifications.feedback, "the retry must remain message-free")
	require.Equal(t, 2, notifications.mutationCalls)
	require.NoError(t, mock2.ExpectationsWereMet())
}

func TestWorkflowRunApprovalHandlerReplayRefreshesCardRevisionFromReceipt(t *testing.T) {
	db, mock := newWorkflowRunApprovalMockDB(t)
	scope := domain.MessageScopeValue{Kind: domain.MessageScopeKindChannel, ScopeID: "chn_abcdefghijklmn"}
	notifications := &workflowRunApprovalNotifications{resolved: app.AppNotificationResult{
		NotificationID: "notification-1", MessageID: "message-1", Status: app.AppNotificationStatusActive, NotificationRevision: 2,
	}}
	sink := &workflowRunDecisionSink{result: workflowRunApprovalDecisionResult(orchestrator.ApprovalDecisionApproved)}
	sink.result.FeedbackScope, sink.result.Comment, sink.result.FeedbackMessageID, sink.result.Replayed = &scope, "comment", "msg-feedback-durable", true
	handler := NewHandler(nil, nil, NewProjector(notifications, NewProvisioner(db)), nil).WithWorkflowRunDecisionSink(sink)
	mock.ExpectQuery("SELECT COALESCE\\(").WithArgs("workspace-1", "member-assignee").WillReturnRows(sqlmock.NewRows([]string{"display_name"}).AddRow("Reviewer"))
	outcome, err := handler.HandleInternalDelivery(context.Background(), workflowRunApprovalActionEnvelopeWithInput(t, ActionApprove, map[string]any{"comment": "comment"}))
	require.NoError(t, err)
	require.Equal(t, runeffect.EffectKindNotificationResolved, outcome.Effect.Kind)
	require.Equal(t, int64(2), notifications.mutated.ExpectedRevision)
	require.Empty(t, notifications.feedback)
	require.NoError(t, mock.ExpectationsWereMet())
}

func TestWorkflowRunApprovalHandlerUsesDurableFeedbackReceiptOnRetry(t *testing.T) {
	db, mock := newWorkflowRunApprovalMockDB(t)
	scope := domain.MessageScopeValue{Kind: domain.MessageScopeKindChannel, ScopeID: "chn_abcdefghijklmn"}
	notifications := &workflowRunApprovalNotifications{resolved: app.AppNotificationResult{
		NotificationID: "notification-1", MessageID: "message-1", Status: app.AppNotificationStatusResolved, NotificationRevision: 2,
	}}
	sink := &workflowRunDecisionSink{result: workflowRunApprovalDecisionResult(orchestrator.ApprovalDecisionApproved)}
	sink.result.FeedbackScope, sink.result.Comment, sink.result.FeedbackMessageID = &scope, "comment", "msg-feedback-durable"
	handler := NewHandler(nil, nil, NewProjector(notifications, NewProvisioner(db)), nil).WithWorkflowRunDecisionSink(sink)
	mock.ExpectQuery("SELECT COALESCE\\(").WithArgs("workspace-1", "member-assignee").WillReturnRows(sqlmock.NewRows([]string{"display_name"}).AddRow("Reviewer"))
	_, err := handler.HandleInternalDelivery(context.Background(), workflowRunApprovalActionEnvelopeWithInput(t, ActionApprove, map[string]any{"comment": "comment"}))
	require.NoError(t, err)
	require.Empty(t, notifications.feedback, "a replay with a durable feedback receipt must not re-authorize or rewrite the message")
	require.Equal(t, 1, notifications.mutationCalls)
	require.NoError(t, mock.ExpectationsWereMet())
}

func TestWorkflowRunApprovalHandlerRejectsInvalidCommentInput(t *testing.T) {
	cases := []map[string]any{
		{"comment": 42}, {"comment": strings.Repeat("x", orchestrator.ApprovalDecisionCommentMaxRunes+1)}, {"comment": "ok", "scope": map[string]any{"kind": "channel"}}, {"reason": "generic reject reason"},
	}
	for index, input := range cases {
		t.Run(fmt.Sprintf("case-%d", index), func(t *testing.T) {
			db, _ := newWorkflowRunApprovalMockDB(t)
			notifications := &workflowRunApprovalNotifications{}
			sink := &workflowRunDecisionSink{result: workflowRunApprovalDecisionResult(orchestrator.ApprovalDecisionApproved)}
			handler := NewHandler(nil, nil, NewProjector(notifications, NewProvisioner(db)), nil).WithWorkflowRunDecisionSink(sink)
			_, err := handler.HandleInternalDelivery(context.Background(), workflowRunApprovalActionEnvelopeWithInput(t, ActionApprove, input))
			require.Error(t, err)
			require.Zero(t, sink.calls)
			require.Empty(t, notifications.feedback)
		})
	}
}

func TestWorkflowRunApprovalHandlerRejectsCommentWithoutFrozenScope(t *testing.T) {
	db, _ := newWorkflowRunApprovalMockDB(t)
	notifications := &workflowRunApprovalNotifications{}
	sink := &workflowRunDecisionSink{result: workflowRunApprovalDecisionResult(orchestrator.ApprovalDecisionApproved)}
	handler := NewHandler(nil, nil, NewProjector(notifications, NewProvisioner(db)), nil).WithWorkflowRunDecisionSink(sink)
	_, err := handler.HandleInternalDelivery(context.Background(), workflowRunApprovalActionEnvelopeWithInput(t, ActionApprove, map[string]any{"comment": "legacy"}))
	require.Error(t, err)
	require.Empty(t, notifications.feedback)
}

func TestWorkflowRunApprovalHandlerRejectsSinkIdentitySubstitution(t *testing.T) {
	db, mock := newWorkflowRunApprovalMockDB(t)
	notifications := &workflowRunApprovalNotifications{}
	sink := &workflowRunDecisionSink{result: workflowRunApprovalDecisionResult(orchestrator.ApprovalDecisionApproved)}
	sink.result.AssigneeMemberID = "member-other"
	sink.result.DecidedByMemberID = "member-other"
	handler := NewHandler(nil, nil, NewProjector(notifications, NewProvisioner(db)), nil).
		WithWorkflowRunDecisionSink(sink)
	outcome, err := handler.HandleInternalDelivery(context.Background(), workflowRunApprovalActionEnvelope(t, ActionApprove))
	require.Error(t, err)
	require.Contains(t, err.Error(), "substituted decision identity")
	require.Equal(t, runeffect.Effect{}, outcome.Effect)
	require.Zero(t, notifications.mutationCalls)
	require.NoError(t, mock.ExpectationsWereMet())
}

func TestWorkflowRunApprovalHandlerSettlesTerminalReceiptButPreservesDecisionConflict(t *testing.T) {
	t.Run("terminal is idempotent", func(t *testing.T) {
		db, mock := newWorkflowRunApprovalMockDB(t)
		notifications := &workflowRunApprovalNotifications{resolved: app.AppNotificationResult{
			NotificationID: "notification-1", MessageID: "message-1",
			Status: app.AppNotificationStatusResolved, NotificationRevision: 2,
		}}
		sink := &workflowRunDecisionSink{err: orchestrator.ErrApprovalNotPending}
		handler := NewHandler(nil, nil, NewProjector(notifications, NewProvisioner(db)), nil).
			WithWorkflowRunDecisionSink(sink)
		outcome, err := handler.HandleInternalDelivery(context.Background(), workflowRunApprovalActionEnvelope(t, ActionApprove))
		require.NoError(t, err)
		require.Equal(t, runeffect.EffectKindNotificationResolved, outcome.Effect.Kind)
		require.Zero(t, notifications.mutationCalls)
		require.NoError(t, mock.ExpectationsWereMet())
	})

	t.Run("different decision remains conflict", func(t *testing.T) {
		db, mock := newWorkflowRunApprovalMockDB(t)
		notifications := &workflowRunApprovalNotifications{resolved: app.AppNotificationResult{
			NotificationID: "notification-1", MessageID: "message-1",
			Status: app.AppNotificationStatusResolved, NotificationRevision: 2,
		}}
		sink := &workflowRunDecisionSink{err: orchestrator.ErrApprovalDecisionConflict}
		handler := NewHandler(nil, nil, NewProjector(notifications, NewProvisioner(db)), nil).
			WithWorkflowRunDecisionSink(sink)
		_, err := handler.HandleInternalDelivery(context.Background(), workflowRunApprovalActionEnvelope(t, ActionApprove))
		require.ErrorIs(t, err, orchestrator.ErrApprovalDecisionConflict)
		require.Zero(t, notifications.mutationCalls)
		require.NoError(t, mock.ExpectationsWereMet())
	})
}

type workflowRunApprovalFeedbackMessage struct {
	User        model.User
	WorkspaceID string
	Scope       domain.MessageScopeValue
	Content     string
	Options     app.SendAppMessageOptions
}

var _ workflowRunFeedbackMessageWriter = (*workflowRunApprovalNotifications)(nil)
var _ workflowRunFeedbackMessageAtomicWriter = (*workflowRunApprovalNotifications)(nil)

type workflowRunApprovalNotifications struct {
	NotificationStore
	resolved       app.AppNotificationResult
	mutated        app.AppNotificationMutationCommand
	lastIdentityID string
	mutationCalls  int
	feedback       []workflowRunApprovalFeedbackMessage
	feedbackErr    error
	mutationErr    error
}

func (store *workflowRunApprovalNotifications) SendMessageForUserWithOptions(
	_ context.Context,
	user model.User,
	workspaceID, scopeID, content string,
	_ []string,
	options message.HumanMessageCreateOptions,
) (message.AppMessage, message.MessageDelivery, error) {
	for _, existing := range store.feedback {
		if existing.Options.ClientMessageID == options.ClientMessageID {
			return message.AppMessage{ID: "feedback-message"}, message.MessageDelivery{MessageID: "feedback-message", IdempotentReplay: true}, nil
		}
	}
	if store.feedbackErr != nil {
		return message.AppMessage{}, message.MessageDelivery{}, store.feedbackErr
	}
	store.feedback = append(store.feedback, workflowRunApprovalFeedbackMessage{
		User: user, WorkspaceID: workspaceID, Scope: domain.MessageScopeValue{ScopeID: scopeID}, Content: content,
		Options: app.SendAppMessageOptions{ClientMessageID: options.ClientMessageID, ExpectedAuthorMemberID: options.ExpectedAuthorMemberID},
	})
	return message.AppMessage{ID: "feedback-message"}, message.MessageDelivery{MessageID: "feedback-message"}, nil
}

func (store *workflowRunApprovalNotifications) WriteMessageForUserInTx(
	_ context.Context,
	_ coredb.Tx,
	user model.User,
	workspaceID, scopeID, content string,
	options message.HumanMessageCreateOptions,
) (message.AppMessage, message.MessageDelivery, error) {
	created, delivery, err := store.SendMessageForUserWithOptions(context.Background(), user, workspaceID, scopeID, content, nil, options)
	if err != nil {
		return message.AppMessage{}, message.MessageDelivery{}, err
	}
	created.AuthorProfile = message.AuthorProfile{ActorType: message.AuthorActorTypeHuman, PrincipalID: user.ID, MemberID: options.ExpectedAuthorMemberID, TargetID: options.ExpectedAuthorMemberID}
	created.ClientMessageID = options.ClientMessageID
	delivery.MessageID = created.ID
	return created, delivery, nil
}

func (store *workflowRunApprovalNotifications) GetAppNotificationForBuiltin(
	context.Context, string, string, string,
) (app.AppNotificationResult, error) {
	return store.resolved, nil
}

func (store *workflowRunApprovalNotifications) MutateAppNotificationForBuiltinDelivery(
	_ context.Context,
	identityID, _ string, _, _ string,
	command app.AppNotificationMutationCommand,
) (app.AppNotificationResult, error) {
	store.lastIdentityID = identityID
	store.mutated = command
	store.mutationCalls++
	if store.mutationErr != nil {
		return app.AppNotificationResult{}, store.mutationErr
	}
	result := store.resolved
	result.NotificationID = command.NotificationID
	result.NotificationRevision = command.ExpectedRevision + 1
	result.Status = app.AppNotificationStatusResolved
	return result, nil
}

type workflowRunDecisionSink struct {
	decision          orchestrator.ApprovalDecision
	result            orchestrator.ApprovalDecisionResult
	err               error
	calls             int
	precommitGuard    orchestrator.ApprovalDecisionPreCommitGuard
	precommitMutation orchestrator.ApprovalDecisionPreCommitMutation
	receiptErr        error
}

func (sink *workflowRunDecisionSink) DecideApproval(_ context.Context, decision orchestrator.ApprovalDecision) (orchestrator.ApprovalDecisionResult, error) {
	sink.calls++
	sink.decision = decision
	if sink.err != nil {
		return orchestrator.ApprovalDecisionResult{}, sink.err
	}
	return sink.result, nil
}

func (sink *workflowRunDecisionSink) DecideApprovalWithPreCommit(_ context.Context, decision orchestrator.ApprovalDecision, guard orchestrator.ApprovalDecisionPreCommitGuard) (orchestrator.ApprovalDecisionResult, error) {
	sink.calls++
	sink.precommitGuard = guard
	sink.decision = decision
	if sink.err != nil {
		return orchestrator.ApprovalDecisionResult{}, sink.err
	}
	return sink.result, nil
}

func (sink *workflowRunDecisionSink) DecideApprovalWithPreCommitWrite(_ context.Context, decision orchestrator.ApprovalDecision, mutation orchestrator.ApprovalDecisionPreCommitMutation) (orchestrator.ApprovalDecisionResult, error) {
	sink.calls++
	sink.precommitMutation = mutation
	sink.decision = decision
	if sink.err != nil {
		return orchestrator.ApprovalDecisionResult{}, sink.err
	}
	return sink.result, nil
}

func (sink *workflowRunDecisionSink) LoadCompletedApproval(context.Context, string, string, string, string) (orchestrator.ApprovalDecisionResult, error) {
	if sink.receiptErr != nil {
		return orchestrator.ApprovalDecisionResult{}, sink.receiptErr
	}
	return sink.result, nil
}

func workflowRunApprovalDecisionResult(decision string) orchestrator.ApprovalDecisionResult {
	return orchestrator.ApprovalDecisionResult{
		WorkspaceID: "workspace-1", PlanID: "plan-1", PlanVersionID: "version-1",
		RunID: "run-1", ActivationID: "activation-1", EffectID: "effect-1",
		NodeID: "approval", AssigneeMemberID: "member-assignee", Summary: "Review the release",
		Decision: decision, DecidedByMemberID: "member-assignee",
		DecidedAt: time.Date(2026, 8, 21, 11, 0, 0, 0, time.UTC),
	}
}

func newWorkflowRunApprovalMockDB(t *testing.T) (coredb.DB, sqlmock.Sqlmock) {
	t.Helper()
	sqlDB, mock, err := sqlmock.New()
	require.NoError(t, err)
	t.Cleanup(func() {
		mock.ExpectClose()
		require.NoError(t, sqlDB.Close())
		require.NoError(t, mock.ExpectationsWereMet())
	})
	return coredb.WrapSQLDB(sqlDB), mock
}

func workflowRunApprovalActionEnvelopeWithInput(t *testing.T, action string, input map[string]any) appruntime.InternalHandlerEnvelope {
	envelope := workflowRunApprovalActionEnvelope(t, action)
	var raw map[string]any
	require.NoError(t, json.Unmarshal(envelope.EventPayload, &raw))
	actionObject := raw["action"].(map[string]any)
	actionObject["input"] = input
	envelope.EventPayload, _ = json.Marshal(raw)
	return envelope
}

func workflowRunApprovalActionEnvelope(t *testing.T, action string) appruntime.InternalHandlerEnvelope {
	t.Helper()
	raw, err := json.Marshal(map[string]any{
		"eventType": "agentteam.app.notification.action",
		"notification": map[string]any{
			"id": "notification-1", "revision": 1, "messageId": "message-1",
			"businessObject": map[string]any{
				"namespace": BusinessNamespace, "type": BusinessTypeWorkflowRunApproval,
				"id": "effect-1", "version": 1,
			},
		},
		"action": map[string]any{
			"invocationId": "invocation-1", "actionKey": action, "input": map[string]any{},
		},
		"actor":         map[string]any{"memberId": "member-assignee", "userId": "user-assignee"},
		"correlationId": "run-1",
	})
	require.NoError(t, err)
	return appruntime.InternalHandlerEnvelope{
		HandlerKey: HandlerKey,
		DeliveryEnvelope: serviceactor.DeliveryEnvelope{
			Delivery: serviceactor.EventDelivery{
				ID: "delivery-1", WorkspaceID: "workspace-1", EventID: "invocation-1",
				ExternalResourceKey: "notification-1",
			},
			Attempt:      serviceactor.DeliveryAttempt{ID: "attempt-1"},
			EventPayload: raw,
		},
	}
}
