package emitter

import (
	"strings"
	"testing"

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

func TestRenderUpdatePlan_RollingBatches(t *testing.T) {
	// count=4, max_parallel=2 → 2 batches of 2.
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 4,
		Update: &jobspec.UpdateBlock{
			Strategy:    "rolling",
			MaxParallel: 2,
		},
	}
	plan, err := RenderUpdatePlan(spec)
	if err != nil {
		t.Fatalf("RenderUpdatePlan: %v", err)
	}
	if len(plan.Steps) != 2 {
		t.Fatalf("got %d steps, want 2", len(plan.Steps))
	}
	for i, s := range plan.Steps {
		if s.Action != "start" {
			t.Errorf("step %d action = %q, want start", i, s.Action)
		}
		if !s.WaitForHealthy {
			t.Errorf("step %d WaitForHealthy = false, want true", i)
		}
		if len(s.Allocs) != 2 {
			t.Errorf("step %d allocs = %d, want 2", i, len(s.Allocs))
		}
	}
	if plan.Steps[0].Allocs[0] != "web-1" || plan.Steps[0].Allocs[1] != "web-2" {
		t.Errorf("step 0 allocs = %v, want [web-1 web-2]", plan.Steps[0].Allocs)
	}
	if plan.Steps[1].Allocs[0] != "web-3" || plan.Steps[1].Allocs[1] != "web-4" {
		t.Errorf("step 1 allocs = %v, want [web-3 web-4]", plan.Steps[1].Allocs)
	}
}

func TestRenderUpdatePlan_RollingUnevenBatches(t *testing.T) {
	// count=5, max_parallel=2 → 3 batches: 2, 2, 1.
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 5,
		Update: &jobspec.UpdateBlock{
			Strategy:    "rolling",
			MaxParallel: 2,
		},
	}
	plan, err := RenderUpdatePlan(spec)
	if err != nil {
		t.Fatalf("RenderUpdatePlan: %v", err)
	}
	if len(plan.Steps) != 3 {
		t.Fatalf("got %d steps, want 3", len(plan.Steps))
	}
	if len(plan.Steps[2].Allocs) != 1 {
		t.Errorf("step 2 allocs = %d, want 1 (remainder)", len(plan.Steps[2].Allocs))
	}
	if plan.Steps[2].Allocs[0] != "web-5" {
		t.Errorf("step 2 allocs = %v, want [web-5]", plan.Steps[2].Allocs)
	}
}

func TestRenderUpdatePlan_RollingMaxParallelUnset(t *testing.T) {
	// max_parallel unset (0) → default 1 → 4 batches of 1.
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 4,
		Update: &jobspec.UpdateBlock{
			Strategy: "rolling",
		},
	}
	plan, err := RenderUpdatePlan(spec)
	if err != nil {
		t.Fatalf("RenderUpdatePlan: %v", err)
	}
	if len(plan.Steps) != 4 {
		t.Fatalf("got %d steps, want 4 (one per alloc, batch=1)", len(plan.Steps))
	}
	for _, s := range plan.Steps {
		if len(s.Allocs) != 1 {
			t.Errorf("allocs = %d, want 1", len(s.Allocs))
		}
	}
}

func TestRenderUpdatePlan_CanaryDefaultOne(t *testing.T) {
	// canary unset → default 1 canary alloc, then 3 in batches of 2.
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 4,
		Update: &jobspec.UpdateBlock{
			Strategy:    "canary",
			MaxParallel: 2,
		},
	}
	plan, err := RenderUpdatePlan(spec)
	if err != nil {
		t.Fatalf("RenderUpdatePlan: %v", err)
	}
	// Step 0: canary (1 alloc). Step 1: promote. Steps 2..: remaining.
	if plan.Steps[0].Action != "start" || len(plan.Steps[0].Allocs) != 1 {
		t.Errorf("step 0 = %+v, want canary start with 1 alloc", plan.Steps[0])
	}
	if plan.Steps[0].Allocs[0] != "web-1" {
		t.Errorf("canary alloc = %q, want web-1", plan.Steps[0].Allocs[0])
	}
	if !plan.Steps[0].WaitForHealthy {
		t.Error("canary step should wait for healthy")
	}
	if plan.Steps[1].Action != "promote" {
		t.Errorf("step 1 action = %q, want promote", plan.Steps[1].Action)
	}
	// Remaining: web-2, web-3, web-4 in batches of 2 → [web-2,web-3], [web-4].
	if len(plan.Steps) != 4 {
		t.Fatalf("got %d steps, want 4 (canary + promote + 2 batches)", len(plan.Steps))
	}
	if len(plan.Steps[2].Allocs) != 2 || plan.Steps[2].Allocs[0] != "web-2" {
		t.Errorf("step 2 = %v, want [web-2 web-3]", plan.Steps[2].Allocs)
	}
	if len(plan.Steps[3].Allocs) != 1 || plan.Steps[3].Allocs[0] != "web-4" {
		t.Errorf("step 3 = %v, want [web-4]", plan.Steps[3].Allocs)
	}
}

func TestRenderUpdatePlan_CanaryPercent(t *testing.T) {
	// count=10, canary=20% → 2 canary allocs, then 8 in batches of 3.
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 10,
		Update: &jobspec.UpdateBlock{
			Strategy:    "canary",
			MaxParallel: 3,
			Canary:      "20%",
		},
	}
	plan, err := RenderUpdatePlan(spec)
	if err != nil {
		t.Fatalf("RenderUpdatePlan: %v", err)
	}
	if len(plan.Steps[0].Allocs) != 2 {
		t.Errorf("canary step allocs = %d, want 2 (20%% of 10)", len(plan.Steps[0].Allocs))
	}
	// Remaining 8 in batches of 3 → ceil(8/3)=3 batches.
	// Steps: canary, promote, batch(3), batch(3), batch(2) = 5 steps.
	if len(plan.Steps) != 5 {
		t.Fatalf("got %d steps, want 5", len(plan.Steps))
	}
}

func TestRenderUpdatePlan_CanaryIntegerCount(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 4,
		Update: &jobspec.UpdateBlock{
			Strategy: "canary",
			Canary:   "2",
		},
	}
	plan, err := RenderUpdatePlan(spec)
	if err != nil {
		t.Fatalf("RenderUpdatePlan: %v", err)
	}
	if len(plan.Steps[0].Allocs) != 2 {
		t.Errorf("canary step allocs = %d, want 2", len(plan.Steps[0].Allocs))
	}
	if plan.Steps[0].Allocs[0] != "web-1" || plan.Steps[0].Allocs[1] != "web-2" {
		t.Errorf("canary allocs = %v, want [web-1 web-2]", plan.Steps[0].Allocs)
	}
}

func TestRenderUpdatePlan_CanaryFullCount(t *testing.T) {
	// canary == count → no remaining allocs after canary.
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 3,
		Update: &jobspec.UpdateBlock{
			Strategy: "canary",
			Canary:   "3",
		},
	}
	plan, err := RenderUpdatePlan(spec)
	if err != nil {
		t.Fatalf("RenderUpdatePlan: %v", err)
	}
	// Steps: canary start (3), promote. No remaining batches.
	if len(plan.Steps) != 2 {
		t.Fatalf("got %d steps, want 2 (canary + promote, no remainder)", len(plan.Steps))
	}
	if plan.Steps[1].Action != "promote" {
		t.Errorf("step 1 action = %q, want promote", plan.Steps[1].Action)
	}
}

func TestRenderUpdatePlan_BlueGreen(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 4,
		Update: &jobspec.UpdateBlock{
			Strategy: "blue-green",
		},
	}
	plan, err := RenderUpdatePlan(spec)
	if err != nil {
		t.Fatalf("RenderUpdatePlan: %v", err)
	}
	// Step 0: start all 4 in parallel. Step 1: cutover.
	if len(plan.Steps) != 2 {
		t.Fatalf("got %d steps, want 2", len(plan.Steps))
	}
	if plan.Steps[0].Action != "start" {
		t.Errorf("step 0 action = %q, want start", plan.Steps[0].Action)
	}
	if len(plan.Steps[0].Allocs) != 4 {
		t.Errorf("step 0 allocs = %d, want 4 (all new in parallel)", len(plan.Steps[0].Allocs))
	}
	if !plan.Steps[0].WaitForHealthy {
		t.Error("blue-green start step should wait for healthy")
	}
	if plan.Steps[1].Action != "cutover" {
		t.Errorf("step 1 action = %q, want cutover", plan.Steps[1].Action)
	}
	if plan.Steps[1].WaitForHealthy {
		t.Error("cutover step should NOT wait for healthy (already healthy)")
	}
}

func TestRenderUpdatePlan_BlueGreenAllocs(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "api",
		Count: 3,
		Update: &jobspec.UpdateBlock{
			Strategy: "blue-green",
		},
	}
	plan, err := RenderUpdatePlan(spec)
	if err != nil {
		t.Fatalf("RenderUpdatePlan: %v", err)
	}
	want := []string{"api-1", "api-2", "api-3"}
	if len(plan.Steps[0].Allocs) != 3 {
		t.Errorf("allocs = %v, want %v", plan.Steps[0].Allocs, want)
	}
	for i, a := range want {
		if plan.Steps[0].Allocs[i] != a {
			t.Errorf("alloc[%d] = %q, want %q", i, plan.Steps[0].Allocs[i], a)
		}
	}
}

func TestRenderUpdatePlan_NilSpec(t *testing.T) {
	_, err := RenderUpdatePlan(nil)
	if err == nil {
		t.Fatal("expected error for nil spec")
	}
	if !strings.Contains(err.Error(), "spec is nil") {
		t.Errorf("error = %q, want 'spec is nil'", err.Error())
	}
}

func TestRenderUpdatePlan_NilUpdate(t *testing.T) {
	spec := &jobspec.WorkloadSpec{Kind: "Service", Name: "web", Count: 1}
	_, err := RenderUpdatePlan(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 TestRenderUpdatePlan_CountZero(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 0,
		Update: &jobspec.UpdateBlock{
			Strategy: "rolling",
		},
	}
	_, err := RenderUpdatePlan(spec)
	if err == nil {
		t.Fatal("expected error for count 0")
	}
	if !strings.Contains(err.Error(), "count must be") {
		t.Errorf("error = %q, want 'count must be'", err.Error())
	}
}

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

func TestRenderUpdatePlan_AllStrategies(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: 3,
				Update: &jobspec.UpdateBlock{
					Strategy:    strat,
					MaxParallel: 1,
				},
			}
			plan, err := RenderUpdatePlan(spec)
			if err != nil {
				t.Fatalf("RenderUpdatePlan(%s): %v", strat, err)
			}
			if len(plan.Steps) == 0 {
				t.Errorf("strategy %s produced 0 steps", strat)
			}
		})
	}
}

func TestRenderUpdatePlan_PromoteStepCarriesCanaryAllocs(t *testing.T) {
	// The promote step lists the canary allocs so the executor knows
	// which allocs are being promoted from canary to stable.
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 4,
		Update: &jobspec.UpdateBlock{
			Strategy: "canary",
			Canary:   "2",
		},
	}
	plan, err := RenderUpdatePlan(spec)
	if err != nil {
		t.Fatalf("RenderUpdatePlan: %v", err)
	}
	promote := plan.Steps[1]
	if promote.Action != "promote" {
		t.Fatalf("step 1 action = %q, want promote", promote.Action)
	}
	if len(promote.Allocs) != 2 {
		t.Errorf("promote allocs = %d, want 2 (the canary allocs)", len(promote.Allocs))
	}
}

func TestRenderUpdatePlan_MaxParallelClampedToCount(t *testing.T) {
	// max_parallel > count is clamped to count (defensive; validator
	// rejects this but the emitter is defensive against direct
	// callers).
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Count: 2,
		Update: &jobspec.UpdateBlock{
			Strategy:    "rolling",
			MaxParallel: 99,
		},
	}
	plan, err := RenderUpdatePlan(spec)
	if err != nil {
		t.Fatalf("RenderUpdatePlan: %v", err)
	}
	// Clamped to 2 → single batch of 2.
	if len(plan.Steps) != 1 {
		t.Errorf("got %d steps, want 1 (clamped)", len(plan.Steps))
	}
	if len(plan.Steps[0].Allocs) != 2 {
		t.Errorf("step 0 allocs = %d, want 2", len(plan.Steps[0].Allocs))
	}
}
