package plan

import (
	"strings"
	"testing"
)

const countedLoopPlan = `apiVersion: loop.plan/v1
kind: PlanVersion
input:
  answer: {type: string, required: true}
output:
  answer: {type: string, required: true}
  iteration: {type: number, required: true}
  last: {type: boolean, required: true}
graph:
  entry:
    nodeId: repeat
    port: enter
    input:
      answer: {ref: plan.input.answer}
      iteration: {value: 0}
      last: {value: false}
  nodes:
  - id: repeat
    type: loop
    loop:
      mode: counted
      iterations: 3
      state:
        answer: {type: string, required: true}
        iteration: {type: number, required: true}
        last: {type: boolean, required: 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: repeat, port: continue}
    bindings:
      answer: {ref: loop.state.answer}
      iteration: {ref: loop.iteration}
      last: {ref: loop.isLast}
  - id: done
    source: {nodeId: repeat, port: done}
    target: {nodeId: success, port: in}
  - id: failed
    source: {nodeId: repeat, port: failed}
    target: {nodeId: failure, port: in}
`

func TestCompileCountedLoopPublishesExactCountAndFinalStateProjection(t *testing.T) {
	artifact, diagnostics, err := Compile([]byte(countedLoopPlan))
	if err != nil || HasErrors(diagnostics) {
		t.Fatalf("Compile() = (%v, %#v)", err, diagnostics)
	}
	if artifact.ArtifactVersion != ArtifactVersion || artifact.APIVersion != APIVersion || artifact.CompilerVersion != CompilerVersion {
		t.Fatalf("envelope = %#v", artifact)
	}

	var loopNode *ArtifactNode
	for index := range artifact.Nodes {
		if artifact.Nodes[index].ID == "repeat" {
			loopNode = &artifact.Nodes[index]
			break
		}
	}
	if loopNode == nil || loopNode.Config.Loop == nil {
		t.Fatalf("counted loop node missing: %#v", artifact.Nodes)
	}
	loop := loopNode.Config.Loop
	if loop.Mode != LoopCounted || loop.Iterations != 3 {
		t.Fatalf("counted loop config = %#v", loop)
	}
	if len(artifact.Scopes) != 1 || len(artifact.Scopes[0].Loop.BreakEdges) != 0 {
		t.Fatalf("counted loop break must be optional: %#v", artifact.Scopes)
	}

	body := countedArtifactEdgeByID(t, artifact, "body")
	loads := map[string]ArtifactLoadSpace{}
	for _, binding := range body.Bindings {
		if binding.Value.Load != nil {
			loads[binding.Target] = binding.Value.Load.Space
		}
	}
	if loads["iteration"] != ArtifactLoadLoopIteration || loads["last"] != ArtifactLoadLoopIsLast {
		t.Fatalf("counted metadata loads = %#v", loads)
	}

	wire, err := artifact.DeterministicJSON()
	if err != nil {
		t.Fatal(err)
	}
	decoded, err := DecodeArtifact(wire)
	if err != nil {
		t.Fatalf("DecodeArtifact() error = %v", err)
	}
	if decoded.Digest != artifact.Digest {
		t.Fatalf("digest changed across round trip: %s != %s", decoded.Digest, artifact.Digest)
	}
}

func TestCompileRejectsLegacyOrInvalidLoopFields(t *testing.T) {
	tests := []struct {
		name string
		raw  string
		code string
	}{
		{
			name: "legacy maxIterations",
			raw:  strings.Replace(countedLoopPlan, "      iterations: 3", "      iterations: 3\n      maxIterations: 3", 1),
			code: "syntax.unknown_field",
		},
		{
			name: "legacy result",
			raw:  strings.Replace(countedLoopPlan, "      state:\n", "      result:\n        answer: {type: string, required: true}\n      state:\n", 1),
			code: "syntax.unknown_field",
		},
		{
			name: "legacy while mode",
			raw:  strings.Replace(countedLoopPlan, "      mode: counted", "      mode: while", 1),
			code: "loop.invalid_mode",
		},
		{
			name: "mode required",
			raw:  strings.Replace(countedLoopPlan, "      mode: counted\n", "", 1),
			code: "loop.invalid_mode",
		},
		{
			name: "iterations required",
			raw:  strings.Replace(countedLoopPlan, "      iterations: 3", "      iterations: 0", 1),
			code: "loop.invalid_iterations",
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			artifact, diagnostics, err := Compile([]byte(test.raw))
			if err != nil {
				t.Fatalf("Compile() error = %v", err)
			}
			if artifact != nil || !hasCode(diagnostics, test.code) {
				t.Fatalf("Compile() = (%#v, %#v), want %s", artifact, diagnostics, test.code)
			}
		})
	}
}

func countedArtifactEdgeByID(t *testing.T, artifact *Artifact, id string) *ArtifactEdge {
	t.Helper()
	for index := range artifact.Edges {
		if artifact.Edges[index].ID == id {
			return &artifact.Edges[index]
		}
	}
	t.Fatalf("artifact edge %q missing", id)
	return nil
}
