package runtimeapi

import (
	"context"
	"strings"
	"testing"

	"github.com/nexuscrm/shared/pkg/models"
)

type flowEligibilityView map[string]*models.FlowVersion

func (v flowEligibilityView) ActiveFlowVersion(name string) (*models.FlowVersion, bool) {
	version, ok := v[strings.ToLower(name)]
	return version, ok
}

func TestSelectBoundedAtomicFlowBindsExactTransitiveClosure(t *testing.T) {
	child := eligibleFlowVersion("child-version", "child-definition", 2, models.FlowMetadata{
		RunAtomically:         true,
		StartElementReference: "ChildAssignment",
		Assignments:           []models.FlowAssignment{{Name: "ChildAssignment"}},
	})
	root := eligibleFlowVersion("root-version", "root-definition", 7, models.FlowMetadata{
		RunAtomically: true,
		Start: &models.FlowStart{
			Connector: &models.FlowConnector{TargetReference: "InvokeChild"},
		},
		Subflows: []models.FlowSubflow{{Name: "InvokeChild", FlowName: "ChildAction"}},
	})
	view := flowEligibilityView{"rootaction": root, "childaction": child}

	first, err := selectBoundedAtomicFlow(context.Background(), view, "RootAction")
	if err != nil {
		t.Fatal(err)
	}
	if first.root.Id != "root-version" || first.maxElementExecutions != 2 ||
		!strings.HasPrefix(first.closureDigest, "sha256:") {
		t.Fatalf("selection = %#v", first)
	}

	child.Metadata.Assignments[0].Connector = &models.FlowConnector{TargetReference: "Second"}
	child.Metadata.Assignments = append(child.Metadata.Assignments, models.FlowAssignment{Name: "Second"})
	second, err := selectBoundedAtomicFlow(context.Background(), view, "RootAction")
	if err != nil {
		t.Fatal(err)
	}
	if second.closureDigest == first.closureDigest || second.maxElementExecutions != 3 {
		t.Fatalf("changed child closure was not bound: first=%#v second=%#v", first, second)
	}
}

func TestSelectBoundedAtomicFlowCountsRepeatedSubflowCallSites(t *testing.T) {
	child := eligibleFlowVersion("child-version", "child-definition", 1, models.FlowMetadata{
		RunAtomically:         true,
		StartElementReference: "ChildAssignment",
		Assignments:           []models.FlowAssignment{{Name: "ChildAssignment"}},
	})
	root := eligibleFlowVersion("root-version", "root-definition", 1, models.FlowMetadata{
		RunAtomically:         true,
		StartElementReference: "FirstChild",
		Subflows: []models.FlowSubflow{
			{
				Name:      "FirstChild",
				FlowName:  "ChildAction",
				Connector: &models.FlowConnector{TargetReference: "SecondChild"},
			},
			{Name: "SecondChild", FlowName: "ChildAction"},
		},
	})
	view := flowEligibilityView{"rootaction": root, "childaction": child}

	selection, err := selectBoundedAtomicFlow(context.Background(), view, "RootAction")
	if err != nil {
		t.Fatal(err)
	}
	if selection.maxElementExecutions != 4 {
		t.Fatalf("element execution bound = %d, want 4", selection.maxElementExecutions)
	}
}

func TestSelectBoundedAtomicFlowRejectsSingleGraphPastExecutionBudget(t *testing.T) {
	assignments := make([]models.FlowAssignment, runtimeFlowMaxElementExecutions+1)
	for index := range assignments {
		assignments[index].Name = "Step" + strings.Repeat("x", index)
		if index+1 < len(assignments) {
			assignments[index].Connector = &models.FlowConnector{
				TargetReference: "Step" + strings.Repeat("x", index+1),
			}
		}
	}
	version := eligibleFlowVersion("version", "definition", 1, models.FlowMetadata{
		RunAtomically:         true,
		StartElementReference: assignments[0].Name,
		Assignments:           assignments,
	})
	if _, err := selectBoundedAtomicFlow(
		context.Background(),
		flowEligibilityView{"root": version},
		"Root",
	); err == nil {
		t.Fatal("graph larger than the execution budget was accepted")
	}
}

func TestSelectBoundedAtomicFlowFailsClosedOnUnboundedOrExternalShapes(t *testing.T) {
	tests := []struct {
		name     string
		metadata models.FlowMetadata
	}{
		{
			name: "not atomic",
			metadata: models.FlowMetadata{
				StartElementReference: "Done",
				Assignments:           []models.FlowAssignment{{Name: "Done"}},
			},
		},
		{
			name: "wait",
			metadata: models.FlowMetadata{
				RunAtomically: true, StartElementReference: "Pause",
				Waits: []models.FlowWait{{Name: "Pause"}},
			},
		},
		{
			name: "external action",
			metadata: models.FlowMetadata{
				RunAtomically: true, StartElementReference: "Call",
				ActionCalls: []models.FlowActionCall{{
					Name: "Call", ActionType: "apex", ActionName: "External",
				}},
			},
		},
		{
			name: "collection lookup",
			metadata: models.FlowMetadata{
				RunAtomically: true, StartElementReference: "Lookup",
				RecordLookups: []models.FlowRecordLookup{{Name: "Lookup", Object: "Widget"}},
			},
		},
		{
			name: "runtime collection update input",
			metadata: models.FlowMetadata{
				RunAtomically: true, StartElementReference: "Update",
				RecordUpdates: []models.FlowRecordUpdate{{
					Name: "Update", InputReference: "requestRecords",
				}},
			},
		},
		{
			name: "runtime collection delete input",
			metadata: models.FlowMetadata{
				RunAtomically: true, StartElementReference: "Delete",
				RecordDeletes: []models.FlowRecordDelete{{
					Name: "Delete", InputReference: "requestRecords",
				}},
			},
		},
		{
			name: "dynamic parent prelock",
			metadata: models.FlowMetadata{
				RunAtomically: true, StartElementReference: "Done",
				Assignments: []models.FlowAssignment{{Name: "Done"}},
				AtomicParentPrelocks: []models.FlowAtomicParentPrelock{{
					Name: "Parents", ParentObject: "Account",
					RecordIDs: []interface{}{"input.parentIds"},
				}},
			},
		},
		{
			name: "element cycle",
			metadata: models.FlowMetadata{
				RunAtomically: true, StartElementReference: "First",
				Assignments: []models.FlowAssignment{
					{Name: "First", Connector: &models.FlowConnector{TargetReference: "Second"}},
					{Name: "Second", Connector: &models.FlowConnector{TargetReference: "First"}},
				},
			},
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			view := flowEligibilityView{
				"root": eligibleFlowVersion("version", "definition", 1, test.metadata),
			}
			if _, err := selectBoundedAtomicFlow(context.Background(), view, "Root"); err == nil {
				t.Fatal("ineligible Flow was accepted")
			}
		})
	}
}

func TestSelectBoundedAtomicFlowAllowsSingleLookupMutationTarget(t *testing.T) {
	version := eligibleFlowVersion(
		"version",
		"definition",
		1,
		models.FlowMetadata{
			RunAtomically:         true,
			StartElementReference: "Lookup",
			RecordLookups: []models.FlowRecordLookup{{
				Name: "Lookup", OutputReference: "oneRecord",
				Object: "Widget", GetFirstRecordOnly: true,
				Connector: &models.FlowConnector{TargetReference: "Update"},
			}},
			RecordUpdates: []models.FlowRecordUpdate{{
				Name: "Update", InputReference: "oneRecord",
			}},
		},
	)
	selection, err := selectBoundedAtomicFlow(
		context.Background(),
		flowEligibilityView{"root": version},
		"Root",
	)
	if err != nil {
		t.Fatal(err)
	}
	if selection.maxElementExecutions != 2 {
		t.Fatalf("element execution bound = %d, want 2", selection.maxElementExecutions)
	}
}

func TestSelectBoundedAtomicFlowRejectsNonCallableProcessType(t *testing.T) {
	version := eligibleFlowVersion(
		"version",
		"definition",
		1,
		models.FlowMetadata{
			RunAtomically:         true,
			StartElementReference: "Done",
			Assignments:           []models.FlowAssignment{{Name: "Done"}},
		},
	)
	version.ProcessType = "Workflow"
	if _, err := selectBoundedAtomicFlow(
		context.Background(),
		flowEligibilityView{"root": version},
		"Root",
	); err == nil {
		t.Fatal("non-callable process type was accepted")
	}
}

func TestSelectBoundedAtomicFlowFailsClosedOnMissingOrCyclicSubflow(t *testing.T) {
	root := eligibleFlowVersion("root-version", "root-definition", 1, models.FlowMetadata{
		RunAtomically: true, StartElementReference: "Child",
		Subflows: []models.FlowSubflow{{Name: "Child", FlowName: "Missing"}},
	})
	view := flowEligibilityView{"root": root}
	if _, err := selectBoundedAtomicFlow(context.Background(), view, "Root"); err == nil {
		t.Fatal("missing child was accepted")
	}

	child := eligibleFlowVersion("child-version", "child-definition", 1, models.FlowMetadata{
		RunAtomically: true, StartElementReference: "RootAgain",
		Subflows: []models.FlowSubflow{{Name: "RootAgain", FlowName: "Root"}},
	})
	view["missing"] = child
	if _, err := selectBoundedAtomicFlow(context.Background(), view, "Root"); err == nil {
		t.Fatal("cyclic child closure was accepted")
	}
}

func TestSelectBoundedAtomicFlowFailsClosedWhenRequestIsCanceled(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	view := flowEligibilityView{"root": eligibleFlowVersion(
		"version",
		"definition",
		1,
		models.FlowMetadata{
			RunAtomically:         true,
			StartElementReference: "Done",
			Assignments:           []models.FlowAssignment{{Name: "Done"}},
		},
	)}
	if _, err := selectBoundedAtomicFlow(ctx, view, "Root"); err == nil {
		t.Fatal("canceled request was accepted")
	}
}

func eligibleFlowVersion(
	id string,
	definitionID string,
	versionNumber int,
	metadata models.FlowMetadata,
) *models.FlowVersion {
	return &models.FlowVersion{
		Id: id, DefinitionId: definitionID, VersionNumber: versionNumber,
		Status: models.FlowStatusActive, ProcessType: "AutoLaunchedFlow",
		DefinitionRuntimeLoaded: true, Metadata: metadata,
	}
}
