package plan

import (
	"reflect"
	"strings"
	"testing"
)

func TestJoinMergeLowersExplicitBranchAndWinnerReferences(t *testing.T) {
	for _, test := range []struct {
		mode string
		ref  string
	}{
		{mode: "all", ref: "branches.left.value"},
		{mode: "any", ref: "winner.value.value"},
	} {
		t.Run(test.mode, func(t *testing.T) {
			raw := strings.Replace(validForkJoinPlan, "mode: all", "mode: "+test.mode, 1)
			raw = strings.Replace(raw, "branches.left.value", test.ref, 1)
			artifact, diagnostics, err := Compile([]byte(raw))
			if err != nil || HasErrors(diagnostics) {
				t.Fatalf("Compile() = (%v, %#v)", err, diagnostics)
			}
			join := artifactNodeByID(artifact, "gather")
			if join == nil || join.Config.Join == nil || len(join.Config.Join.Merge) != 1 || join.Config.Join.Merge[0].Target != "value" || join.Config.Join.Merge[0].Value.Load == nil {
				t.Fatalf("join merge = %#v, want %q", join, test.ref)
			}
			load := join.Config.Join.Merge[0].Value.Load
			if test.mode == "all" && (load.Space != ArtifactLoadJoinBranch || load.Branch != "left" || !reflect.DeepEqual(load.Path, []string{"value"})) {
				t.Fatalf("join merge load = %#v", load)
			}
			if test.mode == "any" && (load.Space != ArtifactLoadJoinWinnerValue || !reflect.DeepEqual(load.Path, []string{"value"})) {
				t.Fatalf("join merge load = %#v", load)
			}
		})
	}
}

func TestJoinMergeRejectsModeForeignAndSelfOutputReferences(t *testing.T) {
	tests := []struct {
		name string
		raw  string
	}{
		{
			name: "all cannot read winner",
			raw:  strings.Replace(validForkJoinPlan, "branches.left.value", "winner.value.value", 1),
		},
		{
			name: "any cannot read branches",
			raw:  strings.Replace(validForkJoinPlan, "mode: all", "mode: any", 1),
		},
		{
			name: "merge cannot read its result",
			raw:  strings.Replace(validForkJoinPlan, "branches.left.value", "output.value", 1),
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			_, diagnostics, err := Compile([]byte(test.raw))
			if err != nil {
				t.Fatalf("Compile() error = %v", err)
			}
			if !hasCode(diagnostics, "expression.unknown_reference") {
				t.Fatalf("diagnostics = %#v, want expression.unknown_reference", diagnostics)
			}
		})
	}
}

func TestJoinMergeRequiresEveryRequiredResult(t *testing.T) {
	raw := strings.Replace(validForkJoinPlan,
		"      merge:\n        value: {ref: branches.left.value}",
		"      merge: {}", 1)
	_, diagnostics, err := Compile([]byte(raw))
	if err != nil {
		t.Fatalf("Compile() error = %v", err)
	}
	if !hasCode(diagnostics, "binding.required_target") {
		t.Fatalf("diagnostics = %#v, want binding.required_target", diagnostics)
	}
}

func TestEdgeReferencesAreSourcePortAware(t *testing.T) {
	tests := []struct {
		name string
		raw  string
	}{
		{
			name: "callback failure has no output",
			raw: strings.Replace(validCallbackPlan,
				"  - id: delivery_failed\n    source: {nodeId: notify, port: failed}\n    target: {nodeId: failure, port: in}",
				"  - id: delivery_failed\n    source: {nodeId: notify, port: failed}\n    target: {nodeId: failure, port: in}\n    bindings:\n      bogus: {ref: output.reply}", 1),
		},
		{
			name: "join failure has no result output",
			raw: strings.Replace(validForkJoinPlan,
				"  - id: join_failed\n    source: {nodeId: gather, port: failed}\n    target: {nodeId: failure, port: in}",
				"  - id: join_failed\n    source: {nodeId: gather, port: failed}\n    target: {nodeId: failure, port: in}\n    bindings:\n      bogus: {ref: output.value}", 1),
		},
		{
			name: "loop failure has no loop state",
			raw: strings.Replace(validLoopPlan,
				"target: {nodeId: failure, port: in}",
				"target: {nodeId: failure, port: in}\n    bindings:\n      bogus: {ref: loop.state.answer}", 1),
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			_, diagnostics, err := Compile([]byte(test.raw))
			if err != nil {
				t.Fatalf("Compile() error = %v", err)
			}
			if !hasCode(diagnostics, "expression.unknown_reference") {
				t.Fatalf("diagnostics = %#v, want expression.unknown_reference", diagnostics)
			}
		})
	}
}

func artifactNodeByID(artifact *Artifact, nodeID string) *ArtifactNode {
	if artifact == nil {
		return nil
	}
	for i := range artifact.Nodes {
		if artifact.Nodes[i].ID == nodeID {
			return &artifact.Nodes[i]
		}
	}
	return nil
}
