package dsl

import (
	"encoding/json"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"
)

func validDefinitionSpec() DefinitionSpec {
	return DefinitionSpec{
		SchemaVersion: SchemaVersionV1,
		Name:          "PR review",
		On:            map[string]any{"manual": map[string]any{}},
		Jobs: map[string]JobSpec{
			"plan": {
				Steps: []StepSpec{{ID: "summarize", Run: "echo Plan"}},
			},
			"approve": {
				Needs: []string{"plan"},
				Steps: []StepSpec{{ID: "approval", Run: "echo Review"}},
			},
		},
	}
}

func TestHasManualTrigger(t *testing.T) {
	tests := []struct {
		name string
		on   any
		want bool
	}{
		{
			name: "string form",
			on:   "manual",
			want: true,
		},
		{
			name: "list form",
			on:   []any{"push", "manual"},
			want: true,
		},
		{
			name: "string list form",
			on:   []string{"push", "manual"},
			want: true,
		},
		{
			name: "map form",
			on:   map[string]any{"push": map[string]any{"branches": []any{"main"}}, "manual": map[string]any{}},
			want: true,
		},
		{
			name: "missing manual",
			on:   []any{"push"},
			want: false,
		},
		{
			name: "missing on",
			on:   nil,
			want: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			require.Equal(t, tt.want, HasManualTrigger(tt.on))
		})
	}
}

func TestValidateDefinitionAcceptsAgentTeamPublishWikiStep(t *testing.T) {
	spec := DefinitionSpec{
		Name: "Publish wiki",
		On:   map[string]any{"manual": map[string]any{}},
		Jobs: map[string]JobSpec{
			"publish": {
				RunsOn: "ubuntu-latest",
				Steps: []StepSpec{{
					Uses: AgentTeamPublishWikiUse,
					With: map[string]any{
						AgentTeamPublishWikiWithRepository: "pingcap/tidb.wiki",
						AgentTeamPublishWikiWithSourcePath: "staged-wiki",
						AgentTeamPublishWikiWithMaxPages:   "1000",
					},
				}},
			},
		},
	}

	report := ValidateDefinition(spec)

	require.True(t, report.Valid, "errors=%+v", report.Errors)
}

func TestValidateDefinitionRejectsInvalidAgentTeamPublishWikiStep(t *testing.T) {
	spec := DefinitionSpec{
		Name: "Publish wiki",
		On:   map[string]any{"manual": map[string]any{}},
		Jobs: map[string]JobSpec{
			"publish": {
				RunsOn: "ubuntu-latest",
				Steps: []StepSpec{{
					Uses: AgentTeamPublishWikiUse,
					With: map[string]any{
						AgentTeamPublishWikiWithRepository: "missing-owner",
						AgentTeamPublishWikiWithSourcePath: "../outside",
					},
				}},
			},
		},
	}

	report := ValidateDefinition(spec)

	require.False(t, report.Valid)
	require.Contains(t, validationCodes(report), "agentteam_publish_wiki_repository_invalid")
	require.Contains(t, validationCodes(report), "agentteam_publish_wiki_source_path_invalid")
}

func TestValidateDefinitionAcceptsAgentTeamPublishTreeStep(t *testing.T) {
	spec := DefinitionSpec{
		Name: "Publish tree",
		On:   map[string]any{"manual": map[string]any{}},
		Jobs: map[string]JobSpec{
			"publish": {
				RunsOn: "ubuntu-latest",
				Steps: []StepSpec{{
					Uses: AgentTeamPublishTreeUse,
					With: map[string]any{
						AgentTeamPublishTreeWithRepository: "pingcap/tidb.insights",
						AgentTeamPublishTreeWithSourcePath: "mounted-insights",
						AgentTeamPublishTreeWithMaxFiles:   "10000",
					},
				}},
			},
		},
	}

	report := ValidateDefinition(spec)

	require.True(t, report.Valid, "errors=%+v", report.Errors)
}

func TestValidateDefinitionRejectsInvalidAgentTeamPublishTreeStep(t *testing.T) {
	spec := DefinitionSpec{
		Name: "Publish tree",
		On:   map[string]any{"manual": map[string]any{}},
		Jobs: map[string]JobSpec{
			"publish": {
				RunsOn: "ubuntu-latest",
				Steps: []StepSpec{{
					Uses: AgentTeamPublishTreeUse,
					With: map[string]any{
						AgentTeamPublishTreeWithRepository: "missing-owner",
						AgentTeamPublishTreeWithSourcePath: "../outside",
					},
				}},
			},
		},
	}

	report := ValidateDefinition(spec)

	require.False(t, report.Valid)
	require.Contains(t, validationCodes(report), "agentteam_publish_tree_repository_invalid")
	require.Contains(t, validationCodes(report), "agentteam_publish_tree_source_path_invalid")
}

func validationCodes(report ValidationReport) []string {
	codes := make([]string, 0, len(report.Errors))
	for _, err := range report.Errors {
		codes = append(codes, err.Code)
	}
	return codes
}

func TestMatchesPushTrigger(t *testing.T) {
	tests := []struct {
		name  string
		on    any
		event PushEvent
		want  bool
	}{
		{
			name:  "string form",
			on:    "push",
			event: PushEvent{Ref: "refs/heads/main", ChangedPathsKnown: true},
			want:  true,
		},
		{
			name:  "list form",
			on:    []any{"manual", "push"},
			event: PushEvent{Ref: "refs/heads/main", ChangedPathsKnown: true},
			want:  true,
		},
		{
			name:  "map form",
			on:    map[string]any{"push": map[string]any{}},
			event: PushEvent{Ref: "refs/heads/main", ChangedPathsKnown: true},
			want:  true,
		},
		{
			name:  "branch include matches",
			on:    map[string]any{"push": map[string]any{"branches": []any{"main", "release/**"}}},
			event: PushEvent{Ref: "refs/heads/release/2026.06", ChangedPathsKnown: true},
			want:  true,
		},
		{
			name:  "branch include question wildcard matches",
			on:    map[string]any{"push": map[string]any{"branches": []any{"release/2026.?6"}}},
			event: PushEvent{Ref: "refs/heads/release/2026.06", ChangedPathsKnown: true},
			want:  true,
		},
		{
			name:  "branch include rejects",
			on:    map[string]any{"push": map[string]any{"branches": []any{"main"}}},
			event: PushEvent{Ref: "refs/heads/feature/a", ChangedPathsKnown: true},
			want:  false,
		},
		{
			name:  "branch ignore rejects",
			on:    map[string]any{"push": map[string]any{"branches-ignore": []any{"dependabot/**"}}},
			event: PushEvent{Ref: "refs/heads/dependabot/go", ChangedPathsKnown: true},
			want:  false,
		},
		{
			name:  "branch filter rejects tag",
			on:    map[string]any{"push": map[string]any{"branches": []any{"main"}}},
			event: PushEvent{Ref: "refs/tags/v1.0.0", ChangedPathsKnown: true},
			want:  false,
		},
		{
			name:  "paths include matches",
			on:    map[string]any{"push": map[string]any{"paths": []any{"cmd/**", "*.md"}}},
			event: PushEvent{Ref: "refs/heads/main", ChangedPaths: []string{"cmd/server/main.go"}, ChangedPathsKnown: true},
			want:  true,
		},
		{
			name:  "paths include rejects",
			on:    map[string]any{"push": map[string]any{"paths": []any{"cmd/**"}}},
			event: PushEvent{Ref: "refs/heads/main", ChangedPaths: []string{"README.md"}, ChangedPathsKnown: true},
			want:  false,
		},
		{
			name:  "paths single star does not cross slash",
			on:    map[string]any{"push": map[string]any{"paths": []any{"*.md"}}},
			event: PushEvent{Ref: "refs/heads/main", ChangedPaths: []string{"docs/guide.md"}, ChangedPathsKnown: true},
			want:  false,
		},
		{
			name:  "paths ignore rejects all ignored",
			on:    map[string]any{"push": map[string]any{"paths-ignore": []any{"docs/**", "*.md"}}},
			event: PushEvent{Ref: "refs/heads/main", ChangedPaths: []string{"README.md", "docs/guide.md"}, ChangedPathsKnown: true},
			want:  false,
		},
		{
			name:  "paths ignore allows unignored",
			on:    map[string]any{"push": map[string]any{"paths-ignore": []any{"docs/**"}}},
			event: PushEvent{Ref: "refs/heads/main", ChangedPaths: []string{"docs/guide.md", "cmd/server/main.go"}, ChangedPathsKnown: true},
			want:  true,
		},
		{
			name:  "unknown paths reject path filtered automation",
			on:    map[string]any{"push": map[string]any{"paths": []any{"cmd/**"}}},
			event: PushEvent{Ref: "refs/heads/main", ChangedPathsKnown: false},
			want:  false,
		},
		{
			name:  "unknown paths allow automation without path filters",
			on:    map[string]any{"push": map[string]any{"branches": []any{"main"}}},
			event: PushEvent{Ref: "refs/heads/main", ChangedPathsKnown: false},
			want:  true,
		},
		{
			name:  "missing push",
			on:    map[string]any{"manual": map[string]any{}},
			event: PushEvent{Ref: "refs/heads/main", ChangedPathsKnown: true},
			want:  false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			require.Equal(t, tt.want, MatchesPushTrigger(tt.on, tt.event))
		})
	}
}

func TestEnvReferences(t *testing.T) {
	spec := DefinitionSpec{
		Env: map[string]any{"TOKEN": "${{ secrets.overridden_token }}"},
		Jobs: map[string]JobSpec{
			"build": {
				Env:   map[string]any{"TOKEN": "literal"},
				Steps: []StepSpec{{Run: "echo build", Env: map[string]any{"SLACK": "${{ secrets.slack_token }}"}}},
			},
			"test": {
				Env:   map[string]any{"TOKEN": "${{ inputs.token }}"},
				Steps: []StepSpec{{Run: "echo test"}},
			},
		},
	}

	secrets, err := SecretReferences(spec)
	require.NoError(t, err)
	require.Equal(t, []string{"slack_token"}, secrets)

	refs, err := JobEnvReferences(JobSpec{
		Env:   map[string]any{"JOB": "${{ secrets.job_token }}", "TARGET": "${{ inputs.target }}"},
		Steps: []StepSpec{{Run: "echo deploy", Env: map[string]any{"STEP": "${{ secrets.step_token }}"}}},
	})
	require.NoError(t, err)
	require.Equal(t, []string{"job_token", "step_token"}, refs.SecretNames)
	require.True(t, refs.UsesInputs)
}

func TestValidateAutomationSpec(t *testing.T) {
	valid := validDefinitionSpec()

	t.Run("valid github actions style automation", func(t *testing.T) {
		report := ValidateDefinition(valid)
		require.True(t, report.Valid, report.Errors)
		require.Empty(t, report.Errors)
	})

	t.Run("missing needed job", func(t *testing.T) {
		spec := validDefinitionSpec()
		approve := spec.Jobs["approve"]
		approve.Needs = []string{"missing"}
		spec.Jobs["approve"] = approve
		requireErrorCode(t, ValidateDefinition(spec), "job_needs_missing")
	})

	t.Run("name and on are optional for source validation", func(t *testing.T) {
		spec := validDefinitionSpec()
		spec.Name = ""
		spec.On = nil
		report := ValidateDefinition(spec)
		require.True(t, report.Valid, report.Errors)
	})

	t.Run("accepts github actions needs string and runs-on payload", func(t *testing.T) {
		var spec DefinitionSpec
		require.NoError(t, json.Unmarshal([]byte(`{
			"jobs": {
				"build": {"runs-on": "ubuntu-latest", "steps": [{"run": "echo build"}]},
				"test": {"needs": "build", "runs-on": ["self-hosted", "macos"], "permissions": {"contents": "read"}, "steps": [{"run": "echo test"}]}
			}
		}`), &spec))
		require.Equal(t, []string{"build"}, spec.Jobs["test"].Needs)
		report := ValidateDefinition(spec)
		require.True(t, report.Valid, report.Errors)
	})

	t.Run("cycle", func(t *testing.T) {
		spec := validDefinitionSpec()
		plan := spec.Jobs["plan"]
		plan.Needs = []string{"approve"}
		spec.Jobs["plan"] = plan
		requireErrorCode(t, ValidateDefinition(spec), "job_needs_cycle")
	})

	t.Run("step needs uses or run", func(t *testing.T) {
		spec := validDefinitionSpec()
		plan := spec.Jobs["plan"]
		plan.Steps = []StepSpec{{ID: "empty", Name: "Empty"}}
		spec.Jobs["plan"] = plan
		requireErrorCode(t, ValidateDefinition(spec), "step_action_required")
	})

	t.Run("step cannot define uses and run", func(t *testing.T) {
		spec := validDefinitionSpec()
		plan := spec.Jobs["plan"]
		plan.Steps = []StepSpec{{ID: "bad", Uses: "actions/checkout@v4", Run: "echo bad"}}
		spec.Jobs["plan"] = plan
		requireErrorCode(t, ValidateDefinition(spec), "step_action_exclusive")
	})

	t.Run("rejects automation expressions in unsupported locations", func(t *testing.T) {
		stepContextSpec := validDefinitionSpec()
		plan := stepContextSpec.Jobs["plan"]
		plan.Steps = []StepSpec{
			{ID: "first", Run: "echo ${{ steps.second.outputs.value }}"},
			{ID: "second", Run: "echo second"},
			{ID: "self", Run: "echo ${{ steps.self.outputs.value }}"},
		}
		stepContextSpec.Jobs["plan"] = plan

		undeclaredNeedsSpec := validDefinitionSpec()
		approve := undeclaredNeedsSpec.Jobs["approve"]
		approve.Needs = nil
		approve.Steps = []StepSpec{{ID: "approval", Run: "echo Review ${{ needs.plan.outputs.summary }}"}}
		undeclaredNeedsSpec.Jobs["approve"] = approve

		tests := []struct {
			name string
			spec DefinitionSpec
		}{
			{
				name: "step contexts",
				spec: stepContextSpec,
			},
			{
				name: "all run contexts until official token serialization exists",
				spec: DefinitionSpec{Jobs: map[string]JobSpec{
					"contexts": {
						Needs: []string{"build"},
						Steps: []StepSpec{
							{ID: "first", Run: "echo first"},
							{ID: "second", Run: "echo ${{ steps.first.outputs.value }} ${{ needs.build.outputs.value }} ${{ inputs.name }} ${{ vars.name }} ${{ secrets.token }} ${{ env.name }}"},
						},
					},
					"build": {Steps: []StepSpec{{Run: "echo build"}}},
				}},
			},
			{
				name: "github fields not provided to the official runner",
				spec: DefinitionSpec{Jobs: map[string]JobSpec{
					"build": {Steps: []StepSpec{{Run: "echo ${{ github.actor }}"}}},
				}},
			},
			{
				name: "compound expressions",
				spec: DefinitionSpec{Jobs: map[string]JobSpec{
					"build": {Steps: []StepSpec{{Run: "echo ${{ github.ref == 'refs/heads/main' }}"}}},
				}},
			},
			{
				name: "braces inside expression body",
				spec: DefinitionSpec{Jobs: map[string]JobSpec{
					"build": {Steps: []StepSpec{{Run: "echo ${{ format('{0}', github.ref) }}"}}},
				}},
			},
			{
				name: "display names",
				spec: DefinitionSpec{
					Name: "Automation ${{ github.ref }}",
					Jobs: map[string]JobSpec{
						"build": {
							Name:  "Build ${{ github.ref }}",
							Steps: []StepSpec{{Name: "Echo ${{ github.ref }}", Run: "echo build"}},
						},
					},
				},
			},
			{
				name: "non-run fields",
				spec: DefinitionSpec{
					RunName: "${{ github.ref }}",
					Jobs: map[string]JobSpec{
						"build": {
							RunsOn: "${{ github.ref }}",
							Steps:  []StepSpec{{Run: "echo build", With: map[string]any{"name": "${{ github.ref }}"}}},
						},
					},
				},
			},
			{
				name: "undeclared needs context",
				spec: undeclaredNeedsSpec,
			},
		}
		for _, tt := range tests {
			t.Run(tt.name, func(t *testing.T) {
				requireErrorCode(t, ValidateDefinition(tt.spec), "expression_unsupported")
			})
		}
	})

	t.Run("accepts automation job and step env subset", func(t *testing.T) {
		spec := DefinitionSpec{
			Env: map[string]any{
				"NODE_ENV": "production",
				"GLOBAL":   "${{ inputs.environment }}",
			},
			Jobs: map[string]JobSpec{
				"deploy": {
					Env: map[string]any{
						"GH_TOKEN": "${{ secrets.github_publish_token }}",
						"LITERAL":  "plain string",
					},
					Steps: []StepSpec{{
						Run: "echo deploy",
						Env: map[string]any{
							"SLACK_TOKEN": "${{ secrets.deploy_slack_token }}",
							"TARGET":      "${{inputs.target_env}}",
						},
					}},
				},
			},
		}
		report := ValidateDefinition(spec)
		require.True(t, report.Valid, report.Errors)

		parsed, err := ParseEnvValue("${{ secrets.deploy_slack_token }}")
		require.NoError(t, err)
		require.Equal(t, EnvValue{Kind: EnvValueSecret, Name: "deploy_slack_token"}, parsed)
		parsed, err = ParseEnvValue("literal $TOKEN")
		require.NoError(t, err)
		require.Equal(t, EnvValue{Kind: EnvValueLiteral, Value: "literal $TOKEN"}, parsed)
	})

	t.Run("rejects unsupported env values", func(t *testing.T) {
		tests := []struct {
			name string
			env  map[string]any
			code string
		}{
			{name: "invalid env name", env: map[string]any{"1TOKEN": "value"}, code: "env_name_invalid"},
			{name: "non string value", env: map[string]any{"TOKEN": 123}, code: "env_value_invalid"},
			{name: "concatenated expression", env: map[string]any{"TOKEN": "Bearer ${{ secrets.token }}"}, code: "env_value_unsupported"},
			{name: "unsupported context", env: map[string]any{"TOKEN": "${{ github.token }}"}, code: "env_value_unsupported"},
			{name: "operator", env: map[string]any{"TOKEN": "${{ secrets.token || inputs.token }}"}, code: "env_value_unsupported"},
			{name: "invalid reference name", env: map[string]any{"TOKEN": "${{ secrets.deploy-token }}"}, code: "env_value_unsupported"},
		}
		for _, tt := range tests {
			t.Run(tt.name, func(t *testing.T) {
				spec := DefinitionSpec{Jobs: map[string]JobSpec{
					"deploy": {
						Steps: []StepSpec{{Run: "echo deploy", Env: tt.env}},
					},
				}}
				requireErrorCode(t, ValidateDefinition(spec), tt.code)
			})
		}
	})

	t.Run("duplicate step id", func(t *testing.T) {
		spec := validDefinitionSpec()
		plan := spec.Jobs["plan"]
		plan.Steps = []StepSpec{{ID: "dup", Run: "echo one"}, {ID: "dup", Run: "echo two"}}
		spec.Jobs["plan"] = plan
		requireErrorCode(t, ValidateDefinition(spec), "step_id_duplicate")
	})

	t.Run("rejects job and step if conditions", func(t *testing.T) {
		spec := DefinitionSpec{Jobs: map[string]JobSpec{
			"build": {
				If:    "${{ github.ref == 'refs/heads/main' }}",
				Steps: []StepSpec{{Run: "echo build", If: "${{ inputs.ready }}"}},
			},
		}}
		report := ValidateDefinition(spec)
		requireErrorCode(t, report, "job_if_unsupported")
		requireErrorCode(t, report, "step_if_unsupported")
	})

	t.Run("rejects non agentteam uses and reports removed agentteam waits", func(t *testing.T) {
		spec := DefinitionSpec{Jobs: map[string]JobSpec{
			"external": {Steps: []StepSpec{{Uses: "actions/checkout@v4"}}},
			"approval": {Steps: []StepSpec{{
				Uses: "agentteam/human-approval",
				With: map[string]any{"prompt": "Approve?"},
			}}},
		}}
		report := ValidateDefinition(spec)
		requireErrorCode(t, report, "step_uses_unsupported")
		requireErrorCode(t, report, "step_uses_removed")
	})

	t.Run("permits checkout mixed with run steps", func(t *testing.T) {
		spec := DefinitionSpec{Jobs: map[string]JobSpec{
			"test": {
				Steps: []StepSpec{
					{Uses: "agentteam/checkout", With: map[string]any{"path": "src", "clean": false}},
					{Run: "test -f src/go.mod"},
				},
			},
		}}
		require.True(t, ValidateDefinition(spec).Valid)
	})

	t.Run("rejects unsupported checkout with fields", func(t *testing.T) {
		spec := DefinitionSpec{Jobs: map[string]JobSpec{
			"bad": {
				Steps: []StepSpec{{
					Uses: "agentteam/checkout",
					With: map[string]any{"repository": "other/repo", "path": "", "clean": 1},
				}},
			},
		}}
		report := ValidateDefinition(spec)
		requireErrorCode(t, report, "agentteam_checkout_with_unsupported")
		requireErrorCode(t, report, "agentteam_checkout_path_invalid")
		requireErrorCode(t, report, "agentteam_checkout_clean_invalid")
	})

	t.Run("rejects checkout paths containing git control directories", func(t *testing.T) {
		for _, checkoutPath := range []string{".git", "src/.git", "src/.GIT/hooks", "src/.git/../safe"} {
			t.Run(checkoutPath, func(t *testing.T) {
				spec := DefinitionSpec{Jobs: map[string]JobSpec{
					"bad": {
						Steps: []StepSpec{{
							Uses: "agentteam/checkout",
							With: map[string]any{"path": checkoutPath},
						}},
					},
				}}
				requireErrorCode(t, ValidateDefinition(spec), "agentteam_checkout_path_invalid")
			})
		}
	})

	t.Run("rejects agentteam wait jobs with mixed steps or if conditions", func(t *testing.T) {
		spec := DefinitionSpec{Jobs: map[string]JobSpec{
			"mixed": {
				Steps: []StepSpec{
					{Uses: "agentteam/wait-event", With: map[string]any{"event_type": "automation_event_received"}},
					{Run: "echo deploy"},
				},
			},
			"job_if": {
				If:    "${{ github.ref == 'refs/heads/main' }}",
				Steps: []StepSpec{{Uses: "agentteam/agent-task"}},
			},
			"step_if": {
				Steps: []StepSpec{{Uses: "agentteam/wait-event", If: "${{ inputs.ready }}"}},
			},
		}}
		report := ValidateDefinition(spec)
		requireErrorCode(t, report, "agentteam_wait_job_mixed_steps")
		requireErrorCode(t, report, "job_if_unsupported")
		requireErrorCode(t, report, "step_if_unsupported")
	})

	t.Run("rejects agent task waits without task identity", func(t *testing.T) {
		spec := DefinitionSpec{Jobs: map[string]JobSpec{
			"missing": {Steps: []StepSpec{{Uses: "agentteam/agent-task"}}},
			"valid":   {Steps: []StepSpec{{Uses: "agentteam/agent-task", With: map[string]any{"task_id": "tsk_123"}}}},
		}}
		report := ValidateDefinition(spec)
		requireErrorCode(t, report, "agentteam_agent_task_id_required")
	})

	t.Run("rejects agent task signal identity overrides", func(t *testing.T) {
		spec := DefinitionSpec{Jobs: map[string]JobSpec{
			"task": {Steps: []StepSpec{{Uses: "agentteam/agent-task", With: map[string]any{"task_id": "tsk_123", "event_type": "custom.task.done"}}}},
		}}
		report := ValidateDefinition(spec)
		requireErrorCode(t, report, "agentteam_agent_task_signal_identity_fixed")
	})

	t.Run("limits jobs steps runner labels and job input", func(t *testing.T) {
		spec := validDefinitionSpec()
		plan := spec.Jobs["plan"]
		plan.Steps = []StepSpec{{Run: "echo 1"}, {Run: "echo 2"}}
		plan.RunsOn = []any{"self-hosted", "linux", "x64"}
		spec.Jobs["plan"] = plan
		report := ValidateDefinitionWithLimits(spec, ValidationLimits{MaxJobs: 1, MaxStepsPerJob: 1, MaxRunnerLabels: 2, MaxJobInputBytes: 32})
		requireErrorCode(t, report, "jobs_limit_exceeded")

		report = ValidateDefinitionWithLimits(DefinitionSpec{Jobs: map[string]JobSpec{"plan": plan}}, ValidationLimits{MaxJobs: 2, MaxStepsPerJob: 1, MaxRunnerLabels: 2, MaxJobInputBytes: 32})
		requireErrorCode(t, report, "job_steps_limit_exceeded")

		plan.Steps = []StepSpec{{Run: "echo ok"}}
		report = ValidateDefinitionWithLimits(DefinitionSpec{Jobs: map[string]JobSpec{"plan": plan}}, ValidationLimits{MaxJobs: 2, MaxStepsPerJob: 2, MaxRunnerLabels: 2, MaxJobInputBytes: 32})
		requireErrorCode(t, report, "runner_labels_limit_exceeded")
		requireErrorCode(t, report, "job_input_limit_exceeded")
	})

	t.Run("limits runner match key lengths", func(t *testing.T) {
		spec := DefinitionSpec{Jobs: map[string]JobSpec{
			"group": {
				RunsOn: map[string]any{"group": strings.Repeat("g", RunnerGroupMaxLength+1), "labels": []any{"self-hosted"}},
				Steps:  []StepSpec{{Run: "echo group"}},
			},
			"labels": {
				RunsOn: []any{strings.Repeat("l", RunnerLabelKeyMaxLength+1)},
				Steps:  []StepSpec{{Run: "echo labels"}},
			},
		}}
		report := ValidateDefinitionWithLimits(spec, ValidationLimits{MaxJobs: 10, MaxStepsPerJob: 10, MaxRunnerLabels: 8, MaxJobInputBytes: 4096})
		requireErrorCode(t, report, "runner_group_length_exceeded")
		requireErrorCode(t, report, "runner_label_key_length_exceeded")
	})

	t.Run("rejects reserved runner group", func(t *testing.T) {
		spec := DefinitionSpec{Jobs: map[string]JobSpec{
			"reserved": {
				RunsOn: map[string]any{"group": ReservedServerManagedRunnerGroup, "labels": []any{"ubuntu-latest"}},
				Steps:  []StepSpec{{Run: "echo reserved"}},
			},
		}}
		requireErrorCode(t, ValidateDefinition(spec), "runner_group_reserved")
	})

	t.Run("counts inherited automation env in job input limit", func(t *testing.T) {
		spec := DefinitionSpec{
			Env: map[string]any{"BIG": strings.Repeat("x", 256)},
			Jobs: map[string]JobSpec{
				"build": {Steps: []StepSpec{{Run: "echo build"}}},
			},
		}
		report := ValidateDefinitionWithLimits(spec, ValidationLimits{MaxJobs: 10, MaxStepsPerJob: 10, MaxJobInputBytes: 128})
		requireErrorCode(t, report, "job_input_limit_exceeded")
	})

	t.Run("fails closed on unsupported p0 fields", func(t *testing.T) {
		spec := DefinitionSpec{
			Concurrency: map[string]any{"group": "main"},
			Env:         map[string]any{"GLOBAL": "true"},
			Defaults:    map[string]any{"run": map[string]any{"shell": "bash"}},
			Jobs: map[string]JobSpec{"build": {
				Strategy:    map[string]any{"matrix": map[string]any{"go": []any{"1.25"}}},
				Services:    map[string]any{"db": map[string]any{"image": "mysql"}},
				Container:   "ubuntu:latest",
				Concurrency: "build",
				Environment: "production",
				Env:         map[string]any{"JOB": "true"},
				Outputs:     map[string]any{"summary": "ok"},
				Defaults:    map[string]any{"run": map[string]any{"working-directory": "src"}},
				Steps:       []StepSpec{{Run: "echo build"}},
			}},
		}
		report := ValidateDefinitionWithLimits(spec, ValidationLimits{MaxJobs: 10, MaxStepsPerJob: 10})
		requireErrorCode(t, report, "concurrency_unsupported")
		requireErrorCode(t, report, "automation_defaults_unsupported")
		requireErrorCode(t, report, "job_strategy_unsupported")
		requireErrorCode(t, report, "job_services_unsupported")
		requireErrorCode(t, report, "job_container_unsupported")
		requireErrorCode(t, report, "job_concurrency_unsupported")
		requireErrorCode(t, report, "job_environment_unsupported")
		requireErrorCode(t, report, "job_outputs_unsupported")
		requireErrorCode(t, report, "job_defaults_unsupported")
	})

	t.Run("accepts step env literals", func(t *testing.T) {
		spec := DefinitionSpec{Jobs: map[string]JobSpec{
			"build": {Steps: []StepSpec{{Run: "echo build", Env: map[string]any{"STEP": "true"}}}},
		}}
		report := ValidateDefinition(spec)
		require.True(t, report.Valid, report.Errors)
	})
}

func requireErrorCode(t *testing.T, report ValidationReport, code string) {
	t.Helper()
	require.False(t, report.Valid)
	for _, err := range report.Errors {
		if err.Code == code {
			return
		}
	}
	require.Failf(t, "missing validation error", "code %s not found in %#v", code, report.Errors)
}
