package automation

import (
	"context"
	"errors"
	"strings"
	"testing"

	apperrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// fakeApprovalSubmitter implements the approvalSubmitter seam without a DB.
type fakeApprovalSubmitter struct {
	pending    bool
	pendingErr error
	result     map[string]interface{}
	err        error

	probeCalled  bool
	probeRecord  string
	probeCtx     context.Context
	submitCalled bool
	gotCtx       context.Context
	gotReq       SubmitRequest
	gotUser      *models.UserSession
}

func (f *fakeApprovalSubmitter) SubmitForApprovalAction(ctx context.Context, req SubmitRequest, user *models.UserSession) (map[string]interface{}, error) {
	f.submitCalled = true
	f.gotCtx = ctx
	f.gotReq = req
	f.gotUser = user
	return f.result, f.err
}

func (f *fakeApprovalSubmitter) HasPendingApprovalInstance(ctx context.Context, req SubmitRequest, user *models.UserSession) (bool, error) {
	f.probeCalled = true
	f.probeRecord = req.RecordID
	f.probeCtx = ctx
	return f.pending, f.pendingErr
}

const submitForApprovalTestRecordID = "a01000000000001"

// submitForApprovalTestAction builds the canonical actionCall fixture: recordId
// bound to $Record.Id (nested-resolver path), all three outputs declared, and a
// success connector. Fixture rows stay elementReference/literal only so a bare
// executor resolves them without metadata/persistence wiring.
func submitForApprovalTestAction() *models.FlowActionCall {
	return &models.FlowActionCall{
		Name:       "AutoSubmit",
		ActionType: "SubmitForApproval",
		InputParameters: []models.FlowActionParameter{
			{Name: "recordId", Value: map[string]interface{}{"elementReference": "$Record.Id"}},
		},
		OutputParameters: []models.FlowActionParameter{
			{Name: "alreadyPending", Value: map[string]interface{}{"elementReference": "vAlreadyPending"}},
			{Name: "processInstanceId", Value: map[string]interface{}{"elementReference": "vInstanceId"}},
			{Name: "instanceStatus", Value: map[string]interface{}{"elementReference": "vInstanceStatus"}},
		},
		Connector: &models.FlowConnector{TargetReference: "AfterSubmit"},
	}
}

func submitForApprovalTestInterview() *models.FlowInterview {
	return &models.FlowInterview{
		Id: "interview-sfa-1",
		ContextData: map[string]interface{}{
			"$Record": models.SObject{"Id": submitForApprovalTestRecordID},
		},
	}
}

func assertSubmitForApprovalOutputs(t *testing.T, interview *models.FlowInterview, wantAlreadyPending bool, wantInstanceID, wantStatus string) {
	t.Helper()
	if got, ok := interview.ContextData["vAlreadyPending"].(bool); !ok || got != wantAlreadyPending {
		t.Fatalf("vAlreadyPending = %v (%T), want %v", interview.ContextData["vAlreadyPending"], interview.ContextData["vAlreadyPending"], wantAlreadyPending)
	}
	if got, ok := interview.ContextData["vInstanceId"].(string); !ok || got != wantInstanceID {
		t.Fatalf("vInstanceId = %v (%T), want %q", interview.ContextData["vInstanceId"], interview.ContextData["vInstanceId"], wantInstanceID)
	}
	if got, ok := interview.ContextData["vInstanceStatus"].(string); !ok || got != wantStatus {
		t.Fatalf("vInstanceStatus = %v (%T), want %q", interview.ContextData["vInstanceStatus"], interview.ContextData["vInstanceStatus"], wantStatus)
	}
}

func TestSubmitForApprovalRegistration_PolicyClassification(t *testing.T) {
	var matches []FlowActionCallRegistration
	for _, registration := range PlatformFlowActionCallRegistrations() {
		if normalizeFlowActionType(registration.ActionType) == "submitforapproval" {
			matches = append(matches, registration)
		}
	}
	if len(matches) != 1 {
		t.Fatalf("PlatformFlowActionCallRegistrations() submitforapproval registrations = %d, want 1", len(matches))
	}
	registration := matches[0]
	if registration.AtomicPolicy != FlowActionCallAtomicPolicyReject {
		t.Fatalf("AtomicPolicy = %v, want Reject", registration.AtomicPolicy)
	}
	// NOTE: FlowActionCallPreviewPolicyReject is the zero value, so this
	// assertion alone cannot catch an omitted field — the genuine preview
	// discriminator is TestSubmitForApprovalAction_PreviewRejected's
	// submitCalled==false.
	if registration.PreviewPolicy != FlowActionCallPreviewPolicyReject {
		t.Fatalf("PreviewPolicy = %v, want Reject", registration.PreviewPolicy)
	}
	if registration.HandlerRef != "platform:automation/submit-for-approval" {
		t.Fatalf("HandlerRef = %q, want platform:automation/submit-for-approval", registration.HandlerRef)
	}
	if registration.SendsEmail {
		t.Fatal("SendsEmail = true, want false")
	}
	if registration.ApprovalNotificationSafe {
		t.Fatal("ApprovalNotificationSafe = true, want false — a notification flow re-submitting records would be a recursion hazard")
	}
	if registration.RequiresContext != FlowActionCallContextNone {
		t.Fatalf("RequiresContext = %v, want None", registration.RequiresContext)
	}
	if registration.AtomicRejectText == nil {
		t.Fatal("AtomicRejectText = nil, want reject text function")
	}
	rejectText := registration.AtomicRejectText(&models.FlowActionCall{ActionType: "SubmitForApproval"})
	if !strings.Contains(rejectText, "after-save flow") {
		t.Fatalf("AtomicRejectText = %q, want mention of after-save flow", rejectText)
	}
}

func TestSubmitForApprovalAction_RequiresWiredSubmitter(t *testing.T) {
	user := &models.UserSession{ID: "user-1"}

	t.Run("no fault connector returns wiring error", func(t *testing.T) {
		e := &FlowExecutor{}
		next, err := e.executeSubmitForApprovalAction(context.Background(), submitForApprovalTestInterview(), submitForApprovalTestAction(), user)
		if err == nil {
			t.Fatal("executeSubmitForApprovalAction() error = nil, want wiring error")
		}
		if !strings.Contains(err.Error(), "SetApprovalSubmitter") {
			t.Fatalf("error = %v, want mention of SetApprovalSubmitter", err)
		}
		if next != "" {
			t.Fatalf("next = %q, want empty", next)
		}
	})

	t.Run("fault connector routes the wiring error", func(t *testing.T) {
		e := &FlowExecutor{}
		action := submitForApprovalTestAction()
		action.FaultConnector = &models.FlowConnector{TargetReference: "OnFault"}
		interview := submitForApprovalTestInterview()
		next, err := e.executeSubmitForApprovalAction(context.Background(), interview, action, user)
		if err != nil {
			t.Fatalf("executeSubmitForApprovalAction() error = %v, want nil (fault routed)", err)
		}
		if next != "OnFault" {
			t.Fatalf("next = %q, want OnFault", next)
		}
		faultMessage, _ := interview.ContextData["$Flow.FaultMessage"].(string)
		if !strings.Contains(faultMessage, "SetApprovalSubmitter") {
			t.Fatalf("$Flow.FaultMessage = %q, want mention of SetApprovalSubmitter", faultMessage)
		}
	})
}

func TestBuildSubmitForApprovalRequest_InputMapping(t *testing.T) {
	user := &models.UserSession{ID: "user-1"}
	param := func(name string, value interface{}) models.FlowActionParameter {
		return models.FlowActionParameter{Name: name, Value: value}
	}

	tests := []struct {
		name       string
		params     []models.FlowActionParameter
		want       SubmitRequest
		wantErr    bool
		errMention string
	}{
		{
			name: "recordId via $Record.Id nested reference",
			params: []models.FlowActionParameter{
				param("recordId", map[string]interface{}{"elementReference": "$Record.Id"}),
			},
			want: SubmitRequest{RecordID: submitForApprovalTestRecordID},
		},
		{
			name: "objectApiName and comments literal passthrough",
			params: []models.FlowActionParameter{
				param("recordId", map[string]interface{}{"elementReference": "$Record.Id"}),
				param("objectApiName", map[string]interface{}{"stringValue": "MyObject__c"}),
				param("comments", map[string]interface{}{"stringValue": "auto submit"}),
			},
			want: SubmitRequest{RecordID: submitForApprovalTestRecordID, ObjectAPIName: "MyObject__c", Comments: "auto submit"},
		},
		{
			name: "input names are case-insensitive",
			params: []models.FlowActionParameter{
				param("RecordID", map[string]interface{}{"elementReference": "$Record.Id"}),
				param("ObjectApiName", map[string]interface{}{"stringValue": "MyObject__c"}),
			},
			want: SubmitRequest{RecordID: submitForApprovalTestRecordID, ObjectAPIName: "MyObject__c"},
		},
		{
			name: "skipEntryCriteria bool literal",
			params: []models.FlowActionParameter{
				param("recordId", map[string]interface{}{"elementReference": "$Record.Id"}),
				param("skipEntryCriteria", true),
			},
			want: SubmitRequest{RecordID: submitForApprovalTestRecordID, SkipEntryCriteria: true},
		},
		{
			name: "skipEntryCriteria booleanValue wrapper",
			params: []models.FlowActionParameter{
				param("recordId", map[string]interface{}{"elementReference": "$Record.Id"}),
				param("skipEntryCriteria", map[string]interface{}{"booleanValue": true}),
			},
			want: SubmitRequest{RecordID: submitForApprovalTestRecordID, SkipEntryCriteria: true},
		},
		{
			name: "skipEntryCriteria string true",
			params: []models.FlowActionParameter{
				param("recordId", map[string]interface{}{"elementReference": "$Record.Id"}),
				param("skipEntryCriteria", map[string]interface{}{"stringValue": "true"}),
			},
			want: SubmitRequest{RecordID: submitForApprovalTestRecordID, SkipEntryCriteria: true},
		},
		{
			name: "skipEntryCriteria string FALSE",
			params: []models.FlowActionParameter{
				param("recordId", map[string]interface{}{"elementReference": "$Record.Id"}),
				param("skipEntryCriteria", map[string]interface{}{"stringValue": "FALSE"}),
			},
			want: SubmitRequest{RecordID: submitForApprovalTestRecordID, SkipEntryCriteria: false},
		},
		{
			name: "skipEntryCriteria rejects flow truthiness strings",
			params: []models.FlowActionParameter{
				param("recordId", map[string]interface{}{"elementReference": "$Record.Id"}),
				param("skipEntryCriteria", map[string]interface{}{"stringValue": "yes"}),
			},
			wantErr:    true,
			errMention: "skipEntryCriteria",
		},
		{
			name:       "missing recordId is a validation error",
			params:     []models.FlowActionParameter{param("comments", map[string]interface{}{"stringValue": "no target"})},
			wantErr:    true,
			errMention: "recordId is required",
		},
		{
			name: "unknown input fails closed listing the supported names",
			params: []models.FlowActionParameter{
				param("recordId", map[string]interface{}{"elementReference": "$Record.Id"}),
				param("foo", map[string]interface{}{"stringValue": "bar"}),
			},
			wantErr:    true,
			errMention: "recordId, objectApiName, comments, and skipEntryCriteria",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			e := &FlowExecutor{}
			action := submitForApprovalTestAction()
			action.InputParameters = tc.params
			got, err := e.buildSubmitForApprovalRequest(context.Background(), submitForApprovalTestInterview(), action, user)
			if tc.wantErr {
				if err == nil {
					t.Fatalf("buildSubmitForApprovalRequest() error = nil, want error mentioning %q", tc.errMention)
				}
				if !strings.Contains(err.Error(), tc.errMention) {
					t.Fatalf("error = %v, want mention of %q", err, tc.errMention)
				}
				return
			}
			if err != nil {
				t.Fatalf("buildSubmitForApprovalRequest() error = %v", err)
			}
			if got != tc.want {
				t.Fatalf("request = %+v, want %+v", got, tc.want)
			}
		})
	}
}

func TestSubmitForApprovalAction_PendingProbeShortCircuits(t *testing.T) {
	fake := &fakeApprovalSubmitter{pending: true}
	e := &FlowExecutor{approvalSubmitter: fake}
	interview := submitForApprovalTestInterview()

	next, err := e.executeSubmitForApprovalAction(context.Background(), interview, submitForApprovalTestAction(), &models.UserSession{ID: "user-1"})
	if err != nil {
		t.Fatalf("executeSubmitForApprovalAction() error = %v", err)
	}
	if next != "AfterSubmit" {
		t.Fatalf("next = %q, want AfterSubmit", next)
	}
	if !fake.probeCalled {
		t.Fatal("HasPendingApprovalInstance was not called")
	}
	if fake.probeRecord != submitForApprovalTestRecordID {
		t.Fatalf("probe recordID = %q, want %q", fake.probeRecord, submitForApprovalTestRecordID)
	}
	// Submit must NEVER run for a pending record — its approval-lock gate
	// would shadow the already-pending semantics for a non-superuser.
	if fake.submitCalled {
		t.Fatal("Submit was called despite the pending pre-probe short-circuit")
	}
	assertSubmitForApprovalOutputs(t, interview, true, "", "")
}

func TestSubmitForApprovalAction_PendingProbeErrorFaults(t *testing.T) {
	probeErr := apperrors.NewInternalError("probe unavailable", nil)

	t.Run("without fault connector the error propagates", func(t *testing.T) {
		fake := &fakeApprovalSubmitter{pendingErr: probeErr}
		e := &FlowExecutor{approvalSubmitter: fake}
		next, err := e.executeSubmitForApprovalAction(context.Background(), submitForApprovalTestInterview(), submitForApprovalTestAction(), &models.UserSession{ID: "user-1"})
		if err == nil {
			t.Fatal("executeSubmitForApprovalAction() error = nil, want probe error")
		}
		if !strings.Contains(err.Error(), "probe unavailable") {
			t.Fatalf("error = %v, want probe error", err)
		}
		if next != "" {
			t.Fatalf("next = %q, want empty", next)
		}
		if fake.submitCalled {
			t.Fatal("Submit was called despite a failed pre-probe (must fail closed)")
		}
	})

	t.Run("fault connector routes the probe error", func(t *testing.T) {
		fake := &fakeApprovalSubmitter{pendingErr: probeErr}
		e := &FlowExecutor{approvalSubmitter: fake}
		action := submitForApprovalTestAction()
		action.FaultConnector = &models.FlowConnector{TargetReference: "OnFault"}
		next, err := e.executeSubmitForApprovalAction(context.Background(), submitForApprovalTestInterview(), action, &models.UserSession{ID: "user-1"})
		if err != nil {
			t.Fatalf("executeSubmitForApprovalAction() error = %v, want nil (fault routed)", err)
		}
		if next != "OnFault" {
			t.Fatalf("next = %q, want OnFault", next)
		}
		if fake.submitCalled {
			t.Fatal("Submit was called despite a failed pre-probe (must fail closed)")
		}
	})
}

// TestSubmitForApprovalAction_AlreadyPendingSentinelIsIdempotentSuccess is the
// sentinel mapping's armor: in the outbox path the pre-probe shares Submit's
// repeatable-read snapshot and short-circuits every in-snapshot competitor
// first, so ErrRecordAlreadyPending out of Submit is reachable only on tx-free
// dispatch paths (probe→Submit race). The wrapped sentinel also proves
// errors.Is survives the real submitErrorWithTarget Unwrap wrapper.
func TestSubmitForApprovalAction_AlreadyPendingSentinelIsIdempotentSuccess(t *testing.T) {
	fake := &fakeApprovalSubmitter{
		pending: false,
		err:     wrapSubmitTargetError(ErrRecordAlreadyPending, submitForApprovalTestRecordID),
	}
	e := &FlowExecutor{approvalSubmitter: fake}
	interview := submitForApprovalTestInterview()

	next, err := e.executeSubmitForApprovalAction(context.Background(), interview, submitForApprovalTestAction(), &models.UserSession{ID: "user-1"})
	if err != nil {
		t.Fatalf("executeSubmitForApprovalAction() error = %v, want nil (idempotent success)", err)
	}
	if next != "AfterSubmit" {
		t.Fatalf("next = %q, want AfterSubmit", next)
	}
	if !fake.submitCalled {
		t.Fatal("Submit was not called")
	}
	assertSubmitForApprovalOutputs(t, interview, true, "", "")
}

func TestSubmitForApprovalAction_DomainErrorFaults(t *testing.T) {
	t.Run("without fault connector the error passes through unmodified", func(t *testing.T) {
		fake := &fakeApprovalSubmitter{err: ErrNoActiveApprovalProcess}
		e := &FlowExecutor{approvalSubmitter: fake}
		next, err := e.executeSubmitForApprovalAction(context.Background(), submitForApprovalTestInterview(), submitForApprovalTestAction(), &models.UserSession{ID: "user-1"})
		if !errors.Is(err, ErrNoActiveApprovalProcess) {
			t.Fatalf("error = %v, want ErrNoActiveApprovalProcess", err)
		}
		// The handler special-cases only the alreadyPending sentinel; every
		// other Submit error must pass through as the same value.
		if err != ErrNoActiveApprovalProcess {
			t.Fatalf("error = %#v, want the identical ErrNoActiveApprovalProcess value", err)
		}
		if next != "" {
			t.Fatalf("next = %q, want empty", next)
		}
	})

	t.Run("fault connector routes the domain error", func(t *testing.T) {
		fake := &fakeApprovalSubmitter{err: ErrNoActiveApprovalProcess}
		e := &FlowExecutor{approvalSubmitter: fake}
		action := submitForApprovalTestAction()
		action.FaultConnector = &models.FlowConnector{TargetReference: "OnFault"}
		next, err := e.executeSubmitForApprovalAction(context.Background(), submitForApprovalTestInterview(), action, &models.UserSession{ID: "user-1"})
		if err != nil {
			t.Fatalf("executeSubmitForApprovalAction() error = %v, want nil (fault routed)", err)
		}
		if next != "OnFault" {
			t.Fatalf("next = %q, want OnFault", next)
		}
	})
}

func TestSubmitForApprovalAction_DemotesSecurityContextForSubmit(t *testing.T) {
	fake := &fakeApprovalSubmitter{result: map[string]interface{}{"ProcessInstanceId": "pi-1", "InstanceStatus": constants.ApprovalStatusPending}}
	e := &FlowExecutor{approvalSubmitter: fake}
	user := &models.UserSession{ID: "user-1"}

	ctx := constants.WithSecurityContext(context.Background(), constants.SystemContext, "record-triggered dispatch elevation")
	if _, err := e.executeSubmitForApprovalAction(ctx, submitForApprovalTestInterview(), submitForApprovalTestAction(), user); err != nil {
		t.Fatalf("executeSubmitForApprovalAction() error = %v", err)
	}
	if !fake.submitCalled {
		t.Fatal("Submit was not called")
	}
	if got := constants.GetSecurityContext(fake.gotCtx); got != constants.UserContext {
		t.Fatalf("Submit security context = %v, want UserContext — flow-driven submit must gate exactly like the interactive path", got)
	}
	if got := constants.GetSecurityContext(fake.probeCtx); got != constants.UserContext {
		t.Fatalf("pre-probe security context = %v, want UserContext — the probe's resolution/authorization gates must run as the running user, not the elevated dispatch ctx", got)
	}
	if fake.gotUser != user {
		t.Fatalf("Submit user = %+v, want the triggering user session", fake.gotUser)
	}
	if fake.gotReq.RecordID != submitForApprovalTestRecordID {
		t.Fatalf("Submit request RecordID = %q, want %q", fake.gotReq.RecordID, submitForApprovalTestRecordID)
	}
}

func TestSubmitForApprovalAction_SuccessOutputs(t *testing.T) {
	fake := &fakeApprovalSubmitter{result: map[string]interface{}{"ProcessInstanceId": "pi-1", "InstanceStatus": constants.ApprovalStatusPending}}
	e := &FlowExecutor{approvalSubmitter: fake}
	interview := submitForApprovalTestInterview()

	next, err := e.executeSubmitForApprovalAction(context.Background(), interview, submitForApprovalTestAction(), &models.UserSession{ID: "user-1"})
	if err != nil {
		t.Fatalf("executeSubmitForApprovalAction() error = %v", err)
	}
	if next != "AfterSubmit" {
		t.Fatalf("next = %q, want AfterSubmit", next)
	}
	assertSubmitForApprovalOutputs(t, interview, false, "pi-1", constants.ApprovalStatusPending)
}

func TestValidateAtomicFlow_RejectsSubmitForApproval(t *testing.T) {
	meta := &models.FlowMetadata{
		RunAtomically: true,
		ActionCalls: []models.FlowActionCall{{
			Name:       "AutoSubmit",
			ActionType: "SubmitForApproval",
		}},
	}
	err := validateAtomicFlowForExecutionWithRegistry(context.Background(), "", meta, newFlowActionCallRegistry(ApprovalFlowActionCallRegistrations()...))
	if err == nil {
		t.Fatal("validateAtomicFlowForExecutionWithRegistry() error = nil, want atomic rejection")
	}
	if !strings.Contains(err.Error(), "approver-notification I/O") || !strings.Contains(err.Error(), "after-save flow") {
		t.Fatalf("error = %v, want the SubmitForApproval atomic reject text", err)
	}
}

// TestSubmitForApprovalAction_PreviewRejected drives the real executeActionCall
// preview gate against a registry seeded with the SubmitForApproval
// registration (a bare executor would fall back to the subflow-only default
// registry, where ANY unknown actionType hits the same generic reject and the
// test would prove nothing). The submitCalled==false assertion is the
// discriminator: if PreviewPolicy ever flipped to Allow/InvokeHandler the
// handler would run and this test fails.
func TestSubmitForApprovalAction_PreviewRejected(t *testing.T) {
	fake := &fakeApprovalSubmitter{result: map[string]interface{}{"ProcessInstanceId": "pi-1", "InstanceStatus": constants.ApprovalStatusPending}}
	e := &FlowExecutor{
		approvalSubmitter:  fake,
		actionCallRegistry: newFlowActionCallRegistry(ApprovalFlowActionCallRegistrations()...),
	}
	interview := submitForApprovalTestInterview()

	ctx := WithPreviewMode(context.Background())
	next, err := e.executeActionCall(ctx, interview, &models.FlowMetadata{}, submitForApprovalTestAction(), &models.UserSession{ID: "user-1"})
	if err == nil {
		t.Fatal("executeActionCall() error = nil, want preview rejection")
	}
	if !errors.Is(err, errPreviewActionCallAttempt) {
		t.Fatalf("error = %v, want errPreviewActionCallAttempt", err)
	}
	if next != "" {
		t.Fatalf("next = %q, want empty", next)
	}
	if fake.submitCalled {
		t.Fatal("Submit was called during a no-DML preview run")
	}
	if fake.probeCalled {
		t.Fatal("HasPendingApprovalInstance was called during a no-DML preview run")
	}
}

// TestBeforeSaveFlowEffects_RejectsSubmitForApproval pins the load-bearing
// dependency that keeps SubmitForApproval out of before-save flows: before-save
// flows run synchronously inside the caller's write transaction, and the ONLY
// guard is ValidateBeforeSaveFlowEffects classifying every actionCall as
// side-effecting. A future allowlist relaxation must not silently open the
// path.
func TestBeforeSaveFlowEffects_RejectsSubmitForApproval(t *testing.T) {
	for _, triggerType := range []string{"RecordBeforeCreate", "RecordBeforeUpdate"} {
		t.Run(triggerType, func(t *testing.T) {
			meta := &models.FlowMetadata{
				Start: &models.FlowStart{TriggerType: triggerType, Object: "MyObject__c"},
				ActionCalls: []models.FlowActionCall{{
					Name:       "AutoSubmit",
					ActionType: "SubmitForApproval",
				}},
			}
			err := ValidateBeforeSaveFlowEffects(meta)
			if err == nil {
				t.Fatal("ValidateBeforeSaveFlowEffects() error = nil, want side-effecting actionCall rejection")
			}
			if !strings.Contains(err.Error(), "AutoSubmit") {
				t.Fatalf("error = %v, want mention of the offending actionCall element", err)
			}
		})
	}
}

// TestBuildSubmitForApproval_DefaultsObjectFromCurrentRecord is the NX#3478
// finding-1 regression: the canonical afterCreate SubmitForApproval element
// wires only recordId (= $Record.Id), so buildSubmitForApprovalRequest defaults
// objectApiName to the current record's own stamped type — without it, Submit
// falls back to the reverse scan that reproduces the bug.
func TestBuildSubmitForApproval_DefaultsObjectFromCurrentRecord(t *testing.T) {
	e := &FlowExecutor{}
	interview := &models.FlowInterview{ContextData: map[string]interface{}{
		"$Record": models.SObject{"Id": "wid-1", "attributes": map[string]interface{}{"type": "Widget__c"}},
	}}
	action := &models.FlowActionCall{
		ActionType:      "SubmitForApproval",
		InputParameters: []models.FlowActionParameter{{Name: "recordId", Value: "wid-1"}},
	}
	req, err := e.buildSubmitForApprovalRequest(context.Background(), interview, action, &models.UserSession{ID: "u1"})
	if err != nil {
		t.Fatalf("buildSubmitForApprovalRequest error = %v", err)
	}
	if req.RecordID != "wid-1" {
		t.Fatalf("RecordID = %q, want wid-1", req.RecordID)
	}
	if req.ObjectAPIName != "Widget__c" {
		t.Fatalf("ObjectAPIName = %q, want Widget__c (defaulted from current $Record type)", req.ObjectAPIName)
	}
}

// TestBuildSubmitForApproval_CrossObjectSubmit_NoDefault protects the documented
// contract flagged in review: a submit whose recordId points at a DIFFERENT
// record than the flow's $Record (a lookup-bound cross-object submit) with a
// blank objectApiName must NOT get a synthesized hint — it stays blank so Submit
// self-resolves the real object, instead of faulting on a hint mismatch.
func TestBuildSubmitForApproval_CrossObjectSubmit_NoDefault(t *testing.T) {
	e := &FlowExecutor{}
	interview := &models.FlowInterview{ContextData: map[string]interface{}{
		// $Record is a Widget, but we submit a different record (a Gadget the
		// author bound via a lookup), leaving objectApiName blank on purpose.
		"$Record": models.SObject{"Id": "wid-1", "attributes": map[string]interface{}{"type": "Widget__c"}},
	}}
	action := &models.FlowActionCall{
		ActionType:      "SubmitForApproval",
		InputParameters: []models.FlowActionParameter{{Name: "recordId", Value: "gad-9"}},
	}
	req, err := e.buildSubmitForApprovalRequest(context.Background(), interview, action, &models.UserSession{ID: "u1"})
	if err != nil {
		t.Fatalf("buildSubmitForApprovalRequest error = %v", err)
	}
	if req.RecordID != "gad-9" {
		t.Fatalf("RecordID = %q, want gad-9", req.RecordID)
	}
	if req.ObjectAPIName != "" {
		t.Fatalf("ObjectAPIName = %q, want \"\" (cross-object submit must self-resolve, not get a synthesized hint)", req.ObjectAPIName)
	}
}

// TestBuildSubmitForApproval_ExplicitObjectWins confirms an author-supplied
// objectApiName input is never overwritten by the current-record default.
func TestBuildSubmitForApproval_ExplicitObjectWins(t *testing.T) {
	e := &FlowExecutor{}
	interview := &models.FlowInterview{ContextData: map[string]interface{}{
		"$Record": models.SObject{"Id": "wid-1", "attributes": map[string]interface{}{"type": "Widget__c"}},
	}}
	action := &models.FlowActionCall{
		ActionType: "SubmitForApproval",
		InputParameters: []models.FlowActionParameter{
			{Name: "recordId", Value: "wid-1"},
			{Name: "objectApiName", Value: "Explicit__c"},
		},
	}
	req, err := e.buildSubmitForApprovalRequest(context.Background(), interview, action, &models.UserSession{ID: "u1"})
	if err != nil {
		t.Fatalf("buildSubmitForApprovalRequest error = %v", err)
	}
	if req.ObjectAPIName != "Explicit__c" {
		t.Fatalf("ObjectAPIName = %q, want Explicit__c (explicit input must win)", req.ObjectAPIName)
	}
}
