package capacitycontroller

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

func TestCalculatePlanRespectsResourceDatabaseAndStepLimits(t *testing.T) {
	input := PlanInput{
		Queued: 1000, Running: 48, InstanceSlots: 24,
		MemoryTargetPercent: 75, MemoryHardPercent: 85, CPUTargetPercent: 70,
		DatabaseConnections: 80, DatabaseConnectionBudget: 150, WorkerDatabasePoolMax: 24,
		SynchronousDatabasePeers: 1, ScaleUpEligible: true,
		Pools: []PoolResources{
			{
				PoolID: "ningbo", CurrentReplicas: 1, MinReplicas: 1, MaxReplicas: 4,
				AllocatableMemoryBytes: 8 << 30, UsedMemoryBytes: 6 << 30,
				WorkerRequestMemoryBytes: 1 << 30,
				AllocatableMilliCPU:      4000, UsedMilliCPU: 2000, WorkerRequestMilliCPU: 500,
			},
			{
				PoolID: "hongkong", CurrentReplicas: 1, MinReplicas: 1, MaxReplicas: 4,
				AllocatableMemoryBytes: 8 << 30, UsedMemoryBytes: 3 << 30,
				WorkerRequestMemoryBytes: 1 << 30,
				AllocatableMilliCPU:      4000, UsedMilliCPU: 500, WorkerRequestMilliCPU: 500,
			},
		},
	}
	plan := CalculatePlan(input)
	if plan.RawDesired != 22 {
		t.Fatalf("raw desired=%d, want 22", plan.RawDesired)
	}
	if plan.DesiredTotal != 4 {
		t.Fatalf("desired total=%d, want step-limited 4: %+v", plan.DesiredTotal, plan)
	}
	if plan.Pools[0].PoolID != "hongkong" || plan.Pools[0].DesiredReplicas != 3 {
		t.Fatalf("pool allocation=%+v, want hongkong=3 ningbo=1", plan.Pools)
	}
}

func TestCalculatePlanFreezesScaleUpOnHardMemoryPressure(t *testing.T) {
	plan := CalculatePlan(PlanInput{
		Queued: 200, InstanceSlots: 24,
		DatabaseConnections: 10, DatabaseConnectionBudget: 150, WorkerDatabasePoolMax: 24,
		SynchronousDatabasePeers: 1, ScaleUpEligible: true,
		Pools: []PoolResources{{
			PoolID: "ningbo", CurrentReplicas: 1, MinReplicas: 1, MaxReplicas: 4,
			AllocatableMemoryBytes: 8 << 30, UsedMemoryBytes: 7 << 30,
			WorkerRequestMemoryBytes: 1 << 30,
		}},
	})
	if plan.DesiredTotal != 1 || plan.FrozenReason != "node_memory_pressure" {
		t.Fatalf("plan=%+v, want frozen at one replica", plan)
	}
}

func TestCalculatePlanReportsConfiguredMinimumOutsideResourceBudget(t *testing.T) {
	plan := CalculatePlan(PlanInput{
		Queued: 200, InstanceSlots: 24,
		DatabaseConnections: 10, DatabaseConnectionBudget: 150, WorkerDatabasePoolMax: 24,
		SynchronousDatabasePeers: 1, ScaleUpEligible: true,
		Pools: []PoolResources{{
			PoolID: "ningbo", CurrentReplicas: 1, MinReplicas: 1, MaxReplicas: 4,
			AllocatableMemoryBytes: 8 << 30, UsedMemoryBytes: 13 << 29,
			WorkerRequestMemoryBytes: 2 << 30,
			AllocatableMilliCPU:      4000, UsedMilliCPU: 2800, WorkerRequestMilliCPU: 500,
		}},
	})
	if plan.DesiredTotal != 1 || plan.FrozenReason != "pool_resource_budget" {
		t.Fatalf("plan=%+v, want the existing minimum preserved and expansion frozen", plan)
	}
}

func TestCalculatePlanWaitsForSafeScaleDownWindow(t *testing.T) {
	input := PlanInput{
		InstanceSlots: 24, DatabaseConnections: 20, DatabaseConnectionBudget: 150,
		WorkerDatabasePoolMax: 24, SynchronousDatabasePeers: 1,
		ScaleDownEligible: false,
		Pools: []PoolResources{
			{PoolID: "ningbo", CurrentReplicas: 2, MinReplicas: 1, MaxReplicas: 4},
			{PoolID: "hongkong", CurrentReplicas: 2, MinReplicas: 1, MaxReplicas: 4},
		},
	}
	plan := CalculatePlan(input)
	if plan.DesiredTotal != 4 || plan.FrozenReason != "scale_down_stabilization" {
		t.Fatalf("plan=%+v, want current replicas during stabilization", plan)
	}
	input.ScaleDownEligible = true
	plan = CalculatePlan(input)
	if plan.DesiredTotal != 2 {
		t.Fatalf("desired total=%d, want min total 2", plan.DesiredTotal)
	}
}

func TestCalculatePlanAddsCapacityAcrossLabeledSiteNodes(t *testing.T) {
	plan := CalculatePlan(PlanInput{
		Queued: 500, InstanceSlots: 24,
		MemoryTargetPercent: 75, MemoryHardPercent: 85, CPUTargetPercent: 70,
		DatabaseConnections: 20, DatabaseConnectionBudget: 150, WorkerDatabasePoolMax: 20,
		SynchronousDatabasePeers: 1, ScaleUpEligible: true,
		Pools: []PoolResources{{
			PoolID: "hongkong", CurrentReplicas: 2, MinReplicas: 1, MaxReplicas: 8,
			WorkerRequestMemoryBytes: 1 << 30, WorkerRequestMilliCPU: 500,
			Nodes: []NodeResources{
				{
					NodeName: "hk-a", CurrentReplicas: 1,
					AllocatableMemoryBytes: 8 << 30, UsedMemoryBytes: 3 << 30,
					AllocatableMilliCPU: 4000, UsedMilliCPU: 1000,
				},
				{
					NodeName: "hk-b", CurrentReplicas: 1,
					AllocatableMemoryBytes: 8 << 30, UsedMemoryBytes: 2 << 30,
					AllocatableMilliCPU: 4000, UsedMilliCPU: 500,
				},
			},
		}},
	})
	if plan.Pools[0].ResourceMax != 7 {
		t.Fatalf("resource max=%d, want database-budgeted multi-node maximum: %+v", plan.Pools[0].ResourceMax, plan)
	}
	if plan.DesiredTotal != 4 {
		t.Fatalf("desired=%d, want two-wave step from 2 to 4", plan.DesiredTotal)
	}
}

func TestCalculatePlanCapsReplicaMaximumByDeclaredDatabasePools(t *testing.T) {
	plan := CalculatePlan(PlanInput{
		Queued: 500, InstanceSlots: 24,
		DatabaseConnections: 20, DatabaseConnectionBudget: 150,
		NonWorkerConnectionBudget: 72, WorkerDatabasePoolMax: 32,
		SynchronousDatabasePeers: 1, ScaleUpEligible: true,
		Pools: []PoolResources{
			{PoolID: "ningbo", CurrentReplicas: 1, MinReplicas: 1, MaxReplicas: 4},
			{PoolID: "hongkong", CurrentReplicas: 1, MinReplicas: 1, MaxReplicas: 4},
		},
	})
	if plan.DesiredTotal != 2 {
		t.Fatalf("desired=%d, want the current 1+1 database-safe topology: %+v", plan.DesiredTotal, plan)
	}
	resourceMax := 0
	for _, pool := range plan.Pools {
		resourceMax += pool.ResourceMax
	}
	if resourceMax != 2 {
		t.Fatalf("resource maximum=%d, want floor((150-72)/32)=2: %+v", resourceMax, plan)
	}
}

func TestPlanJSONUsesAcceptanceContractFieldNames(t *testing.T) {
	payload, err := json.Marshal(Plan{
		RawDesired: 3,
		Pools:      []PoolPlan{{PoolID: "hongkong", ResourceMax: 2}},
	})
	if err != nil {
		t.Fatal(err)
	}
	text := string(payload)
	for _, field := range []string{`"rawDesired":3`, `"pools"`, `"poolId":"hongkong"`, `"resourceMax":2`} {
		if !strings.Contains(text, field) {
			t.Fatalf("plan JSON %s does not contain %s", text, field)
		}
	}
}
