package automation

import (
	"context"
	"fmt"
	"sort"
	"strings"
	"testing"

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

func TestFlowActionCallRegistryDrivesAtomicMetadataBackedClassification(t *testing.T) {
	meta := &models.FlowMetadata{
		RunAtomically: true,
		ActionCalls: []models.FlowActionCall{{
			Name:       "RunCustom",
			ActionName: "CustomAction",
			ActionType: "customAction",
		}},
	}

	err := validateAtomicFlowForExecutionWithRegistry(context.Background(), "", meta, newFlowActionCallRegistry(FlowActionCallRegistration{
		ActionType:   "customAction",
		AtomicPolicy: FlowActionCallAtomicPolicySafe,
		Handler: func(context.Context, *FlowExecutor, *models.FlowInterview, *models.FlowMetadata, *models.FlowActionCall, *models.UserSession) (string, error) {
			return "", nil
		},
	}))
	if err != nil {
		t.Fatalf("validateAtomicFlowForExecutionWithRegistry() error = %v, want nil for registered action", err)
	}

	err = validateAtomicFlowForExecution(context.Background(), "", meta)
	if err == nil {
		t.Fatal("validateAtomicFlowForExecution() error = nil, want metadata-backed rejection for unregistered action")
	}
	if !strings.Contains(err.Error(), "metadata-backed action calls") {
		t.Fatalf("error = %v, want metadata-backed action rejection", err)
	}
}

func TestFlowActionCallRegistryRejectsUnclassifiedRegistration(t *testing.T) {
	expectFlowActionCallRegistryPanic(t, "requires atomicPolicy", func() {
		newFlowActionCallRegistry(FlowActionCallRegistration{
			ActionType: "customAction",
			Handler: func(context.Context, *FlowExecutor, *models.FlowInterview, *models.FlowMetadata, *models.FlowActionCall, *models.UserSession) (string, error) {
				return "", nil
			},
		})
	})
}

func TestFlowActionCallRegistryRejectsDuplicateActionType(t *testing.T) {
	expectFlowActionCallRegistryPanic(t, `duplicate flow action-call registration for " customaction "`, func() {
		newFlowActionCallRegistry(
			flowActionCallRegistryTestRegistration("CustomAction"),
			flowActionCallRegistryTestRegistration(" customaction "),
		)
	})
}

func TestDefaultFlowActionCallRegistryContainsOnlyGenericFlowSubstrate(t *testing.T) {
	registry := defaultFlowActionCallRegistry()
	if _, ok := registry.Lookup("flow"); !ok {
		t.Fatal(`default registry missing generic "flow" actionCall`)
	}

	for _, actionType := range []string{
		flowSendEmailActionType,
		"emailSimple",
		flowResolveEmailRecipientsActionType,
		"emailAlert",
		constants.ActionTypePushToConnector,
		"stampConvertedAnchors",
		"applyConnectorFieldMappings",
		"registerConnectorInboundRemoteAuthorship",
		"setRetriable",
		"previewRecordUpdate",
	} {
		if _, ok := registry.Lookup(actionType); ok {
			t.Fatalf("default registry unexpectedly contains concrete actionCall %q", actionType)
		}
	}
}

func TestExternalFlowActionCallRegistrationRejectsRuntimeOwnedFlowActionType(t *testing.T) {
	restore := flowactions.ResetForTest()
	t.Cleanup(restore)

	err := flowactions.Register(flowactions.Registration{
		ActionType:   "flow",
		HandlerRef:   "host:workflow/subflow",
		AtomicPolicy: flowactions.AtomicPolicySafe,
		Handler: func(context.Context, flowactions.Invocation) (string, error) {
			return "", nil
		},
	})
	if err == nil {
		t.Fatal(`flowactions.Register("flow") error = nil`)
	}
	if !strings.Contains(err.Error(), `platform-reserved actionType "flow"`) {
		t.Fatalf("flowactions.Register() error = %v, want platform-reserved actionType collision", err)
	}
}

const crmStampConvertedAnchorsActionTypeForTest = "stampConvertedAnchors"

func TestExternalFlowActionCallRegistrationAllowsCRMNameWithoutL0RuntimeReservation(t *testing.T) {
	restore := flowactions.ResetForTest()
	t.Cleanup(restore)

	err := flowactions.Register(flowactions.Registration{
		ActionType:   crmStampConvertedAnchorsActionTypeForTest,
		HandlerRef:   "host:workflow/stamp-converted-anchors",
		AtomicPolicy: flowactions.AtomicPolicySafe,
		Handler: func(context.Context, flowactions.Invocation) (string, error) {
			return "", nil
		},
	})
	if err != nil {
		t.Fatalf("flowactions.Register(\"stampConvertedAnchors\") error = %v, want nil because CRM transitional names are not public L0 reservations", err)
	}
	effective, err := EffectiveFlowActionCallRegistrations()
	if err != nil {
		t.Fatalf("EffectiveFlowActionCallRegistrations() error = %v, want nil without composed CRM app registration", err)
	}
	found := false
	for _, registration := range effective {
		if normalizeFlowActionType(registration.ActionType) == normalizeFlowActionType(crmStampConvertedAnchorsActionTypeForTest) {
			found = true
		}
	}
	if !found {
		t.Fatal("EffectiveFlowActionCallRegistrations() missing external stampConvertedAnchors registration")
	}
}

func TestRuntimeOwnedActionTypesExcludeCRMStampConvertedAnchors(t *testing.T) {
	for _, actionType := range flowactions.PlatformReservedActionTypes() {
		if normalizeFlowActionType(actionType) == normalizeFlowActionType(crmStampConvertedAnchorsActionTypeForTest) {
			t.Fatalf("flowactions.PlatformReservedActionTypes() contains CRM actionType %q; CRM verbs must not be L0 runtime-owned vocabulary", actionType)
		}
	}
}

func TestPlatformFlowActionCallRegistrationsExcludeCRMStampConvertedAnchors(t *testing.T) {
	for _, registration := range PlatformFlowActionCallRegistrations() {
		if normalizeFlowActionType(registration.ActionType) == normalizeFlowActionType(crmStampConvertedAnchorsActionTypeForTest) {
			t.Fatalf("PlatformFlowActionCallRegistrations() contains CRM actionType %q; CRM verbs must be supplied by an app-owned registration", registration.ActionType)
		}
	}
}

func TestPushToConnectorIsReservedAndHostRegisteredNotPlatformOwned(t *testing.T) {
	reserved := false
	for _, actionType := range flowactions.PlatformReservedActionTypes() {
		if normalizeFlowActionType(actionType) == normalizeFlowActionType(constants.ActionTypePushToConnector) {
			reserved = true
		}
	}
	if !reserved {
		t.Fatal("flowactions.PlatformReservedActionTypes() missing reserved connector host actionType PushToConnector")
	}
	for _, registration := range PlatformFlowActionCallRegistrations() {
		if normalizeFlowActionType(registration.ActionType) == normalizeFlowActionType(constants.ActionTypePushToConnector) {
			t.Fatalf("PlatformFlowActionCallRegistrations() contains connector host actionType %q", registration.ActionType)
		}
	}
	descriptors, err := EffectiveFlowActionCallRegistrationDescriptors()
	if err != nil {
		t.Fatalf("EffectiveFlowActionCallRegistrationDescriptors() error = %v", err)
	}
	foundContract := false
	for _, descriptor := range descriptors {
		if normalizeFlowActionType(descriptor.ActionType) == normalizeFlowActionType(constants.ActionTypePushToConnector) {
			foundContract = true
			if descriptor.HandlerRef != ConnectorPushFlowActionCallRegistrationDescriptor().HandlerRef {
				t.Fatalf("PushToConnector contract handlerRef = %q", descriptor.HandlerRef)
			}
		}
	}
	if !foundContract {
		t.Fatal("contract descriptors missing PushToConnector")
	}

	effective, err := EffectiveFlowActionCallRegistrations()
	if err != nil {
		t.Fatalf("EffectiveFlowActionCallRegistrations() without host PushToConnector error = %v", err)
	}
	for _, registration := range effective {
		if normalizeFlowActionType(registration.ActionType) == normalizeFlowActionType(constants.ActionTypePushToConnector) {
			t.Fatalf("EffectiveFlowActionCallRegistrations() without host registration contains %q", registration.ActionType)
		}
	}

	effective, err = EffectiveFlowActionCallRegistrations(RuntimeOutboundIntentFlowActionCallRegistration())
	if err != nil {
		t.Fatalf("EffectiveFlowActionCallRegistrations() with host PushToConnector error = %v", err)
	}
	found := false
	for _, registration := range effective {
		if normalizeFlowActionType(registration.ActionType) == normalizeFlowActionType(constants.ActionTypePushToConnector) {
			found = true
			if registration.Handler == nil {
				t.Fatal("host PushToConnector registration has nil handler")
			}
		}
	}
	if !found {
		t.Fatal("EffectiveFlowActionCallRegistrations() with host registration missing PushToConnector")
	}
}

func TestUninstalledHostOnlyActionTypesFailClosedBeforeMetadataDispatch(t *testing.T) {
	for _, actionType := range []string{
		constants.ActionTypePushToConnector,
		" EnqueueOutboundMail ",
	} {
		t.Run(strings.TrimSpace(actionType), func(t *testing.T) {
			_, err := (&FlowExecutor{}).executeActionCall(
				context.Background(),
				&models.FlowInterview{ContextData: map[string]interface{}{}},
				&models.FlowMetadata{},
				&models.FlowActionCall{ActionType: actionType, ActionName: "MetadataBackedFallback"},
				&models.UserSession{ID: "user-1"},
			)
			if err == nil || !strings.Contains(err.Error(), "handler is not installed") {
				t.Fatalf("executeActionCall(%q) error = %v, want uninstalled reserved-handler rejection", actionType, err)
			}
		})
	}
}

func TestEffectiveFlowActionCallRegistrationsRejectDuplicateAppOwnedActionTypes(t *testing.T) {
	_, err := EffectiveFlowActionCallRegistrations(
		flowActionCallRegistryTestRegistration("resolveIssue"),
		flowActionCallRegistryTestRegistration(" ResolveIssue "),
	)
	if err == nil {
		t.Fatal("EffectiveFlowActionCallRegistrations() duplicate app action error = nil")
	}
	if !strings.Contains(err.Error(), `duplicate flow action-call registration for " ResolveIssue "`) {
		t.Fatalf("EffectiveFlowActionCallRegistrations() error = %v, want duplicate registration diagnostic", err)
	}
}

func TestRuntimeOwnedActionTypesCoverInBinaryRuntimeRegistrations(t *testing.T) {
	registrations := genericFlowActionCallRegistrations()
	registrations = append(registrations, PlatformFlowActionCallRegistrations()...)
	registry := newFlowActionCallRegistry(registrations...)

	runtimeOwned := make(map[string]bool, len(flowactions.PlatformReservedActionTypes()))
	for _, actionType := range flowactions.PlatformReservedActionTypes() {
		runtimeOwned[normalizeFlowActionType(actionType)] = true
	}

	got := make([]string, 0, len(registry.byType))
	for actionType := range registry.byType {
		got = append(got, actionType)
		if !runtimeOwned[actionType] {
			t.Fatalf("in-binary runtime registration %q is not runtime-owned/reserved", actionType)
		}
	}
	sort.Strings(got)

	hostOnly := map[string]bool{
		normalizeFlowActionType(constants.ActionTypePushToConnector):         true,
		normalizeFlowActionType(flowactions.ActionTypeEnqueueOutboundMail):   true,
		normalizeFlowActionType(flowactions.ActionTypeEnqueueOutboundMailV3): true,
	}

	var missing []string
	for _, actionType := range flowactions.PlatformReservedActionTypes() {
		normalized := normalizeFlowActionType(actionType)
		if hostOnly[normalized] {
			continue
		}
		if _, ok := registry.byType[normalized]; !ok {
			missing = append(missing, normalized)
		}
	}
	sort.Strings(missing)
	if len(missing) > 0 {
		t.Fatalf("in-binary runtime actionTypes = %v, missing non-host-only runtime-owned actionTypes = %v", got, missing)
	}
}

func TestPlatformFlowActionCallRegistryIncludesAppOwnedStampConvertedAnchors(t *testing.T) {
	registry, err := platformFlowActionCallRegistry(stampConvertedAnchorsTestRegistration())
	if err != nil {
		t.Fatalf("platformFlowActionCallRegistry() error = %v", err)
	}
	registration, ok := registry.Lookup("stampConvertedAnchors")
	if !ok {
		t.Fatal(`platform registry missing "stampConvertedAnchors" actionCall`)
	}
	if registration.AtomicPolicy != FlowActionCallAtomicPolicySafe {
		t.Fatalf("AtomicPolicy = %v, want FlowActionCallAtomicPolicySafe", registration.AtomicPolicy)
	}
	if registration.RequiresContext != FlowActionCallContextStandardActionProtected {
		t.Fatalf("RequiresContext = %v, want FlowActionCallContextStandardActionProtected", registration.RequiresContext)
	}
	if registry.actionCallUsesMetadataBackedAction("stampConvertedAnchors") {
		t.Fatal(`stampConvertedAnchors classified as metadata-backed actionCall`)
	}

	meta := &models.FlowMetadata{
		RunAtomically: true,
		ActionCalls: []models.FlowActionCall{{
			Name:       "StampConvertedAnchors",
			ActionName: "stampConvertedAnchors",
			ActionType: "stampConvertedAnchors",
		}},
	}
	if err := validateAtomicFlowForExecutionWithRegistry(context.Background(), "", meta, registry); err != nil {
		t.Fatalf("validateAtomicFlowForExecutionWithRegistry() error = %v, want nil for app-registered atomic-safe stampConvertedAnchors", err)
	}
}

func TestValidateAtomicFlowForExecutionUsesDefaultAppFlowActionRegistrations(t *testing.T) {
	restore := ResetDefaultAppFlowActionCallRegistrationsForTest()
	t.Cleanup(restore)
	RegisterDefaultAppFlowActionCallRegistrations(testEmailCompatibilityFlowActionCallRegistrations()...)

	err := validateAtomicFlowForExecution(context.Background(), "", &models.FlowMetadata{
		RunAtomically: true,
		ActionCalls: []models.FlowActionCall{{
			Name:       "SendEmail",
			ActionType: "emailSimple",
		}},
	})
	if err == nil {
		t.Fatal("validateAtomicFlowForExecution() error = nil, want app-owned emailSimple atomic rejection")
	}
	if !strings.Contains(err.Error(), "emailSimple action calls") {
		t.Fatalf("validateAtomicFlowForExecution() error = %v, want app-owned emailSimple policy", err)
	}
	if strings.Contains(err.Error(), "metadata-backed action calls") {
		t.Fatalf("validateAtomicFlowForExecution() error = %v, want default app registration path instead of metadata-backed fallback", err)
	}
}

func TestAppRegisteredStampConvertedAnchorsDispatchesToHandler(t *testing.T) {
	exec := newFlowExecutorWithPlatformActionCalls(nil, nil, nil, nil, nil, nil, nil, WithFlowActionCall(stampConvertedAnchorsTestRegistration()))
	ctx := constants.WithStandardActionExecutionRootFlow(context.Background(), "standard-action-1", "Std_ConvertLead")
	ctx = constants.WithStandardActionProtectedFlowExecution(ctx, "Std_ConvertLead")

	_, err := exec.executeActionCall(ctx, &models.FlowInterview{ContextData: map[string]interface{}{}}, &models.FlowMetadata{}, &models.FlowActionCall{
		Name:       "StampConvertedAnchors",
		ActionName: "stampConvertedAnchors",
		ActionType: "stampConvertedAnchors",
	}, &models.UserSession{ID: "user-1"})
	if err == nil {
		t.Fatal("executeActionCall() error = nil, want stampConvertedAnchors handler validation error")
	}
	if !strings.Contains(err.Error(), "stampConvertedAnchors requires sourceObject and sourceId") {
		t.Fatalf("executeActionCall() error = %v, want stampConvertedAnchors handler validation", err)
	}
}

func TestExternalFlowActionCallRegistrationCollidesWithAppOwnedStampConvertedAnchors(t *testing.T) {
	restore := flowactions.ResetForTest()
	t.Cleanup(restore)

	err := flowactions.Register(flowactions.Registration{
		ActionType:   "stampConvertedAnchors",
		HandlerRef:   "crm:standard-action/stamp-converted-anchors",
		AtomicPolicy: flowactions.AtomicPolicySafe,
		Handler: func(context.Context, flowactions.Invocation) (string, error) {
			return "", nil
		},
	})
	if err != nil {
		t.Fatalf("flowactions.Register(\"stampConvertedAnchors\") error = %v, want nil before internal composition", err)
	}
	if got := flowactions.Registrations(); len(got) != 1 || got[0].ActionType != "stampconvertedanchors" {
		t.Fatalf("flowactions.Registrations() = %#v, want public registry to store CRM transitional external input", got)
	}
	_, err = EffectiveFlowActionCallRegistrationDescriptors(stampConvertedAnchorsTestRegistration())
	if err == nil {
		t.Fatal("EffectiveFlowActionCallRegistrationDescriptors() error = nil, want generic duplicate actionType collision")
	}
	if !strings.Contains(err.Error(), `duplicate flow action-call registration`) || !strings.Contains(err.Error(), `stampconvertedanchors`) {
		t.Fatalf("EffectiveFlowActionCallRegistrationDescriptors() error = %v, want generic duplicate CRM actionType collision", err)
	}
}

func TestExternalFlowActionCallRegistrationDrivesDispatchAndAtomicValidation(t *testing.T) {
	restore := flowactions.ResetForTest()
	t.Cleanup(restore)

	actionType := "resolveIssueExternal"
	called := false
	flowactions.MustRegister(flowactions.Registration{
		ActionType:    actionType,
		HandlerRef:    "host:workflow/resolve-issue-external",
		AtomicPolicy:  flowactions.AtomicPolicySafe,
		PreviewPolicy: flowactions.PreviewPolicyAllow,
		Handler: func(_ context.Context, invocation flowactions.Invocation) (string, error) {
			called = true
			invocation.Interview().ContextData["externalAction"] = invocation.Action().ActionType
			return "Done", nil
		},
	})
	installRuntimeFlowActionReservationsForTest()

	meta := &models.FlowMetadata{
		RunAtomically: true,
		ActionCalls: []models.FlowActionCall{{
			Name:       "ResolveIssue",
			ActionName: "ResolveIssue",
			ActionType: actionType,
		}},
	}
	if err := validateAtomicFlowForExecution(context.Background(), "", meta); err != nil {
		t.Fatalf("validateAtomicFlowForExecution() error = %v, want nil for external registered action", err)
	}

	effective, err := EffectiveFlowActionCallRegistrations()
	if err != nil {
		t.Fatalf("EffectiveFlowActionCallRegistrations() error = %v", err)
	}
	exec := NewFlowExecutor(nil, nil, nil, nil, nil, nil, nil, WithFlowActionCalls(effective...))
	interview := &models.FlowInterview{ContextData: map[string]interface{}{}}
	next, err := exec.executeActionCall(context.Background(), interview, &models.FlowMetadata{}, &models.FlowActionCall{
		Name:       "ResolveIssue",
		ActionName: "ResolveIssue",
		ActionType: actionType,
	}, &models.UserSession{ID: "user-1"})
	if err != nil {
		t.Fatalf("executeActionCall() error = %v", err)
	}
	if next != "Done" {
		t.Fatalf("next = %q, want Done", next)
	}
	if !called {
		t.Fatal("external action handler was not called")
	}
	if got := interview.ContextData["externalAction"]; got != actionType {
		t.Fatalf("ContextData[externalAction] = %v, want %q", got, actionType)
	}
}

func TestExternalFlowActionCallRoutesHandlerErrorToFaultConnector(t *testing.T) {
	restore := flowactions.ResetForTest()
	t.Cleanup(restore)
	installRuntimeFlowActionReservationsForTest()

	flowactions.MustRegister(flowactions.Registration{
		ActionType:   "resolveIssueExternal",
		HandlerRef:   "host:workflow/resolve-issue-external",
		AtomicPolicy: flowactions.AtomicPolicySafe,
		Handler: func(context.Context, flowactions.Invocation) (string, error) {
			return "", fmt.Errorf("external handler failed")
		},
	})

	effective, err := EffectiveFlowActionCallRegistrations()
	if err != nil {
		t.Fatalf("EffectiveFlowActionCallRegistrations() error = %v", err)
	}
	exec := NewFlowExecutor(nil, nil, nil, nil, nil, nil, nil, WithFlowActionCalls(effective...))
	interview := &models.FlowInterview{ContextData: map[string]interface{}{}}
	next, err := exec.executeActionCall(context.Background(), interview, &models.FlowMetadata{}, &models.FlowActionCall{
		Name:           "ResolveIssue",
		ActionName:     "ResolveIssue",
		ActionType:     "resolveIssueExternal",
		FaultConnector: &models.FlowConnector{TargetReference: "Fault"},
	}, &models.UserSession{ID: "user-1"})
	if err != nil {
		t.Fatalf("executeActionCall() error = %v, want fault-routed nil error", err)
	}
	if next != "Fault" {
		t.Fatalf("next = %q, want Fault", next)
	}
	message, _ := interview.ContextData["$Flow.FaultMessage"].(string)
	if !strings.Contains(message, "external handler failed") {
		t.Fatalf("$Flow.FaultMessage = %q, want external handler failure", message)
	}
}

func TestExternalFlowActionBridgePanicsOnUnknownPreviewPolicy(t *testing.T) {
	expectFlowActionCallRegistryPanic(t, `unsupported external flow action-call previewPolicy "future_policy"`, func() {
		flowActionCallPreviewPolicyFromExternal(flowactions.PreviewPolicy("future_policy"))
	})
}

func TestExternalFlowActionBridgePanicsOnUnknownContextRequirement(t *testing.T) {
	expectFlowActionCallRegistryPanic(t, `unsupported external flow action-call requiresContext "future_context"`, func() {
		flowActionCallContextRequirementFromExternal(flowactions.ContextRequirement("future_context"))
	})
}

func TestRegisteredActionCallRequiresContextOnLivePath(t *testing.T) {
	exec := NewFlowExecutor(nil, nil, nil, nil, nil, nil, nil)
	called := false
	exec.RegisterFlowActionCalls(FlowActionCallRegistration{
		ActionType:      "syntheticProtectedAction",
		AtomicPolicy:    FlowActionCallAtomicPolicySafe,
		RequiresContext: FlowActionCallContextStandardActionProtected,
		Handler: func(context.Context, *FlowExecutor, *models.FlowInterview, *models.FlowMetadata, *models.FlowActionCall, *models.UserSession) (string, error) {
			called = true
			return "", nil
		},
	})

	interview := &models.FlowInterview{ContextData: map[string]interface{}{}}
	next, err := exec.executeActionCall(context.Background(), interview, &models.FlowMetadata{}, &models.FlowActionCall{
		Name:           "Protected",
		ActionType:     "syntheticProtectedAction",
		FaultConnector: &models.FlowConnector{TargetReference: "Fault"},
	}, &models.UserSession{ID: "user-1"})
	if err != nil {
		t.Fatalf("executeActionCall() error = %v, want fault-routed nil error", err)
	}
	if next != "Fault" {
		t.Fatalf("next = %q, want Fault", next)
	}
	if called {
		t.Fatal("handler was called before required context validation")
	}
	message, _ := interview.ContextData["$Flow.FaultMessage"].(string)
	if !strings.Contains(message, "requires protected standard action flow execution context") {
		t.Fatalf("$Flow.FaultMessage = %q, want protected-context validation", message)
	}
}

func TestRegisteredActionCallRequiresContextOnPreviewInvokeHandlerPath(t *testing.T) {
	exec := NewFlowExecutor(nil, nil, nil, nil, nil, nil, nil)
	called := false
	exec.RegisterFlowActionCalls(FlowActionCallRegistration{
		ActionType:      "syntheticDisabledAction",
		AtomicPolicy:    FlowActionCallAtomicPolicySafe,
		PreviewPolicy:   FlowActionCallPreviewPolicyInvokeHandler,
		RequiresContext: FlowActionCallContextStandardActionProtected,
		Handler: func(context.Context, *FlowExecutor, *models.FlowInterview, *models.FlowMetadata, *models.FlowActionCall, *models.UserSession) (string, error) {
			called = true
			return "", nil
		},
	})

	interview := &models.FlowInterview{ContextData: map[string]interface{}{}}
	next, err := exec.executeActionCall(WithPreviewMode(context.Background()), interview, &models.FlowMetadata{}, &models.FlowActionCall{
		Name:           "Disabled",
		ActionType:     "syntheticDisabledAction",
		FaultConnector: &models.FlowConnector{TargetReference: "Fault"},
	}, &models.UserSession{ID: "user-1"})
	if err != nil {
		t.Fatalf("executeActionCall() error = %v, want fault-routed nil error", err)
	}
	if next != "Fault" {
		t.Fatalf("next = %q, want Fault", next)
	}
	if called {
		t.Fatal("handler was called before required context validation")
	}
	message, _ := interview.ContextData["$Flow.FaultMessage"].(string)
	if !strings.Contains(message, "requires protected standard action flow execution context") {
		t.Fatalf("$Flow.FaultMessage = %q, want protected-context validation", message)
	}
}

// TestValidateAtomicFlow_AcceptsSendNotificationContrastRejectsSendEmail proves
// sendNotification's AtomicPolicySafe classification (the Notification insert
// joins the caller's active transaction via the ordinary PersistenceProvider
// port; no external I/O holds the tx open), contrasted against sendEmail,
// which is atomic-unsafe because provider I/O would hold the DB transaction
// open across external network calls.
func TestValidateAtomicFlow_AcceptsSendNotificationContrastRejectsSendEmail(t *testing.T) {
	registry := newFlowActionCallRegistry(PlatformFlowActionCallRegistrations()...)

	notifyMeta := &models.FlowMetadata{
		RunAtomically: true,
		ActionCalls: []models.FlowActionCall{{
			Name:       "Notify_Owner",
			ActionType: "sendNotification",
		}},
	}
	if err := validateAtomicFlowForExecutionWithRegistry(context.Background(), "", notifyMeta, registry); err != nil {
		t.Fatalf("validateAtomicFlowForExecutionWithRegistry(sendNotification) error = %v, want nil (AtomicPolicySafe)", err)
	}

	emailMeta := &models.FlowMetadata{
		RunAtomically: true,
		ActionCalls: []models.FlowActionCall{{
			Name:       "Send_Email",
			ActionType: flowSendEmailActionType,
		}},
	}
	err := validateAtomicFlowForExecutionWithRegistry(context.Background(), "", emailMeta, registry)
	if err == nil {
		t.Fatal("validateAtomicFlowForExecutionWithRegistry(sendEmail) error = nil, want atomic rejection (contrast case)")
	}
	if !strings.Contains(err.Error(), "sendEmail action calls") {
		t.Fatalf("error = %v, want sendEmail atomic rejection text", err)
	}
}

func TestFlowExecutorAtomicValidationUsesItsInstalledActionRegistry(t *testing.T) {
	exec := NewFlowExecutor(
		nil, nil, nil, nil, nil, nil, nil,
		WithFlowActionCalls(ConnectorPlatformFlowActionCallRegistrations()...),
	)
	meta := &models.FlowMetadata{
		RunAtomically: true,
		ActionCalls: []models.FlowActionCall{{
			Name: "Notify_Owner", ActionType: flowactions.ActionTypeSendNotification,
		}},
	}
	if err := ValidateAtomicFlowForExecution(context.Background(), "", meta); err != nil {
		t.Fatalf("global platform validation rejected sendNotification: %v", err)
	}
	if err := exec.ValidateAtomicFlowForExecution(context.Background(), "", meta); err == nil {
		t.Fatal("executor validation accepted action absent from its installed registry")
	}
}

func installRuntimeFlowActionReservationsForTest() {
	if err := ReserveRuntimeFlowActionCallTypes(); err != nil {
		panic(err)
	}
}

func flowActionCallRegistryTestRegistration(actionType string) FlowActionCallRegistration {
	return FlowActionCallRegistration{
		ActionType:   actionType,
		AtomicPolicy: FlowActionCallAtomicPolicySafe,
		Handler: func(context.Context, *FlowExecutor, *models.FlowInterview, *models.FlowMetadata, *models.FlowActionCall, *models.UserSession) (string, error) {
			return "", nil
		},
	}
}

func stampConvertedAnchorsTestRegistration() FlowActionCallRegistration {
	return FlowActionCallRegistration{
		ActionType:      crmStampConvertedAnchorsActionTypeForTest,
		HandlerRef:      "crm:standard-action/stamp-converted-anchors",
		AtomicPolicy:    FlowActionCallAtomicPolicySafe,
		RequiresContext: FlowActionCallContextStandardActionProtected,
		Handler: RequireStandardActionProtectedContext(crmStampConvertedAnchorsActionTypeForTest, func(ctx context.Context, e *FlowExecutor, interview *models.FlowInterview, _ *models.FlowMetadata, action *models.FlowActionCall, _ *models.UserSession) (string, error) {
			return e.RouteActionCallFault(ctx, interview, action, fmt.Errorf("stampConvertedAnchors requires sourceObject and sourceId"))
		}),
	}
}

func expectFlowActionCallRegistryPanic(t *testing.T, want string, fn func()) {
	t.Helper()
	defer func() {
		got := recover()
		if got == nil {
			t.Fatalf("panic = nil, want %q", want)
		}
		if !strings.Contains(fmt.Sprint(got), want) {
			t.Fatalf("panic = %q, want substring %q", got, want)
		}
	}()
	fn()
}
