package plan

import (
	"strings"
	"testing"
)

func TestCompileRejectsNonNormalizedSchemaDefaultLiteralsAsDiagnostics(t *testing.T) {
	tests := []struct {
		name          string
		schema        string
		code          string
		path          string
		preservedCode string
	}{
		{
			name:          "unquoted timestamp",
			schema:        "  value: {type: datetime, default: 2026-07-20T10:00:00Z}\n",
			code:          "literal.unsupported_type",
			path:          "input.value.default",
			preservedCode: "schema.default_type",
		},
		{
			name:          "nested non-finite number",
			schema:        "  value:\n    type: array\n    items: {type: number}\n    default: [1, .nan]\n",
			code:          "literal.non_finite_number",
			path:          "input.value.default[1]",
			preservedCode: "schema.default_type",
		},
		{
			name:   "nested non-string-key map",
			schema: "  value:\n    type: object\n    default:\n      valid: ok\n      nested: {1: invalid}\n",
			code:   "literal.unsupported_type",
			path:   "input.value.default.nested",
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			raw := "apiVersion: loop.plan/v1\nkind: PlanVersion\ninput:\n" + test.schema + literalOnlyEndGraph
			artifact, diagnostics, err := Compile([]byte(raw))
			if err != nil {
				t.Fatalf("Compile() error = %v; author mistakes must be diagnostics", err)
			}
			if artifact != nil {
				t.Fatalf("Compile() artifact = %#v, want nil", artifact)
			}
			diagnostic, ok := findCode(diagnostics, test.code)
			if !ok {
				t.Fatalf("Compile() diagnostics = %#v, want %s", diagnostics, test.code)
			}
			if diagnostic.Path != test.path {
				t.Fatalf("%s path = %q, want %q", test.code, diagnostic.Path, test.path)
			}
			if diagnostic.Line == 0 || diagnostic.Column == 0 {
				t.Fatalf("diagnostic lacks source position: %#v", diagnostic)
			}
			if test.preservedCode != "" && !hasCode(diagnostics, test.preservedCode) {
				t.Fatalf("Compile() diagnostics = %#v, want preserved %s", diagnostics, test.preservedCode)
			}
		})
	}
}

func TestCompileRejectsNonNormalizedValueExprLiteralsBeforeLowering(t *testing.T) {
	tests := []struct {
		name   string
		raw    string
		code   string
		path   string
		nodeID string
		edgeID string
	}{
		{
			name:   "entry timestamp",
			raw:    literalEntryPlan("{stamp: 2026-07-20T10:00:00Z}"),
			code:   "literal.unsupported_type",
			path:   "graph.entry.input.payload.value.stamp",
			nodeID: "$entry",
		},
		{
			name:   "edge nan",
			raw:    literalEdgePlan("{ratio: .nan}"),
			code:   "literal.non_finite_number",
			path:   "graph.edges[0].bindings.payload.value.ratio",
			nodeID: "pause",
			edgeID: "completed",
		},
		{
			name:   "set variable nested map",
			raw:    literalSetVariablePlan("{nested: {1: invalid}}"),
			code:   "literal.unsupported_type",
			path:   "graph.nodes[0].setVariable.assignments[0].value.value.nested",
			nodeID: "action",
		},
		{
			name:   "wait nan",
			raw:    literalWaitPlan(".nan"),
			code:   "literal.non_finite_number",
			path:   "graph.nodes[0].wait.until.value",
			nodeID: "pause",
		},
		{
			name: "join merge timestamp",
			raw: strings.Replace(
				validForkJoinPlan,
				"value: {ref: branches.left.value}",
				"value: {value: 2026-07-20T10:00:00Z}",
				1,
			),
			code:   "literal.unsupported_type",
			path:   "graph.nodes[1].join.merge.value.value",
			nodeID: "gather",
		},
	}

	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; author mistakes must be diagnostics", err)
			}
			if artifact != nil {
				t.Fatalf("Compile() artifact = %#v, want nil", artifact)
			}
			diagnostic, ok := findCode(diagnostics, test.code)
			if !ok {
				t.Fatalf("Compile() diagnostics = %#v, want %s", diagnostics, test.code)
			}
			if diagnostic.Path != test.path || diagnostic.NodeID != test.nodeID || diagnostic.EdgeID != test.edgeID {
				t.Fatalf("diagnostic identity = %#v, want path=%q node=%q edge=%q", diagnostic, test.path, test.nodeID, test.edgeID)
			}
			if diagnostic.Line == 0 || diagnostic.Column == 0 {
				t.Fatalf("diagnostic lacks source position: %#v", diagnostic)
			}
		})
	}
}

func TestCompileAcceptsNestedNormalizedJSONLiterals(t *testing.T) {
	state := `state:
  settings:
    type: object
    default:
      retries: 3
      labels: [fast, null]
      nested: {enabled: true}
  history:
    type: array
    items: {type: object}
    default:
    - name: first
      values: [1, 2.5, null, false]
`
	raw := strings.Replace(
		literalEdgePlan(`{items: [ok, 7, null, true], metadata: {startedAt: "2026-07-20T10:00:00Z"}}`),
		"output:\n",
		state+"output:\n",
		1,
	)

	artifact, diagnostics, err := Compile([]byte(raw))
	if err != nil || HasErrors(diagnostics) {
		t.Fatalf("Compile() = (%v, %#v)", err, diagnostics)
	}
	if artifact == nil || !artifact.VerifyDigest() {
		t.Fatalf("Compile() artifact/digest invalid: %#v", artifact)
	}
}

const literalOnlyEndGraph = `graph:
  entry: {nodeId: done, port: in}
  nodes:
  - id: done
    type: end
    end: {outcome: succeeded}
  edges: []`

func literalEntryPlan(value string) string {
	return `apiVersion: loop.plan/v1
kind: PlanVersion
output:
  payload: {type: object, required: true}
graph:
  entry:
    nodeId: done
    port: in
    input:
      payload: {value: ` + value + `}
  nodes:
  - id: done
    type: end
    end: {outcome: succeeded}
  edges: []`
}

func literalEdgePlan(value string) string {
	return `apiVersion: loop.plan/v1
kind: PlanVersion
output:
  payload: {type: object, required: true}
graph:
  entry: {nodeId: pause, port: in}
  nodes:
  - id: pause
    type: wait
    wait:
      until: {value: PT1S}
      timeout: PT1M
      retry: {maxAttempts: 1, backoff: 1s}
  - id: done
    type: end
    end: {outcome: succeeded}
  - id: failure
    type: end
    end: {outcome: failed, code: wait_failed}
  edges:
  - id: completed
    source: {nodeId: pause, port: completed}
    target: {nodeId: done, port: in}
    bindings:
      payload: {value: ` + value + `}
  - id: failed
    source: {nodeId: pause, port: failed}
    target: {nodeId: failure, port: in}
  - id: timed_out
    source: {nodeId: pause, port: timed_out}
    target: {nodeId: failure, port: in}`
}

func literalSetVariablePlan(value string) string {
	return `apiVersion: loop.plan/v1
kind: PlanVersion
state:
  payload: {type: object, required: true}
graph:
  entry: {nodeId: action, port: in}
  nodes:
  - id: action
    type: set_variable
    setVariable:
      assignments:
      - state: payload
        value: {value: ` + value + `}
  - id: done
    type: end
    end: {outcome: succeeded}
  - id: failure
    type: end
    end: {outcome: failed, code: assignment_failed}
  edges:
  - id: completed
    source: {nodeId: action, port: completed}
    target: {nodeId: done, port: in}
  - id: failed
    source: {nodeId: action, port: failed}
    target: {nodeId: failure, port: in}`
}

func literalWaitPlan(value string) string {
	return `apiVersion: loop.plan/v1
kind: PlanVersion
graph:
  entry: {nodeId: pause, port: in}
  nodes:
  - id: pause
    type: wait
    wait:
      until: {value: ` + value + `}
      timeout: PT1M
      retry: {maxAttempts: 1, backoff: 1s}
  - id: done
    type: end
    end: {outcome: succeeded}
  - id: failure
    type: end
    end: {outcome: failed, code: wait_failed}
  edges:
  - id: completed
    source: {nodeId: pause, port: completed}
    target: {nodeId: done, port: in}
  - id: failed
    source: {nodeId: pause, port: failed}
    target: {nodeId: failure, port: in}
  - id: timed_out
    source: {nodeId: pause, port: timed_out}
    target: {nodeId: failure, port: in}`
}
