package sfcrmappservice

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"reflect"
	"slices"
	"strings"
	"testing"
	"time"

	crmwire "github.com/pingcap-inc/nexuscrm-app-packs/sfcrm/wire"
	catalogsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/catalog/consumerv1"
	commonsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
	permissionsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/effectivepermissionsv1"
	flowsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/flowv1"
)

func standardActionsBehavior(t *testing.T) string {
	t.Helper()
	definitions := []map[string]any{
		{
			"name": "convertLead", "label": "Convert Lead",
			"flowDeveloperName": "Std_ConvertLead", "isAtomic": true,
			"createPreviewSupported":   true,
			"previewFlowDeveloperName": "Std_ConvertLeadPreview",
			"sortOrder":                0,
			"requiredPermissions":      []string{"ConvertLeads"},
			"targetObjects":            []string{"Lead"},
			"rendererId":               "app-pack:crm/action-renderer/lead-convert",
			"config": map[string]any{
				"leadConvertMatch": map[string]any{
					"contactByAccountAndEmail": map[string]any{
						"enabled": true, "accountInput": "accountId", "emailField": "Email",
					},
				},
			},
			"inputs": []map[string]any{{
				"apiName": "leadId", "dataType": "id", "isRequired": true,
				"targetObject": "Lead", "targetFieldAPIName": "Id",
			}},
			"outputs": []map[string]any{{"apiName": "contactId", "dataType": "reference"}},
			"inputGuards": []map[string]any{{
				"condition": "true", "requiredObject": "Lead", "requiredOperation": "Edit",
			}},
		},
		{
			"name": "createQuote", "label": "Create Quote",
			"flowDeveloperName": "Std_CreateQuote", "isAtomic": true,
			"sortOrder":           10,
			"requiredPermissions": []string{"ConvertLeads"},
			"targetObjects":       []string{"Opportunity"},
			"inputs":              []map[string]any{{"apiName": "opportunityId", "dataType": "id", "isRequired": true}},
			"outputs":             []map[string]any{}, "inputGuards": []map[string]any{},
		},
		{
			"name": "sendRecordEmail", "label": "Email",
			"flowDeveloperName": "Std_SendRecordEmail", "isAtomic": false,
			"sortOrder":           30,
			"requiredPermissions": []string{"SendRecordEmail"},
			"targetObjects":       []string{"Lead", "Contact"},
			"inputs":              []map[string]any{{"apiName": "recordId", "dataType": "id", "isRequired": true}},
			"outputs":             []map[string]any{}, "inputGuards": []map[string]any{},
		},
	}
	encoded, err := json.Marshal(definitions)
	if err != nil {
		t.Fatal(err)
	}
	return string(encoded)
}

func TestStandardActionFieldProjectionPreservesCollectionShape(t *testing.T) {
	collection := standardActionFieldToWire(standardActionSchemaField{
		APIName: "attachments", DataType: "id", IsCollection: true,
	})
	scalar := standardActionFieldToWire(standardActionSchemaField{
		APIName: "recordId", DataType: "id",
	})
	if !collection.IsCollection || scalar.IsCollection {
		t.Fatalf("collection=%#v scalar=%#v", collection, scalar)
	}
	encoded, err := json.Marshal([]standardActionWireField{collection, scalar})
	if err != nil {
		t.Fatal(err)
	}
	if !strings.Contains(string(encoded), `"name":"attachments","type":"id","isCollection":true`) ||
		strings.Contains(string(encoded), `"name":"recordId","type":"id","isCollection"`) {
		t.Fatalf("standard action fields = %s", encoded)
	}
}

func standardActionsTestService(
	t *testing.T,
	permissions permissionsemantic.Response,
	execute RuntimeExecuteCatalogActionFunc,
	preview ...RuntimePreviewCatalogActionFunc,
) *Service {
	return standardActionsTestServiceWithEffectiveReader(
		t,
		func(
			context.Context,
			string,
			permissionsemantic.Request,
		) (permissionsemantic.Response, error) {
			return permissions, nil
		},
		execute,
		preview...,
	)
}

func standardActionsTestServiceWithEffectiveReader(
	t *testing.T,
	readEffective RuntimeReadEffectivePermissionsFunc,
	execute RuntimeExecuteCatalogActionFunc,
	preview ...RuntimePreviewCatalogActionFunc,
) *Service {
	t.Helper()
	view := flowActionsTestView(
		t,
		flowActionsTestComponent(t, invocableActionKind, "bootstrap_standard_actions", standardActionsBehavior(t)),
		flowActionsTestComponent(t, flowComponentKind, "Std_ConvertLead", flowBehavior("Convert", true, "", "")),
		flowActionsTestComponent(t, flowComponentKind, "Std_ConvertLeadPreview", flowBehavior("Preview", false, "", "")),
		flowActionsTestComponent(t, flowComponentKind, "Std_CreateQuote", flowBehavior("Quote", true, "", "")),
		flowActionsTestComponent(t, flowComponentKind, "Std_SendRecordEmail", flowBehavior("Email", false, "", "")),
	)
	selectedReader := func(
		ctx context.Context,
		bearer string,
		request permissionsemantic.Request,
	) (permissionsemantic.Response, error) {
		response, err := readEffective(ctx, bearer, request)
		if err != nil {
			return permissionsemantic.Response{}, err
		}
		return selectedPermissionTestResponse(response, request), nil
	}
	config := permissionMetadataServiceConfig(
		selectedReader,
		func(
			context.Context,
			catalogsemantic.GetRequest,
		) (catalogsemantic.AppMetadataView, error) {
			return view, nil
		},
	)
	config.RuntimeGetAppMetadataViewV3 = func(
		context.Context,
		catalogsemantic.GetRequest,
	) (catalogsemantic.AppMetadataViewV3, error) {
		return standardActionsV3View(view), nil
	}
	config.RuntimeExecuteCatalogAction = execute
	config.RuntimePreviewCatalogAction = func(
		context.Context,
		string,
		flowsemantic.CatalogActionPreviewRequest,
	) (flowsemantic.CatalogActionPreviewResponse, error) {
		t.Fatal("unexpected catalog action preview")
		return flowsemantic.CatalogActionPreviewResponse{}, nil
	}
	if len(preview) == 1 {
		config.RuntimePreviewCatalogAction = preview[0]
	} else if len(preview) > 1 {
		t.Fatal("multiple catalog action preview test ports")
	}
	service, err := NewService(config)
	if err != nil {
		t.Fatal(err)
	}
	return service
}

func standardActionsV3View(
	view catalogsemantic.AppMetadataView,
) catalogsemantic.AppMetadataViewV3 {
	complete := catalogsemantic.AppMetadataViewV3{
		OrgID: view.OrgID, AppInstallationID: view.AppInstallationID,
		AppID: view.AppID, AppReleaseDigest: view.AppReleaseDigest,
		Lineage: view.Lineage, AppComponents: append(
			[]catalogsemantic.AppMetadataComponent(nil), view.Components...,
		),
		Receipts: view.Receipts,
	}
	complete.Receipts.CapabilityVersion = catalogsemantic.AppMetadataV3CapabilityName
	complete.Lineage.ViewDigest = catalogsemantic.ComputeAppMetadataViewV3Digest(complete)
	return complete
}

func TestStandardActionV3ViewOverlaysRevisionBoundTenantRegistryOnL1Anchor(t *testing.T) {
	base := flowActionsTestView(
		t,
		flowActionsTestComponent(t, invocableActionKind, "bootstrap_standard_actions", standardActionsBehavior(t)),
	)
	view := standardActionsV3View(base)
	behavior := []byte(`[{"name":"taskWeeklyUpdate","label":"Weekly Update","flowDeveloperName":"Std_CreateTaskActivity","isAtomic":true,"requiredPermissions":["EditTask"],"targetObjects":["Task"],"inputs":[],"outputs":[],"inputGuards":[]}]`)
	view.EffectiveComponents = []catalogsemantic.EffectiveMetadataComponent{{
		ComponentKind:          catalogsemantic.EffectiveStandardActionComponentKind,
		ComponentFormatVersion: 1,
		BusinessKey:            "bootstrap_standard_actions",
		ContentDigest:          catalogsemantic.ComputeEffectiveMetadataComponentDigest(behavior),
		CanonicalBehavior:      behavior,
	}}
	view.Lineage.ViewDigest = catalogsemantic.ComputeAppMetadataViewV3Digest(view)
	normalized, effective, err := standardActionViewFromV3(view)
	if err != nil {
		t.Fatal(err)
	}
	definitions, err := standardActionDefinitionsFromViewProfile(normalized, false)
	if err != nil {
		t.Fatal(err)
	}
	var anchorID string
	for _, component := range base.Components {
		if component.ComponentKind == invocableActionKind &&
			component.BusinessKey == "bootstrap_standard_actions" {
			anchorID = component.ComponentID
		}
	}
	definition, found := definitions["taskWeeklyUpdate"]
	if !effective || !found ||
		anchorID == "" || definition.component.ComponentID != anchorID ||
		definition.FlowDeveloperName != "Std_CreateTaskActivity" {
		t.Fatalf("effective=%t found=%t component=%q want=%q flow=%q",
			effective, found, definition.component.ComponentID,
			anchorID, definition.FlowDeveloperName)
	}
}

func standardActionsPermissions() permissionsemantic.Response {
	permissions := flowActionsPermissions(true)
	permissions.SystemPermissions = append(
		permissions.SystemPermissions,
		permissionsemantic.PermissionGrant{PermissionID: "ConvertLeads", Granted: true},
		permissionsemantic.PermissionGrant{PermissionID: "SendRecordEmail", Granted: true},
	)
	for index := range permissions.ObjectPermissions {
		if permissions.ObjectPermissions[index].ResourceType == "Lead" {
			permissions.ObjectPermissions[index].CanEdit = true
		}
	}
	sortPermissionResponse(&permissions)
	return permissions
}

func sortPermissionResponse(response *permissionsemantic.Response) {
	// The semantic validator requires deterministic, duplicate-free ordering.
	slices.SortFunc(response.SystemPermissions, func(left, right permissionsemantic.PermissionGrant) int {
		return strings.Compare(left.PermissionID, right.PermissionID)
	})
	slices.SortFunc(response.CustomPermissions, func(left, right permissionsemantic.PermissionGrant) int {
		return strings.Compare(left.PermissionID, right.PermissionID)
	})
	slices.SortFunc(response.ObjectPermissions, func(left, right permissionsemantic.ObjectPermission) int {
		return strings.Compare(left.ResourceType, right.ResourceType)
	})
}

func TestStandardActionsListUsesRevisionBoundCatalogAndExcludesUnsupportedOwner(t *testing.T) {
	service := standardActionsTestService(
		t, standardActionsPermissions(),
		func(context.Context, string, flowsemantic.CatalogActionRequest) (flowsemantic.ResponseV2, error) {
			t.Fatal("list reached ExecuteCatalogAction")
			return flowsemantic.ResponseV2{}, nil
		},
	)
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, flowActionsTestRequest(
		http.MethodGet,
		"/services/data/v63.0/actions/standard?targetObject=Lead",
		"",
	))
	if recorder.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	var response struct {
		Actions []standardActionWireDescriptor `json:"actions"`
	}
	if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
		t.Fatal(err)
	}
	if len(response.Actions) != 1 || response.Actions[0].Name != "convertLead" ||
		response.Actions[0].Endpoint != "/services/data/v63.0/actions/standard/convertLead" ||
		len(response.Actions[0].Inputs) != 1 || response.Actions[0].Inputs[0].Name != "leadId" ||
		len(response.Actions[0].InputGuards) != 1 || !response.Actions[0].InputGuards[0].CallerHasPermission ||
		string(response.Actions[0].Config) != `{"leadConvertMatch":{"contactByAccountAndEmail":{"accountInput":"accountId","emailField":"Email","enabled":true}}}` {
		t.Fatalf("actions=%#v", response.Actions)
	}
	if strings.Contains(recorder.Body.String(), "sendRecordEmail") {
		t.Fatalf("unsupported external-effect action was disclosed: %s", recorder.Body.String())
	}
}

func TestStandardActionsListReadsOnlyAuthoredPermissionClosure(t *testing.T) {
	var requests []permissionsemantic.Request
	service := standardActionsTestServiceWithEffectiveReader(
		t,
		func(
			_ context.Context,
			_ string,
			request permissionsemantic.Request,
		) (permissionsemantic.Response, error) {
			requests = append(requests, request)
			return standardActionsPermissions(), nil
		},
		func(context.Context, string, flowsemantic.CatalogActionRequest) (flowsemantic.ResponseV2, error) {
			t.Fatal("list reached ExecuteCatalogAction")
			return flowsemantic.ResponseV2{}, nil
		},
	)
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, flowActionsTestRequest(
		http.MethodGet, "/services/data/v63.0/actions/standard", "",
	))
	if recorder.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	if len(requests) != 2 || requests[0].Selection == nil || requests[1].Selection == nil ||
		!slices.Equal(requests[0].Selection.Sections, []permissionsemantic.Section{
			permissionsemantic.SectionSystemPermissions,
			permissionsemantic.SectionCustomPermissions,
		}) || len(requests[0].Selection.ResourceTypes) != 0 ||
		!slices.Equal(requests[1].Selection.Sections, []permissionsemantic.Section{
			permissionsemantic.SectionObjectPermissions,
		}) || !slices.Equal(requests[1].Selection.ResourceTypes, []string{"Lead"}) {
		t.Fatalf("permission requests = %#v", requests)
	}
}

func TestStandardActionsListAdvertisesOnlyAuthoredCreatePreviewSupport(t *testing.T) {
	service := standardActionsTestService(
		t, standardActionsPermissions(),
		func(context.Context, string, flowsemantic.CatalogActionRequest) (flowsemantic.ResponseV2, error) {
			t.Fatal("list reached ExecuteCatalogAction")
			return flowsemantic.ResponseV2{}, nil
		},
	)
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, flowActionsTestRequest(
		http.MethodGet, "/services/data/v63.0/actions/standard", "",
	))
	if recorder.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	var response struct {
		Actions []standardActionWireDescriptor `json:"actions"`
	}
	if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
		t.Fatal(err)
	}
	if len(response.Actions) != 2 {
		t.Fatalf("actions=%#v", response.Actions)
	}
	if response.Actions[0].Name != "convertLead" || response.Actions[1].Name != "createQuote" {
		t.Fatalf("action order does not preserve pack sortOrder: %#v", response.Actions)
	}
	byName := make(map[string]standardActionWireDescriptor, len(response.Actions))
	for _, action := range response.Actions {
		byName[action.Name] = action
	}
	if byName["convertLead"].PreviewEndpoint == "" {
		t.Fatalf("authored create preview was not advertised: %#v", byName["convertLead"])
	}
	if byName["createQuote"].PreviewEndpoint != "" {
		t.Fatalf("undeclared create preview was advertised: %#v", byName["createQuote"])
	}
}

func TestStandardActionCreatePreviewRejectsUndeclaredSupportBeforeRuntime(t *testing.T) {
	previewCalls := 0
	service := standardActionsTestService(
		t, standardActionsPermissions(),
		func(context.Context, string, flowsemantic.CatalogActionRequest) (flowsemantic.ResponseV2, error) {
			t.Fatal("create preview reached ExecuteCatalogAction")
			return flowsemantic.ResponseV2{}, nil
		},
		func(context.Context, string, flowsemantic.CatalogActionPreviewRequest) (flowsemantic.CatalogActionPreviewResponse, error) {
			previewCalls++
			return flowsemantic.CatalogActionPreviewResponse{}, nil
		},
	)
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, flowActionsTestRequest(
		http.MethodPost,
		"/services/data/v63.0/actions/standard/createQuote/preview",
		`not-json`,
	))
	if recorder.Code != http.StatusNotFound || previewCalls != 0 ||
		!strings.Contains(recorder.Body.String(), "has no create preview") {
		t.Fatalf(
			"undeclared create preview status=%d calls=%d body=%s",
			recorder.Code, previewCalls, recorder.Body.String(),
		)
	}
}

func TestStandardActionProjectionAcceptsExactLaunchCatalog(t *testing.T) {
	raw, err := os.ReadFile(filepath.Join(
		"..", "..", "salesforce-crm-standard", "actions", "invocable_standard_actions.json",
	))
	if err != nil {
		t.Fatal(err)
	}
	var definitions []standardActionDefinition
	if err := json.Unmarshal(raw, &definitions); err != nil {
		t.Fatal(err)
	}
	if len(definitions) != 4 {
		t.Fatalf("launch action count=%d", len(definitions))
	}
	byName := make(map[string]standardActionDefinition, len(definitions))
	for _, definition := range definitions {
		if err := validateStandardActionDefinition(definition); err != nil {
			t.Fatalf("action %q: %v", definition.Name, err)
		}
		byName[definition.Name] = definition
	}
	convertLead, found := byName["convertLead"]
	if !found || convertLead.SortOrder != 0 || convertLead.HeaderFallbackEligible != nil ||
		convertLead.CreatePreviewSupported ||
		!strings.Contains(string(convertLead.Config), `"leadConvertMatch"`) {
		t.Fatalf("launch convertLead config/sort/presence projection=%#v", convertLead)
	}
	wantPrefillSources := map[string]string{
		"contactFirstName": "FirstName",
		"contactLastName":  "LastName",
		"contactTitle":     "Title",
		"contactEmail":     "Email",
		"opportunityName":  "Company",
	}
	for _, input := range convertLead.Inputs {
		want, required := wantPrefillSources[input.APIName]
		if !required {
			continue
		}
		if input.PrefillSourceFieldAPIName != want {
			t.Errorf("convertLead input %q prefill source=%q, want %q", input.APIName, input.PrefillSourceFieldAPIName, want)
		}
		delete(wantPrefillSources, input.APIName)
	}
	if len(wantPrefillSources) != 0 {
		t.Fatalf("convertLead missing prefill inputs: %#v", wantPrefillSources)
	}
	sendRecordEmail, found := byName["sendRecordEmail"]
	if !found || sendRecordEmail.HeaderFallbackEligible == nil ||
		*sendRecordEmail.HeaderFallbackEligible ||
		!reflect.DeepEqual(sendRecordEmail.LegacyNames, []string{"taskEmail"}) {
		t.Fatalf("launch sendRecordEmail fallback projection=%#v", sendRecordEmail)
	}
	omittedWire, err := json.Marshal(standardActionToWire(
		convertLead, standardActionsPermissions(), "v63.0",
	))
	if err != nil || bytes.Contains(omittedWire, []byte(`"headerFallbackEligible"`)) ||
		bytes.Contains(omittedWire, []byte(`"previewEndpoint"`)) {
		t.Fatalf("omitted fallback wire=%s error=%v", omittedWire, err)
	}
	explicitFalseWire, err := json.Marshal(standardActionToWire(
		sendRecordEmail, standardActionsPermissions(), "v63.0",
	))
	if err != nil || !bytes.Contains(
		explicitFalseWire, []byte(`"headerFallbackEligible":false`),
	) || !bytes.Contains(explicitFalseWire, []byte(`"legacyNames":["taskEmail"]`)) {
		t.Fatalf("explicit false fallback wire=%s error=%v", explicitFalseWire, err)
	}
}

func TestStandardActionProjectionPreservesExactNumericDefaults(t *testing.T) {
	behavior := strings.Replace(
		standardActionsBehavior(t),
		`"apiName":"leadId"`,
		`"apiName":"leadId","defaulted":true,"defaultValue":9007199254740993.125`,
		1,
	)
	if !strings.Contains(behavior, `"defaultValue":9007199254740993.125`) {
		t.Fatal("test action behavior was not amended with an exact numeric default")
	}
	view := flowActionsTestView(
		t,
		flowActionsTestComponent(t, invocableActionKind, "bootstrap_standard_actions", behavior),
		flowActionsTestComponent(t, flowComponentKind, "Std_ConvertLead", flowBehavior("Convert", true, "", "")),
		flowActionsTestComponent(t, flowComponentKind, "Std_ConvertLeadPreview", flowBehavior("Preview", false, "", "")),
		flowActionsTestComponent(t, flowComponentKind, "Std_CreateQuote", flowBehavior("Quote", true, "", "")),
		flowActionsTestComponent(t, flowComponentKind, "Std_SendRecordEmail", flowBehavior("Email", false, "", "")),
	)
	definitions, err := standardActionDefinitionsFromView(view)
	if err != nil {
		t.Fatal(err)
	}
	defaultValue, ok := definitions["convertLead"].Inputs[0].DefaultValue.(json.Number)
	if !ok || defaultValue.String() != "9007199254740993.125" {
		t.Fatalf("default value=%#v (%T), want exact json.Number", definitions["convertLead"].Inputs[0].DefaultValue, definitions["convertLead"].Inputs[0].DefaultValue)
	}
	wired, err := json.Marshal(standardActionToWire(
		definitions["convertLead"], standardActionsPermissions(), "v63.0",
	))
	if err != nil || !bytes.Contains(wired, []byte(`"defaultValue":9007199254740993.125`)) {
		t.Fatalf("wire=%s error=%v", wired, err)
	}
}

func TestStandardActionProjectionFailsClosedOnMalformedAuthority(t *testing.T) {
	valid := standardActionDefinition{
		Name: "doThing", Label: "Do Thing", FlowDeveloperName: "Do_Thing",
		RequiredPermissions: []string{"DoThing"},
		Inputs:              []standardActionSchemaField{{APIName: "recordId", DataType: "id"}},
		InputGuards: []standardActionInputGuard{{
			Condition: "true", RequiredObject: "Task", RequiredOperation: "Edit",
		}},
	}
	tests := []struct {
		name   string
		mutate func(*standardActionDefinition)
	}{
		{name: "missing required permission", mutate: func(value *standardActionDefinition) {
			value.RequiredPermissions = nil
		}},
		{name: "duplicate permission", mutate: func(value *standardActionDefinition) {
			value.RequiredPermissions = []string{"DoThing", "dothing"}
		}},
		{name: "ambiguous guard", mutate: func(value *standardActionDefinition) {
			value.InputGuards[0].RequiredPermission = "DoThing"
		}},
		{name: "unsupported object operation", mutate: func(value *standardActionDefinition) {
			value.InputGuards[0].RequiredOperation = "Upsert"
		}},
		{name: "duplicate input field", mutate: func(value *standardActionDefinition) {
			value.Inputs = append(value.Inputs, standardActionSchemaField{APIName: "RECORDID", DataType: "id"})
		}},
		{name: "canonical legacy identity", mutate: func(value *standardActionDefinition) {
			value.LegacyNames = []string{"doThing"}
		}},
		{name: "duplicate legacy identity", mutate: func(value *standardActionDefinition) {
			value.LegacyNames = []string{"oldDoThing", "OLDDOTHING"}
		}},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			candidate := valid
			candidate.RequiredPermissions = append([]string(nil), valid.RequiredPermissions...)
			candidate.Inputs = append([]standardActionSchemaField(nil), valid.Inputs...)
			candidate.InputGuards = append([]standardActionInputGuard(nil), valid.InputGuards...)
			test.mutate(&candidate)
			if err := validateStandardActionDefinition(candidate); err == nil {
				t.Fatal("malformed Catalog action authority was accepted")
			}
		})
	}
}

func TestStandardActionLayoutReferencesMayOverlapOnlyAcrossDisjointObjects(t *testing.T) {
	definitions := map[string]standardActionDefinition{
		"taskEmail": {
			Name: "taskEmail", TargetObjects: []string{"Opportunity"},
		},
		"sendRecordEmail": {
			Name: "sendRecordEmail", LegacyNames: []string{"taskEmail"},
			TargetObjects: []string{"Contact", "Lead"},
		},
	}
	if err := validateStandardActionReferenceAuthority(definitions); err != nil {
		t.Fatalf("disjoint legacy reference: %v", err)
	}

	conflict := definitions["sendRecordEmail"]
	conflict.TargetObjects = []string{"Opportunity"}
	definitions["sendRecordEmail"] = conflict
	if err := validateStandardActionReferenceAuthority(definitions); err == nil ||
		!strings.Contains(err.Error(), "ambiguous") {
		t.Fatalf("overlapping legacy reference error=%v", err)
	}
}

func TestStandardActionProjectionRejectsMixedActionAndFlowAuthority(t *testing.T) {
	view := flowActionsTestView(
		t,
		flowActionsTestComponent(t, invocableActionKind, "bootstrap_standard_actions", standardActionsBehavior(t)),
		flowActionsTestComponent(t, flowComponentKind, "Std_ConvertLead", flowBehavior("Convert", false, "", "")),
		flowActionsTestComponent(t, flowComponentKind, "Std_SendRecordEmail", flowBehavior("Email", false, "", "")),
	)
	if _, err := standardActionDefinitionsFromView(view); err == nil ||
		!strings.Contains(err.Error(), "inconsistent") {
		t.Fatalf("mixed action/Flow authority error=%v", err)
	}
}

func TestStandardActionExecutePinsCatalogActionWithoutL1FlowSelection(t *testing.T) {
	var got flowsemantic.CatalogActionRequest
	receipts := commonsemantic.Receipts{
		ProtocolVersion: "1.0", CapabilityVersion: flowsemantic.CapabilityName,
		CatalogRevision: 17, SchemaGeneration: 8, EngineGeneration: 9, SecurityEpoch: 3,
	}
	service := standardActionsTestService(
		t, standardActionsPermissions(),
		func(_ context.Context, bearer string, request flowsemantic.CatalogActionRequest) (flowsemantic.ResponseV2, error) {
			if bearer != "user-secret" {
				t.Fatalf("bearer=%q", bearer)
			}
			got = request
			return flowsemantic.ResponseV2{
				ExecutionID:      "standard-action-execution",
				CanonicalOutputs: []byte(`{"contactId":"003000000000001"}`),
				Receipts:         receipts,
			}, nil
		},
	)
	request := flowActionsTestRequest(
		http.MethodPost,
		"/services/data/v63.0/actions/standard/convertLead",
		`{"inputs":[{"leadId":"00Q000000000001"}]}`,
	)
	request.Header.Set(crmMutationIdempotencyHeader, "standard-action-42")
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, request)
	if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), `"actionName":"convertLead"`) ||
		!strings.Contains(recorder.Body.String(), `"contactId":"003000000000001"`) {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	if got.CatalogAction.ActionRef != "convertLead" ||
		got.CatalogAction.ComponentRef == "" || got.MinCatalogRevision == nil || *got.MinCatalogRevision != 17 ||
		got.IdempotencyKey == "" || string(got.CanonicalInputs) != `{"leadId":"00Q000000000001"}` {
		t.Fatalf("request=%#v", got)
	}
}

func TestStandardActionExecuteCanonicalizesHTMLInputs(t *testing.T) {
	var got flowsemantic.CatalogActionRequest
	receipts := commonsemantic.Receipts{
		ProtocolVersion: "1.0", CapabilityVersion: flowsemantic.CapabilityName,
		CatalogRevision: 17, SchemaGeneration: 8, EngineGeneration: 9, SecurityEpoch: 3,
	}
	service := standardActionsTestService(
		t, standardActionsPermissions(),
		func(_ context.Context, _ string, request flowsemantic.CatalogActionRequest) (flowsemantic.ResponseV2, error) {
			got = request
			return flowsemantic.ResponseV2{
				ExecutionID:      "rich-email-execution",
				CanonicalOutputs: []byte(`{"contactId":"003000000000001"}`),
				Receipts:         receipts,
			}, nil
		},
	)
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, flowActionsTestRequest(
		http.MethodPost,
		"/services/data/v63.0/actions/standard/convertLead",
		`{"inputs":[{"htmlBody":"<p>Hello &amp; team</p>","leadId":"00Q000000000001"}]}`,
	))
	if recorder.Code != http.StatusOK ||
		!strings.Contains(recorder.Body.String(), `"isSuccess":true`) ||
		string(got.CanonicalInputs) != `{"htmlBody":"<p>Hello &amp; team</p>","leadId":"00Q000000000001"}` {
		t.Fatalf("status=%d request=%#v body=%s", recorder.Code, got, recorder.Body.String())
	}
}

func TestStandardActionExecuteResolvesPublishedIdentityCaseInsensitively(t *testing.T) {
	receipts := commonsemantic.Receipts{
		ProtocolVersion: "1.0", CapabilityVersion: flowsemantic.CapabilityName,
		CatalogRevision: 17, SchemaGeneration: 8, EngineGeneration: 9, SecurityEpoch: 3,
	}
	var gotActionRef string
	service := standardActionsTestService(
		t, standardActionsPermissions(),
		func(_ context.Context, _ string, request flowsemantic.CatalogActionRequest) (flowsemantic.ResponseV2, error) {
			gotActionRef = request.CatalogAction.ActionRef
			return flowsemantic.ResponseV2{
				ExecutionID:      "case-insensitive-standard-action",
				CanonicalOutputs: []byte(`{"contactId":"003000000000001"}`),
				Receipts:         receipts,
			}, nil
		},
	)
	request := flowActionsTestRequest(
		http.MethodPost,
		"/services/data/v63.0/actions/standard/ConvertLead",
		`{"inputs":[{"leadId":"00Q000000000001"}]}`,
	)
	request.Header.Set(crmMutationIdempotencyHeader, "case-insensitive-action")
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, request)
	if recorder.Code != http.StatusOK || gotActionRef != "convertLead" ||
		!strings.Contains(recorder.Body.String(), `"actionName":"convertLead"`) {
		t.Fatalf("status=%d actionRef=%q body=%s", recorder.Code, gotActionRef, recorder.Body.String())
	}
}

func TestStandardActionExecuteUsesStablePerInputIdempotencyKeys(t *testing.T) {
	receipts := commonsemantic.Receipts{
		ProtocolVersion: "1.0", CapabilityVersion: flowsemantic.CapabilityName,
		CatalogRevision: 17, SchemaGeneration: 8, EngineGeneration: 9, SecurityEpoch: 3,
	}
	var keys []string
	service := standardActionsTestService(
		t, standardActionsPermissions(),
		func(_ context.Context, _ string, request flowsemantic.CatalogActionRequest) (flowsemantic.ResponseV2, error) {
			keys = append(keys, request.IdempotencyKey)
			return flowsemantic.ResponseV2{
				ExecutionID:      "execution-" + request.IdempotencyKey,
				CanonicalOutputs: []byte(`{}`), Receipts: receipts,
			}, nil
		},
	)
	request := flowActionsTestRequest(
		http.MethodPost, "/services/data/v63.0/actions/standard/convertLead",
		`{"inputs":[{"leadId":"00Q1"},{"leadId":"00Q2"}]}`,
	)
	request.Header.Set(crmMutationIdempotencyHeader, "standard-action-request")
	baseKey, err := mutationCommandID([]string{"standard-action-request"})
	if err != nil {
		t.Fatal(err)
	}
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, request)
	if recorder.Code != http.StatusOK || !slices.Equal(keys, []string{
		baseKey + "-0", baseKey + "-1",
	}) {
		t.Fatalf("status=%d keys=%v body=%s", recorder.Code, keys, recorder.Body.String())
	}
}

func TestStandardActionBatchPreservesCompletedResultsIfAuthorityChanges(t *testing.T) {
	receipts := commonsemantic.Receipts{
		ProtocolVersion: "1.0", CapabilityVersion: flowsemantic.CapabilityName,
		CatalogRevision: 17, SchemaGeneration: 8, EngineGeneration: 9, SecurityEpoch: 3,
	}
	calls := 0
	service := standardActionsTestServiceWithEffectiveReader(
		t,
		func(context.Context, string, permissionsemantic.Request) (permissionsemantic.Response, error) {
			t.Fatal("execute read complete permissions")
			return permissionsemantic.Response{}, nil
		},
		func(context.Context, string, flowsemantic.CatalogActionRequest) (flowsemantic.ResponseV2, error) {
			calls++
			if calls == 2 {
				return flowsemantic.ResponseV2{}, &commonsemantic.Failure{Category: commonsemantic.FailureMissing}
			}
			return flowsemantic.ResponseV2{
				ExecutionID: "completed-first-input", CanonicalOutputs: []byte(`{}`), Receipts: receipts,
			}, nil
		},
	)
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, flowActionsTestRequest(
		http.MethodPost,
		"/services/data/v63.0/actions/standard/convertLead",
		`{"inputs":[{"leadId":"00Q1"},{"leadId":"00Q2"}]}`,
	))
	body := recorder.Body.String()
	if recorder.Code != http.StatusOK || calls != 2 ||
		strings.Count(body, `"isSuccess":true`) != 1 ||
		strings.Count(body, `"isSuccess":false`) != 1 ||
		!strings.Contains(body, `"statusCode":"NOT_FOUND"`) {
		t.Fatalf("status=%d calls=%d body=%s", recorder.Code, calls, body)
	}
}

func TestStandardActionExecutePreservesRecordProjectionAndDecimalPrecision(t *testing.T) {
	receipts := commonsemantic.Receipts{
		ProtocolVersion: "1.0", CapabilityVersion: flowsemantic.CapabilityName,
		CatalogRevision: 17, SchemaGeneration: 8, EngineGeneration: 9, SecurityEpoch: 3,
	}
	service := standardActionsTestService(
		t, standardActionsPermissions(),
		func(context.Context, string, flowsemantic.CatalogActionRequest) (flowsemantic.ResponseV2, error) {
			return flowsemantic.ResponseV2{
				ExecutionID:      "standard-action-execution",
				CanonicalOutputs: standardActionCanonicalPreview(t, `{"account":{"_objectType":"Account","_metadata":{"internal":true},"_loopDepth":3,"__r_cache":{"secret":true},"Id":"001000000000001AAA","Amount":12345678901234567890.1234,"Owner":{"_objectType":"User","_metadata":{"internal":true},"Id":"005000000000001AAA"}}}`),
				Receipts:         receipts,
			}, nil
		},
	)
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, flowActionsTestRequest(
		http.MethodPost, "/services/data/v63.0/actions/standard/convertLead",
		`{"inputs":[{"leadId":"00Q000000000001"}]}`,
	))
	want := `[{"actionName":"convertLead","isSuccess":true,"outputValues":{"account":{"Amount":12345678901234567890.1234,"Id":"001000000000001AAA","Owner":{"Id":"005000000000001AAA","attributes":{"type":"User"}},"attributes":{"type":"Account"}}}}]`
	if recorder.Code != http.StatusOK || strings.TrimSpace(recorder.Body.String()) != want {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
}

func TestStandardActionExecutePreservesLegacyTimeoutProfiles(t *testing.T) {
	deadline := &commonsemantic.Failure{Category: commonsemantic.FailureDeadlineExceeded}
	for _, test := range []struct {
		name       string
		body       string
		wantStatus int
		wantCalls  int
		wantBody   string
	}{
		{
			name: "single input remains a top-level service unavailable",
			body: `{"inputs":[{"leadId":"00Q1"}]}`, wantStatus: http.StatusServiceUnavailable,
			wantCalls: 1,
			wantBody:  `[{"errorCode":"LIMIT_EXCEEDED","message":"standard action \"convertLead\" exceeded its execution time budget"}]`,
		},
		{
			name: "batch timeout remains one result per input",
			body: `{"inputs":[{"leadId":"00Q1"},{"leadId":"00Q2"}]}`, wantStatus: http.StatusOK,
			wantCalls: 2,
			wantBody:  `[{"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}]`,
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			calls := 0
			service := standardActionsTestService(
				t, standardActionsPermissions(),
				func(context.Context, string, flowsemantic.CatalogActionRequest) (flowsemantic.ResponseV2, error) {
					calls++
					return flowsemantic.ResponseV2{}, deadline
				},
			)
			recorder := httptest.NewRecorder()
			service.ServeHTTP(recorder, flowActionsTestRequest(
				http.MethodPost, "/services/data/v63.0/actions/standard/convertLead", test.body,
			))
			if recorder.Code != test.wantStatus || calls != test.wantCalls ||
				strings.TrimSpace(recorder.Body.String()) != test.wantBody {
				t.Fatalf("status=%d calls=%d body=%s", recorder.Code, calls, recorder.Body.String())
			}
		})
	}
}

func TestStandardActionExecutePropagatesCancellationAndRejectsOversizedBatch(t *testing.T) {
	started := make(chan struct{})
	service := standardActionsTestService(
		t, standardActionsPermissions(),
		func(ctx context.Context, _ string, _ flowsemantic.CatalogActionRequest) (flowsemantic.ResponseV2, error) {
			close(started)
			<-ctx.Done()
			return flowsemantic.ResponseV2{}, ctx.Err()
		},
	)
	request := flowActionsTestRequest(
		http.MethodPost, "/services/data/v63.0/actions/standard/convertLead",
		`{"inputs":[{"leadId":"00Q1"}]}`,
	)
	ctx, cancel := context.WithCancel(request.Context())
	request = request.WithContext(ctx)
	done := make(chan *httptest.ResponseRecorder, 1)
	go func() {
		recorder := httptest.NewRecorder()
		service.ServeHTTP(recorder, request)
		done <- recorder
	}()
	select {
	case <-started:
		cancel()
	case <-time.After(time.Second):
		t.Fatal("Runtime execution did not start")
	}
	select {
	case recorder := <-done:
		if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), "Flow execution canceled") {
			t.Fatalf("canceled status=%d body=%s", recorder.Code, recorder.Body.String())
		}
	case <-time.After(time.Second):
		t.Fatal("HTTP handler did not observe cancellation")
	}

	inputs := make([]map[string]any, standardActionMaxInputs+1)
	for index := range inputs {
		inputs[index] = map[string]any{"leadId": index}
	}
	body, err := json.Marshal(map[string]any{"inputs": inputs})
	if err != nil {
		t.Fatal(err)
	}
	called := false
	service = standardActionsTestService(
		t, standardActionsPermissions(),
		func(context.Context, string, flowsemantic.CatalogActionRequest) (flowsemantic.ResponseV2, error) {
			called = true
			return flowsemantic.ResponseV2{}, nil
		},
	)
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, flowActionsTestRequest(
		http.MethodPost, "/services/data/v63.0/actions/standard/convertLead", string(body),
	))
	if recorder.Code != http.StatusBadRequest || called || !strings.Contains(recorder.Body.String(), "LIMIT_EXCEEDED") {
		t.Fatalf("oversized status=%d called=%v body=%s", recorder.Code, called, recorder.Body.String())
	}
}

func TestStandardActionExecuteFailsClosedOnMixedReceiptsAndMalformedResult(t *testing.T) {
	tests := []struct {
		name     string
		response flowsemantic.ResponseV2
	}{
		{
			name: "mixed revision",
			response: flowsemantic.ResponseV2{
				ExecutionID: "wrong-revision", CanonicalOutputs: []byte(`{}`),
				Receipts: commonsemantic.Receipts{
					ProtocolVersion: "1.0", CapabilityVersion: flowsemantic.CapabilityName,
					CatalogRevision: 18, SchemaGeneration: 8, EngineGeneration: 9, SecurityEpoch: 3,
				},
			},
		},
		{
			name: "malformed canonical outputs",
			response: flowsemantic.ResponseV2{
				ExecutionID: "malformed-output", CanonicalOutputs: []byte(`{"value":1} trailing`),
				Receipts: commonsemantic.Receipts{
					ProtocolVersion: "1.0", CapabilityVersion: flowsemantic.CapabilityName,
					CatalogRevision: 17, SchemaGeneration: 8, EngineGeneration: 9, SecurityEpoch: 3,
				},
			},
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			service := standardActionsTestService(
				t, standardActionsPermissions(),
				func(context.Context, string, flowsemantic.CatalogActionRequest) (flowsemantic.ResponseV2, error) {
					return test.response, nil
				},
			)
			recorder := httptest.NewRecorder()
			service.ServeHTTP(recorder, flowActionsTestRequest(
				http.MethodPost, "/services/data/v63.0/actions/standard/convertLead",
				`{"inputs":[{"leadId":"00Q1"}]}`,
			))
			body := recorder.Body.String()
			if recorder.Code != http.StatusOK || !strings.Contains(body, `"isSuccess":false`) ||
				(!strings.Contains(body, crmwire.SFGenericInternalErrorMessage) &&
					!strings.Contains(body, "Flow execution temporarily unavailable")) ||
				strings.Contains(body, "wrong-revision") || strings.Contains(body, "trailing") {
				t.Fatalf("status=%d body=%s", recorder.Code, body)
			}
		})
	}
}

func TestStandardActionNonAtomicOwnerFailsClosedBeforeExecute(t *testing.T) {
	service := standardActionsTestService(
		t, standardActionsPermissions(),
		func(context.Context, string, flowsemantic.CatalogActionRequest) (flowsemantic.ResponseV2, error) {
			t.Fatal("non-atomic action reached ExecuteCatalogAction")
			return flowsemantic.ResponseV2{}, nil
		},
	)
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, flowActionsTestRequest(
		http.MethodPost,
		"/services/data/v63.0/actions/standard/sendRecordEmail",
		`{"inputs":[{"recordId":"00Q000000000001"}]}`,
	))
	if recorder.Code != http.StatusNotFound || !strings.Contains(recorder.Body.String(), "NOT_FOUND") {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
}

func TestStandardActionExecuteDelegatesVisibilityWithoutReadingCompletePermissions(t *testing.T) {
	readCalls := 0
	executeCalls := 0
	service := standardActionsTestServiceWithEffectiveReader(
		t,
		func(context.Context, string, permissionsemantic.Request) (permissionsemantic.Response, error) {
			readCalls++
			return permissionsemantic.Response{}, errors.New("execute read complete permissions")
		},
		func(context.Context, string, flowsemantic.CatalogActionRequest) (flowsemantic.ResponseV2, error) {
			executeCalls++
			return flowsemantic.ResponseV2{}, &commonsemantic.Failure{Category: commonsemantic.FailureMissing}
		},
	)
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, flowActionsTestRequest(
		http.MethodPost,
		"/services/data/v63.0/actions/standard/convertLead",
		`{"inputs":[{"leadId":"00Q000000000001"}]}`,
	))
	if recorder.Code != http.StatusNotFound || readCalls != 0 || executeCalls != 1 ||
		!strings.Contains(recorder.Body.String(), "NOT_FOUND") {
		t.Fatalf("status=%d reads=%d executes=%d body=%s",
			recorder.Code, readCalls, executeCalls, recorder.Body.String())
	}
}

func TestStandardActionInputGuardDenialRemainsActionLevelFailure(t *testing.T) {
	readCalls := 0
	service := standardActionsTestServiceWithEffectiveReader(
		t,
		func(context.Context, string, permissionsemantic.Request) (permissionsemantic.Response, error) {
			readCalls++
			return permissionsemantic.Response{}, errors.New("execute read complete permissions")
		},
		func(context.Context, string, flowsemantic.CatalogActionRequest) (flowsemantic.ResponseV2, error) {
			return flowsemantic.ResponseV2{}, &commonsemantic.Failure{Category: commonsemantic.FailureForbidden}
		},
	)
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, flowActionsTestRequest(
		http.MethodPost,
		"/services/data/v63.0/actions/standard/convertLead",
		`{"inputs":[{"leadId":"00Q000000000001"}]}`,
	))
	if recorder.Code != http.StatusOK || readCalls != 0 ||
		!strings.Contains(recorder.Body.String(), `"statusCode":"INSUFFICIENT_ACCESS"`) {
		t.Fatalf("status=%d reads=%d body=%s", recorder.Code, readCalls, recorder.Body.String())
	}
}

func TestStandardActionCreatePreviewPreservesLegacyWireAndCatalogAuthority(t *testing.T) {
	receipts := commonsemantic.Receipts{
		ProtocolVersion: "1.0", CapabilityVersion: flowsemantic.CapabilityName,
		CatalogRevision: 17, SchemaGeneration: 8, EngineGeneration: 9, SecurityEpoch: 3,
	}
	previewCalls := 0
	permissionReads := 0
	service := standardActionsTestServiceWithEffectiveReader(
		t,
		func(context.Context, string, permissionsemantic.Request) (permissionsemantic.Response, error) {
			permissionReads++
			return permissionsemantic.Response{}, errors.New("preview must not export complete permissions")
		},
		func(context.Context, string, flowsemantic.CatalogActionRequest) (flowsemantic.ResponseV2, error) {
			t.Fatal("preview reached ExecuteCatalogAction")
			return flowsemantic.ResponseV2{}, nil
		},
		func(_ context.Context, bearer string, request flowsemantic.CatalogActionPreviewRequest) (flowsemantic.CatalogActionPreviewResponse, error) {
			previewCalls++
			if bearer != "user-secret" || request.Mode != flowsemantic.CatalogActionPreviewCreateRecords ||
				request.CatalogAction.ActionRef != "convertLead" || request.CatalogAction.ComponentRef == "" ||
				request.MinCatalogRevision == nil || *request.MinCatalogRevision != 17 ||
				string(request.CanonicalInputs) != `{"sourceRecordId":"00Qtest001"}` {
				t.Fatalf("preview request = %#v bearer %q", request, bearer)
			}
			return flowsemantic.CatalogActionPreviewResponse{
				CanonicalResult: standardActionCanonicalPreview(t, `{"actionName":"convertLead","isSuccess":true,"errors":[{"code":"required_field_missing","message":"required field missing","fields":["Company"]}],"parentPreview":{"object":"Account","fields":{"_objectType":"Account","_metadata":{"internal":true},"_loopDepth":2,"Name":"Acme","Owner":{"_objectType":"User","_metadata":{"internal":true},"Id":"005000000000001AAA"}},"siblingMaxDefaultedFields":["SortOrder__c"]},"childPreview":[{"object":"Contact","count":1,"rows":[{"_objectType":"Contact","__r_cache":{"secret":true},"FirstName":"Ada"}]}]}`),
				Receipts:        receipts,
			}, nil
		},
	)
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, flowActionsTestRequest(
		http.MethodPost,
		"/services/data/v63.0/actions/standard/convertLead/preview",
		`{"input":{"sourceRecordId":"00Qtest001"}}`,
	))
	want := `{"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"}]}]}`
	if recorder.Code != http.StatusOK || strings.TrimSpace(recorder.Body.String()) != want ||
		previewCalls != 1 || permissionReads != 0 {
		t.Fatalf("status=%d calls=%d permissionReads=%d body=%s", recorder.Code, previewCalls, permissionReads, recorder.Body.String())
	}

	recorder = httptest.NewRecorder()
	service.ServeHTTP(recorder, flowActionsTestRequest(
		http.MethodPost,
		"/services/data/v63.0/actions/standard/convertLead/preview",
		`{"input":{"sourceRecordId":"00Qtest001"}} {}`,
	))
	if recorder.Code != http.StatusBadRequest || previewCalls != 1 {
		t.Fatalf("trailing create preview status=%d calls=%d body=%s", recorder.Code, previewCalls, recorder.Body.String())
	}
}

func TestStandardActionCreatePreviewCanonicalizesHTMLInputs(t *testing.T) {
	receipts := commonsemantic.Receipts{
		ProtocolVersion: "1.0", CapabilityVersion: flowsemantic.CapabilityName,
		CatalogRevision: 17, SchemaGeneration: 8, EngineGeneration: 9, SecurityEpoch: 3,
	}
	var got flowsemantic.CatalogActionPreviewRequest
	service := standardActionsTestService(
		t,
		standardActionsPermissions(),
		func(context.Context, string, flowsemantic.CatalogActionRequest) (flowsemantic.ResponseV2, error) {
			t.Fatal("preview reached ExecuteCatalogAction")
			return flowsemantic.ResponseV2{}, nil
		},
		func(_ context.Context, _ string, request flowsemantic.CatalogActionPreviewRequest) (flowsemantic.CatalogActionPreviewResponse, error) {
			got = request
			return flowsemantic.CatalogActionPreviewResponse{
				CanonicalResult: standardActionCanonicalPreview(
					t,
					`{"actionName":"convertLead","isSuccess":true,"parentPreview":{"object":"Account","fields":{}},"childPreview":[]}`,
				),
				Receipts: receipts,
			}, nil
		},
	)
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, flowActionsTestRequest(
		http.MethodPost,
		"/services/data/v63.0/actions/standard/convertLead/preview",
		`{"input":{"htmlBody":"<p>Hello &amp; team</p>","sourceRecordId":"00Qtest001"}}`,
	))
	if recorder.Code != http.StatusOK ||
		string(got.CanonicalInputs) != `{"htmlBody":"<p>Hello &amp; team</p>","sourceRecordId":"00Qtest001"}` {
		t.Fatalf("status=%d request=%#v body=%s", recorder.Code, got, recorder.Body.String())
	}
}

func TestStandardActionCreatePreviewOmitsEmptyNestedOptionalFields(t *testing.T) {
	receipts := commonsemantic.Receipts{
		ProtocolVersion: "1.0", CapabilityVersion: flowsemantic.CapabilityName,
		CatalogRevision: 17, SchemaGeneration: 8, EngineGeneration: 9, SecurityEpoch: 3,
	}
	service := standardActionsTestService(
		t,
		standardActionsPermissions(),
		func(context.Context, string, flowsemantic.CatalogActionRequest) (flowsemantic.ResponseV2, error) {
			t.Fatal("preview reached ExecuteCatalogAction")
			return flowsemantic.ResponseV2{}, nil
		},
		func(context.Context, string, flowsemantic.CatalogActionPreviewRequest) (flowsemantic.CatalogActionPreviewResponse, error) {
			return flowsemantic.CatalogActionPreviewResponse{
				CanonicalResult: standardActionCanonicalPreview(t, `{"actionName":"convertLead","isSuccess":true,"parentPreview":{"object":"Account","fields":{}},"childPreview":[{"object":"Contact","count":0}]}`),
				Receipts:        receipts,
			}, nil
		},
	)
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, flowActionsTestRequest(
		http.MethodPost,
		"/services/data/v63.0/actions/standard/convertLead/preview",
		`{"input":{"sourceRecordId":"00Qtest001"}}`,
	))
	want := `{"actionName":"convertLead","isSuccess":true,"parentPreview":{"object":"Account","fields":{}},"childPreview":[{"object":"Contact","count":0}]}`
	if recorder.Code != http.StatusOK || strings.TrimSpace(recorder.Body.String()) != want {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
}

func TestStandardActionOutputPreviewStrictlyBindsRecordAndPreservesWire(t *testing.T) {
	receipts := commonsemantic.Receipts{
		ProtocolVersion: "1.0", CapabilityVersion: flowsemantic.CapabilityName,
		CatalogRevision: 17, SchemaGeneration: 8, EngineGeneration: 9, SecurityEpoch: 3,
	}
	previewCalls := 0
	service := standardActionsTestService(
		t,
		standardActionsPermissions(),
		func(context.Context, string, flowsemantic.CatalogActionRequest) (flowsemantic.ResponseV2, error) {
			t.Fatal("preview reached ExecuteCatalogAction")
			return flowsemantic.ResponseV2{}, nil
		},
		func(_ context.Context, _ string, request flowsemantic.CatalogActionPreviewRequest) (flowsemantic.CatalogActionPreviewResponse, error) {
			previewCalls++
			if request.Mode != flowsemantic.CatalogActionPreviewOutputs ||
				request.RecordID != "00Qtest001" || len(request.CanonicalInputs) != 0 {
				t.Fatalf("output preview request = %#v", request)
			}
			return flowsemantic.CatalogActionPreviewResponse{
				CanonicalResult: standardActionCanonicalPreview(t, `{"outputValues":{"campaignFound":true,"lead":{"_objectType":"Lead","_metadata":{"internal":true},"_loopDepth":1,"Id":"00Qtest001"}},"isSuccess":true}`),
				Receipts:        receipts,
			}, nil
		},
	)

	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, flowActionsTestRequest(
		http.MethodPost,
		"/services/data/v63.0/actions/standard/convertLead/output-preview",
		`{"recordId":"00Qtest001"}`,
	))
	want := `{"outputValues":{"campaignFound":true,"lead":{"Id":"00Qtest001","attributes":{"type":"Lead"}}},"isSuccess":true,"errors":null}`
	if recorder.Code != http.StatusOK || strings.TrimSpace(recorder.Body.String()) != want || previewCalls != 1 {
		t.Fatalf("status=%d calls=%d body=%s", recorder.Code, previewCalls, recorder.Body.String())
	}

	recorder = httptest.NewRecorder()
	service.ServeHTTP(recorder, flowActionsTestRequest(
		http.MethodPost,
		"/services/data/v63.0/actions/standard/convertLead/output-preview",
		`{"recordId":"00Qtest001","inputs":{"leadId":"fake"}}`,
	))
	if recorder.Code != http.StatusBadRequest || previewCalls != 1 ||
		!strings.Contains(recorder.Body.String(), "unknown field") {
		t.Fatalf("strict output preview status=%d calls=%d body=%s", recorder.Code, previewCalls, recorder.Body.String())
	}

	recorder = httptest.NewRecorder()
	service.ServeHTTP(recorder, flowActionsTestRequest(
		http.MethodPost,
		"/services/data/v63.0/actions/standard/convertLead/output-preview",
		`{"recordId":"00Qtest001"} {}`,
	))
	if recorder.Code != http.StatusBadRequest || previewCalls != 1 {
		t.Fatalf("trailing output preview status=%d calls=%d body=%s", recorder.Code, previewCalls, recorder.Body.String())
	}
}

func TestStandardActionPreviewFailsClosedOnMixedReceiptsAndMalformedResult(t *testing.T) {
	tests := []struct {
		name       string
		response   flowsemantic.CatalogActionPreviewResponse
		wantStatus int
	}{
		{
			name: "mixed revision", wantStatus: http.StatusServiceUnavailable,
			response: flowsemantic.CatalogActionPreviewResponse{
				CanonicalResult: []byte(`{"isSuccess":true,"outputValues":{}}`),
				Receipts: commonsemantic.Receipts{
					ProtocolVersion: "1.0", CapabilityVersion: flowsemantic.CapabilityName,
					CatalogRevision: 18, SchemaGeneration: 8, EngineGeneration: 9, SecurityEpoch: 3,
				},
			},
		},
		{
			name: "unknown Runtime field", wantStatus: http.StatusInternalServerError,
			response: flowsemantic.CatalogActionPreviewResponse{
				CanonicalResult: standardActionCanonicalPreview(t, `{"isSuccess":true,"outputValues":{},"internal":"secret"}`),
				Receipts: commonsemantic.Receipts{
					ProtocolVersion: "1.0", CapabilityVersion: flowsemantic.CapabilityName,
					CatalogRevision: 17, SchemaGeneration: 8, EngineGeneration: 9, SecurityEpoch: 3,
				},
			},
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			service := standardActionsTestService(
				t, standardActionsPermissions(),
				func(context.Context, string, flowsemantic.CatalogActionRequest) (flowsemantic.ResponseV2, error) {
					t.Fatal("preview reached execute")
					return flowsemantic.ResponseV2{}, nil
				},
				func(context.Context, string, flowsemantic.CatalogActionPreviewRequest) (flowsemantic.CatalogActionPreviewResponse, error) {
					return test.response, nil
				},
			)
			recorder := httptest.NewRecorder()
			service.ServeHTTP(recorder, flowActionsTestRequest(
				http.MethodPost, "/services/data/v63.0/actions/standard/convertLead/output-preview",
				`{"recordId":"00Q1"}`,
			))
			if recorder.Code != test.wantStatus || strings.Contains(recorder.Body.String(), "secret") {
				t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
			}
		})
	}
}

func TestStandardActionOutputPreviewWithoutProducerFailsBeforeBodyAndRuntime(t *testing.T) {
	previewCalls := 0
	service := standardActionsTestService(
		t,
		standardActionsPermissions(),
		func(context.Context, string, flowsemantic.CatalogActionRequest) (flowsemantic.ResponseV2, error) {
			t.Fatal("output preflight reached ExecuteCatalogAction")
			return flowsemantic.ResponseV2{}, nil
		},
		func(context.Context, string, flowsemantic.CatalogActionPreviewRequest) (flowsemantic.CatalogActionPreviewResponse, error) {
			previewCalls++
			return flowsemantic.CatalogActionPreviewResponse{}, nil
		},
	)
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, flowActionsTestRequest(
		http.MethodPost,
		"/services/data/v63.0/actions/standard/createQuote/output-preview",
		`not-json`,
	))
	if recorder.Code != http.StatusNotFound || previewCalls != 0 ||
		!strings.Contains(recorder.Body.String(), "has no preview producer") {
		t.Fatalf("preflight status=%d calls=%d body=%s", recorder.Code, previewCalls, recorder.Body.String())
	}
}

func standardActionCanonicalPreview(t *testing.T, raw string) []byte {
	t.Helper()
	decoder := json.NewDecoder(strings.NewReader(raw))
	decoder.UseNumber()
	var value any
	if err := decoder.Decode(&value); err != nil {
		t.Fatal(err)
	}
	encoded, err := json.Marshal(value)
	if err != nil {
		t.Fatal(err)
	}
	return encoded
}
