package schema

import (
	"strings"
	"testing"

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

func TestUpdateValidator_ValidRolling(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 4,
		Update: &jobspec.UpdateBlock{
			Strategy:        "rolling",
			MaxParallel:     2,
			MinHealthyTime:  "30s",
			HealthyDeadline: "5m",
		},
	}
	v := UpdateValidator{}
	if err := v.Validate(spec); err != nil {
		t.Fatalf("expected nil, got %v", err)
	}
}

func TestUpdateValidator_ValidCanary(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 4,
		Update: &jobspec.UpdateBlock{
			Strategy:        "canary",
			MaxParallel:     2,
			MinHealthyTime:  "30s",
			HealthyDeadline: "5m",
			Canary:          "10%",
			AutoPromote:     true,
		},
	}
	if err := (UpdateValidator{}).Validate(spec); err != nil {
		t.Fatalf("expected nil, got %v", err)
	}
}

func TestUpdateValidator_ValidBlueGreen(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 3,
		Update: &jobspec.UpdateBlock{
			Strategy:        "blue-green",
			MinHealthyTime:  "1m",
			HealthyDeadline: "10m",
		},
	}
	if err := (UpdateValidator{}).Validate(spec); err != nil {
		t.Fatalf("expected nil, got %v", err)
	}
}

func TestUpdateValidator_ValidCanaryIntegerCount(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 4,
		Update: &jobspec.UpdateBlock{
			Strategy: "canary",
			Canary:   "1",
		},
	}
	if err := (UpdateValidator{}).Validate(spec); err != nil {
		t.Fatalf("integer canary count 1 should be valid, got %v", err)
	}
}

func TestUpdateValidator_ValidCanaryZeroPercent(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 4,
		Update: &jobspec.UpdateBlock{
			Strategy: "canary",
			Canary:   "0%",
		},
	}
	if err := (UpdateValidator{}).Validate(spec); err != nil {
		t.Fatalf("0%% canary should be valid, got %v", err)
	}
}

func TestUpdateValidator_ValidCanaryHundredPercent(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 4,
		Update: &jobspec.UpdateBlock{
			Strategy: "canary",
			Canary:   "100%",
		},
	}
	if err := (UpdateValidator{}).Validate(spec); err != nil {
		t.Fatalf("100%% canary should be valid, got %v", err)
	}
}

func TestUpdateValidator_EmptyDurationsOK(t *testing.T) {
	// Empty min_healthy_time / healthy_deadline should be accepted
	// (they are optional; defaults are applied by the executor).
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 2,
		Update: &jobspec.UpdateBlock{
			Strategy: "rolling",
		},
	}
	if err := (UpdateValidator{}).Validate(spec); err != nil {
		t.Fatalf("empty durations should be valid, got %v", err)
	}
}

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

func TestUpdateValidator_NilUpdateBlock(t *testing.T) {
	spec := &jobspec.WorkloadSpec{Kind: "Service", Name: "web", Count: 2}
	err := (UpdateValidator{}).Validate(spec)
	if err == nil {
		t.Fatal("expected error for nil update block")
	}
	if !strings.Contains(err.Error(), "update block is nil") {
		t.Errorf("error = %q, want 'update block is nil'", err.Error())
	}
}

func TestUpdateValidator_InvalidStrategy(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 2,
		Update: &jobspec.UpdateBlock{
			Strategy: "recreate",
		},
	}
	err := (UpdateValidator{}).Validate(spec)
	if err == nil {
		t.Fatal("expected error for invalid strategy")
	}
	if !strings.Contains(err.Error(), "strategy") || !strings.Contains(err.Error(), "invalid") {
		t.Errorf("error = %q, want 'strategy ... invalid'", err.Error())
	}
}

func TestUpdateValidator_EmptyStrategy(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 2,
		Update: &jobspec.UpdateBlock{
			Strategy: "",
		},
	}
	err := (UpdateValidator{}).Validate(spec)
	if err == nil {
		t.Fatal("expected error for empty strategy")
	}
	if !strings.Contains(err.Error(), "strategy required") {
		t.Errorf("error = %q, want 'strategy required'", err.Error())
	}
}

func TestUpdateValidator_MaxParallelZero(t *testing.T) {
	// max_parallel=0 means "unset" → default 1; accepted.
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 2,
		Update: &jobspec.UpdateBlock{
			Strategy:    "rolling",
			MaxParallel: 0,
		},
	}
	if err := (UpdateValidator{}).Validate(spec); err != nil {
		t.Fatalf("max_parallel=0 (unset) should be valid, got %v", err)
	}
}

func TestUpdateValidator_MaxParallelNegative(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 2,
		Update: &jobspec.UpdateBlock{
			Strategy:    "rolling",
			MaxParallel: -1,
		},
	}
	err := (UpdateValidator{}).Validate(spec)
	if err == nil {
		t.Fatal("expected error for negative max_parallel")
	}
	if !strings.Contains(err.Error(), "max_parallel") {
		t.Errorf("error = %q, want 'max_parallel'", err.Error())
	}
}

func TestUpdateValidator_MaxParallelExceedsCount(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 2,
		Update: &jobspec.UpdateBlock{
			Strategy:    "rolling",
			MaxParallel: 5,
		},
	}
	err := (UpdateValidator{}).Validate(spec)
	if err == nil {
		t.Fatal("expected error for max_parallel > count")
	}
	if !strings.Contains(err.Error(), "exceeds count") {
		t.Errorf("error = %q, want 'exceeds count'", err.Error())
	}
}

func TestUpdateValidator_MaxParallelEqualsCount(t *testing.T) {
	// max_parallel == count is the upper bound; valid.
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 3,
		Update: &jobspec.UpdateBlock{
			Strategy:    "rolling",
			MaxParallel: 3,
		},
	}
	if err := (UpdateValidator{}).Validate(spec); err != nil {
		t.Fatalf("max_parallel==count should be valid, got %v", err)
	}
}

func TestUpdateValidator_InvalidMinHealthyTime(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 2,
		Update: &jobspec.UpdateBlock{
			Strategy:       "rolling",
			MinHealthyTime: "not-a-duration",
		},
	}
	err := (UpdateValidator{}).Validate(spec)
	if err == nil {
		t.Fatal("expected error for invalid min_healthy_time")
	}
	if !strings.Contains(err.Error(), "min_healthy_time") {
		t.Errorf("error = %q, want 'min_healthy_time'", err.Error())
	}
}

func TestUpdateValidator_InvalidHealthyDeadline(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 2,
		Update: &jobspec.UpdateBlock{
			Strategy:        "rolling",
			HealthyDeadline: "nope",
		},
	}
	err := (UpdateValidator{}).Validate(spec)
	if err == nil {
		t.Fatal("expected error for invalid healthy_deadline")
	}
	if !strings.Contains(err.Error(), "healthy_deadline") {
		t.Errorf("error = %q, want 'healthy_deadline'", err.Error())
	}
}

func TestUpdateValidator_CanaryNegative(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 4,
		Update: &jobspec.UpdateBlock{
			Strategy: "canary",
			Canary:   "-1",
		},
	}
	err := (UpdateValidator{}).Validate(spec)
	if err == nil {
		t.Fatal("expected error for negative canary count")
	}
	if !strings.Contains(err.Error(), "canary") {
		t.Errorf("error = %q, want 'canary'", err.Error())
	}
}

func TestUpdateValidator_CanaryExceedsCount(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 4,
		Update: &jobspec.UpdateBlock{
			Strategy: "canary",
			Canary:   "5",
		},
	}
	err := (UpdateValidator{}).Validate(spec)
	if err == nil {
		t.Fatal("expected error for canary > count")
	}
	if !strings.Contains(err.Error(), "exceeds count") {
		t.Errorf("error = %q, want 'exceeds count'", err.Error())
	}
}

func TestUpdateValidator_CanaryPercentOver100(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 4,
		Update: &jobspec.UpdateBlock{
			Strategy: "canary",
			Canary:   "150%",
		},
	}
	err := (UpdateValidator{}).Validate(spec)
	if err == nil {
		t.Fatal("expected error for canary > 100%")
	}
	if !strings.Contains(err.Error(), "out of range") {
		t.Errorf("error = %q, want 'out of range'", err.Error())
	}
}

func TestUpdateValidator_CanaryPercentNegative(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 4,
		Update: &jobspec.UpdateBlock{
			Strategy: "canary",
			Canary:   "-10%",
		},
	}
	err := (UpdateValidator{}).Validate(spec)
	if err == nil {
		t.Fatal("expected error for negative canary percent")
	}
	if !strings.Contains(err.Error(), "out of range") {
		t.Errorf("error = %q, want 'out of range'", err.Error())
	}
}

func TestUpdateValidator_CanaryNotANumber(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 4,
		Update: &jobspec.UpdateBlock{
			Strategy: "canary",
			Canary:   "abc",
		},
	}
	err := (UpdateValidator{}).Validate(spec)
	if err == nil {
		t.Fatal("expected error for non-numeric canary")
	}
	if !strings.Contains(err.Error(), "not a valid") {
		t.Errorf("error = %q, want 'not a valid'", err.Error())
	}
}

func TestUpdateValidator_CanaryPercentNotANumber(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 4,
		Update: &jobspec.UpdateBlock{
			Strategy: "canary",
			Canary:   "xx%",
		},
	}
	err := (UpdateValidator{}).Validate(spec)
	if err == nil {
		t.Fatal("expected error for non-numeric canary percent")
	}
	if !strings.Contains(err.Error(), "not a valid percentage") {
		t.Errorf("error = %q, want 'not a valid percentage'", err.Error())
	}
}

func TestUpdateValidator_CanaryCountZeroOK(t *testing.T) {
	// canary=0 is the lower bound; valid.
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 4,
		Update: &jobspec.UpdateBlock{
			Strategy: "canary",
			Canary:   "0",
		},
	}
	if err := (UpdateValidator{}).Validate(spec); err != nil {
		t.Fatalf("canary=0 should be valid, got %v", err)
	}
}

func TestUpdateValidator_CanaryPercentWithoutCountOK(t *testing.T) {
	// A percentage canary does not depend on count; valid even when
	// count is 0 (e.g. DaemonSet-shaped spec bypassing the Service
	// validator — defensive).
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 0,
		Update: &jobspec.UpdateBlock{
			Strategy: "canary",
			Canary:   "25%",
		},
	}
	if err := (UpdateValidator{}).Validate(spec); err != nil {
		t.Fatalf("percentage canary with count=0 should be valid, got %v", err)
	}
}

func TestUpdateValidator_MultipleErrors(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 2,
		Update: &jobspec.UpdateBlock{
			Strategy:        "recreate",
			MaxParallel:     99,
			MinHealthyTime:  "nope",
			HealthyDeadline: "also-nope",
			Canary:          "200%",
		},
	}
	err := (UpdateValidator{}).Validate(spec)
	if err == nil {
		t.Fatal("expected error, got nil")
	}
	for _, want := range []string{
		"strategy",
		"max_parallel",
		"min_healthy_time",
		"healthy_deadline",
		"canary",
	} {
		if !strings.Contains(err.Error(), want) {
			t.Errorf("error %q missing %q", err.Error(), want)
		}
	}
}

// Compile-time assertion that UpdateValidator implements Validator.
var _ Validator = UpdateValidator{}
