package automation

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

	"github.com/nexuscrm/kernel/pkg/formula"
	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/metadata"
	"github.com/nexuscrm/metadata/catalogsnapshot"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

type flowFormulaUnavailableRelationQuery struct {
	ports.QueryService
}

func (*flowFormulaUnavailableRelationQuery) QueryWithFilter(context.Context, string, string, *models.UserSession, string, string, int) (*models.QueryResult, error) {
	return nil, errors.New("query unavailable")
}

func TestFlowFormulaResourceUsesRequestBoundRuntimeFunctionBundle(t *testing.T) {
	view := flowFormulaRuntimeViewWithAddMonths(t)
	ctx := metadata.WithRuntimeMetadataView(context.Background(), view)
	interview := &models.FlowInterview{ContextData: map[string]interface{}{}}
	ctx = withFlowFormulaResourceScope(ctx, interview, []models.FlowFormula{{
		Name:       "QuarterEnd",
		DataType:   "Date",
		Expression: "ADDMONTHS(DATE(2024, 1, 31), 1)",
	}})

	got, present, err := (&FlowExecutor{}).ResolveFlowResource(
		ctx,
		interview,
		nil,
		map[string]interface{}{"elementReference": "QuarterEnd"},
	)
	if err != nil || !present || got != "2024-02-29" {
		t.Fatalf("ResolveFlowResource = (%v, %v, %v), want (2024-02-29, true, nil)", got, present, err)
	}
}

func flowFormulaRuntimeViewWithAddMonths(t *testing.T) *catalogsnapshot.RuntimeMetadataView {
	t.Helper()
	bundle, err := formula.ParseFunctionBundlePayload([]byte(`{
		"functions": [{
			"name": "ADDMONTHS", "minArgs": 2, "maxArgs": 2,
			"return": {"kind": "fixed", "type": "date"},
			"args": [{"dateLikeLiteral": true}, {"type": "number"}],
			"evaluator": {
				"kind": "formula_expr_v1", "params": ["arg0", "arg1"],
				"body": "__DATE_ADD_MONTHS__(arg0, arg1)"
			}
		}]
	}`))
	if err != nil {
		t.Fatal(err)
	}
	revision := catalogsnapshot.CatalogRevision(1)
	artifact, err := catalogsnapshot.CompileCatalogSnapshot(catalogsnapshot.CompileInput{
		OrgID:   "org-flow-formula",
		Lineage: catalogsnapshot.Lineage{CatalogRevision: revision, SchemaGeneration: 1, EngineGeneration: 1},
		Objects: []catalogsnapshot.ObjectInput{{CatalogRevision: revision, Metadata: &models.ObjectMetadata{
			ID: "quote-object", APIName: "Quote", Label: "Quote", PluralLabel: "Quotes",
			SharingModel: constants.SharingModelPublicReadWrite,
			Fields:       []models.FieldMetadata{{ID: "quote-id", APIName: "Id", Label: "ID", Type: constants.FieldTypeString, PhysicalName: "Id"}},
		}}},
		FormulaFunctionBundles: []catalogsnapshot.FormulaFunctionBundleInput{{CatalogRevision: revision, Bundle: bundle}},
		Projections: []catalogsnapshot.RuntimeProjectionInput{{
			CatalogRevision: revision, AppInstallationID: "app-installation",
			CapabilityID: "runtime.records.mutate.v1", Profile: catalogsnapshot.ProjectionRecordMutationV1,
			ObjectAPINames: []string{"Quote"},
		}},
	})
	if err != nil {
		t.Fatal(err)
	}
	snapshot, err := catalogsnapshot.OpenCatalogSnapshot(artifact)
	if err != nil {
		t.Fatal(err)
	}
	viewArtifact, err := catalogsnapshot.ProjectRuntimeMetadataView(snapshot, catalogsnapshot.ProjectionKey{
		AppInstallationID: "app-installation", CapabilityID: "runtime.records.mutate.v1",
	})
	if err != nil {
		t.Fatal(err)
	}
	view, err := catalogsnapshot.OpenRuntimeMetadataView(viewArtifact)
	if err != nil {
		t.Fatal(err)
	}
	return view
}

func TestFlowFormulaResourceEvaluatesOnEachAccessAgainstCurrentRecord(t *testing.T) {
	resources := []models.FlowFormula{{
		Name:       "NextSequence",
		DataType:   "Number",
		Expression: "$Record.Sequence__c + 1",
	}}
	interview := &models.FlowInterview{ContextData: map[string]interface{}{
		"$Record": models.SObject{"Sequence__c": float64(1)},
		// Simulate an eager result serialized by an older runtime.
		"NextSequence": float64(999),
	}}
	ctx := withFlowFormulaResourceScope(context.Background(), interview, resources)
	executor := &FlowExecutor{}

	first, present, err := executor.ResolveFlowResource(ctx, interview, nil, map[string]interface{}{"elementReference": "NextSequence"})
	if err != nil || !present || first != float64(2) {
		t.Fatalf("first ResolveFlowResource = (%v, %v, %v), want (2, true, nil)", first, present, err)
	}
	if _, persisted := interview.ContextData["NextSequence"]; persisted {
		t.Fatal("formula result was persisted in ContextData")
	}
	interview.ContextData["$Record"].(models.SObject)["Sequence__c"] = float64(7)
	second, present, err := executor.ResolveFlowResource(ctx, interview, nil, map[string]interface{}{"elementReference": "NextSequence"})
	if err != nil || !present || second != float64(8) {
		t.Fatalf("second ResolveFlowResource = (%v, %v, %v), want (8, true, nil)", second, present, err)
	}
}

func TestFlowFormulaResourceSupportsForwardDependency(t *testing.T) {
	resources := []models.FlowFormula{
		{Name: "Second", DataType: "Number", Expression: "First + 1"},
		{Name: "First", DataType: "Number", Expression: "1"},
	}
	interview := &models.FlowInterview{ContextData: map[string]interface{}{}}
	ctx := withFlowFormulaResourceScope(context.Background(), interview, resources)
	got, present, err := (&FlowExecutor{}).ResolveFlowResource(ctx, interview, nil, map[string]interface{}{"elementReference": "Second"})
	if err != nil || !present || got != float64(2) {
		t.Fatalf("ResolveFlowResource = (%v, %v, %v), want (2, true, nil)", got, present, err)
	}
}

func TestFlowFormulaResourceDataEvaluationFailureReturnsNull(t *testing.T) {
	resources := []models.FlowFormula{
		{
			Name:       "StartMonthShift",
			DataType:   "Number",
			Expression: "(14 - MONTH({!$Record.Service_Start_Date__c}) - MOD(14 - MONTH({!$Record.Service_Start_Date__c}), 12)) / 12",
		},
		{
			Name:       "StartDaySerial",
			DataType:   "Number",
			Expression: "DAY({!$Record.Service_Start_Date__c}) + 365 * (YEAR({!$Record.Service_Start_Date__c}) + 4800 - {!StartMonthShift})",
		},
		{
			Name:       "EndMonthShift",
			DataType:   "Number",
			Expression: "(14 - MONTH({!$Record.Service_End_Date__c}) - MOD(14 - MONTH({!$Record.Service_End_Date__c}), 12)) / 12",
		},
		{
			Name:       "EndDaySerial",
			DataType:   "Number",
			Expression: "DAY({!$Record.Service_End_Date__c}) + 365 * (YEAR({!$Record.Service_End_Date__c}) + 4800 - {!EndMonthShift})",
		},
		{
			Name:       "DerivedTermMonths",
			DataType:   "Number",
			Expression: "ROUND(({!EndDaySerial} - {!StartDaySerial} + 1) / 30.4375, 1)",
		},
	}
	interview := &models.FlowInterview{ContextData: map[string]interface{}{
		"$Record": models.SObject{
			"Service_Start_Date__c": "2026-08-01",
			"Service_End_Date__c":   nil,
		},
	}}
	ctx := withFlowFormulaResourceScope(context.Background(), interview, resources)
	executor := &FlowExecutor{}

	got, present, err := executor.ResolveFlowResource(
		ctx,
		interview,
		nil,
		map[string]interface{}{"elementReference": "DerivedTermMonths"},
	)
	if err != nil || !present || got != nil {
		t.Fatalf("ResolveFlowResource(DerivedTermMonths) = (%v, %v, %v), want (nil, true, nil)", got, present, err)
	}

	matched, err := executor.evaluateConditionsCtxE(ctx, interview, nil, []models.FlowCondition{
		{
			LeftValueReference: "$Record.Service_Start_Date__c",
			Operator:           "IsNull",
			RightValue:         map[string]interface{}{"booleanValue": false},
		},
		{
			LeftValueReference: "$Record.Service_End_Date__c",
			Operator:           "IsNull",
			RightValue:         map[string]interface{}{"booleanValue": false},
		},
		{
			LeftValueReference: "DerivedTermMonths",
			Operator:           "GreaterThan",
			RightValue:         map[string]interface{}{"numberValue": float64(0)},
		},
	}, "and")
	if err != nil || matched {
		t.Fatalf("evaluateConditionsCtxE = (%v, %v), want (false, nil)", matched, err)
	}
}

func TestFlowFormulaResourceRelationshipReadFailureStillPropagates(t *testing.T) {
	relationshipName := "Account"
	schema := &models.ObjectMetadata{
		APIName: "QuoteLineItem",
		Fields: []models.FieldMetadata{{
			APIName:          "AccountId",
			Type:             constants.FieldTypeReference,
			ReferenceTo:      []string{"Account"},
			RelationshipName: &relationshipName,
		}},
	}
	interview := &models.FlowInterview{ContextData: map[string]interface{}{
		"$Record": models.SObject{"AccountId": "account-1"},
	}}
	ctx := withFlowFormulaResourceScope(context.Background(), interview, []models.FlowFormula{{
		Name:       "AccountName",
		DataType:   "String",
		Expression: "$Record.Account.Name",
	}})
	runtimeContext := flowRuntimeContextFromCtx(ctx)
	runtimeContext.SetObjectType("QuoteLineItem")
	ctx = withFlowRuntimeContext(ctx, runtimeContext)
	executor := &FlowExecutor{
		query: &flowFormulaUnavailableRelationQuery{},
		objectSchemaLookup: func(context.Context, string) *models.ObjectMetadata {
			return schema
		},
	}

	_, present, err := executor.ResolveFlowResource(
		ctx,
		interview,
		nil,
		map[string]interface{}{"elementReference": "AccountName"},
	)
	if err == nil || present || !errors.Is(err, formula.ErrRelationFetch) {
		t.Fatalf("ResolveFlowResource(AccountName) = (present=%v, err=%v), want ErrRelationFetch", present, err)
	}
}

func TestFlowFormulaResourceParseErrorIsolatedToReferencedDependencyGraph(t *testing.T) {
	resources := []models.FlowFormula{
		{Name: "Valid", DataType: "Number", Expression: "1 + 1"},
		{Name: "Invalid", DataType: "Number", Expression: "ISBLANK("},
		{Name: "DependsOnInvalid", DataType: "Number", Expression: "Invalid + 1"},
	}
	interview := &models.FlowInterview{ContextData: map[string]interface{}{}}
	ctx := withFlowFormulaResourceScope(context.Background(), interview, resources)
	executor := &FlowExecutor{}

	got, present, err := executor.ResolveFlowResource(ctx, interview, nil, map[string]interface{}{"elementReference": "Valid"})
	if err != nil || !present || got != float64(2) {
		t.Fatalf("ResolveFlowResource(Valid) = (%v, %v, %v), want (2, true, nil)", got, present, err)
	}

	_, present, err = executor.ResolveFlowResource(ctx, interview, nil, map[string]interface{}{"elementReference": "Invalid"})
	if err == nil || present || !strings.Contains(err.Error(), `parse flow formula resource "Invalid" dependencies`) {
		t.Fatalf("ResolveFlowResource(Invalid) = (present=%v, err=%v), want isolated parse error", present, err)
	}

	_, present, err = executor.ResolveFlowResource(ctx, interview, nil, map[string]interface{}{"elementReference": "DependsOnInvalid"})
	if err == nil || present || !strings.Contains(err.Error(), `parse flow formula resource "Invalid" dependencies`) {
		t.Fatalf("ResolveFlowResource(DependsOnInvalid) = (present=%v, err=%v), want dependency parse error", present, err)
	}
}

func TestWaitEventTimeResolvesTypedFormulaResourceParameters(t *testing.T) {
	const wakeAt = "2099-01-02T03:04:05Z"
	wakeTime, err := time.Parse(time.RFC3339, wakeAt)
	if err != nil {
		t.Fatal(err)
	}
	interview := &models.FlowInterview{ContextData: map[string]interface{}{}}
	ctx := withFlowFormulaResourceScope(context.Background(), interview, []models.FlowFormula{
		{Name: "WakeAt", DataType: "DateTime", Expression: `"` + wakeAt + `"`},
		{Name: "UnusedInvalidUnit", DataType: "String", Expression: "ISBLANK("},
	})
	executor := &FlowExecutor{}

	tests := []struct {
		name   string
		params []models.FlowActionParameter
		want   time.Time
	}{
		{
			name: "typed formula DateRef without offset",
			params: []models.FlowActionParameter{{
				Name: "DateRef", Value: map[string]interface{}{"elementReference": "WakeAt"},
			}},
			want: wakeTime,
		},
		{
			name: "typed formula DateRef with typed offset",
			params: []models.FlowActionParameter{
				{Name: "DateRef", Value: map[string]interface{}{"elementReference": "WakeAt"}},
				{Name: "TimeOffset", Value: map[string]interface{}{"numberValue": float64(60)}},
				{Name: "TimeOffsetUnit", Value: map[string]interface{}{"stringValue": "Minutes"}},
			},
			want: wakeTime.Add(time.Hour),
		},
		{
			name: "typed formula DateRef with legacy bare offset",
			params: []models.FlowActionParameter{
				{Name: "DateRef", Value: map[string]interface{}{"elementReference": "WakeAt"}},
				{Name: "TimeOffset", Value: float64(60)},
				{Name: "TimeOffsetUnit", Value: "Minutes"},
			},
			want: wakeTime.Add(time.Hour),
		},
		{
			name: "typed AlarmTime literal",
			params: []models.FlowActionParameter{{
				Name: "AlarmTime", Value: map[string]interface{}{"dateTimeValue": wakeAt},
			}},
			want: wakeTime,
		},
		{
			name:   "legacy bare DateRef",
			params: []models.FlowActionParameter{{Name: "DateRef", Value: "WakeAt"}},
			want:   wakeTime,
		},
		{
			name: "DateRef without offset ignores unused unit",
			params: []models.FlowActionParameter{
				{Name: "DateRef", Value: map[string]interface{}{"elementReference": "WakeAt"}},
				{Name: "TimeOffsetUnit", Value: map[string]interface{}{"elementReference": "UnusedInvalidUnit"}},
			},
			want: wakeTime,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, ok, err := executor.resolveWaitEventTime(ctx, interview, nil, &models.FlowWaitEvent{InputParameters: tt.params})
			if err != nil || !ok || !got.Equal(tt.want) {
				t.Fatalf("resolveWaitEventTime = (%v, %v, %v), want (%v, true, nil)", got, ok, err, tt.want)
			}
		})
	}
}

func TestFlowFormulaResourcePreservesObjectFieldTraversal(t *testing.T) {
	interview := &models.FlowInterview{ContextData: map[string]interface{}{
		"sourceRecord": models.SObject{"Id": "record-1", "Name": "Current"},
	}}
	ctx := withFlowFormulaResourceScope(context.Background(), interview, []models.FlowFormula{{
		Name:       "CurrentRecord",
		DataType:   "Object",
		Expression: "sourceRecord",
	}})

	got, present, err := (&FlowExecutor{}).ResolveFlowResource(
		ctx,
		interview,
		nil,
		map[string]interface{}{"elementReference": "CurrentRecord.Id"},
	)
	if err != nil || !present || got != "record-1" {
		t.Fatalf("ResolveFlowResource(CurrentRecord.Id) = (%v, %v, %v), want (record-1, true, nil)", got, present, err)
	}
}

func TestFlowElementsConsumeFormulaResourcesThroughCanonicalResolver(t *testing.T) {
	record := models.SObject{
		constants.FieldID: "record-1",
		"Name":            "Current",
		"attributes":      map[string]interface{}{"type": "Widget"},
	}
	interview := &models.FlowInterview{ContextData: map[string]interface{}{
		"payload": map[string]interface{}{"records": []models.SObject{record}},
		"record":  record,
	}}
	baseCtx := withFlowRuntimeContext(context.Background(), NewFlowRuntimeContext(context.Background(), nil))
	ctx := withFlowFormulaResourceScope(baseCtx, interview, []models.FlowFormula{
		{Name: "CurrentPayload", DataType: "Object", Expression: "payload"},
		{Name: "CurrentRecord", DataType: "Object", Expression: "record"},
	})

	t.Run("loop collection", func(t *testing.T) {
		loop := &models.FlowLoop{
			Name:                       "EachRecord",
			CollectionReference:        "CurrentPayload.records",
			AssignNextValueToReference: "current",
			NextValueConnector:         &models.FlowConnector{TargetReference: "Body"},
		}
		next, err := (&FlowExecutor{}).executeLoop(ctx, interview, loop, nil)
		if err != nil || next != "Body" {
			t.Fatalf("executeLoop = (%q, %v), want (Body, nil)", next, err)
		}
		if got := interview.ContextData["current"].(models.SObject)[constants.FieldID]; got != "record-1" {
			t.Fatalf("loop current Id = %v, want record-1", got)
		}
	})

	t.Run("record update target", func(t *testing.T) {
		capture := &captureUpdate{}
		executor := minimalPersistenceWithCapture(capture)
		next, err := executor.executeRecordUpdate(ctx, interview, nil, &models.FlowRecordUpdate{
			Name: "UpdateCurrent", Object: "Widget", InputReference: "CurrentRecord",
			InputAssignments: []models.FlowInputAssignment{{
				Field: "Name", Value: map[string]interface{}{"stringValue": "Updated"},
			}},
			Connector: &models.FlowConnector{TargetReference: "Next"},
		}, nil)
		if err != nil || next != "Next" {
			t.Fatalf("executeRecordUpdate = (%q, %v), want (Next, nil)", next, err)
		}
		if len(capture.calls) != 1 || capture.calls[0].id != "record-1" {
			t.Fatalf("update calls = %#v, want one call for record-1", capture.calls)
		}
	})

	t.Run("record delete target", func(t *testing.T) {
		persistence := &stubPersistenceCapture{}
		executor := &FlowExecutor{persistence: persistence}
		next, err := executor.executeRecordDelete(ctx, interview, &models.FlowRecordDelete{
			Name: "DeleteCurrent", Object: "Widget", InputReference: "CurrentRecord",
			Connector: &models.FlowConnector{TargetReference: "Next"},
		}, nil)
		if err != nil || next != "Next" {
			t.Fatalf("executeRecordDelete = (%q, %v), want (Next, nil)", next, err)
		}
		if len(persistence.deleteCalls) != 1 || persistence.deleteCalls[0].id != "record-1" {
			t.Fatalf("delete calls = %#v, want one call for record-1", persistence.deleteCalls)
		}
	})
}

func TestFlowFormulaResourceCycleFailsLoudly(t *testing.T) {
	resources := []models.FlowFormula{
		{Name: "First", DataType: "Number", Expression: "Second + 1"},
		{Name: "Second", DataType: "Number", Expression: "First + 1"},
	}
	interview := &models.FlowInterview{ContextData: map[string]interface{}{}}
	ctx := withFlowFormulaResourceScope(context.Background(), interview, resources)
	_, present, err := (&FlowExecutor{}).ResolveFlowResource(ctx, interview, nil, map[string]interface{}{"elementReference": "First"})
	if err == nil || present || !strings.Contains(err.Error(), "circular") {
		t.Fatalf("ResolveFlowResource cycle = (present=%v, err=%v), want circular error", present, err)
	}
}

func TestRecordLookupFilterPropagatesFormulaResourceError(t *testing.T) {
	resources := []models.FlowFormula{
		{Name: "First", DataType: "Number", Expression: "Second + 1"},
		{Name: "Second", DataType: "Number", Expression: "First + 1"},
	}
	interview := &models.FlowInterview{ContextData: map[string]interface{}{}}
	ctx := withFlowFormulaResourceScope(context.Background(), interview, resources)
	_, err := (&FlowExecutor{}).buildFilterFromConditionsCtxE(ctx, interview, nil, []models.FlowRecordFilter{{
		Field:    "Sequence__c",
		Operator: "EqualTo",
		Value:    map[string]interface{}{"elementReference": "First"},
	}}, "and")
	if err == nil || !strings.Contains(err.Error(), "circular") {
		t.Fatalf("buildFilterFromConditionsCtxE error = %v, want circular Formula Resource error", err)
	}
}

func TestFilteredRecordDeleteRoutesFormulaResourceErrorToFaultConnector(t *testing.T) {
	resources := []models.FlowFormula{
		{Name: "First", DataType: "Number", Expression: "Second + 1"},
		{Name: "Second", DataType: "Number", Expression: "First + 1"},
	}
	interview := &models.FlowInterview{ContextData: map[string]interface{}{}}
	ctx := withFlowFormulaResourceScope(context.Background(), interview, resources)
	next, err := (&FlowExecutor{}).executeRecordDelete(ctx, interview, &models.FlowRecordDelete{
		Name:        "DeleteByFormula",
		Object:      "Widget",
		FilterLogic: "and",
		Filters: []models.FlowRecordFilter{{
			Field:    "Sequence__c",
			Operator: "EqualTo",
			Value:    map[string]interface{}{"elementReference": "First"},
		}},
		FaultConnector: &models.FlowConnector{TargetReference: "HandleFormulaFault"},
	}, nil)
	if err != nil || next != "HandleFormulaFault" {
		t.Fatalf("executeRecordDelete = (%q, %v), want fault connector", next, err)
	}
	if message, _ := interview.ContextData["$Flow.FaultMessage"].(string); !strings.Contains(message, "circular") {
		t.Fatalf("$Flow.FaultMessage = %q, want circular Formula Resource error", message)
	}
}

func TestFormulaErrorsFollowElementFaultConnectors(t *testing.T) {
	resources := []models.FlowFormula{
		{Name: "First", DataType: "Number", Expression: "Second + 1"},
		{Name: "Second", DataType: "Number", Expression: "First + 1"},
	}
	reference := map[string]interface{}{"elementReference": "First"}
	tests := []struct {
		name        string
		elementName string
		faultTarget string
		meta        models.FlowMetadata
	}{
		{
			name:        "record create",
			elementName: "Create",
			faultTarget: "CreateFault",
			meta: models.FlowMetadata{RecordCreates: []models.FlowRecordCreate{{
				Name: "Create", Object: "Widget",
				InputAssignments: []models.FlowInputAssignment{{Field: "Sequence__c", Value: reference}},
				FaultConnector:   &models.FlowConnector{TargetReference: "CreateFault"},
			}}},
		},
		{
			name:        "record update",
			elementName: "Update",
			faultTarget: "UpdateFault",
			meta: models.FlowMetadata{RecordUpdates: []models.FlowRecordUpdate{{
				Name: "Update", Object: "Widget",
				InputAssignments: []models.FlowInputAssignment{{Field: "Sequence__c", Value: reference}},
				FaultConnector:   &models.FlowConnector{TargetReference: "UpdateFault"},
			}}},
		},
		{
			name:        "wait",
			elementName: "Wait",
			faultTarget: "WaitFault",
			meta: models.FlowMetadata{Waits: []models.FlowWait{{
				Name: "Wait",
				WaitEvents: []models.FlowWaitEvent{{
					Name: "Ready",
					Conditions: []models.FlowCondition{{
						LeftValueReference: "First",
						Operator:           "EqualTo",
						RightValue:         map[string]interface{}{"numberValue": float64(1)},
					}},
				}},
				FaultConnector: &models.FlowConnector{TargetReference: "WaitFault"},
			}}},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			interview := &models.FlowInterview{ContextData: map[string]interface{}{}}
			ctx := withFlowFormulaResourceScope(context.Background(), interview, resources)
			next, err := (&FlowExecutor{}).executeElement(ctx, interview, &tt.meta, tt.elementName, nil)
			if err != nil || next != tt.faultTarget {
				t.Fatalf("executeElement = (%q, %v), want (%q, nil)", next, err, tt.faultTarget)
			}
			if message, _ := interview.ContextData["$Flow.FaultMessage"].(string); !strings.Contains(message, "circular") {
				t.Fatalf("$Flow.FaultMessage = %q, want circular Formula Resource error", message)
			}
		})
	}
}

func TestAssignmentReadsFormulaAfterEarlierRecordAssignment(t *testing.T) {
	meta := &models.FlowMetadata{
		Formulas: []models.FlowFormula{{
			Name:       "DisplayName",
			DataType:   "String",
			Expression: `"Top-up #" & TEXT($Record.Topup_Sequence__c)`,
		}},
		Assignments: []models.FlowAssignment{{
			Name: "StampName",
			Items: []models.FlowAssignmentItem{{
				AssignToReference: "$Record.Name",
				Operator:          "Assign",
				Value:             map[string]interface{}{"elementReference": "DisplayName"},
			}},
		}},
	}
	interview := &models.FlowInterview{ContextData: map[string]interface{}{
		"$Record": models.SObject{"Topup_Sequence__c": float64(4)},
	}}
	ctx := withFlowFormulaResourceScope(context.Background(), interview, meta.Formulas)
	if _, err := (&FlowExecutor{}).executeAssignment(ctx, interview, meta, &meta.Assignments[0], nil); err != nil {
		t.Fatalf("executeAssignment: %v", err)
	}
	if got := interview.ContextData["$Record"].(models.SObject)["Name"]; got != "Top-up #4" {
		t.Fatalf("$Record.Name = %v, want Top-up #4", got)
	}
}

func TestExecuteFlowVersionFormulaReadsEarlierAssignment(t *testing.T) {
	engine := formula.NewEngine()
	executor := &FlowExecutor{
		formulaEngine: engine,
		interviews:    NewFlowInterviewService(nil, nil),
	}
	version := &models.FlowVersion{
		Id:           "formula-order-version",
		DefinitionId: "formula-order-definition",
		ProcessType:  "AutoLaunchedFlow",
		Metadata: models.FlowMetadata{
			StartElementReference: "AssignSequence",
			Variables: []models.FlowVariable{{
				Name: "resultName", DataType: "String", IsOutput: true,
			}},
			Formulas: []models.FlowFormula{{
				Name:       "DisplayName",
				DataType:   "String",
				Expression: `"Top-up #" & TEXT($Record.Topup_Sequence__c)`,
			}},
			Assignments: []models.FlowAssignment{
				{
					Name: "AssignSequence",
					Items: []models.FlowAssignmentItem{{
						AssignToReference: "$Record.Topup_Sequence__c",
						Operator:          "Assign",
						Value:             map[string]interface{}{"numberValue": float64(4)},
					}},
					Connector: &models.FlowConnector{TargetReference: "ReadDisplayName"},
				},
				{
					Name: "ReadDisplayName",
					Items: []models.FlowAssignmentItem{{
						AssignToReference: "resultName",
						Operator:          "Assign",
						Value:             map[string]interface{}{"elementReference": "DisplayName"},
					}},
				},
			},
		},
	}

	result, err := executor.executeFlowVersion(
		WithTransientFlowInterviews(context.Background()),
		version,
		"FormulaOrder",
		map[string]interface{}{"$Record": models.SObject{}},
		nil,
	)
	if err != nil {
		t.Fatalf("executeFlowVersion: %v", err)
	}
	if result == nil || !result.IsSuccess {
		t.Fatalf("executeFlowVersion result = %#v, want success", result)
	}
	if got := result.OutputValues["resultName"]; got != "Top-up #4" {
		t.Fatalf("resultName = %v, want Top-up #4", got)
	}
}
