package schema

import (
	"strings"
	"testing"

	"git.cloudinit.dev/coreci/orca/internal/jobspec"
)

func TestJobValidator_ValidMinimal(t *testing.T) {
	spec := &jobspec.WorkloadSpec{Kind: "Job", Name: "backup", Count: 1}
	v := JobValidator{}
	if err := v.Validate(spec); err != nil {
		t.Fatalf("expected nil, got %v", err)
	}
}

func TestJobValidator_ValidWithSchedule(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:     "Job",
		Name:     "backup",
		Count:    1,
		Schedule: &jobspec.ScheduleBlock{Cron: "0 2 * * *"},
		Timeout:  "1h",
	}
	v := JobValidator{}
	if err := v.Validate(spec); err != nil {
		t.Fatalf("expected nil, got %v", err)
	}
}

func TestJobValidator_ValidUnsetCount(t *testing.T) {
	spec := &jobspec.WorkloadSpec{Kind: "Job", Name: "one-shot"}
	v := JobValidator{}
	if err := v.Validate(spec); err != nil {
		t.Fatalf("unset count should default-accept, got %v", err)
	}
}

func TestJobValidator_MissingName(t *testing.T) {
	spec := &jobspec.WorkloadSpec{Kind: "Job", Count: 1}
	err := JobValidator{}.Validate(spec)
	if err == nil {
		t.Fatal("expected error for missing name, got nil")
	}
	if !strings.Contains(err.Error(), "name is required") {
		t.Errorf("error = %q, want 'name is required'", err.Error())
	}
}

func TestJobValidator_CountGreaterThanOne(t *testing.T) {
	spec := &jobspec.WorkloadSpec{Kind: "Job", Name: "batch", Count: 3}
	err := JobValidator{}.Validate(spec)
	if err == nil {
		t.Fatal("expected error for count > 1, got nil")
	}
	if !strings.Contains(err.Error(), "count must be 1") {
		t.Errorf("error = %q, want 'count must be 1'", err.Error())
	}
}

func TestJobValidator_ServiceBlockRejected(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:    "Job",
		Name:    "x",
		Count:   1,
		Service: &jobspec.ServiceBlock{Host: "x.example"},
	}
	err := JobValidator{}.Validate(spec)
	if err == nil {
		t.Fatal("expected error for service block on Job, got nil")
	}
	if !strings.Contains(err.Error(), "service block") {
		t.Errorf("error = %q, want 'service block'", err.Error())
	}
}

func TestJobValidator_NilSpec(t *testing.T) {
	v := JobValidator{}
	if err := v.Validate(nil); err == nil {
		t.Fatal("expected error for nil spec")
	}
}

func TestServiceValidator_ValidFull(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:    "Service",
		Name:    "web",
		Count:   3,
		Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: "/bin/http"},
		Restart: &jobspec.RestartBlock{Mode: "service"},
		Update:  &jobspec.UpdateBlock{Strategy: "rolling", MaxSurge: 1},
		Health:  &jobspec.HealthBlock{CheckType: "http", Interval: "5s"},
		Ports:   []jobspec.PortSpec{{Name: "http", Port: 8080}},
	}
	v := ServiceValidator{}
	if err := v.Validate(spec); err != nil {
		t.Fatalf("expected nil, got %v", err)
	}
}

func TestServiceValidator_MissingPorts(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:    "Service",
		Name:    "web",
		Count:   2,
		Runtime: &jobspec.RuntimeBlock{OneOf: "process"},
		Restart: &jobspec.RestartBlock{Mode: "service"},
		Update:  &jobspec.UpdateBlock{Strategy: "rolling"},
	}
	err := ServiceValidator{}.Validate(spec)
	if err == nil {
		t.Fatal("expected error for missing ports, got nil")
	}
	if !strings.Contains(err.Error(), "ports required") {
		t.Errorf("error = %q, want 'ports required'", err.Error())
	}
}

func TestServiceValidator_CountZero(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:    "Service",
		Name:    "web",
		Count:   0,
		Runtime: &jobspec.RuntimeBlock{OneOf: "process"},
		Restart: &jobspec.RestartBlock{Mode: "service"},
		Update:  &jobspec.UpdateBlock{Strategy: "rolling"},
		Ports:   []jobspec.PortSpec{{Name: "http", Port: 8080}},
	}
	err := ServiceValidator{}.Validate(spec)
	if err == nil {
		t.Fatal("expected error for count 0, got nil")
	}
	if !strings.Contains(err.Error(), "count must be") {
		t.Errorf("error = %q, want 'count must be'", err.Error())
	}
}

func TestServiceValidator_MissingRestart(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:    "Service",
		Name:    "web",
		Count:   1,
		Runtime: &jobspec.RuntimeBlock{OneOf: "process"},
		Update:  &jobspec.UpdateBlock{Strategy: "rolling"},
		Ports:   []jobspec.PortSpec{{Name: "http", Port: 8080}},
	}
	err := ServiceValidator{}.Validate(spec)
	if err == nil {
		t.Fatal("expected error for missing restart, got nil")
	}
	if !strings.Contains(err.Error(), "restart block required") {
		t.Errorf("error = %q, want 'restart block required'", err.Error())
	}
}

func TestServiceValidator_WrongRestartMode(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:    "Service",
		Name:    "web",
		Count:   1,
		Runtime: &jobspec.RuntimeBlock{OneOf: "process"},
		Restart: &jobspec.RestartBlock{Mode: "always"},
		Update:  &jobspec.UpdateBlock{Strategy: "rolling"},
		Health:  &jobspec.HealthBlock{CheckType: "http"},
		Ports:   []jobspec.PortSpec{{Name: "http", Port: 8080}},
	}
	err := ServiceValidator{}.Validate(spec)
	if err == nil {
		t.Fatal("expected error for invalid restart mode, got nil")
	}
	if !strings.Contains(err.Error(), "restart mode") {
		t.Errorf("error = %q, want 'restart mode'", err.Error())
	}
}

func TestServiceValidator_AcceptedRestartModes(t *testing.T) {
	// R-012: restart.mode accepts service / on-failure / never for
	// Service; the default per R-012 is "service" but the validator
	// accepts the full enum (a Service that wants on-failure is
	// unusual but not invalid — only "always" and unknown modes are
	// rejected).
	for _, mode := range []string{"service", "on-failure", "never"} {
		t.Run(mode, func(t *testing.T) {
			spec := &jobspec.WorkloadSpec{
				Kind:    "Service",
				Name:    "web",
				Count:   1,
				Runtime: &jobspec.RuntimeBlock{OneOf: "process"},
				Restart: &jobspec.RestartBlock{Mode: mode},
				Update:  &jobspec.UpdateBlock{Strategy: "rolling"},
				Health:  &jobspec.HealthBlock{CheckType: "http"},
				Ports:   []jobspec.PortSpec{{Name: "http", Port: 8080}},
			}
			v := ServiceValidator{}
			if err := v.Validate(spec); err != nil {
				t.Errorf("mode %q should be accepted, got: %v", mode, err)
			}
		})
	}
}

func TestServiceValidator_MissingUpdate(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:    "Service",
		Name:    "web",
		Count:   1,
		Runtime: &jobspec.RuntimeBlock{OneOf: "process"},
		Restart: &jobspec.RestartBlock{Mode: "service"},
		Ports:   []jobspec.PortSpec{{Name: "http", Port: 8080}},
	}
	err := ServiceValidator{}.Validate(spec)
	if err == nil {
		t.Fatal("expected error for missing update, got nil")
	}
	if !strings.Contains(err.Error(), "update block required") {
		t.Errorf("error = %q, want 'update block required'", err.Error())
	}
}

func TestServiceValidator_MissingRuntime(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:    "Service",
		Name:    "web",
		Count:   1,
		Restart: &jobspec.RestartBlock{Mode: "service"},
		Update:  &jobspec.UpdateBlock{Strategy: "rolling"},
		Ports:   []jobspec.PortSpec{{Name: "http", Port: 8080}},
	}
	err := ServiceValidator{}.Validate(spec)
	if err == nil {
		t.Fatal("expected error for missing runtime, got nil")
	}
	if !strings.Contains(err.Error(), "runtime block required") {
		t.Errorf("error = %q, want 'runtime block required'", err.Error())
	}
}

func TestServiceValidator_NilSpec(t *testing.T) {
	v := ServiceValidator{}
	if err := v.Validate(nil); err == nil {
		t.Fatal("expected error for nil spec")
	}
}

func TestServiceValidator_MissingHealth(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:    "Service",
		Name:    "web",
		Count:   1,
		Runtime: &jobspec.RuntimeBlock{OneOf: "process"},
		Restart: &jobspec.RestartBlock{Mode: "service"},
		Update:  &jobspec.UpdateBlock{Strategy: "rolling"},
		Ports:   []jobspec.PortSpec{{Name: "http", Port: 8080}},
	}
	err := ServiceValidator{}.Validate(spec)
	if err == nil {
		t.Fatal("expected error for missing health block, got nil")
	}
	if !strings.Contains(err.Error(), "health block required") {
		t.Errorf("error = %q, want 'health block required'", err.Error())
	}
}

func TestServiceValidator_InvalidRestartMode(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:    "Service",
		Name:    "web",
		Count:   1,
		Runtime: &jobspec.RuntimeBlock{OneOf: "process"},
		Restart: &jobspec.RestartBlock{Mode: "always"},
		Update:  &jobspec.UpdateBlock{Strategy: "rolling"},
		Health:  &jobspec.HealthBlock{CheckType: "http"},
		Ports:   []jobspec.PortSpec{{Name: "http", Port: 8080}},
	}
	err := ServiceValidator{}.Validate(spec)
	if err == nil {
		t.Fatal("expected error for invalid restart mode, got nil")
	}
	if !strings.Contains(err.Error(), "restart mode") || !strings.Contains(err.Error(), "invalid") {
		t.Errorf("error = %q, want 'restart mode ... invalid'", err.Error())
	}
}

func TestServiceValidator_EmptyRestartMode(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:    "Service",
		Name:    "web",
		Count:   1,
		Runtime: &jobspec.RuntimeBlock{OneOf: "process"},
		Restart: &jobspec.RestartBlock{Mode: ""},
		Update:  &jobspec.UpdateBlock{Strategy: "rolling"},
		Health:  &jobspec.HealthBlock{CheckType: "http"},
		Ports:   []jobspec.PortSpec{{Name: "http", Port: 8080}},
	}
	err := ServiceValidator{}.Validate(spec)
	if err == nil {
		t.Fatal("expected error for empty restart mode, got nil")
	}
	if !strings.Contains(err.Error(), "restart mode required") {
		t.Errorf("error = %q, want 'restart mode required'", err.Error())
	}
}

func TestServiceValidator_InvalidUpdateStrategy(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:    "Service",
		Name:    "web",
		Count:   1,
		Runtime: &jobspec.RuntimeBlock{OneOf: "process"},
		Restart: &jobspec.RestartBlock{Mode: "service"},
		Update:  &jobspec.UpdateBlock{Strategy: "recreate"},
		Health:  &jobspec.HealthBlock{CheckType: "http"},
		Ports:   []jobspec.PortSpec{{Name: "http", Port: 8080}},
	}
	err := ServiceValidator{}.Validate(spec)
	if err == nil {
		t.Fatal("expected error for invalid update strategy, got nil")
	}
	if !strings.Contains(err.Error(), "update strategy") || !strings.Contains(err.Error(), "invalid") {
		t.Errorf("error = %q, want 'update strategy ... invalid'", err.Error())
	}
}

func TestServiceValidator_EmptyUpdateStrategy(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:    "Service",
		Name:    "web",
		Count:   1,
		Runtime: &jobspec.RuntimeBlock{OneOf: "process"},
		Restart: &jobspec.RestartBlock{Mode: "service"},
		Update:  &jobspec.UpdateBlock{Strategy: ""},
		Health:  &jobspec.HealthBlock{CheckType: "http"},
		Ports:   []jobspec.PortSpec{{Name: "http", Port: 8080}},
	}
	err := ServiceValidator{}.Validate(spec)
	if err == nil {
		t.Fatal("expected error for empty update strategy, got nil")
	}
	if !strings.Contains(err.Error(), "update strategy required") {
		t.Errorf("error = %q, want 'update strategy required'", err.Error())
	}
}

func TestServiceValidator_AcceptedUpdateStrategies(t *testing.T) {
	for _, strat := range []string{"rolling", "canary", "blue-green"} {
		t.Run(strat, func(t *testing.T) {
			spec := &jobspec.WorkloadSpec{
				Kind:    "Service",
				Name:    "web",
				Count:   1,
				Runtime: &jobspec.RuntimeBlock{OneOf: "process"},
				Restart: &jobspec.RestartBlock{Mode: "service"},
				Update:  &jobspec.UpdateBlock{Strategy: strat},
				Health:  &jobspec.HealthBlock{CheckType: "http"},
				Ports:   []jobspec.PortSpec{{Name: "http", Port: 8080}},
			}
			v := ServiceValidator{}
			if err := v.Validate(spec); err != nil {
				t.Errorf("strategy %q should be accepted, got: %v", strat, err)
			}
		})
	}
}

func TestServiceValidator_InvalidServiceBind(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:    "Service",
		Name:    "web",
		Count:   1,
		Runtime: &jobspec.RuntimeBlock{OneOf: "process"},
		Restart: &jobspec.RestartBlock{Mode: "service"},
		Update:  &jobspec.UpdateBlock{Strategy: "rolling"},
		Health:  &jobspec.HealthBlock{CheckType: "http"},
		Ports:   []jobspec.PortSpec{{Name: "http", Port: 8080}},
		Service: &jobspec.ServiceBlock{Bind: "not-an-ip"},
	}
	err := ServiceValidator{}.Validate(spec)
	if err == nil {
		t.Fatal("expected error for invalid service.bind, got nil")
	}
	if !strings.Contains(err.Error(), "service.bind") || !strings.Contains(err.Error(), "valid IP") {
		t.Errorf("error = %q, want 'service.bind ... valid IP'", err.Error())
	}
}

func TestServiceValidator_ValidServiceBindLoopback(t *testing.T) {
	// R-007: 127.0.0.1 is the documented opt-in for a non-socket bind.
	spec := &jobspec.WorkloadSpec{
		Kind:    "Service",
		Name:    "web",
		Count:   1,
		Runtime: &jobspec.RuntimeBlock{OneOf: "process"},
		Restart: &jobspec.RestartBlock{Mode: "service"},
		Update:  &jobspec.UpdateBlock{Strategy: "rolling"},
		Health:  &jobspec.HealthBlock{CheckType: "http"},
		Ports:   []jobspec.PortSpec{{Name: "http", Port: 8080}},
		Service: &jobspec.ServiceBlock{Bind: "127.0.0.1"},
	}
	v := ServiceValidator{}
	if err := v.Validate(spec); err != nil {
		t.Fatalf("127.0.0.1 should be accepted, got: %v", err)
	}
}

func TestServiceValidator_ValidServiceBindIPv6(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:    "Service",
		Name:    "web",
		Count:   1,
		Runtime: &jobspec.RuntimeBlock{OneOf: "process"},
		Restart: &jobspec.RestartBlock{Mode: "service"},
		Update:  &jobspec.UpdateBlock{Strategy: "rolling"},
		Health:  &jobspec.HealthBlock{CheckType: "http"},
		Ports:   []jobspec.PortSpec{{Name: "http", Port: 8080}},
		Service: &jobspec.ServiceBlock{Bind: "::1"},
	}
	v := ServiceValidator{}
	if err := v.Validate(spec); err != nil {
		t.Fatalf("::1 should be accepted, got: %v", err)
	}
}

func TestServiceValidator_EmptyServiceBindOK(t *testing.T) {
	// R-007: empty bind = default = socket; valid.
	spec := &jobspec.WorkloadSpec{
		Kind:    "Service",
		Name:    "web",
		Count:   1,
		Runtime: &jobspec.RuntimeBlock{OneOf: "process"},
		Restart: &jobspec.RestartBlock{Mode: "service"},
		Update:  &jobspec.UpdateBlock{Strategy: "rolling"},
		Health:  &jobspec.HealthBlock{CheckType: "http"},
		Ports:   []jobspec.PortSpec{{Name: "http", Port: 8080}},
		Service: &jobspec.ServiceBlock{Bind: ""},
	}
	v := ServiceValidator{}
	if err := v.Validate(spec); err != nil {
		t.Fatalf("empty bind should default to socket (valid), got: %v", err)
	}
}

func TestServiceValidator_MultipleErrors(t *testing.T) {
	// Multiple violations should all surface in the composed error.
	spec := &jobspec.WorkloadSpec{
		Kind:    "Service",
		Name:    "",
		Count:   0,
		Restart: &jobspec.RestartBlock{Mode: "always"},
		Update:  &jobspec.UpdateBlock{Strategy: "recreate"},
		Service: &jobspec.ServiceBlock{Bind: "not-an-ip"},
	}
	err := ServiceValidator{}.Validate(spec)
	if err == nil {
		t.Fatal("expected error, got nil")
	}
	for _, want := range []string{
		"name is required",
		"ports required",
		"count must be",
		"restart mode",
		"update strategy",
		"runtime block required",
		"health block required",
		"service.bind",
	} {
		if !strings.Contains(err.Error(), want) {
			t.Errorf("error %q missing %q", err.Error(), want)
		}
	}
}

func TestDaemonSetValidator_Valid(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:     "DaemonSet",
		Name:     "log-shipper",
		Schedule: &jobspec.ScheduleBlock{Mode: "every-node"},
		Restart:  &jobspec.RestartBlock{Mode: "on-failure"},
	}
	v := DaemonSetValidator{}
	if err := v.Validate(spec); err != nil {
		t.Fatalf("expected nil, got %v", err)
	}
}

func TestDaemonSetValidator_ValidMatchingMode(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:     "DaemonSet",
		Name:     "x",
		Schedule: &jobspec.ScheduleBlock{Mode: "matching"},
		Restart:  &jobspec.RestartBlock{Mode: "on-failure"},
	}
	v := DaemonSetValidator{}
	if err := v.Validate(spec); err != nil {
		t.Fatalf("matching mode should be accepted, got %v", err)
	}
}

func TestDaemonSetValidator_ValidMandatoryMode(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:     "DaemonSet",
		Name:     "x",
		Schedule: &jobspec.ScheduleBlock{Mode: "mandatory"},
		Restart:  &jobspec.RestartBlock{Mode: "on-failure"},
	}
	v := DaemonSetValidator{}
	if err := v.Validate(spec); err != nil {
		t.Fatalf("mandatory mode should be accepted, got %v", err)
	}
}

func TestDaemonSetValidator_MissingScheduleMode(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:     "DaemonSet",
		Name:     "x",
		Schedule: &jobspec.ScheduleBlock{Mode: ""},
		Restart:  &jobspec.RestartBlock{Mode: "on-failure"},
	}
	err := DaemonSetValidator{}.Validate(spec)
	if err == nil {
		t.Fatal("expected error for missing schedule mode, got nil")
	}
	if !strings.Contains(err.Error(), "schedule mode required") {
		t.Errorf("error = %q, want 'schedule mode required'", err.Error())
	}
}

func TestDaemonSetValidator_MissingScheduleBlock(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:    "DaemonSet",
		Name:    "x",
		Restart: &jobspec.RestartBlock{Mode: "on-failure"},
	}
	err := DaemonSetValidator{}.Validate(spec)
	if err == nil {
		t.Fatal("expected error for missing schedule block, got nil")
	}
	if !strings.Contains(err.Error(), "schedule block required") {
		t.Errorf("error = %q, want 'schedule block required'", err.Error())
	}
}

func TestDaemonSetValidator_InvalidScheduleMode(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:     "DaemonSet",
		Name:     "x",
		Schedule: &jobspec.ScheduleBlock{Mode: "always"},
		Restart:  &jobspec.RestartBlock{Mode: "on-failure"},
	}
	err := DaemonSetValidator{}.Validate(spec)
	if err == nil {
		t.Fatal("expected error for invalid schedule mode, got nil")
	}
	if !strings.Contains(err.Error(), "schedule mode") {
		t.Errorf("error = %q, want 'schedule mode'", err.Error())
	}
}

func TestDaemonSetValidator_HasPorts(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:     "DaemonSet",
		Name:     "x",
		Schedule: &jobspec.ScheduleBlock{Mode: "every-node"},
		Restart:  &jobspec.RestartBlock{Mode: "on-failure"},
		Ports:    []jobspec.PortSpec{{Name: "http", Port: 80}},
	}
	err := DaemonSetValidator{}.Validate(spec)
	if err == nil {
		t.Fatal("expected error for ports on DaemonSet, got nil")
	}
	if !strings.Contains(err.Error(), "ports not allowed") {
		t.Errorf("error = %q, want 'ports not allowed'", err.Error())
	}
}

func TestDaemonSetValidator_HasCount(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:     "DaemonSet",
		Name:     "x",
		Count:    3,
		Schedule: &jobspec.ScheduleBlock{Mode: "every-node"},
		Restart:  &jobspec.RestartBlock{Mode: "on-failure"},
	}
	err := DaemonSetValidator{}.Validate(spec)
	if err == nil {
		t.Fatal("expected error for count on DaemonSet, got nil")
	}
	if !strings.Contains(err.Error(), "count not allowed") {
		t.Errorf("error = %q, want 'count not allowed'", err.Error())
	}
}

func TestDaemonSetValidator_MissingRestart(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:     "DaemonSet",
		Name:     "x",
		Schedule: &jobspec.ScheduleBlock{Mode: "every-node"},
	}
	err := DaemonSetValidator{}.Validate(spec)
	if err == nil {
		t.Fatal("expected error for missing restart on DaemonSet, got nil")
	}
	if !strings.Contains(err.Error(), "restart block required") {
		t.Errorf("error = %q, want 'restart block required'", err.Error())
	}
}

func TestDaemonSetValidator_NilSpec(t *testing.T) {
	v := DaemonSetValidator{}
	if err := v.Validate(nil); err == nil {
		t.Fatal("expected error for nil spec")
	}
}

func TestValidatorFor_EachKind(t *testing.T) {
	cases := []struct {
		kind string
		want string
	}{
		{"Job", "schema.JobValidator"},
		{"Service", "schema.ServiceValidator"},
		{"DaemonSet", "schema.DaemonSetValidator"},
	}
	for _, tc := range cases {
		t.Run(tc.kind, func(t *testing.T) {
			v, err := ValidatorFor(tc.kind)
			if err != nil {
				t.Fatalf("ValidatorFor(%q): %v", tc.kind, err)
			}
			got := fmtType(v)
			if got != tc.want {
				t.Errorf("ValidatorFor(%q) type = %q, want %q", tc.kind, got, tc.want)
			}
		})
	}
}

func TestValidatorFor_UnknownKind(t *testing.T) {
	_, err := ValidatorFor("CronJob")
	if err == nil {
		t.Fatal("expected error for unknown kind, got nil")
	}
	if !strings.Contains(err.Error(), "unknown kind") {
		t.Errorf("error = %q, want 'unknown kind'", err.Error())
	}
}

// fmtType returns a readable type name for a validator. Uses fmt.Sprintf
// with %T rather than reflection to keep the test surface minimal.
func fmtType(v Validator) string {
	switch v.(type) {
	case JobValidator:
		return "schema.JobValidator"
	case ServiceValidator:
		return "schema.ServiceValidator"
	case DaemonSetValidator:
		return "schema.DaemonSetValidator"
	default:
		return "unknown"
	}
}

// Ensure composeErrors returns nil for empty input (covers the
// short-circuit branch that the validators rely on).
func TestComposeErrors_Empty(t *testing.T) {
	if err := composeErrors("schema/X", nil); err != nil {
		t.Errorf("composeErrors(nil) = %v, want nil", err)
	}
	if err := composeErrors("schema/X", []string{}); err != nil {
		t.Errorf("composeErrors([]) = %v, want nil", err)
	}
}

// Ensure the error type returned by composeErrors is a non-nil error
// when violations are present (guards against accidental nil-return).
func TestComposeErrors_NonEmpty(t *testing.T) {
	err := composeErrors("schema/X", []string{"a", "b"})
	if err == nil {
		t.Fatal("expected non-nil error")
	}
	if !strings.Contains(err.Error(), "a") || !strings.Contains(err.Error(), "b") {
		t.Errorf("error = %q, want both 'a' and 'b'", err.Error())
	}
}

// Compile-time assertion that the validators implement the interface.
var (
	_ Validator = JobValidator{}
	_ Validator = ServiceValidator{}
	_ Validator = DaemonSetValidator{}
)

func TestTaskGroup_Valid(t *testing.T) {
	// P06: a valid task group — two tasks, each with a unique name
	// and a resolvable command (own command). The top-level runtime
	// is optional when each task carries its own.
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 1,
		Tasks: []jobspec.TaskGroupTask{
			{Name: "app", Command: "/usr/bin/httpd"},
			{Name: "sidecar", Command: "/bin/wasm-runner sidecar.wasm"},
		},
		Restart: &jobspec.RestartBlock{Mode: "service"},
		Update:  &jobspec.UpdateBlock{Strategy: "rolling"},
		Health:  &jobspec.HealthBlock{CheckType: "http"},
		Ports:   []jobspec.PortSpec{{Name: "http", Port: 8080}},
	}
	if err := (ServiceValidator{}).Validate(spec); err != nil {
		t.Fatalf("expected nil, got %v", err)
	}
}

func TestTaskGroup_ValidInheritsTopLevelRuntime(t *testing.T) {
	// P06: tasks without their own runtime inherit the top-level
	// runtime command. The validator accepts this as long as the
	// resolved command is non-empty.
	spec := &jobspec.WorkloadSpec{
		Kind:    "Service",
		Name:    "web",
		Count:   1,
		Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: "/bin/default"},
		Tasks: []jobspec.TaskGroupTask{
			{Name: "app"},
			{Name: "sidecar"},
		},
		Restart: &jobspec.RestartBlock{Mode: "service"},
		Update:  &jobspec.UpdateBlock{Strategy: "rolling"},
		Health:  &jobspec.HealthBlock{CheckType: "http"},
		Ports:   []jobspec.PortSpec{{Name: "http", Port: 8080}},
	}
	if err := (ServiceValidator{}).Validate(spec); err != nil {
		t.Fatalf("expected nil, got %v", err)
	}
}

func TestTaskGroup_ValidTaskRuntimeCommand(t *testing.T) {
	// P06: a task whose command is provided via the task's own
	// runtime.command (no top-level runtime) is valid.
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 1,
		Tasks: []jobspec.TaskGroupTask{
			{Name: "app", Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: "/usr/bin/httpd"}},
		},
		Restart: &jobspec.RestartBlock{Mode: "service"},
		Update:  &jobspec.UpdateBlock{Strategy: "rolling"},
		Health:  &jobspec.HealthBlock{CheckType: "http"},
		Ports:   []jobspec.PortSpec{{Name: "http", Port: 8080}},
	}
	if err := (ServiceValidator{}).Validate(spec); err != nil {
		t.Fatalf("expected nil, got %v", err)
	}
}

func TestTaskGroup_MissingTaskName(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind: "Service",
		Name: "web",
		Tasks: []jobspec.TaskGroupTask{
			{Command: "/usr/bin/httpd"},
			{Name: "sidecar", Command: "/bin/wasm-runner"},
		},
		Restart: &jobspec.RestartBlock{Mode: "service"},
		Update:  &jobspec.UpdateBlock{Strategy: "rolling"},
		Health:  &jobspec.HealthBlock{CheckType: "http"},
		Ports:   []jobspec.PortSpec{{Name: "http", Port: 8080}},
	}
	err := ServiceValidator{}.Validate(spec)
	if err == nil {
		t.Fatal("expected error for missing task name, got nil")
	}
	if !strings.Contains(err.Error(), "name is required") {
		t.Errorf("error = %q, want 'name is required'", err.Error())
	}
}

func TestTaskGroup_DuplicateTaskNames(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind: "Service",
		Name: "web",
		Tasks: []jobspec.TaskGroupTask{
			{Name: "app", Command: "/usr/bin/httpd"},
			{Name: "app", Command: "/bin/other"},
		},
		Restart: &jobspec.RestartBlock{Mode: "service"},
		Update:  &jobspec.UpdateBlock{Strategy: "rolling"},
		Health:  &jobspec.HealthBlock{CheckType: "http"},
		Ports:   []jobspec.PortSpec{{Name: "http", Port: 8080}},
	}
	err := ServiceValidator{}.Validate(spec)
	if err == nil {
		t.Fatal("expected error for duplicate task names, got nil")
	}
	if !strings.Contains(err.Error(), "duplicate task name") {
		t.Errorf("error = %q, want 'duplicate task name'", err.Error())
	}
}

func TestTaskGroup_MissingCommand(t *testing.T) {
	// P06: a task with no resolvable command (no task.Command, no
	// task.Runtime, no top-level Runtime) is rejected.
	spec := &jobspec.WorkloadSpec{
		Kind: "Service",
		Name: "web",
		Tasks: []jobspec.TaskGroupTask{
			{Name: "app"},
		},
		Restart: &jobspec.RestartBlock{Mode: "service"},
		Update:  &jobspec.UpdateBlock{Strategy: "rolling"},
		Health:  &jobspec.HealthBlock{CheckType: "http"},
		Ports:   []jobspec.PortSpec{{Name: "http", Port: 8080}},
	}
	err := ServiceValidator{}.Validate(spec)
	if err == nil {
		t.Fatal("expected error for missing task command, got nil")
	}
	if !strings.Contains(err.Error(), "command is required") {
		t.Errorf("error = %q, want 'command is required'", err.Error())
	}
}

func TestTaskGroup_JobAcceptsTaskGroup(t *testing.T) {
	// P06: task groups apply to all kinds, not just Service. Job
	// accepts a task group with unique names + resolvable commands.
	spec := &jobspec.WorkloadSpec{
		Kind:  "Job",
		Name:  "batch",
		Count: 1,
		Tasks: []jobspec.TaskGroupTask{
			{Name: "step1", Command: "/bin/extract"},
			{Name: "step2", Command: "/bin/transform"},
		},
	}
	if err := (JobValidator{}).Validate(spec); err != nil {
		t.Fatalf("expected nil, got %v", err)
	}
}

func TestTaskGroup_DaemonSetAcceptsTaskGroup(t *testing.T) {
	// P06: DaemonSet accepts a task group.
	spec := &jobspec.WorkloadSpec{
		Kind:     "DaemonSet",
		Name:     "log-shipper",
		Schedule: &jobspec.ScheduleBlock{Mode: "every-node"},
		Restart:  &jobspec.RestartBlock{Mode: "on-failure"},
		Tasks: []jobspec.TaskGroupTask{
			{Name: "collector", Command: "/bin/collect"},
			{Name: "forwarder", Command: "/bin/forward"},
		},
	}
	if err := (DaemonSetValidator{}).Validate(spec); err != nil {
		t.Fatalf("expected nil, got %v", err)
	}
}

func TestTaskGroup_NoTasksBackwardCompat(t *testing.T) {
	// Backward compat: a spec with no Tasks is validated by the
	// existing kind-specific rules (no task-group check fires).
	spec := &jobspec.WorkloadSpec{
		Kind:    "Job",
		Name:    "backup",
		Count:   1,
		Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: "/bin/rsync"},
	}
	if err := (JobValidator{}).Validate(spec); err != nil {
		t.Fatalf("expected nil, got %v", err)
	}
}
