package plan

import (
	"strings"
	"testing"
)

func TestCompileAcceptsProperlyNestedCountedLoops(t *testing.T) {
	_, diagnostics, err := Compile([]byte(validNestedLoopPlan))
	if err != nil || HasErrors(diagnostics) {
		t.Fatalf("Compile() = (%v, %#v)", err, diagnostics)
	}
}

func TestCompileRejectsCrossLoopBoundary(t *testing.T) {
	raw := strings.Replace(validNestedLoopPlan,
		"target: {nodeId: inner, port: continue}",
		"target: {nodeId: outer, port: continue}", 1)
	_, diagnostics, err := Compile([]byte(raw))
	if err != nil {
		t.Fatalf("Compile() error = %v", err)
	}
	if !hasCode(diagnostics, "loop.cross_loop_boundary") {
		t.Fatalf("diagnostics = %#v, want loop.cross_loop_boundary", diagnostics)
	}
}

func TestCompileRejectsOutsideLoopBoundaryBypass(t *testing.T) {
	raw := strings.Replace(validLoopPlan,
		"    nodeId: repeat\n    port: enter",
		"    nodeId: starter\n    port: in", 1)
	raw = strings.Replace(raw, "  nodes:\n  - id: repeat", `  nodes:
  - id: starter
    type: switch
    input:
      answer: {type: string, required: true}
    switch:
      mode: first_match
      cases:
      - port: enter
        when: 'input.answer == "enter"'
      - port: bypass
        isDefault: true
  - id: repeat`, 1)
	raw = strings.Replace(raw, "  edges:\n  - id: body", `  edges:
  - id: enter_loop
    source: {nodeId: starter, port: enter}
    target: {nodeId: repeat, port: enter}
    bindings:
      answer: {ref: input.answer}
  - id: bypass_body
    source: {nodeId: starter, port: bypass}
    target: {nodeId: repeat, port: continue}
    bindings:
      answer: {ref: input.answer}
  - id: body`, 1)
	_, diagnostics, err := Compile([]byte(raw))
	if err != nil {
		t.Fatalf("Compile() error = %v", err)
	}
	if !hasCode(diagnostics, "loop.outside_boundary") {
		t.Fatalf("diagnostics = %#v, want loop.outside_boundary", diagnostics)
	}
}

func TestCompileRejectsLoopBodyEscape(t *testing.T) {
	raw := strings.Replace(validLoopPlan,
		"target: {nodeId: repeat, port: continue}",
		"target: {nodeId: failure, port: in}", 1)
	_, diagnostics, err := Compile([]byte(raw))
	if err != nil {
		t.Fatalf("Compile() error = %v", err)
	}
	if !hasCode(diagnostics, "loop.open_body") {
		t.Fatalf("diagnostics = %#v, want loop.open_body", diagnostics)
	}
}

func TestCompileRejectsLoopEntryAtContinueOrBreak(t *testing.T) {
	for _, port := range []string{"continue", "break"} {
		t.Run(port, func(t *testing.T) {
			raw := strings.Replace(validLoopPlan, "port: enter", "port: "+port, 1)
			_, diagnostics, err := Compile([]byte(raw))
			if err != nil {
				t.Fatalf("Compile() error = %v", err)
			}
			if !hasCode(diagnostics, "loop.invalid_entry_port") {
				t.Fatalf("diagnostics = %#v, want loop.invalid_entry_port", diagnostics)
			}
		})
	}
}

const validNestedLoopPlan = `apiVersion: loop.plan/v1
kind: PlanVersion
input:
  answer: {type: string, required: true}
output:
  answer: {type: string, required: true}
graph:
  entry:
    nodeId: outer
    port: enter
    input:
      answer: {ref: plan.input.answer}
  nodes:
  - id: outer
    type: loop
    loop:
      mode: counted
      iterations: 3
      state:
        answer: {type: string, required: true}
  - id: inner
    type: loop
    loop:
      mode: counted
      iterations: 2
      state:
        answer: {type: string, required: true}
  - id: inner_route
    type: switch
    input:
      answer: {type: string, required: true}
    switch:
      mode: first_match
      cases:
      - port: again
        when: 'input.answer == "inner"'
      - port: stop
        isDefault: true
  - id: outer_route
    type: switch
    input:
      answer: {type: string, required: true}
    switch:
      mode: first_match
      cases:
      - port: again
        when: 'input.answer == "outer"'
      - port: stop
        isDefault: true
  - id: success
    type: end
    end: {outcome: succeeded}
  - id: failure
    type: end
    end: {outcome: failed, code: nested_loop_failed}
  edges:
  - id: outer_body
    source: {nodeId: outer, port: body}
    target: {nodeId: inner, port: enter}
    bindings:
      answer: {ref: loop.state.answer}
  - id: inner_body
    source: {nodeId: inner, port: body}
    target: {nodeId: inner_route, port: in}
    bindings:
      answer: {ref: loop.state.answer}
  - id: inner_again
    source: {nodeId: inner_route, port: again}
    target: {nodeId: inner, port: continue}
    bindings:
      answer: {ref: input.answer}
  - id: inner_stop
    source: {nodeId: inner_route, port: stop}
    target: {nodeId: inner, port: break}
    bindings:
      answer: {ref: input.answer}
  - id: inner_done
    source: {nodeId: inner, port: done}
    target: {nodeId: outer_route, port: in}
    bindings:
      answer: {ref: loop.state.answer}
  - id: inner_failed
    source: {nodeId: inner, port: failed}
    target: {nodeId: outer, port: break}
    bindings:
      answer: {ref: plan.input.answer}
  - id: outer_again
    source: {nodeId: outer_route, port: again}
    target: {nodeId: outer, port: continue}
    bindings:
      answer: {ref: input.answer}
  - id: outer_stop
    source: {nodeId: outer_route, port: stop}
    target: {nodeId: outer, port: break}
    bindings:
      answer: {ref: input.answer}
  - id: outer_done
    source: {nodeId: outer, port: done}
    target: {nodeId: success, port: in}
    bindings:
      answer: {ref: loop.state.answer}
  - id: outer_failed
    source: {nodeId: outer, port: failed}
    target: {nodeId: failure, port: in}`
