package plan

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

func TestArtifactLowersSortedStateReadsAndWritesFromEverySite(t *testing.T) {
	predicate := Predicate(`state.g == "yes"`)
	document := &Document{
		APIVersion: APIVersion,
		Kind:       Kind,
		State: map[string]Schema{
			"a": {Type: TypeString}, "b": {Type: TypeString}, "c": {Type: TypeString},
			"d": {Type: TypeDuration}, "e": {Type: TypeString}, "f": {Type: TypeString},
			"g": {Type: TypeString}, "h": {Type: TypeString}, "i": {Type: TypeString},
			"j": {Type: TypeString}, "x": {Type: TypeString},
		},
		Graph: Graph{
			Entry: Entry{NodeID: "send", Port: "in", Input: map[string]ValueExpr{
				"entry": {Ref: "state.j"},
			}},
			Nodes: []Node{
				{ID: "send", Type: NodeSendMessage, SendMessage: &SendMessageConfig{
					Target: `{{ state.a }}`, Text: `{{ state.b }} {{ "state.fake" }}`,
					Completion: CompletionSent, Retry: &RetryPolicy{MaxAttempts: 1, Backoff: "1s"},
				}},
				{ID: "set", Type: NodeSetVariable, SetVariable: &SetVariableConfig{Assignments: []StateAssignment{
					{State: "x", Value: ValueExpr{Expr: `state.c`}},
				}}},
				{ID: "wait", Type: NodeWait, Wait: &WaitConfig{Until: ValueExpr{Ref: "state.d"}, Timeout: "1s", Retry: &RetryPolicy{MaxAttempts: 1, Backoff: "1s"}}},
				{ID: "approval", Type: NodeRequestApproval, RequestApproval: &RequestApprovalConfig{
					Assignee: `{{ state.e }}`, Summary: `{{ state.f }}`, Retry: &RetryPolicy{MaxAttempts: 1, Backoff: "1s"},
				}},
				{ID: "switch", Type: NodeSwitch, Switch: &SwitchConfig{Mode: SwitchFirstMatch, Cases: []SwitchCase{{When: &predicate}}}},
				{ID: "owner", Type: NodeFork, Fork: &ForkConfig{Branches: []string{"left", "right"}}},
				{ID: "join", Type: NodeJoin, Join: &JoinConfig{ForkID: "owner", Mode: JoinAll, Merge: map[string]ValueExpr{
					"merged": {Expr: `state.h`},
				}}},
			},
			Edges: []Edge{{
				ID: "mapped", Source: Endpoint{NodeID: "send", Port: "completed"},
				Target:   Endpoint{NodeID: "set", Port: "in"},
				Bindings: map[string]ValueExpr{"mapped": {Ref: "state.i"}},
			}},
		},
	}
	artifact, err := buildArtifact(document, compilerAnalysis{})
	if err != nil {
		t.Fatalf("buildArtifact() error = %v", err)
	}
	assertArtifactEffects(t, artifact, "send", []string{"a", "b", "i", "j"}, []string{})
	assertArtifactEffects(t, artifact, "set", []string{"c"}, []string{"x"})
	assertArtifactEffects(t, artifact, "wait", []string{"d"}, []string{})
	assertArtifactEffects(t, artifact, "approval", []string{"e", "f"}, []string{})
	assertArtifactEffects(t, artifact, "switch", []string{"g"}, []string{})
	assertArtifactEffects(t, artifact, "join", []string{"h"}, []string{})
	if artifact.CompilerVersion != CompilerVersion {
		t.Fatalf("compilerVersion = %q, want %q", artifact.CompilerVersion, CompilerVersion)
	}
	if !artifact.VerifyDigest() {
		t.Fatal("artifact digest should include the compiler-owned representation")
	}
	artifact.CompilerVersion += ".changed"
	if !artifact.VerifyDigest() {
		t.Fatal("compiler provenance is intentionally excluded from semantic digest")
	}
}

func TestCompileRejectsParallelWriteReadConflict(t *testing.T) {
	raw := strings.Replace(parallelWriteConflictPlan, `  - id: right_writer
    type: set_variable
    setVariable:
      assignments:
      - state: shared
        value: {ref: plan.input.value}`, `  - id: right_writer
    type: switch
    switch:
      mode: first_match
      cases:
      - port: completed
        when: 'state.shared == "written"'
      - port: failed
        isDefault: true`, 1)
	_, diagnostics, err := Compile([]byte(raw))
	if err != nil {
		t.Fatalf("Compile() error = %v", err)
	}
	diagnostic, ok := findCode(diagnostics, "parallel.state_access_conflict")
	if !ok {
		t.Fatalf("diagnostics = %#v, want parallel.state_access_conflict", diagnostics)
	}
	if !strings.Contains(diagnostic.Message, `state "shared"`) {
		t.Fatalf("diagnostic = %#v", diagnostic)
	}
}

func TestParallelConflictIncludesOwnerBranchEdgeBindings(t *testing.T) {
	raw := strings.Replace(parallelWriteConflictPlan, `  - id: right_writer
    type: set_variable
    setVariable:
      assignments:
      - state: shared
        value: {ref: plan.input.value}`, `  - id: right_writer
    type: switch
    input:
      value: {type: string, required: true}
    switch:
      mode: first_match
      cases:
      - port: completed
        when: 'input.value == "written"'
      - port: failed
        isDefault: true`, 1)
	raw = strings.Replace(raw, `  - id: start_right
    source: {nodeId: fanout, port: right}
    target: {nodeId: right_writer, port: in}`, `  - id: start_right
    source: {nodeId: fanout, port: right}
    target: {nodeId: right_writer, port: in}
    bindings:
      value: {ref: state.shared}`, 1)
	_, diagnostics, err := Compile([]byte(raw))
	if err != nil || !hasCode(diagnostics, "parallel.state_access_conflict") {
		t.Fatalf("Compile() = (%v, %#v), want parallel.state_access_conflict", err, diagnostics)
	}
}

func assertArtifactEffects(t *testing.T, artifact *Artifact, nodeID string, reads, writes []string) {
	t.Helper()
	node := artifactNodeByID(artifact, nodeID)
	if node == nil {
		t.Fatalf("artifact has no node %q", nodeID)
	}
	if !reflect.DeepEqual(node.StateEffects.Reads, reads) || !reflect.DeepEqual(node.StateEffects.Writes, writes) {
		t.Fatalf("%s effects = %#v, want reads=%v writes=%v", nodeID, node.StateEffects, reads, writes)
	}
}
