package plan

import (
	"bytes"
	"encoding/json"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

func TestSemanticDigestNormalizesAuthoringOnlyDifferences(t *testing.T) {
	base := compileArtifactForTest(t, validCallbackPlan)

	withHints := strings.Replace(validCallbackPlan,
		`channel: {type: channel, required: true}`,
		`channel: {type: channel, required: true, label: Channel, description: author-only hint}`, 1)
	withHints = strings.Replace(withHints, `  - id: notify`, "  - id: notify\n    description: visual authoring note", 1)
	assertSameSemanticDigest(t, base, compileArtifactForTest(t, withHints), "authoring hints")

	durationSpelling := strings.ReplaceAll(validCallbackPlan, "PT30M", "30m")
	durationSpelling = strings.ReplaceAll(durationSpelling, "backoff: 1s", "backoff: PT1S")
	assertSameSemanticDigest(t, base, compileArtifactForTest(t, durationSpelling), "duration spelling")

	expressionWhitespace := strings.Replace(validCallbackPlan,
		`{{ length(input.channel) }}`, `{{   length( input.channel )   }}`, 1)
	assertSameSemanticDigest(t, base, compileArtifactForTest(t, expressionWhitespace), "expression whitespace")

	omitted := strings.Replace(validCallbackPlan, `    port: in
    input:
      channel: {ref: plan.input.channel}`, `    port: in`, 1)
	omitted = strings.Replace(omitted, `    bindings:
      reply: {ref: output.reply}`, ``, 1)
	assertSameSemanticDigest(t, base, compileArtifactForTest(t, omitted), "provable omitted mappings")

	fork := compileArtifactForTest(t, validForkJoinPlan)
	reorderedBranches := strings.Replace(validForkJoinPlan, `branches: [left, right]`, `branches: [right, left]`, 1)
	assertSameSemanticDigest(t, fork, compileArtifactForTest(t, reorderedBranches), "unconditional fork branch order")

	allMatch := allMatchJoinPlan()
	reorderedCases := strings.Replace(allMatch, `      - port: left
        when: 'contains(input.value, "left") == true'
      - port: right
        when: 'contains(input.value, "right") == true'`, `      - port: right
        when: 'contains(input.value, "right") == true'
      - port: left
        when: 'contains(input.value, "left") == true'`, 1)
	assertSameSemanticDigest(t, compileArtifactForTest(t, allMatch), compileArtifactForTest(t, reorderedCases), "all_match case order")

	withEnum := strings.Replace(validForkJoinPlan, "input:\n  value:", "input:\n  choice: {type: enum, values: [a, b]}\n  value:", 1)
	withEnumReordered := strings.Replace(withEnum, "values: [a, b]", "values: [b, a]", 1)
	assertSameSemanticDigest(t, compileArtifactForTest(t, withEnum), compileArtifactForTest(t, withEnumReordered), "enum set order")

	// Node and edge list position is explicitly not identity.
	document, diagnostics := Parse([]byte(validCallbackPlan))
	if HasErrors(diagnostics) {
		t.Fatalf("Parse() diagnostics = %#v", diagnostics)
	}
	validated, analysis := validate(document, nil)
	if HasErrors(validated) {
		t.Fatalf("validate() diagnostics = %#v", validated)
	}
	reverseNodes(document.Graph.Nodes)
	reverseEdges(document.Graph.Edges)
	validated, reorderedAnalysis := validate(document, nil)
	if HasErrors(validated) {
		t.Fatalf("reordered validate() diagnostics = %#v", validated)
	}
	reordered, err := buildArtifact(document, reorderedAnalysis)
	if err != nil {
		t.Fatalf("reordered buildArtifact() error = %v", err)
	}
	_ = analysis
	assertSameSemanticDigest(t, base, reordered, "node/edge list order")

	optionalOmitted := linearActionPlan("state:\n  note: {type: string}\n", `  - id: action
    type: set_variable
    setVariable:
      assignments:
      - state: note
        value: {value: ok}`, []string{"completed", "failed"})
	optionalFalse := strings.Replace(optionalOmitted, `{type: string}`, `{type: string, required: false}`, 1)
	assertSameSemanticDigest(t, compileArtifactForTest(t, optionalOmitted), compileArtifactForTest(t, optionalFalse), "required omitted vs false")

	emptyOutputOmitted := linearActionPlan("", `  - id: action
    type: wait
    wait:
      until: {value: PT1S}
      timeout: PT1M
      retry: {maxAttempts: 1, backoff: PT1S}`, []string{"completed", "failed", "timed_out"})
	emptyOutputExplicit := strings.Replace(emptyOutputOmitted, "kind: PlanVersion\n", "kind: PlanVersion\noutput: {}\n", 1)
	assertSameSemanticDigest(t, compileArtifactForTest(t, emptyOutputOmitted), compileArtifactForTest(t, emptyOutputExplicit), "nil vs empty contract")
}

func TestSemanticDigestChangesForRuntimeSemantics(t *testing.T) {
	base := compileArtifactForTest(t, validCallbackPlan)
	changes := map[string]string{
		"message":      strings.Replace(validCallbackPlan, "@agent answer", "@agent respond", 1),
		"retry":        strings.Replace(validCallbackPlan, "maxAttempts: 3", "maxAttempts: 4", 1),
		"failure code": strings.Replace(validCallbackPlan, "message_failed", "message_permanently_failed", 1),
	}
	for name, source := range changes {
		t.Run(name, func(t *testing.T) {
			other := compileArtifactForTest(t, source)
			if base.Digest == other.Digest {
				t.Fatalf("semantic change %q retained digest %s", name, base.Digest)
			}
		})
	}

	all := compileArtifactForTest(t, validForkJoinPlan)
	anySource := strings.Replace(validForkJoinPlan, "mode: all", "mode: any", 1)
	anySource = strings.Replace(anySource, "branches.left.value", "winner.value.value", 1)
	if all.Digest == compileArtifactForTest(t, anySource).Digest {
		t.Fatal("join all/any must have distinct semantic digests")
	}

	loop := compileArtifactForTest(t, validLoopPlan)
	switchCases := strings.Replace(validLoopPlan, `      - port: again
        when: 'input.answer == "again"'
      - port: stop
        when: 'contains(input.answer, "stop") == true'`, `      - port: stop
        when: 'contains(input.answer, "stop") == true'
      - port: again
        when: 'input.answer == "again"'`, 1)
	if loop.Digest == compileArtifactForTest(t, switchCases).Digest {
		t.Fatal("first_match case order must affect semantic digest")
	}
}

func TestArtifactDecodeEnvelopeAndTamperDetection(t *testing.T) {
	artifact := compileArtifactForTest(t, validCallbackPlan)
	encoded, err := artifact.DeterministicJSON()
	if err != nil {
		t.Fatalf("DeterministicJSON() error = %v", err)
	}
	decoded, err := DecodeArtifact(encoded)
	if err != nil {
		t.Fatalf("DecodeArtifact() error = %v\n%s", err, encoded)
	}
	if decoded.Digest != artifact.Digest || !decoded.VerifyDigest() {
		t.Fatalf("decoded artifact = %#v", decoded)
	}
	wire := string(encoded)
	for _, rawSource := range []string{"{{", "length(input.channel)", "output.reply", "plan.input.channel"} {
		if strings.Contains(wire, rawSource) {
			t.Fatalf("artifact retained raw authoring expression/template %q: %s", rawSource, wire)
		}
	}

	// compilerVersion is provenance, not semantic identity; an older valid
	// compiler envelope remains decodable during rolling deploy/replay.
	decoded.CompilerVersion = "loop.plan.compiler/v0"
	if !decoded.VerifyDigest() || ValidateEnvelope(decoded) != nil {
		t.Fatalf("compiler provenance unexpectedly changed semantics/envelope")
	}
	decoded.ArtifactVersion = "loop.plan.artifact/v999"
	if !decoded.VerifyDigest() || ValidateEnvelope(decoded) == nil {
		t.Fatalf("unsupported artifactVersion must fail envelope but not semantic hash")
	}

	tampered, err := DecodeArtifact(encoded)
	if err != nil {
		t.Fatal(err)
	}
	tampered.ConfigTextForTest("notify", " altered")
	if tampered.VerifyDigest() || ValidateEnvelope(tampered) == nil {
		t.Fatal("semantic tamper was not detected")
	}

	var object map[string]any
	if err := json.Unmarshal(encoded, &object); err != nil {
		t.Fatal(err)
	}
	object["unknownEnvelopeField"] = true
	unknown, _ := json.Marshal(object)
	if _, err := DecodeArtifact(unknown); err == nil {
		t.Fatal("DecodeArtifact accepted unknown envelope field")
	}
}

func TestDecodeArtifactRejectsDeepInvariantTamperingWithRecomputedDigest(t *testing.T) {
	setVariableSource := linearActionPlan("state:\n  count: {type: number, required: true, default: 0}\n", `  - id: action
    type: set_variable
    setVariable:
      assignments:
      - state: count
        value: {value: 7}`, []string{"completed", "failed"})
	arrayStateSource := linearActionPlan("state:\n  tags: {type: array, required: true, default: [ok], items: {type: string}}\n", `  - id: action
    type: set_variable
    setVariable:
      assignments:
      - state: tags
        value: {value: [ok]}`, []string{"completed", "failed"})
	stringConst := func(value string) ArtifactExpression {
		payload, _ := json.Marshal(value)
		return ArtifactExpression{Op: ArtifactOpConst, ResultType: TypeString, Value: ArtifactLiteral(payload)}
	}
	numberConst := func(value string) ArtifactExpression {
		return ArtifactExpression{Op: ArtifactOpConst, ResultType: TypeNumber, Value: ArtifactLiteral(value)}
	}
	objectConst := func() ArtifactExpression {
		return ArtifactExpression{Op: ArtifactOpConst, ResultType: TypeObject, Value: ArtifactLiteral(`{}`)}
	}
	tests := []struct {
		name   string
		source string
		mutate func(*Artifact)
	}{
		{"retry attempts", validCallbackPlan, func(a *Artifact) { artifactNodeForTest(a, "notify").Config.SendMessage.Retry.MaxAttempts = 0 }},
		{"retry backoff", validCallbackPlan, func(a *Artifact) { artifactNodeForTest(a, "notify").Config.SendMessage.Retry.BackoffNanos = 0 }},
		{"excessive retry attempts", validCallbackPlan, func(a *Artifact) {
			artifactNodeForTest(a, "notify").Config.SendMessage.Retry.MaxAttempts = MaxRetryAttempts + 1
		}},
		{"nil callback contract", validCallbackPlan, func(a *Artifact) { artifactNodeForTest(a, "notify").Config.SendMessage.CallbackOutput = nil }},
		{"nil fork branches", validForkJoinPlan, func(a *Artifact) { artifactNodeForTest(a, "fanout").Config.Fork.Branches = nil }},
		{"unsorted fork branches", validForkJoinPlan, func(a *Artifact) {
			branches := artifactNodeForTest(a, "fanout").Config.Fork.Branches
			branches[0], branches[1] = branches[1], branches[0]
		}},
		{"duplicate assignments", setVariableSource, func(a *Artifact) {
			config := artifactNodeForTest(a, "action").Config.SetVariable
			config.Assignments = append(config.Assignments, config.Assignments[0])
		}},
		{"unknown expression result type", validCallbackPlan, func(a *Artifact) {
			artifactNodeForTest(a, "notify").Config.SendMessage.Target.Segments[0].Expression.ResultType = "invented"
		}},
		{"load result type disagrees with contract", validCallbackPlan, func(a *Artifact) {
			artifactNodeForTest(a, "notify").Config.SendMessage.Target.Segments[0].Expression.ResultType = TypeString
		}},
		{"const literal disagrees with result type", setVariableSource, func(a *Artifact) {
			artifactNodeForTest(a, "action").Config.SetVariable.Assignments[0].Value.Value = ArtifactLiteral(`"wrong"`)
		}},
		{"empty template", validCallbackPlan, func(a *Artifact) {
			artifactNodeForTest(a, "notify").Config.SendMessage.Text.Segments = []ArtifactTemplateSegment{}
		}},
		{"adjacent template text", validCallbackPlan, func(a *Artifact) {
			template := &artifactNodeForTest(a, "notify").Config.SendMessage.Text
			template.Segments = append([]ArtifactTemplateSegment{{Kind: ArtifactTemplateSegmentText, Text: "one"}, {Kind: ArtifactTemplateSegmentText, Text: "two"}}, template.Segments...)
		}},
		{"parallel scope owner opcode", validForkJoinPlan, func(a *Artifact) { a.Scopes[0].OwnerID = "success" }},
		{"parallel scope member", validForkJoinPlan, func(a *Artifact) { a.Scopes[0].Parallel.Branches[0].Members = []string{"unknown"} }},
		{"parallel scope edge", validForkJoinPlan, func(a *Artifact) { a.Scopes[0].Parallel.Branches[0].EntryEdges = []string{"unknown"} }},
		{"loop scope member", validLoopPlan, func(a *Artifact) { a.Scopes[0].Loop.Members = []string{"unknown"} }},
		{"loop scope edge", validLoopPlan, func(a *Artifact) { a.Scopes[0].Loop.BodyEntryEdges = []string{"unknown"} }},
		{"scope transition", validForkJoinPlan, func(a *Artifact) { artifactEdgeForTest(a, "left").ScopeTransitions = []ArtifactScopeTransition{} }},
		{"state effects", validCallbackPlan, func(a *Artifact) { artifactNodeForTest(a, "notify").StateEffects.Reads = []string{"invented"} }},
		{"length object operand", validCallbackPlan, func(a *Artifact) {
			length := artifactExpressionOpForTest(artifactNodeForTest(a, "notify").Config.SendMessage.Text.Segments[1].Expression, ArtifactOpLength)
			length.Args[0] = objectConst()
		}},
		{"starts_with non-string operand", validLoopPlan, func(a *Artifact) {
			contains := artifactExpressionOpForTest(artifactNodeForTest(a, "route").Config.Switch.Cases[1].Condition, ArtifactOpContains)
			contains.Op = ArtifactOpStartsWith
			contains.Args[1] = objectConst()
		}},
		{"contains string non-string needle", validLoopPlan, func(a *Artifact) {
			contains := artifactExpressionOpForTest(artifactNodeForTest(a, "route").Config.Switch.Cases[1].Condition, ArtifactOpContains)
			contains.Args[1] = numberConst("1")
		}},
		{"contains array item mismatch", arrayPredicatePlan, func(a *Artifact) {
			contains := artifactExpressionOpForTest(artifactNodeForTest(a, "route").Config.Switch.Cases[0].Condition, ArtifactOpContains)
			contains.Args[1] = numberConst("1")
		}},
		{"in array item mismatch", validLoopPlan, func(a *Artifact) {
			condition := artifactNodeForTest(a, "route").Config.Switch.Cases[0].Condition
			*condition = ArtifactExpression{Op: ArtifactOpIn, ResultType: TypeBoolean, Args: []ArtifactExpression{
				numberConst("1"),
				{Op: ArtifactOpConst, ResultType: TypeArray, Value: ArtifactLiteral(`["one","two"]`)},
			}}
		}},
		{"heterogeneous dynamic array", validCallbackPlan, func(a *Artifact) {
			length := artifactExpressionOpForTest(artifactNodeForTest(a, "notify").Config.SendMessage.Text.Segments[1].Expression, ArtifactOpLength)
			length.Args[0] = ArtifactExpression{Op: ArtifactOpArray, ResultType: TypeArray, Args: []ArtifactExpression{stringConst("one"), numberConst("2")}}
		}},
		{"destination-incompatible dynamic array", arrayStateSource, func(a *Artifact) {
			assignment := &artifactNodeForTest(a, "action").Config.SetVariable.Assignments[0].Value
			*assignment = ArtifactExpression{Op: ArtifactOpArray, ResultType: TypeArray, Args: []ArtifactExpression{numberConst("1")}}
		}},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			artifact := compileArtifactForTest(t, test.source)
			test.mutate(artifact)
			digest, err := artifact.computeDigest()
			if err != nil {
				t.Fatal(err)
			}
			artifact.Digest = digest // attacker can always recompute an unkeyed digest.
			wire, err := artifact.DeterministicJSON()
			if err != nil {
				t.Fatal(err)
			}
			if _, err := DecodeArtifact(wire); err == nil {
				t.Fatalf("DecodeArtifact accepted structurally impossible artifact\n%s", wire)
			}
		})
	}
}

func TestDecodeArtifactAcceptsJoinRootLoads(t *testing.T) {
	allSource := joinRootLoadPlan("all")
	anySource := joinRootLoadPlan("any")
	for name, source := range map[string]string{"all": allSource, "any": anySource} {
		t.Run(name, func(t *testing.T) {
			artifact := compileArtifactForTest(t, source)
			wire, err := artifact.DeterministicJSON()
			if err != nil {
				t.Fatal(err)
			}
			if _, err := DecodeArtifact(wire); err != nil {
				t.Fatalf("golden artifact does not satisfy decode boundary: %v", err)
			}
			if _, err := DecodeArtifact(wire); err != nil {
				t.Fatalf("DecodeArtifact() error = %v\n%s", err, wire)
			}
		})
	}
}

func TestCompiledIRGoldenCapabilities(t *testing.T) {
	tests := map[string]string{
		"send_message_callback": validCallbackPlan,
		"send_message_sent": linearActionPlan("", `  - id: action
    type: send_message
    sendMessage:
      target: "#workflow"
      text: "Start the next step"
      completion: sent
      retry: {maxAttempts: 2, backoff: 500ms}`, []string{"completed", "failed"}),
		"set_variable": linearActionPlan("state:\n  count: {type: number, required: true, default: 0}\n", `  - id: action
    type: set_variable
    setVariable:
      assignments:
      - state: count
        value: {expr: 'length(["one", "two"])'}`, []string{"completed", "failed"}),
		"wait": linearActionPlan("", `  - id: action
    type: wait
    wait:
      until: {value: PT1M}
      timeout: PT2M
      retry: {maxAttempts: 2, backoff: 1s}`, []string{"completed", "failed", "timed_out"}),
		"request_approval": linearActionPlan("", `  - id: action
    type: request_approval
    requestApproval:
      assignee: alice
      summary: Approve this run
      retry: {maxAttempts: 2, backoff: 1s}`, []string{"approved", "rejected", "failed"}),
		"request_approval_feedback_scope": requestApprovalFeedbackScopePlan,
		"bounded_loop_first_match":        validLoopPlan,
		"fork_join_all":                   validForkJoinPlan,
		"fork_join_any":                   strings.Replace(strings.Replace(validForkJoinPlan, "mode: all", "mode: any", 1), "branches.left.value", "winner.value.value", 1),
		"switch_all_match":                allMatchJoinPlan(),
		"nested_parallel":                 nestedParallelPlan,
		"parallel_in_loop":                parallelInLoopPlan,
	}
	for name, source := range tests {
		t.Run(name, func(t *testing.T) {
			artifact := compileArtifactForTest(t, source)
			wire, err := artifact.DeterministicJSON()
			if err != nil {
				t.Fatal(err)
			}
			if _, err := DecodeArtifact(wire); err != nil {
				t.Fatalf("golden artifact does not satisfy decode boundary: %v", err)
			}
			var formatted bytes.Buffer
			if err := json.Indent(&formatted, wire, "", "  "); err != nil {
				t.Fatal(err)
			}
			formatted.WriteByte('\n')
			path := filepath.Join("testdata", "ir", name+".json")
			if os.Getenv("UPDATE_PLAN_IR_GOLDEN") == "1" {
				if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
					t.Fatal(err)
				}
				if err := os.WriteFile(path, formatted.Bytes(), 0o644); err != nil {
					t.Fatal(err)
				}
			}
			expected, err := os.ReadFile(path)
			if err != nil {
				t.Fatalf("read golden %s: %v (run UPDATE_PLAN_IR_GOLDEN=1)", path, err)
			}
			if !bytes.Equal(expected, formatted.Bytes()) {
				t.Fatalf("compiled IR differs from %s\n--- expected\n%s\n--- actual\n%s", path, expected, formatted.Bytes())
			}
		})
	}
}

func compileArtifactForTest(t *testing.T, source string) *Artifact {
	t.Helper()
	artifact, diagnostics, err := Compile([]byte(source))
	if err != nil || HasErrors(diagnostics) {
		t.Fatalf("Compile() = (%v, %#v)\n%s", err, diagnostics, source)
	}
	if artifact == nil || !artifact.VerifyDigest() {
		t.Fatalf("invalid artifact %#v", artifact)
	}
	return artifact
}

func artifactNodeForTest(artifact *Artifact, id string) *ArtifactNode {
	for index := range artifact.Nodes {
		if artifact.Nodes[index].ID == id {
			return &artifact.Nodes[index]
		}
	}
	panic("artifact node not found: " + id)
}

func artifactEdgeForTest(artifact *Artifact, id string) *ArtifactEdge {
	for index := range artifact.Edges {
		if artifact.Edges[index].ID == id {
			return &artifact.Edges[index]
		}
	}
	panic("artifact edge not found: " + id)
}

func artifactExpressionOpForTest(expression *ArtifactExpression, op ArtifactExpressionOp) *ArtifactExpression {
	if expression == nil {
		panic("artifact expression is nil")
	}
	if expression.Op == op {
		return expression
	}
	for index := range expression.Args {
		if found := artifactExpressionOpForTestOrNil(&expression.Args[index], op); found != nil {
			return found
		}
	}
	panic("artifact expression opcode not found: " + op)
}

func artifactExpressionOpForTestOrNil(expression *ArtifactExpression, op ArtifactExpressionOp) *ArtifactExpression {
	if expression == nil {
		return nil
	}
	if expression.Op == op {
		return expression
	}
	for index := range expression.Args {
		if found := artifactExpressionOpForTestOrNil(&expression.Args[index], op); found != nil {
			return found
		}
	}
	return nil
}

func assertSameSemanticDigest(t *testing.T, left, right *Artifact, reason string) {
	t.Helper()
	if left.Digest != right.Digest {
		t.Fatalf("%s changed semantic digest: %s != %s", reason, left.Digest, right.Digest)
	}
}

func reverseNodes(values []Node) {
	for left, right := 0, len(values)-1; left < right; left, right = left+1, right-1 {
		values[left], values[right] = values[right], values[left]
	}
}

func reverseEdges(values []Edge) {
	for left, right := 0, len(values)-1; left < right; left, right = left+1, right-1 {
		values[left], values[right] = values[right], values[left]
	}
}

func (artifact *Artifact) ConfigTextForTest(nodeID, suffix string) {
	for i := range artifact.Nodes {
		if artifact.Nodes[i].ID != nodeID || artifact.Nodes[i].Config.SendMessage == nil {
			continue
		}
		segments := artifact.Nodes[i].Config.SendMessage.Text.Segments
		for j := range segments {
			if segments[j].Kind == ArtifactTemplateSegmentText {
				segments[j].Text += suffix
				return
			}
		}
	}
}

func joinRootLoadPlan(mode string) string {
	ref := "branches"
	if mode == "any" {
		ref = "winner.value"
	}
	plan := strings.Replace(validForkJoinPlan, "mode: all", "mode: "+mode, 1)
	plan = strings.Replace(plan, "output:\n  value: {type: string, required: true}", "output:\n  whole: {type: object, required: true}", 1)
	plan = strings.Replace(plan, `      result:
        value: {type: string, required: true}
      merge:
        value: {ref: branches.left.value}`, `      result:
        whole: {type: object, required: true}
      merge:
        whole: {ref: `+ref+`}`, 1)
	plan = strings.Replace(plan, `    bindings:
      value: {ref: output.value}`, `    bindings:
      whole: {ref: output.whole}`, 1)
	return plan
}

const arrayPredicatePlan = `apiVersion: loop.plan/v1
kind: PlanVersion
input:
  tags:
    type: array
    required: true
    items: {type: string}
graph:
  entry:
    nodeId: route
    port: in
    input:
      tags: {ref: plan.input.tags}
  nodes:
  - id: route
    type: switch
    input:
      tags:
        type: array
        required: true
        items: {type: string}
    switch:
      mode: first_match
      cases:
      - port: matched
        when: 'contains(input.tags, "vip") == true'
      - port: fallback
        isDefault: true
  - id: success
    type: end
    end: {outcome: succeeded}
  - id: failure
    type: end
    end: {outcome: failed, code: no_match}
  edges:
  - id: matched
    source: {nodeId: route, port: matched}
    target: {nodeId: success, port: in}
  - id: fallback
    source: {nodeId: route, port: fallback}
    target: {nodeId: failure, port: in}`

const nestedParallelPlan = `apiVersion: loop.plan/v1
kind: PlanVersion
input:
  value: {type: string, required: true}
output:
  value: {type: string, required: true}
graph:
  entry:
    nodeId: outer
    port: in
    input:
      value: {ref: plan.input.value}
  nodes:
  - id: outer
    type: fork
    input:
      value: {type: string, required: true}
    fork:
      branches: [left, right]
  - id: inner
    type: fork
    input:
      value: {type: string, required: true}
    fork:
      branches: [a, b]
  - id: inner_join
    type: join
    input:
      value: {type: string, required: true}
    join:
      forkId: inner
      mode: all
      result:
        value: {type: string, required: true}
      merge:
        value: {ref: branches.a.value}
  - id: outer_join
    type: join
    input:
      value: {type: string, required: true}
    join:
      forkId: outer
      mode: all
      result:
        value: {type: string, required: true}
      merge:
        value: {ref: branches.left.value}
  - id: success
    type: end
    end: {outcome: succeeded}
  - id: failure
    type: end
    end: {outcome: failed, code: parallel_failed}
  edges:
  - id: outer_left
    source: {nodeId: outer, port: left}
    target: {nodeId: inner, port: in}
    bindings:
      value: {ref: input.value}
  - id: outer_right
    source: {nodeId: outer, port: right}
    target: {nodeId: outer_join, port: right}
    bindings:
      value: {ref: input.value}
  - id: inner_a
    source: {nodeId: inner, port: a}
    target: {nodeId: inner_join, port: a}
    bindings:
      value: {ref: input.value}
  - id: inner_b
    source: {nodeId: inner, port: b}
    target: {nodeId: inner_join, port: b}
    bindings:
      value: {ref: input.value}
  - id: inner_completed
    source: {nodeId: inner_join, port: completed}
    target: {nodeId: outer_join, port: left}
    bindings:
      value: {ref: output.value}
  - id: inner_failed
    source: {nodeId: inner_join, port: failed}
    target: {nodeId: failure, port: in}
  - id: outer_completed
    source: {nodeId: outer_join, port: completed}
    target: {nodeId: success, port: in}
    bindings:
      value: {ref: output.value}
  - id: outer_failed
    source: {nodeId: outer_join, port: failed}
    target: {nodeId: failure, port: in}
`

const parallelInLoopPlan = `apiVersion: loop.plan/v1
kind: PlanVersion
input:
  value: {type: string, required: true}
output:
  value: {type: string, required: true}
graph:
  entry:
    nodeId: repeat
    port: enter
    input:
      value: {ref: plan.input.value}
  nodes:
  - id: repeat
    type: loop
    loop:
      mode: counted
      iterations: 3
      state:
        value: {type: string, required: true}
  - id: fanout
    type: fork
    input:
      value: {type: string, required: true}
    fork:
      branches: [left, right]
  - id: gather
    type: join
    input:
      value: {type: string, required: true}
    join:
      forkId: fanout
      mode: all
      result:
        value: {type: string, required: true}
      merge:
        value: {ref: branches.left.value}
  - id: route
    type: switch
    input:
      value: {type: string, required: true}
    switch:
      mode: first_match
      cases:
      - port: again
        when: 'input.value == "again"'
      - port: stop
        isDefault: true
  - id: success
    type: end
    end: {outcome: succeeded}
  - id: failure
    type: end
    end: {outcome: failed, code: loop_failed}
  edges:
  - id: body
    source: {nodeId: repeat, port: body}
    target: {nodeId: fanout, port: in}
    bindings:
      value: {ref: loop.state.value}
  - id: left
    source: {nodeId: fanout, port: left}
    target: {nodeId: gather, port: left}
    bindings:
      value: {ref: input.value}
  - id: right
    source: {nodeId: fanout, port: right}
    target: {nodeId: gather, port: right}
    bindings:
      value: {ref: input.value}
  - id: joined
    source: {nodeId: gather, port: completed}
    target: {nodeId: route, port: in}
    bindings:
      value: {ref: output.value}
  - id: join_failed
    source: {nodeId: gather, port: failed}
    target: {nodeId: repeat, port: break}
    bindings:
      value: {ref: input.value}
  - id: again
    source: {nodeId: route, port: again}
    target: {nodeId: repeat, port: continue}
    bindings:
      value: {ref: input.value}
  - id: stop
    source: {nodeId: route, port: stop}
    target: {nodeId: repeat, port: break}
    bindings:
      value: {ref: input.value}
  - id: done
    source: {nodeId: repeat, port: done}
    target: {nodeId: success, port: in}
    bindings:
      value: {ref: loop.state.value}
  - id: loop_failed
    source: {nodeId: repeat, port: failed}
    target: {nodeId: failure, port: in}`
