package dialectsfcrm

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"log"
	"net/http"
	"strings"
	"testing"
	"time"

	"github.com/nexuscrm/dialect-sf-crm/wire"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/dialect"
	"github.com/nexuscrm/shared/pkg/models"
)

func TestStandardActionDispatchTimeoutDefaultsToNinetySeconds(t *testing.T) {
	t.Setenv("STANDARD_ACTION_FLOW_TIMEOUT", "")
	if got := standardActionDispatchTimeout(); got != 90*time.Second {
		t.Fatalf("standardActionDispatchTimeout() = %s, want 90s", got)
	}
}

func TestStandardActionDispatchTimeoutHonorsExplicitDuration(t *testing.T) {
	t.Setenv("STANDARD_ACTION_FLOW_TIMEOUT", "45s")
	if got := standardActionDispatchTimeout(); got != 45*time.Second {
		t.Fatalf("standardActionDispatchTimeout() = %s, want 45s", got)
	}
}

func TestStandardActionDispatchTimeoutInvalidOverrideUsesNinetySeconds(t *testing.T) {
	t.Setenv("STANDARD_ACTION_FLOW_TIMEOUT", "not-a-duration")
	if got := standardActionDispatchTimeout(); got != 90*time.Second {
		t.Fatalf("standardActionDispatchTimeout() = %s, want 90s", got)
	}
}

func TestStandardActionsListNeutralHandlerProjectsV63DiscoveryEnvelope(t *testing.T) {
	svc := &standardActionServiceStub{
		listDescriptors: []contract.StandardActionDescriptor{{
			Name:                   "convertLead",
			Label:                  "Convert Lead",
			Description:            "Convert a lead",
			Category:               "record",
			FlowName:               "Std_ConvertLead",
			IsAtomic:               true,
			RequiredPermissions:    []string{"ConvertLeads"},
			TargetObjects:          []string{"Lead"},
			RendererID:             "app-pack:crm/action-renderer/lead-convert",
			VisibilityCondition:    "Status == 'Qualified'",
			PreviewFlowName:        "Std_ConvertLead_Preview",
			HeaderFallbackEligible: true,
			CreatePreviewSupported: true,
			InputGuards: []contract.StandardActionInputGuard{{
				GatedInput:          "targetAccountId",
				RequiredPermission:  "CreateAccounts",
				RequiredObject:      "Account",
				RequiredOperation:   "create",
				Condition:           "true",
				CallerHasPermission: true,
			}},
			Inputs: []contract.StandardActionSchemaField{
				{
					Name:         "leadId",
					Type:         "id",
					Required:     true,
					ReferenceTo:  "Lead",
					Label:        "Lead Id",
					UISection:    "main",
					UIMode:       "hidden",
					TargetObject: "Lead",
				},
				{Name: "attachments", Type: "id", IsCollection: true},
			},
			Outputs: []contract.StandardActionSchemaField{{
				Name:  "accountId",
				Type:  "id",
				Label: "Account Id",
			}},
		}},
	}

	resp, err := ListStandardActionsNeutralHandler(context.Background(), &dialect.Request{
		Version: "v60.0",
		User:    testUser(),
	}, standardActionDeps(svc))
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if resp.Status != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	assertBody(t, "standard-action-list", resp.Body, "", `{"actions":[{"name":"convertLead","label":"Convert Lead","description":"Convert a lead","category":"record","flowDeveloperName":"Std_ConvertLead","isAtomic":true,"requiredPermissions":["ConvertLeads"],"targetObjects":["Lead"],"headerFallbackEligible":true,"rendererId":"app-pack:crm/action-renderer/lead-convert","inputGuards":[{"gatedInput":"targetAccountId","requiredPermission":"CreateAccounts","requiredObject":"Account","requiredOperation":"create","condition":"true","callerHasPermission":true}],"inputs":[{"name":"leadId","type":"id","required":true,"referenceTo":"Lead","label":"Lead Id","uiSection":"main","uiMode":"hidden","targetObject":"Lead"},{"name":"attachments","type":"id","isCollection":true}],"outputs":[{"name":"accountId","type":"id","label":"Account Id"}],"endpoint":"/services/data/v60.0/actions/standard/convertLead","previewEndpoint":"/services/data/v60.0/actions/standard/convertLead/preview","visibilityCondition":"Status == 'Qualified'","previewFlowDeveloperName":"Std_ConvertLead_Preview"}]}`)
}

func TestStandardActionsListNeutralHandlerProjectsHeaderFallbackEligibility(t *testing.T) {
	svc := &standardActionServiceStub{
		listDescriptors: []contract.StandardActionDescriptor{{
			Name:                   "taskCall",
			Label:                  "Log a Call",
			Category:               "ActivityTimeline",
			FlowName:               "Std_TaskCall",
			TargetObjects:          []string{"Contact"},
			HeaderFallbackEligible: false,
		}},
	}

	resp, err := ListStandardActionsNeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0",
		User:    testUser(),
	}, standardActionDeps(svc))
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if resp.Status != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	assertBody(t, "standard-action-header-fallback-eligible", resp.Body, "", `{"actions":[{"name":"taskCall","label":"Log a Call","category":"ActivityTimeline","flowDeveloperName":"Std_TaskCall","isAtomic":false,"requiredPermissions":null,"targetObjects":["Contact"],"headerFallbackEligible":false,"inputGuards":[],"inputs":[],"outputs":[],"endpoint":"/services/data/v63.0/actions/standard/taskCall"}]}`)
}

func TestStandardActionsListNeutralHandlerScopesByTargetObject(t *testing.T) {
	svc := &standardActionServiceStub{
		scopedDescriptors: []contract.StandardActionDescriptor{{
			Name:                   "createQuote",
			Label:                  "Create Quote",
			FlowName:               "Std_CreateQuote",
			RequiredPermissions:    []string{"CreateQuotes"},
			TargetObjects:          []string{"Opportunity"},
			HeaderFallbackEligible: true,
		}},
	}

	resp, err := ListStandardActionsNeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0",
		User:    testUser(),
		Query:   map[string][]string{"targetObject": {"Opportunity"}},
	}, standardActionDeps(svc))
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if resp.Status != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	if svc.scopedCalls != 1 || svc.scopedTarget != "Opportunity" {
		t.Fatalf("scoped calls = %d target = %q, want one Opportunity call", svc.scopedCalls, svc.scopedTarget)
	}
	assertBody(t, "target-scoped-standard-action-list", resp.Body, "", `{"actions":[{"name":"createQuote","label":"Create Quote","flowDeveloperName":"Std_CreateQuote","isAtomic":false,"requiredPermissions":["CreateQuotes"],"targetObjects":["Opportunity"],"headerFallbackEligible":true,"inputGuards":[],"inputs":[],"outputs":[],"endpoint":"/services/data/v63.0/actions/standard/createQuote"}]}`)
}

func TestStandardActionPreviewNeutralHandlerProjectsRecordCreateImage(t *testing.T) {
	svc := &standardActionServiceStub{
		resolution: activeStandardActionResolution("convertLead", false),
		previewResult: &contract.StandardActionPreviewResult{
			ActionName: "convertLead",
			IsSuccess:  true,
			Errors: []contract.StandardActionError{{
				Code:    wire.SFCodeRequiredFieldMissing,
				Message: "required field missing",
				Fields:  []string{"Company"},
			}},
			ParentPreview: &contract.StandardActionCreatePreviewImage{
				Object:                    "Account",
				SiblingMaxDefaultedFields: []string{"SortOrder__c"},
				Fields: contract.Record{
					models.ObjectTypeMetadataKey: "Account",
					"Name":                       "Acme",
					"Owner": contract.Record{
						models.ObjectTypeMetadataKey: "User",
						"Id":                         "005000000000001AAA",
					},
				},
			},
			ChildPreview: []contract.StandardActionChildPreview{{
				Object: "Contact",
				Count:  1,
				Rows: []contract.Record{{
					models.ObjectTypeMetadataKey: "Contact",
					"FirstName":                  "Ada",
				}},
			}},
		},
	}

	resp, err := PreviewStandardActionNeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0",
		User:    testUser(),
		Params:  map[string]string{"name": "convertLead"},
		Body:    []byte(`{"input":{"sourceRecordId":"00Qtest001"}}`),
	}, standardActionDeps(svc))
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if resp.Status != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	if svc.previewInputs["sourceRecordId"] != "00Qtest001" {
		t.Fatalf("preview input = %#v, want sourceRecordId forwarded", svc.previewInputs)
	}
	assertBody(t, "standard-action-preview", resp.Body, "", `{"actionName":"convertLead","isSuccess":true,"errors":[{"statusCode":"REQUIRED_FIELD_MISSING","message":"required field missing","fields":["Company"]}],"parentPreview":{"object":"Account","fields":{"Name":"Acme","Owner":{"Id":"005000000000001AAA","attributes":{"type":"User"}}},"siblingMaxDefaultedFields":["SortOrder__c"]},"childPreview":[{"object":"Contact","count":1,"rows":[{"FirstName":"Ada"}]}]}`)
}

func TestStandardActionDispatchNeutralHandlerInvokesRegistryAction(t *testing.T) {
	svc := &standardActionServiceStub{
		resolution: activeStandardActionResolution("convertLead", false),
		invokeResults: []contract.StandardActionInvocationResult{{
			ActionName: "convertLead",
			IsSuccess:  true,
			OutputValues: map[string]any{
				"account": contract.Record{
					models.ObjectTypeMetadataKey: "Account",
					"Id":                         "001000000000001AAA",
					"Name":                       "Acme",
				},
			},
		}},
	}

	resp, err := DispatchStandardActionNeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0",
		User:    testUser(),
		Params:  map[string]string{"name": "convertLead"},
		Body:    []byte(`{"inputs":[{"leadId":"00Qtest001"}]}`),
	}, standardActionDeps(svc))
	if err != nil {
		t.Fatalf("DispatchStandardActionNeutralHandler err = %v", err)
	}
	if resp.Status != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	if svc.authorizeCalls != 1 || svc.invokeResolvedCalls != 1 || len(svc.invokeInputs) != 1 || svc.invokeInputs[0]["leadId"] != "00Qtest001" {
		t.Fatalf("authorize/invoke calls/inputs = %d/%d/%#v", svc.authorizeCalls, svc.invokeResolvedCalls, svc.invokeInputs)
	}
	assertBody(t, "standard-action-dispatch-registry", resp.Body, "",
		`[{"actionName":"convertLead","isSuccess":true,"outputValues":{"account":{"Id":"001000000000001AAA","Name":"Acme"}}}]`)
}

func TestSendRecordEmailDispatchAlwaysAuthorizesResolvedBinding(t *testing.T) {
	svc := &standardActionServiceStub{
		resolution: activeStandardActionResolution(sendRecordEmailStandardActionName, false),
		authorizeOutcome: &contract.StandardActionOutcome{
			Kind: contract.StandardActionOutcomeUnauthorized, Message: "authorization denied",
		},
	}

	resp, err := DispatchStandardActionNeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0", User: &models.UserSession{ID: "005-user"},
		Params: map[string]string{"name": sendRecordEmailStandardActionName},
		Body:   []byte(`{"inputs":[{"requestId":"request-1","recordId":"003-contact","objectApiName":"Contact","subject":"Hello","body":"Body"}]}`),
	}, standardActionDeps(svc))
	if err != nil || resp.Status != http.StatusForbidden || svc.authorizeCalls != 1 || svc.invokeResolvedCalls != 0 {
		t.Fatalf("response/error=%#v/%v authorize/invoke=%d/%d", resp, err, svc.authorizeCalls, svc.invokeResolvedCalls)
	}
}

func TestStandardActionDispatchNeutralHandlerBatchTimeoutsRemainResultLevelFailures(t *testing.T) {
	svc := &standardActionServiceStub{
		resolution: activeStandardActionResolution("convertLead", false),
		invokeOutcome: &contract.StandardActionOutcome{
			Kind:    contract.StandardActionOutcomeTimeout,
			Message: `standard action "convertLead" exceeded its execution time budget`,
		},
	}

	resp, err := DispatchStandardActionNeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0",
		User:    testUser(),
		Params:  map[string]string{"name": "convertLead"},
		Body:    []byte(`{"inputs":[{"leadId":"00Q1"},{"leadId":"00Q2"}]}`),
	}, standardActionDeps(svc))
	if err != nil {
		t.Fatalf("DispatchStandardActionNeutralHandler err = %v", err)
	}
	if resp.Status != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	if svc.invokeResolvedCalls != 2 {
		t.Fatalf("invoke calls = %d, want 2", svc.invokeResolvedCalls)
	}
	assertBody(t, "standard-action-dispatch-batch-timeout", resp.Body, "",
		`[{"actionName":"convertLead","errors":[{"statusCode":"LIMIT_EXCEEDED","message":"standard action \"convertLead\" exceeded its execution time budget"}],"isSuccess":false},{"actionName":"convertLead","errors":[{"statusCode":"LIMIT_EXCEEDED","message":"standard action \"convertLead\" exceeded its execution time budget"}],"isSuccess":false}]`)
}

func TestStandardActionDispatchNeutralHandlerBatchSharesAggregateTimeout(t *testing.T) {
	t.Setenv("STANDARD_ACTION_FLOW_TIMEOUT", "20ms")
	svc := &standardActionServiceStub{
		resolution: activeStandardActionResolution("convertLead", false),
		invokeFn: func(ctx context.Context, _ *contract.StandardActionRef, _ []map[string]any, _ *models.UserSession) ([]contract.StandardActionInvocationResult, *contract.StandardActionOutcome) {
			<-ctx.Done()
			return nil, &contract.StandardActionOutcome{Kind: contract.StandardActionOutcomeTimeout}
		},
	}

	resp, err := DispatchStandardActionNeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0",
		User:    testUser(),
		Params:  map[string]string{"name": "convertLead"},
		Body:    []byte(`{"inputs":[{"leadId":"00Q1"},{"leadId":"00Q2"}]}`),
	}, standardActionDeps(svc))
	if err != nil {
		t.Fatalf("DispatchStandardActionNeutralHandler err = %v", err)
	}
	if resp.Status != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	if svc.invokeResolvedCalls != 1 {
		t.Fatalf("invoke calls = %d, want 1 after the aggregate deadline", svc.invokeResolvedCalls)
	}
	assertBody(t, "standard-action-dispatch-batch-aggregate-timeout", resp.Body, "",
		`[{"actionName":"convertLead","errors":[{"statusCode":"LIMIT_EXCEEDED","message":"standard action \"convertLead\" exceeded its execution time budget"}],"isSuccess":false},{"actionName":"convertLead","errors":[{"statusCode":"LIMIT_EXCEEDED","message":"standard action \"convertLead\" exceeded its execution time budget"}],"isSuccess":false}]`)
}

func TestStandardActionDispatchNeutralHandlerPreservesLegacyChangeOwnerWireShape(t *testing.T) {
	svc := &standardActionServiceStub{}
	records := newLegacyStandardActionRecordsStub()
	records.put("Account", "001old", contract.Record{"Id": "001old", "OwnerId": "user-1", "Name": "Acme"})
	records.put(constants.TableUser, "005new", contract.Record{"Id": "005new", constants.FieldIsActive: true})

	resp, err := DispatchStandardActionNeutralHandler(context.Background(), &dialect.Request{
		User:   testUser(),
		Params: map[string]string{"name": "changeOwner"},
		Body:   []byte(`{"inputs":[{"recordId":"001old","objectApiName":"Account","newOwnerId":"005new"}]}`),
	}, standardActionDepsWithContracts(svc, contract.Deps{
		Records:                   records,
		RecordMutationCoordinator: records,
		Metadata:                  &legacyStandardActionMetadataStub{},
		Policy:                    &legacyStandardActionPolicyStub{},
	}))
	if err != nil {
		t.Fatalf("DispatchStandardActionNeutralHandler err = %v", err)
	}
	if resp.Status != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	if records.txCalls != 1 {
		t.Fatalf("pessimistic tx calls = %d, want 1", records.txCalls)
	}
	assertBody(t, "standard-action-dispatch-legacy-change-owner", resp.Body, "",
		`[{"errors":[],"isSuccess":true,"outputValues":{"newOwnerId":"005new","previousOwnerId":"user-1","recordId":"001old"}}]`)
}

func TestStandardActionDispatchLegacyChangeOwnerAllowsObjectSpecificTransferPermission(t *testing.T) {
	svc := &standardActionServiceStub{}
	records := newLegacyStandardActionRecordsStub()
	records.put("Lead", "00Qold", contract.Record{"Id": "00Qold", "OwnerId": "user-1", "Name": "Acme"})
	records.put(constants.TableUser, "005new", contract.Record{"Id": "005new", constants.FieldIsActive: true})
	records.updateCheck = func(ctx context.Context) error {
		if !constants.IsScopedInternalAccess(ctx, "Lead", constants.PermEdit) {
			t.Fatal("non-owner transfer must stamp scoped edit access for the target object")
		}
		return nil
	}
	policy := &legacyStandardActionPolicyStub{grants: map[contract.Capability]bool{
		contract.Capability("TransferAnyLead"): true,
	}}

	resp, err := DispatchStandardActionNeutralHandler(context.Background(), &dialect.Request{
		User:   &models.UserSession{ID: "005new"},
		Params: map[string]string{"name": "changeOwner"},
		Body:   []byte(`{"inputs":[{"recordId":"00Qold","objectApiName":"Lead","newOwnerId":"005new"}]}`),
	}, standardActionDepsWithContracts(svc, contract.Deps{
		Records:                   records,
		RecordMutationCoordinator: records,
		Metadata:                  &legacyStandardActionMetadataStub{},
		Policy:                    policy,
	}))
	if err != nil {
		t.Fatalf("DispatchStandardActionNeutralHandler err = %v", err)
	}
	if resp.Status != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	if got := policy.checkedCaps; len(got) != 2 || got[0] != constants.PermTransferAnyEntity || got[1] != contract.Capability("TransferAnyLead") {
		t.Fatalf("checked capabilities = %v, want [TransferAnyEntity TransferAnyLead]", got)
	}
}

func TestExecuteChangeOwnerActionAllowsObjectSpecificTransferPermission(t *testing.T) {
	records := newLegacyStandardActionRecordsStub()
	records.put("Lead", "00Qold", contract.Record{"Id": "00Qold", "OwnerId": "user-1", "Name": "Acme"})
	records.put(constants.TableUser, "005new", contract.Record{"Id": "005new", constants.FieldIsActive: true})
	records.updateCheck = func(ctx context.Context) error {
		if !constants.IsScopedInternalAccess(ctx, "Lead", constants.PermEdit) {
			t.Fatal("non-owner transfer must stamp scoped edit access for the target object")
		}
		return nil
	}
	policy := &legacyStandardActionPolicyStub{grants: map[contract.Capability]bool{
		contract.Capability("TransferAnyLead"): true,
	}}

	result, err := ExecuteChangeOwnerAction(context.Background(), ChangeOwnerActionDeps{
		Records:                   records,
		RecordMutationCoordinator: records,
		Metadata:                  &legacyStandardActionMetadataStub{},
		Policy:                    policy,
	}, ChangeOwnerActionInput{
		RecordID:      "00Qold",
		ObjectAPIName: "Lead",
		NewOwnerID:    "005new",
	}, &models.UserSession{ID: "005new"})
	if err != nil {
		t.Fatalf("ExecuteChangeOwnerAction err = %v", err)
	}
	if result == nil || !result.IsSuccess {
		t.Fatalf("result = %#v, want success", result)
	}
	if records.records["Lead"]["00Qold"]["OwnerId"] != "005new" {
		t.Fatalf("OwnerId = %v, want 005new", records.records["Lead"]["00Qold"]["OwnerId"])
	}
	if got := result.OutputValues["previousOwnerId"]; got != "user-1" {
		t.Fatalf("previousOwnerId = %v, want user-1", got)
	}
	if got := policy.checkedCaps; len(got) != 2 || got[0] != constants.PermTransferAnyEntity || got[1] != contract.Capability("TransferAnyLead") {
		t.Fatalf("checked capabilities = %v, want [TransferAnyEntity TransferAnyLead]", got)
	}
}

func TestStandardActionDispatchLegacyChangeOwnerAllowsGlobalTransferPermission(t *testing.T) {
	svc := &standardActionServiceStub{}
	records := newLegacyStandardActionRecordsStub()
	records.put("Lead", "00Qold", contract.Record{"Id": "00Qold", "OwnerId": "user-1", "Name": "Acme"})
	records.put(constants.TableUser, "005new", contract.Record{"Id": "005new", constants.FieldIsActive: true})
	records.updateCheck = func(ctx context.Context) error {
		if !constants.IsScopedInternalAccess(ctx, "Lead", constants.PermEdit) {
			t.Fatal("global transfer permission must still stamp scoped edit access for the target object")
		}
		return nil
	}
	policy := &legacyStandardActionPolicyStub{grants: map[contract.Capability]bool{
		constants.PermTransferAnyEntity: true,
	}}

	resp, err := DispatchStandardActionNeutralHandler(context.Background(), &dialect.Request{
		User:   &models.UserSession{ID: "005new"},
		Params: map[string]string{"name": "changeOwner"},
		Body:   []byte(`{"inputs":[{"recordId":"00Qold","objectApiName":"Lead","newOwnerId":"005new"}]}`),
	}, standardActionDepsWithContracts(svc, contract.Deps{
		Records:                   records,
		RecordMutationCoordinator: records,
		Metadata:                  &legacyStandardActionMetadataStub{},
		Policy:                    policy,
	}))
	if err != nil {
		t.Fatalf("DispatchStandardActionNeutralHandler err = %v", err)
	}
	if resp.Status != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	if got := policy.checkedCaps; len(got) != 1 || got[0] != constants.PermTransferAnyEntity {
		t.Fatalf("checked capabilities = %v, want [TransferAnyEntity]", got)
	}
}

func TestStandardActionDispatchLegacyChangeOwnerRejectsNonOwnerWithoutTransferPermission(t *testing.T) {
	svc := &standardActionServiceStub{}
	records := newLegacyStandardActionRecordsStub()
	records.put("Lead", "00Qold", contract.Record{"Id": "00Qold", "OwnerId": "user-1", "Name": "Acme"})
	records.put(constants.TableUser, "005new", contract.Record{"Id": "005new", constants.FieldIsActive: true})
	policy := &legacyStandardActionPolicyStub{}

	resp, err := DispatchStandardActionNeutralHandler(context.Background(), &dialect.Request{
		User:   &models.UserSession{ID: "005new"},
		Params: map[string]string{"name": "changeOwner"},
		Body:   []byte(`{"inputs":[{"recordId":"00Qold","objectApiName":"Lead","newOwnerId":"005new"}]}`),
	}, standardActionDepsWithContracts(svc, contract.Deps{
		Records:                   records,
		RecordMutationCoordinator: records,
		Metadata:                  &legacyStandardActionMetadataStub{},
		Policy:                    policy,
	}))
	if err != nil {
		t.Fatalf("DispatchStandardActionNeutralHandler err = %v", err)
	}
	if resp.Status != http.StatusOK {
		t.Fatalf("status = %d, want 200 result-level failure; body=%#v", resp.Status, resp.Body)
	}
	if records.records["Lead"]["00Qold"]["OwnerId"] != "user-1" {
		t.Fatalf("OwnerId mutated without permission: %#v", records.records["Lead"]["00Qold"])
	}
	if got := policy.checkedCaps; len(got) != 2 || got[0] != constants.PermTransferAnyEntity || got[1] != contract.Capability("TransferAnyLead") {
		t.Fatalf("checked capabilities = %v, want [TransferAnyEntity TransferAnyLead]", got)
	}
	if !strings.Contains(fmt.Sprint(resp.Body), "permission denied") {
		t.Fatalf("body = %v, want permission denied result", resp.Body)
	}
}

func TestStandardActionDispatchNeutralHandlerPreservesLegacyCreateRecordWireShape(t *testing.T) {
	target := "Task"
	svc := &standardActionServiceStub{}
	records := newLegacyStandardActionRecordsStub()
	records.put("Account", "001old", contract.Record{"Id": "001old", "Name": "Acme"})
	records.insertResult = contract.Record{"Id": "00Tnew", "Subject": "Follow up"}

	resp, err := DispatchStandardActionNeutralHandler(context.Background(), &dialect.Request{
		User:   testUser(),
		Params: map[string]string{"name": "createRecord"},
		Body:   []byte(`{"inputs":[{"actionId":"act-create-task","recordId":"001old"}]}`),
	}, standardActionDepsWithContracts(svc, contract.Deps{
		Records:                   records,
		RecordMutationCoordinator: records,
		Metadata: &legacyStandardActionMetadataStub{actions: map[string]*models.ActionMetadata{
			"act-create-task": {
				ID:            "act-create-task",
				ObjectAPIName: "Account",
				Type:          constants.ActionTypeCreateRecord,
				TargetObject:  &target,
				Config: map[string]interface{}{
					"lookupField": "WhatId",
					"workflow": map[string]interface{}{
						"defaultValues": map[string]interface{}{"Subject": "Follow up"},
						"fieldMappings": map[string]interface{}{"Description": "Name"},
					},
				},
			},
		}},
		Policy: &legacyStandardActionPolicyStub{},
	}))
	if err != nil {
		t.Fatalf("DispatchStandardActionNeutralHandler err = %v", err)
	}
	if resp.Status != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	if records.txCalls != 1 {
		t.Fatalf("pessimistic tx calls = %d, want 1", records.txCalls)
	}
	if got := records.inserted["Description"]; got != "Acme" {
		t.Fatalf("mapped field Description = %#v, want Acme; inserted=%#v", got, records.inserted)
	}
	if got := records.inserted["WhatId"]; got != "001old" {
		t.Fatalf("lookup field WhatId = %#v, want 001old; inserted=%#v", got, records.inserted)
	}
	assertBody(t, "standard-action-dispatch-legacy-create-record", resp.Body, "",
		`[{"errors":[],"isSuccess":true,"outputValues":{"record":{"Id":"00Tnew","Subject":"Follow up"}}}]`)
}

func TestStandardActionDispatchNeutralHandlerLegacyCreateRecordLeavesSourceAwareInsertUnwrapped(t *testing.T) {
	target := constants.TableUser
	svc := &standardActionServiceStub{}
	records := newLegacyStandardActionRecordsStub()
	records.sourceAware[target] = true
	records.put("Lead", "00Qold", contract.Record{"Id": "00Qold", "Name": "Ada"})
	records.insertResult = contract.Record{"Id": "005new"}

	resp, err := DispatchStandardActionNeutralHandler(context.Background(), &dialect.Request{
		User:   testUser(),
		Params: map[string]string{"name": "createRecord"},
		Body:   []byte(`{"inputs":[{"actionId":"act-create-user","recordId":"00Qold"}]}`),
	}, standardActionDepsWithContracts(svc, contract.Deps{
		Records:                   records,
		RecordMutationCoordinator: records,
		Metadata: &legacyStandardActionMetadataStub{actions: map[string]*models.ActionMetadata{
			"act-create-user": {
				ID:            "act-create-user",
				ObjectAPIName: "Lead",
				Type:          constants.ActionTypeCreateRecord,
				TargetObject:  &target,
			},
		}},
		Policy: &legacyStandardActionPolicyStub{},
	}))
	if err != nil {
		t.Fatalf("DispatchStandardActionNeutralHandler err = %v", err)
	}
	if resp.Status != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	if records.txCalls != 0 {
		t.Fatalf("pessimistic tx calls = %d, want 0 for source-aware target", records.txCalls)
	}
	assertBody(t, "standard-action-dispatch-legacy-create-record-source-aware", resp.Body, "",
		`[{"errors":[],"isSuccess":true,"outputValues":{"record":{"Id":"005new"}}}]`)
}

func TestStandardActionDispatchNeutralHandlerDisabledDoesNotFallbackOrParseBody(t *testing.T) {
	svc := &standardActionServiceStub{
		resolution: &contract.StandardActionResolution{State: contract.StandardActionDisabled},
	}

	resp, err := DispatchStandardActionNeutralHandler(context.Background(), &dialect.Request{
		User:   testUser(),
		Params: map[string]string{"name": "changeOwner"},
		Body:   []byte(`{"inputs":`),
	}, standardActionDepsWithContracts(svc, contract.Deps{
		Records:                   newLegacyStandardActionRecordsStub(),
		RecordMutationCoordinator: newLegacyStandardActionRecordsStub(),
		Metadata:                  &legacyStandardActionMetadataStub{},
	}))
	if err != nil {
		t.Fatalf("DispatchStandardActionNeutralHandler err = %v", err)
	}
	if resp.Status != http.StatusNotFound {
		t.Fatalf("status = %d, want 404; body=%#v", resp.Status, resp.Body)
	}
	assertBody(t, "standard-action-dispatch-disabled", resp.Body,
		mustJSON(t, wire.SFErrorResponse(wire.SFCodeNotFound, `standard action "changeOwner" not found`)),
		`[{"errorCode":"NOT_FOUND","message":"standard action \"changeOwner\" not found"}]`)
}

func TestStandardActionDispatchNeutralHandlerLegacyBadBodyPreservesOldInvalidRequestMessage(t *testing.T) {
	svc := &standardActionServiceStub{}

	resp, err := DispatchStandardActionNeutralHandler(context.Background(), &dialect.Request{
		User:   testUser(),
		Params: map[string]string{"name": "changeOwner"},
		Body:   []byte(`{"inputs":`),
	}, standardActionDepsWithContracts(svc, contract.Deps{
		Records:                   newLegacyStandardActionRecordsStub(),
		RecordMutationCoordinator: newLegacyStandardActionRecordsStub(),
		Metadata:                  &legacyStandardActionMetadataStub{},
	}))
	if err != nil {
		t.Fatalf("DispatchStandardActionNeutralHandler err = %v", err)
	}
	if resp.Status != http.StatusBadRequest {
		t.Fatalf("status = %d, want 400; body=%#v", resp.Status, resp.Body)
	}
	assertBody(t, "standard-action-dispatch-legacy-bad-body", resp.Body,
		mustJSON(t, wire.SFErrorResponse(wire.SFCodeInvalidRequest, "inputs array is required")),
		`[{"errorCode":"INVALID_REQUEST","message":"inputs array is required"}]`)
}

func TestStandardActionOutputPreviewNeutralHandlerStrictlyBindsRecordContext(t *testing.T) {
	svc := &standardActionServiceStub{
		resolution: activeStandardActionResolution("convertLead", true),
		outputResult: &contract.StandardActionOutputPreview{
			IsSuccess:    true,
			OutputValues: map[string]any{"campaignFound": true, "campaignName": "Summer Campaign"},
		},
	}

	resp, err := OutputPreviewStandardActionNeutralHandler(context.Background(), &dialect.Request{
		User:   testUser(),
		Params: map[string]string{"name": "convertLead"},
		Body:   []byte(`{"recordId":"00Qtest001"}`),
	}, standardActionDeps(svc))
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if resp.Status != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	if svc.outputRecordID != "00Qtest001" {
		t.Fatalf("record id = %q, want 00Qtest001", svc.outputRecordID)
	}
	assertBody(t, "standard-action-output-preview", resp.Body, "", `{"errors":null,"isSuccess":true,"outputValues":{"campaignFound":true,"campaignName":"Summer Campaign"}}`)

	svc.outputCalls = 0
	resp, err = OutputPreviewStandardActionNeutralHandler(context.Background(), &dialect.Request{
		User:   testUser(),
		Params: map[string]string{"name": "convertLead"},
		Body:   []byte(`{"recordId":"00Qtest001","inputs":{"leadId":"fake"}}`),
	}, standardActionDeps(svc))
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if resp.Status != http.StatusBadRequest {
		t.Fatalf("status = %d, want 400; body=%#v", resp.Status, resp.Body)
	}
	if svc.outputCalls != 0 {
		t.Fatalf("PreviewResolvedOutputs called for strict-body failure")
	}
	got := mustJSON(t, resp.Body)
	if !strings.Contains(got, `"errorCode":"INVALID_REQUEST"`) || !strings.Contains(got, `unknown field \"inputs\"`) {
		t.Fatalf("strict body error = %s, want INVALID_REQUEST unknown-field message", got)
	}
}

func TestStandardActionOutputPreviewNeutralHandlerPreflightsBeforeBody(t *testing.T) {
	svc := &standardActionServiceStub{
		resolution: activeStandardActionResolution("convertLead", false),
	}

	resp, err := OutputPreviewStandardActionNeutralHandler(context.Background(), &dialect.Request{
		User:   testUser(),
		Params: map[string]string{"name": "convertLead"},
		Body:   []byte(`{`),
	}, standardActionDeps(svc))
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if resp.Status != http.StatusNotFound {
		t.Fatalf("status = %d, want 404; body=%#v", resp.Status, resp.Body)
	}
	assertBody(t, "standard-action-output-preview-no-producer", resp.Body,
		mustJSON(t, wire.SFErrorResponse(wire.SFCodeNotFound, `standard action "convertLead" has no preview producer`)),
		`[{"errorCode":"NOT_FOUND","message":"standard action \"convertLead\" has no preview producer"}]`)
}

func TestStandardActionOutcomeResponsePreservesV63Codes(t *testing.T) {
	svc := &standardActionServiceStub{
		resolution: activeStandardActionResolution("convertLead", true),
		authorizeOutcome: &contract.StandardActionOutcome{
			Kind:    contract.StandardActionOutcomeUnauthorized,
			Message: `permission denied: "ConvertLeads" is required`,
		},
	}

	resp, err := OutputPreviewStandardActionNeutralHandler(context.Background(), &dialect.Request{
		User:   testUser(),
		Params: map[string]string{"name": "convertLead"},
		Body:   []byte(`{"recordId":"00Qtest001"}`),
	}, standardActionDeps(svc))
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if resp.Status != http.StatusForbidden {
		t.Fatalf("status = %d, want 403; body=%#v", resp.Status, resp.Body)
	}
	assertBody(t, "standard-action-output-preview-unauthorized", resp.Body,
		mustJSON(t, wire.SFErrorResponse(wire.SFCodeInsufficientPerms, `permission denied: "ConvertLeads" is required`)),
		`[{"errorCode":"INSUFFICIENT_PERMISSIONS","message":"permission denied: \"ConvertLeads\" is required"}]`)
}

func TestStandardActionOutcomeTimeoutPreservesLimitExceededWireAndLogsCause(t *testing.T) {
	const secret = "flow deadline secret"
	svc := &standardActionServiceStub{
		resolution: activeStandardActionResolution("convertLead", true),
		outputOutcome: &contract.StandardActionOutcome{
			Kind:    contract.StandardActionOutcomeTimeout,
			Message: "standard action convertLead timed out",
			Cause:   errors.New(secret),
		},
	}

	var resp *dialect.Response
	logs := captureStandardActionLogs(t, func() {
		var err error
		resp, err = OutputPreviewStandardActionNeutralHandler(context.Background(), &dialect.Request{
			User:   testUser(),
			Params: map[string]string{"name": "convertLead"},
			Body:   []byte(`{"recordId":"00Qtest001"}`),
		}, standardActionDeps(svc))
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
	})
	if resp.Status != http.StatusServiceUnavailable {
		t.Fatalf("status = %d, want 503; body=%#v", resp.Status, resp.Body)
	}
	assertBody(t, "standard-action-timeout-limit-exceeded", resp.Body,
		mustJSON(t, wire.SFErrorResponse(wire.SFCodeLimitExceeded, "standard action convertLead timed out")),
		`[{"errorCode":"LIMIT_EXCEEDED","message":"standard action convertLead timed out"}]`)
	got := mustJSON(t, resp.Body)
	if strings.Contains(got, secret) || strings.Contains(got, wire.SFCodeInternalError) {
		t.Fatalf("timeout body leaked implementation detail: %s", got)
	}
	if !strings.Contains(logs, secret) || !strings.Contains(logs, "output preview standard action") {
		t.Fatalf("logs = %q, want timeout cause and operation", logs)
	}
}

func TestStandardActionNeutralHandlerMissingContractIsSanitizedInternalError(t *testing.T) {
	resp, err := ListStandardActionsNeutralHandler(context.Background(), &dialect.Request{User: testUser()}, dialect.Deps{})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if resp.Status != http.StatusInternalServerError {
		t.Fatalf("status = %d, want 500; body=%#v", resp.Status, resp.Body)
	}
	assertBody(t, "standard-action-missing-contract", resp.Body,
		mustJSON(t, wire.SFGenericInternalErrorResponse()),
		`[{"errorCode":"INTERNAL_ERROR","message":"An internal server error occurred. Contact your system administrator."}]`)
}

func standardActionDeps(service contract.StandardActionService) dialect.Deps {
	return dialect.Deps{Contracts: contract.Deps{StandardActions: service}}
}

func standardActionDepsWithContracts(service contract.StandardActionService, deps contract.Deps) dialect.Deps {
	deps.StandardActions = service
	return dialect.Deps{Contracts: deps}
}

func activeStandardActionResolution(name string, hasOutputPreviewProducer bool) *contract.StandardActionResolution {
	return &contract.StandardActionResolution{
		State:                    contract.StandardActionActive,
		HasOutputPreviewProducer: hasOutputPreviewProducer,
		Action: &contract.StandardActionRef{
			State: contract.StandardActionActive,
			Name:  name,
		},
	}
}

type standardActionServiceStub struct {
	listDescriptors   []contract.StandardActionDescriptor
	listErr           error
	scopedDescriptors []contract.StandardActionDescriptor
	scopedErr         error
	scopedCalls       int
	scopedTarget      string

	resolution *contract.StandardActionResolution
	resolveErr error

	authorizeOutcome *contract.StandardActionOutcome
	authorizeCalls   int

	invokeResults       []contract.StandardActionInvocationResult
	invokeOutcome       *contract.StandardActionOutcome
	invokeFn            func(context.Context, *contract.StandardActionRef, []map[string]any, *models.UserSession) ([]contract.StandardActionInvocationResult, *contract.StandardActionOutcome)
	invokeResolvedCalls int
	invokeInputs        []map[string]any

	previewResult  *contract.StandardActionPreviewResult
	previewOutcome *contract.StandardActionOutcome
	previewInputs  map[string]any

	outputResult   *contract.StandardActionOutputPreview
	outputOutcome  *contract.StandardActionOutcome
	outputRecordID string
	outputCalls    int
}

func (s *standardActionServiceStub) ResolveStandardAction(context.Context, string) (*contract.StandardActionResolution, error) {
	if s.resolveErr != nil {
		return nil, s.resolveErr
	}
	if s.resolution != nil {
		return s.resolution, nil
	}
	return &contract.StandardActionResolution{State: contract.StandardActionAbsent}, nil
}

func (s *standardActionServiceStub) AuthorizeResolvedStandardAction(context.Context, *contract.StandardActionRef, *models.UserSession) *contract.StandardActionOutcome {
	s.authorizeCalls++
	return s.authorizeOutcome
}

func (s *standardActionServiceStub) ListStandardActions(context.Context, *models.UserSession) ([]contract.StandardActionDescriptor, error) {
	if s.listErr != nil {
		return nil, s.listErr
	}
	return s.listDescriptors, nil
}

func (s *standardActionServiceStub) ListStandardActionsForTarget(_ context.Context, _ *models.UserSession, targetObject string) ([]contract.StandardActionDescriptor, error) {
	s.scopedCalls++
	s.scopedTarget = targetObject
	if s.scopedErr != nil {
		return nil, s.scopedErr
	}
	if s.scopedDescriptors != nil {
		return s.scopedDescriptors, nil
	}
	return filterStandardActionDescriptorsForTarget(s.listDescriptors, targetObject), nil
}

func (s *standardActionServiceStub) InvokeResolvedStandardAction(ctx context.Context, action *contract.StandardActionRef, inputs []map[string]any, user *models.UserSession) ([]contract.StandardActionInvocationResult, *contract.StandardActionOutcome) {
	s.invokeResolvedCalls++
	s.invokeInputs = inputs
	if s.invokeFn != nil {
		return s.invokeFn(ctx, action, inputs, user)
	}
	if s.invokeOutcome != nil {
		return nil, s.invokeOutcome
	}
	if s.invokeResults != nil {
		return s.invokeResults, nil
	}
	return nil, &contract.StandardActionOutcome{Kind: contract.StandardActionOutcomeInternal, Cause: errors.New("unexpected invoke call")}
}

func (s *standardActionServiceStub) PreviewResolvedRecordCreate(_ context.Context, _ *contract.StandardActionRef, input map[string]any, _ *models.UserSession) (*contract.StandardActionPreviewResult, *contract.StandardActionOutcome) {
	s.previewInputs = input
	return s.previewResult, s.previewOutcome
}

func (s *standardActionServiceStub) PreviewResolvedOutputs(_ context.Context, _ *contract.StandardActionRef, recordID string, _ *models.UserSession) (*contract.StandardActionOutputPreview, *contract.StandardActionOutcome) {
	s.outputCalls++
	s.outputRecordID = recordID
	return s.outputResult, s.outputOutcome
}

func captureStandardActionLogs(t *testing.T, fn func()) string {
	t.Helper()
	var buf bytes.Buffer
	previousWriter := log.Writer()
	previousFlags := log.Flags()
	log.SetOutput(&buf)
	log.SetFlags(0)
	t.Cleanup(func() {
		log.SetOutput(previousWriter)
		log.SetFlags(previousFlags)
	})
	fn()
	return buf.String()
}

type legacyStandardActionRecordsStub struct {
	records      map[string]map[string]contract.Record
	inserted     contract.Record
	insertResult contract.Record
	txCalls      int
	sourceAware  map[string]bool
	updateCheck  func(context.Context) error
}

func newLegacyStandardActionRecordsStub() *legacyStandardActionRecordsStub {
	return &legacyStandardActionRecordsStub{
		records:     make(map[string]map[string]contract.Record),
		sourceAware: make(map[string]bool),
	}
}

func (s *legacyStandardActionRecordsStub) put(objectName, id string, record contract.Record) {
	if s.records[objectName] == nil {
		s.records[objectName] = make(map[string]contract.Record)
	}
	s.records[objectName][id] = cloneTestRecord(record)
}

func (s *legacyStandardActionRecordsStub) Insert(_ context.Context, objectName string, data contract.Record, _ *models.UserSession) (contract.Record, error) {
	s.inserted = cloneTestRecord(data)
	if s.insertResult != nil {
		return cloneTestRecord(s.insertResult), nil
	}
	out := cloneTestRecord(data)
	out["Id"] = "created-" + objectName
	return out, nil
}

func (s *legacyStandardActionRecordsStub) Retrieve(_ context.Context, objectName string, id string, _ *models.UserSession) (contract.Record, error) {
	if byID := s.records[objectName]; byID != nil {
		if record := byID[id]; record != nil {
			return cloneTestRecord(record), nil
		}
	}
	return nil, errors.New("record not found")
}

func (s *legacyStandardActionRecordsStub) RetrieveMany(context.Context, string, []string, *models.UserSession) (map[string]contract.Record, error) {
	return nil, nil
}

func (s *legacyStandardActionRecordsStub) Update(ctx context.Context, objectName string, id string, updates contract.Record, _ *models.UserSession) error {
	if s.updateCheck != nil {
		if err := s.updateCheck(ctx); err != nil {
			return err
		}
	}
	if s.records[objectName] == nil {
		s.records[objectName] = make(map[string]contract.Record)
	}
	record := cloneTestRecord(s.records[objectName][id])
	if record == nil {
		record = contract.Record{"Id": id}
	}
	for key, value := range updates {
		record[key] = value
	}
	s.records[objectName][id] = record
	return nil
}

func (s *legacyStandardActionRecordsStub) Delete(context.Context, string, string, *models.UserSession) error {
	return nil
}

func (s *legacyStandardActionRecordsStub) UpsertByExternalID(context.Context, string, string, string, contract.Record, *models.UserSession) (contract.Record, bool, error) {
	return nil, false, nil
}

func (s *legacyStandardActionRecordsStub) RetrieveByField(context.Context, string, string, string, *models.UserSession) (contract.Record, error) {
	return nil, nil
}

func (s *legacyStandardActionRecordsStub) CreateRecords(context.Context, []contract.Record, bool, *models.UserSession) ([]contract.RecordMutationResult, error) {
	return nil, nil
}

func (s *legacyStandardActionRecordsStub) UpdateRecords(context.Context, []contract.Record, bool, *models.UserSession) ([]contract.RecordMutationResult, error) {
	return nil, nil
}

func (s *legacyStandardActionRecordsStub) DeleteRecords(context.Context, []string, bool, *models.UserSession) ([]contract.RecordMutationResult, error) {
	return nil, nil
}

func (s *legacyStandardActionRecordsStub) UpsertRecords(context.Context, string, string, []contract.Record, bool, *models.UserSession) ([]contract.RecordMutationResult, error) {
	return nil, nil
}

func (s *legacyStandardActionRecordsStub) RetrieveRecords(context.Context, string, []string, []string, *models.UserSession) ([]contract.Record, error) {
	return nil, nil
}

func (s *legacyStandardActionRecordsStub) ObjectTypeForID(context.Context, string) (string, error) {
	return "", nil
}

func (s *legacyStandardActionRecordsStub) RunInPessimisticTransaction(ctx context.Context, fn func(context.Context) error) error {
	s.txCalls++
	return fn(ctx)
}

func (s *legacyStandardActionRecordsStub) RequiresSourceAwareTransaction(objectName string) bool {
	return s.sourceAware[objectName]
}

type legacyStandardActionMetadataStub struct {
	actions map[string]*models.ActionMetadata
	schemas map[string]*models.ObjectMetadata
}

func (*legacyStandardActionMetadataStub) ResolvePicklistValuesForRecordType(context.Context, string, string, string) (*models.ScopedPicklistValueSet, error) {
	return nil, nil
}

func (s *legacyStandardActionMetadataStub) Describe(_ context.Context, objectName string) (*models.ObjectMetadata, error) {
	if s != nil && s.schemas != nil {
		return s.schemas[objectName], nil
	}
	return nil, nil
}

func (s *legacyStandardActionMetadataStub) ListObjects(context.Context) ([]models.ObjectMetadata, error) {
	return nil, nil
}

func (s *legacyStandardActionMetadataStub) Relationships(context.Context, string) ([]*models.Relationship, error) {
	return nil, nil
}

func (s *legacyStandardActionMetadataStub) ChildRelationships(context.Context, string) ([]*models.ObjectMetadata, error) {
	return nil, nil
}

func (s *legacyStandardActionMetadataStub) KeyPrefix(context.Context, string) (string, bool) {
	return "", false
}

func (s *legacyStandardActionMetadataStub) ResolveLayout(context.Context, string, *models.UserSession, string) (*models.PageLayout, error) {
	return nil, nil
}

func (s *legacyStandardActionMetadataStub) ResolveRelatedListResolution(context.Context, string, string) (*contract.RelatedListResolution, bool) {
	return nil, false
}

func (s *legacyStandardActionMetadataStub) Actions(context.Context, string) ([]*models.ActionMetadata, error) {
	return nil, nil
}

func (s *legacyStandardActionMetadataStub) ActionByID(_ context.Context, actionID string) (*models.ActionMetadata, error) {
	if s == nil || s.actions == nil {
		return nil, nil
	}
	return s.actions[actionID], nil
}

type legacyStandardActionPolicyStub struct {
	allowed     bool
	grants      map[contract.Capability]bool
	checkedCaps []contract.Capability
}

func (s *legacyStandardActionPolicyStub) Access(context.Context, *models.UserSession, string, contract.AccessRequest) (*contract.EffectiveObjectAccess, error) {
	return nil, nil
}

func (s *legacyStandardActionPolicyStub) HasCapability(_ context.Context, _ *models.UserSession, capability contract.Capability) bool {
	if s == nil {
		return false
	}
	s.checkedCaps = append(s.checkedCaps, capability)
	if s.grants != nil {
		return s.grants[capability]
	}
	return s.allowed
}

func cloneTestRecord(src contract.Record) contract.Record {
	if src == nil {
		return nil
	}
	dst := make(contract.Record, len(src))
	for key, value := range src {
		dst[key] = value
	}
	return dst
}
