/*
 * Copyright (c) 2025 Huawei Technologies Co., Ltd.
 * openFuyao is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *          http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package controller

import (
	"context"
	"testing"
	"time"

	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"

	serverlessdbv1 "serverlessdb/api/v1"
	"serverlessdb/internal/testutil"
)

func newTestScheme() *runtime.Scheme {
	return testutil.NewTestScheme()
}

func newFakeClient(objs ...client.Object) client.Client {
	return testutil.NewFakeClient(objs...)
}

func poolWithImage(image string) *serverlessdbv1.DBResourcePool {
	return &serverlessdbv1.DBResourcePool{
		ObjectMeta: metav1.ObjectMeta{Name: "pool1", Namespace: "ns"},
		Spec: serverlessdbv1.DBResourcePoolSpec{
			PodTemplate: corev1.PodTemplateSpec{
				Spec: corev1.PodSpec{
					Containers: []corev1.Container{{Name: serverlessdbv1.ContainerNameCompute, Image: image}},
				},
			},
		},
	}
}

func buildPodForTest(t *testing.T, pool *serverlessdbv1.DBResourcePool, instance *serverlessdbv1.DBInstance) *corev1.Pod {
	t.Helper()
	pod, err := buildPod(pool, instance, newTestScheme())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	return pod
}

func computeRes(name string, res serverlessdbv1.InstanceResources) serverlessdbv1.ContainerResources {
	return serverlessdbv1.ContainerResources{Name: name, Resources: res}
}

func syncStatusReconciler(pod *corev1.Pod, instance *serverlessdbv1.DBInstance) (*DBResourcePoolReconciler, client.Client) {
	cl := newFakeClient(pod, instance)
	r := &DBResourcePoolReconciler{Client: cl, Scheme: newTestScheme()}
	return r, cl
}

func runSyncStatuses(r *DBResourcePoolReconciler, instance *serverlessdbv1.DBInstance) {
	pool := &serverlessdbv1.DBResourcePool{ObjectMeta: metav1.ObjectMeta{Name: "pool1", Namespace: "ns"}}
	instanceList := &serverlessdbv1.DBInstanceList{Items: []serverlessdbv1.DBInstance{*instance}}
	r.syncInstanceStatuses(context.Background(), pool, instanceList, 5432)
}

func resizeTestSetup(specJSON string) (*DBResourcePoolReconciler, *serverlessdbv1.DBInstance, *corev1.Pod) {
	pod := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{Name: "ins1", Namespace: "ns"},
		Spec: corev1.PodSpec{Containers: []corev1.Container{
			{Name: serverlessdbv1.ContainerNameCompute, Resources: corev1.ResourceRequirements{
				Requests: corev1.ResourceList{
					corev1.ResourceCPU:    resource.MustParse("1000m"),
					corev1.ResourceMemory: resource.MustParse("1Gi"),
				},
				Limits: corev1.ResourceList{
					corev1.ResourceCPU:    resource.MustParse("2000m"),
					corev1.ResourceMemory: resource.MustParse("2Gi"),
				},
			}},
		}},
	}
	instance := &serverlessdbv1.DBInstance{
		ObjectMeta: metav1.ObjectMeta{
			Name:        "ins1",
			Namespace:   "ns",
			Annotations: map[string]string{serverlessdbv1.AnnotationKeyResizeSpec: specJSON},
		},
		Status: serverlessdbv1.DBInstanceStatus{Phase: serverlessdbv1.DBInstancePhaseScaling, PodName: "ins1"},
	}
	cl := newFakeClient(pod, instance)
	r := &DBResourcePoolReconciler{Client: cl, Scheme: newTestScheme()}
	return r, instance, pod
}

func TestBuildPod(t *testing.T) {
	scheme := newTestScheme()
	pool := &serverlessdbv1.DBResourcePool{
		ObjectMeta: metav1.ObjectMeta{Name: "pool1", Namespace: "ns"},
		Spec: serverlessdbv1.DBResourcePoolSpec{
			PgVersion: 17,
			PodTemplate: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "test"}},
				Spec: corev1.PodSpec{
					Containers: []corev1.Container{
						{Name: "compute", Image: "postgres:17"},
					},
				},
			},
		},
	}
	instance := &serverlessdbv1.DBInstance{
		ObjectMeta: metav1.ObjectMeta{Name: "ins1", Namespace: "ns"},
	}

	pod, err := buildPod(pool, instance, scheme)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if pod.Name != "ins1" {
		t.Fatalf("Pod Name = %q, want ins1", pod.Name)
	}
	if pod.Namespace != "ns" {
		t.Fatalf("Pod Namespace = %q, want ns", pod.Namespace)
	}
	if pod.Labels[serverlessdbv1.LabelKeyDBResourcePool] != "pool1" {
		t.Fatalf("Label dbresourcepool = %q, want pool1", pod.Labels[serverlessdbv1.LabelKeyDBResourcePool])
	}
	if pod.Labels["app"] != "test" {
		t.Fatalf("Label app = %q, want test (should inherit from template)", pod.Labels["app"])
	}
	if len(pod.OwnerReferences) != 1 {
		t.Fatalf("expected 1 owner reference, got %d", len(pod.OwnerReferences))
	}
	if pod.OwnerReferences[0].Name != "ins1" {
		t.Fatalf("OwnerRef Name = %q, want ins1", pod.OwnerReferences[0].Name)
	}
}

// poolWithComputeResources builds a pool whose compute container has the given
// template resources, for use in buildPod resource tests.
func poolWithComputeResources(t *testing.T, requests, limits corev1.ResourceList) *serverlessdbv1.DBResourcePool {
	t.Helper()
	return &serverlessdbv1.DBResourcePool{
		ObjectMeta: metav1.ObjectMeta{Name: "pool1", Namespace: "ns"},
		Spec: serverlessdbv1.DBResourcePoolSpec{
			PodTemplate: corev1.PodTemplateSpec{
				Spec: corev1.PodSpec{
					Containers: []corev1.Container{
						{
							Name: serverlessdbv1.ContainerNameCompute,
							Resources: corev1.ResourceRequirements{
								Requests: requests,
								Limits:   limits,
							},
						},
					},
				},
			},
		},
	}
}

func computeContainer(pod *corev1.Pod) *corev1.Container {
	for i := range pod.Spec.Containers {
		if pod.Spec.Containers[i].Name == serverlessdbv1.ContainerNameCompute {
			return &pod.Spec.Containers[i]
		}
	}
	return nil
}

func TestBuildPod_PreservesAppRefLabel(t *testing.T) {
	pool := poolWithImage("postgres:17")
	instance := &serverlessdbv1.DBInstance{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "ins1",
			Namespace: "ns",
			Labels:    map[string]string{serverlessdbv1.LabelKeyAppRef: "app1"},
		},
	}

	pod := buildPodForTest(t, pool, instance)
	if pod.Labels[serverlessdbv1.LabelKeyAppRef] != "app1" {
		t.Fatalf("Label app-ref = %q, want app1", pod.Labels[serverlessdbv1.LabelKeyAppRef])
	}
	if pod.Labels[serverlessdbv1.LabelKeyDBResourcePool] != "pool1" {
		t.Fatalf("Label dbresourcepool = %q, want pool1", pod.Labels[serverlessdbv1.LabelKeyDBResourcePool])
	}
}

func TestBuildPod_NoAppRefWhenUnallocated(t *testing.T) {
	pool := poolWithImage("postgres:17")
	instance := &serverlessdbv1.DBInstance{
		ObjectMeta: metav1.ObjectMeta{Name: "ins1", Namespace: "ns"},
	}

	pod := buildPodForTest(t, pool, instance)
	if _, ok := pod.Labels[serverlessdbv1.LabelKeyAppRef]; ok {
		t.Fatalf("Label app-ref should be absent for unallocated instance, got %q", pod.Labels[serverlessdbv1.LabelKeyAppRef])
	}
}

func TestBuildPod_RestoresCurrentResources(t *testing.T) {
	pool := poolWithComputeResources(t,
		corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("1000m"), corev1.ResourceMemory: resource.MustParse("1Gi")},
		corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("2000m"), corev1.ResourceMemory: resource.MustParse("2Gi")},
	)
	instance := &serverlessdbv1.DBInstance{
		ObjectMeta: metav1.ObjectMeta{Name: "ins1", Namespace: "ns"},
		Status: serverlessdbv1.DBInstanceStatus{
			CurrentResources: []serverlessdbv1.ContainerResources{
				computeRes(serverlessdbv1.ContainerNameCompute, serverlessdbv1.InstanceResources{Cpu: "2000m", Memory: "2Gi", CpuLimit: "4", MemoryLimit: "4Gi"}),
			},
		},
	}

	pod := buildPodForTest(t, pool, instance)
	c := computeContainer(pod)
	if c == nil {
		t.Fatalf("compute container not found")
	}
	if got := c.Resources.Requests[corev1.ResourceCPU]; got.String() != "2" {
		t.Fatalf("Requests CPU = %q, want 2", got.String())
	}
	if got := c.Resources.Requests[corev1.ResourceMemory]; got.String() != "2Gi" {
		t.Fatalf("Requests Memory = %q, want 2Gi", got.String())
	}
	if got := c.Resources.Limits[corev1.ResourceCPU]; got.String() != "4" {
		t.Fatalf("Limits CPU = %q, want 4", got.String())
	}
	if got := c.Resources.Limits[corev1.ResourceMemory]; got.String() != "4Gi" {
		t.Fatalf("Limits Memory = %q, want 4Gi", got.String())
	}
}

func TestBuildPod_NilCurrentResources_KeepsTemplate(t *testing.T) {
	pool := poolWithComputeResources(t,
		corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("1000m"), corev1.ResourceMemory: resource.MustParse("1Gi")},
		corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("2000m"), corev1.ResourceMemory: resource.MustParse("2Gi")},
	)
	instance := &serverlessdbv1.DBInstance{
		ObjectMeta: metav1.ObjectMeta{Name: "ins1", Namespace: "ns"},
		Status:     serverlessdbv1.DBInstanceStatus{CurrentResources: nil},
	}

	pod := buildPodForTest(t, pool, instance)
	c := computeContainer(pod)
	if c == nil {
		t.Fatalf("compute container not found")
	}
	if got := c.Resources.Requests[corev1.ResourceCPU]; got.String() != "1" {
		t.Fatalf("Requests CPU = %q, want 1 (template)", got.String())
	}
	if got := c.Resources.Requests[corev1.ResourceMemory]; got.String() != "1Gi" {
		t.Fatalf("Requests Memory = %q, want 1Gi (template)", got.String())
	}
	if got := c.Resources.Limits[corev1.ResourceCPU]; got.String() != "2" {
		t.Fatalf("Limits CPU = %q, want 2 (template)", got.String())
	}
	if got := c.Resources.Limits[corev1.ResourceMemory]; got.String() != "2Gi" {
		t.Fatalf("Limits Memory = %q, want 2Gi (template)", got.String())
	}
}

func TestBuildPod_RestoresCurrentResources_MultiContainer(t *testing.T) {
	pool := poolWithTwoContainers()
	instance := &serverlessdbv1.DBInstance{
		ObjectMeta: metav1.ObjectMeta{Name: "ins1", Namespace: "ns"},
		Status: serverlessdbv1.DBInstanceStatus{
			CurrentResources: []serverlessdbv1.ContainerResources{
				computeRes(serverlessdbv1.ContainerNameCompute, serverlessdbv1.InstanceResources{Cpu: "2000m", Memory: "2Gi", CpuLimit: "4", MemoryLimit: "4Gi"}),
				computeRes("sidecar", serverlessdbv1.InstanceResources{Cpu: "500m"}),
			},
		},
	}
	pod := buildPodForTest(t, pool, instance)
	c := computeContainer(pod)
	if c == nil {
		t.Fatalf("compute container not found")
	}
	if got := c.Resources.Requests[corev1.ResourceCPU]; got.String() != "2" {
		t.Fatalf("compute Requests CPU = %q, want 2", got.String())
	}
	sidecar := findContainer(pod, "sidecar")
	if sidecar == nil {
		t.Fatalf("sidecar container not found")
	}
	if got := sidecar.Resources.Requests[corev1.ResourceCPU]; got.String() != "500m" {
		t.Fatalf("sidecar Requests CPU = %q, want 500m", got.String())
	}
}

func poolWithTwoContainers() *serverlessdbv1.DBResourcePool {
	return &serverlessdbv1.DBResourcePool{
		ObjectMeta: metav1.ObjectMeta{Name: "pool1", Namespace: "ns"},
		Spec: serverlessdbv1.DBResourcePoolSpec{
			PodTemplate: corev1.PodTemplateSpec{Spec: corev1.PodSpec{Containers: []corev1.Container{
				{Name: serverlessdbv1.ContainerNameCompute, Resources: corev1.ResourceRequirements{
					Requests: corev1.ResourceList{
						corev1.ResourceCPU:    resource.MustParse("1000m"),
						corev1.ResourceMemory: resource.MustParse("1Gi"),
					},
				}},
				{Name: "sidecar", Resources: corev1.ResourceRequirements{
					Requests: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("100m")},
				}},
			}}},
		},
	}
}

func findContainer(pod *corev1.Pod, name string) *corev1.Container {
	for i := range pod.Spec.Containers {
		if pod.Spec.Containers[i].Name == name {
			return &pod.Spec.Containers[i]
		}
	}
	return nil
}

func TestRecreateMissingPod_PreservesAppRefAndResources(t *testing.T) {
	pool := poolWithComputeResources(t,
		corev1.ResourceList{
			corev1.ResourceCPU:    resource.MustParse("1000m"),
			corev1.ResourceMemory: resource.MustParse("1Gi"),
		},
		nil,
	)
	instance := &serverlessdbv1.DBInstance{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "ins1",
			Namespace: "ns",
			Labels: map[string]string{
				serverlessdbv1.LabelKeyDBResourcePool: "pool1",
				serverlessdbv1.LabelKeyAppRef:         "app1",
			},
		},
		Status: serverlessdbv1.DBInstanceStatus{
			Phase: serverlessdbv1.DBInstancePhaseRunning,
			CurrentResources: []serverlessdbv1.ContainerResources{
				computeRes(serverlessdbv1.ContainerNameCompute, serverlessdbv1.InstanceResources{Cpu: "2000m", Memory: "2Gi", CpuLimit: "4", MemoryLimit: "4Gi"}),
			},
		},
	}
	cl := newFakeClient(pool, instance)
	r := &DBResourcePoolReconciler{Client: cl, Scheme: newTestScheme()}

	r.recreateMissingPod(context.Background(), pool, instance, 5432)

	got := &corev1.Pod{}
	if err := cl.Get(context.Background(), client.ObjectKey{Namespace: "ns", Name: "ins1"}, got); err != nil {
		t.Fatalf("pod should be created: %v", err)
	}
	if got.Labels[serverlessdbv1.LabelKeyAppRef] != "app1" {
		t.Fatalf("Label app-ref = %q, want app1", got.Labels[serverlessdbv1.LabelKeyAppRef])
	}
	if got.Labels[serverlessdbv1.LabelKeyDBResourcePool] != "pool1" {
		t.Fatalf("Label dbresourcepool = %q, want pool1", got.Labels[serverlessdbv1.LabelKeyDBResourcePool])
	}
	c := computeContainer(got)
	if c == nil {
		t.Fatalf("compute container not found")
	}
	if gotQ := c.Resources.Requests[corev1.ResourceCPU]; gotQ.String() != "2" {
		t.Fatalf("Requests CPU = %q, want 2", gotQ.String())
	}
	if gotQ := c.Resources.Requests[corev1.ResourceMemory]; gotQ.String() != "2Gi" {
		t.Fatalf("Requests Memory = %q, want 2Gi", gotQ.String())
	}
	if gotQ := c.Resources.Limits[corev1.ResourceCPU]; gotQ.String() != "4" {
		t.Fatalf("Limits CPU = %q, want 4", gotQ.String())
	}
	if gotQ := c.Resources.Limits[corev1.ResourceMemory]; gotQ.String() != "4Gi" {
		t.Fatalf("Limits Memory = %q, want 4Gi", gotQ.String())
	}
}

func TestReconcileStaleServices_TTLDisabled(t *testing.T) {
	r := &DBResourcePoolReconciler{Client: newFakeClient(), ServiceTTL: 0}
	hasPending, err := r.reconcileStaleServices(context.Background(), "ns")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if hasPending {
		t.Fatalf("expected hasPending=false when TTL=0")
	}
}

func TestReconcileStaleServices_PodExists_ClearsAnnotation(t *testing.T) {
	svc := &corev1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "app1",
			Namespace: "ns",
			Labels:    map[string]string{serverlessdbv1.LabelKeyManaged: "true"},
			Annotations: map[string]string{
				serverlessdbv1.AnnotationKeyPodAbsentSince: "2024-01-01T00:00:00Z",
			},
		},
	}
	pod := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "pod1",
			Namespace: "ns",
			Labels:    map[string]string{serverlessdbv1.LabelKeyAppRef: "app1"},
		},
	}
	cl := newFakeClient(svc, pod)
	r := &DBResourcePoolReconciler{Client: cl, ServiceTTL: 30 * time.Minute}

	hasPending, err := r.reconcileStaleServices(context.Background(), "ns")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if hasPending {
		t.Fatalf("expected hasPending=false when pod exists")
	}

	got := &corev1.Service{}
	if err := cl.Get(context.Background(), client.ObjectKey{Namespace: "ns", Name: "app1"}, got); err != nil {
		t.Fatalf("failed to get service: %v", err)
	}
	if _, ok := got.Annotations[serverlessdbv1.AnnotationKeyPodAbsentSince]; ok {
		t.Fatalf("annotation should be cleared")
	}
}

func TestReconcileStaleServices_NoPod_SetsAnnotation(t *testing.T) {
	svc := &corev1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "app1",
			Namespace: "ns",
			Labels:    map[string]string{serverlessdbv1.LabelKeyManaged: "true"},
		},
	}
	cl := newFakeClient(svc)
	r := &DBResourcePoolReconciler{Client: cl, ServiceTTL: 30 * time.Minute}

	hasPending, err := r.reconcileStaleServices(context.Background(), "ns")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if !hasPending {
		t.Fatalf("expected hasPending=true when pod absent and annotation just set")
	}

	got := &corev1.Service{}
	if err := cl.Get(context.Background(), client.ObjectKey{Namespace: "ns", Name: "app1"}, got); err != nil {
		t.Fatalf("failed to get service: %v", err)
	}
	if _, ok := got.Annotations[serverlessdbv1.AnnotationKeyPodAbsentSince]; !ok {
		t.Fatalf("annotation should be set")
	}
}

func TestReconcileStaleServices_NoPod_ExpiredTTL_DeletesService(t *testing.T) {
	pastTime := metav1.Time{Time: time.Now().Add(-31 * time.Minute)}.Format(time.RFC3339)
	svc := &corev1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "app1",
			Namespace: "ns",
			Labels:    map[string]string{serverlessdbv1.LabelKeyManaged: "true"},
			Annotations: map[string]string{
				serverlessdbv1.AnnotationKeyPodAbsentSince: pastTime,
			},
		},
	}
	cl := newFakeClient(svc)
	r := &DBResourcePoolReconciler{Client: cl, ServiceTTL: 30 * time.Minute}

	hasPending, err := r.reconcileStaleServices(context.Background(), "ns")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if hasPending {
		t.Fatalf("expected hasPending=false when service deleted")
	}

	got := &corev1.Service{}
	if err := cl.Get(context.Background(), client.ObjectKey{Namespace: "ns", Name: "app1"}, got); err == nil {
		t.Fatalf("service should be deleted")
	}
}

func TestReconcileStaleServices_NoPod_WithinTTL_Waits(t *testing.T) {
	recentTime := metav1.Time{Time: time.Now().Add(-5 * time.Minute)}.Format(time.RFC3339)
	svc := &corev1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "app1",
			Namespace: "ns",
			Labels:    map[string]string{serverlessdbv1.LabelKeyManaged: "true"},
			Annotations: map[string]string{
				serverlessdbv1.AnnotationKeyPodAbsentSince: recentTime,
			},
		},
	}
	cl := newFakeClient(svc)
	r := &DBResourcePoolReconciler{Client: cl, ServiceTTL: 30 * time.Minute}

	hasPending, err := r.reconcileStaleServices(context.Background(), "ns")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if !hasPending {
		t.Fatalf("expected hasPending=true when within TTL")
	}

	got := &corev1.Service{}
	if err := cl.Get(context.Background(), client.ObjectKey{Namespace: "ns", Name: "app1"}, got); err != nil {
		t.Fatalf("service should not be deleted: %v", err)
	}
}

func TestScaleDown_DeletesUnallocated(t *testing.T) {
	ins1 := &serverlessdbv1.DBInstance{
		ObjectMeta: metav1.ObjectMeta{Name: "ins1", Namespace: "ns", Labels: map[string]string{
			serverlessdbv1.LabelKeyDBResourcePool: "pool1", serverlessdbv1.LabelKeyAppRef: "app1",
		}},
	}
	ins2 := &serverlessdbv1.DBInstance{
		ObjectMeta: metav1.ObjectMeta{Name: "ins2", Namespace: "ns", Labels: map[string]string{
			serverlessdbv1.LabelKeyDBResourcePool: "pool1",
		}},
	}
	ins3 := &serverlessdbv1.DBInstance{
		ObjectMeta: metav1.ObjectMeta{Name: "ins3", Namespace: "ns", Labels: map[string]string{
			serverlessdbv1.LabelKeyDBResourcePool: "pool1",
		}},
	}
	cl := newFakeClient(ins1, ins2, ins3)
	r := &DBResourcePoolReconciler{Client: cl}

	list := &serverlessdbv1.DBInstanceList{Items: []serverlessdbv1.DBInstance{*ins1, *ins2, *ins3}}
	r.scaleDown(context.Background(), list, 2)

	if err := cl.Get(context.Background(), client.ObjectKey{Namespace: "ns", Name: "ins1"}, &serverlessdbv1.DBInstance{}); err != nil {
		t.Fatalf("ins1 (allocated) should remain: %v", err)
	}
	if err := cl.Get(context.Background(), client.ObjectKey{Namespace: "ns", Name: "ins2"}, &serverlessdbv1.DBInstance{}); err == nil {
		t.Fatalf("ins2 (unallocated) should be deleted")
	}
	if err := cl.Get(context.Background(), client.ObjectKey{Namespace: "ns", Name: "ins3"}, &serverlessdbv1.DBInstance{}); err == nil {
		t.Fatalf("ins3 (unallocated) should be deleted")
	}
}

func TestScaleDown_AllAllocated_NoDeletion(t *testing.T) {
	ins1 := &serverlessdbv1.DBInstance{
		ObjectMeta: metav1.ObjectMeta{Name: "ins1", Namespace: "ns", Labels: map[string]string{
			serverlessdbv1.LabelKeyDBResourcePool: "pool1", serverlessdbv1.LabelKeyAppRef: "app1",
		}},
	}
	ins2 := &serverlessdbv1.DBInstance{
		ObjectMeta: metav1.ObjectMeta{Name: "ins2", Namespace: "ns", Labels: map[string]string{
			serverlessdbv1.LabelKeyDBResourcePool: "pool1", serverlessdbv1.LabelKeyAppRef: "app2",
		}},
	}
	cl := newFakeClient(ins1, ins2)
	r := &DBResourcePoolReconciler{Client: cl}

	list := &serverlessdbv1.DBInstanceList{Items: []serverlessdbv1.DBInstance{*ins1, *ins2}}
	r.scaleDown(context.Background(), list, 1)

	if err := cl.Get(context.Background(), client.ObjectKey{Namespace: "ns", Name: "ins1"}, &serverlessdbv1.DBInstance{}); err != nil {
		t.Fatalf("ins1 should remain: %v", err)
	}
	if err := cl.Get(context.Background(), client.ObjectKey{Namespace: "ns", Name: "ins2"}, &serverlessdbv1.DBInstance{}); err != nil {
		t.Fatalf("ins2 should remain: %v", err)
	}
}

func TestUpdatePoolStatus(t *testing.T) {
	pool := &serverlessdbv1.DBResourcePool{
		ObjectMeta: metav1.ObjectMeta{Name: "pool1", Namespace: "ns"},
	}
	ins1 := &serverlessdbv1.DBInstance{
		ObjectMeta: metav1.ObjectMeta{Name: "ins1", Namespace: "ns", Labels: map[string]string{serverlessdbv1.LabelKeyDBResourcePool: "pool1"}},
		Status:     serverlessdbv1.DBInstanceStatus{Phase: serverlessdbv1.DBInstancePhaseWarmReady},
	}
	ins2 := &serverlessdbv1.DBInstance{
		ObjectMeta: metav1.ObjectMeta{Name: "ins2", Namespace: "ns", Labels: map[string]string{serverlessdbv1.LabelKeyDBResourcePool: "pool1", serverlessdbv1.LabelKeyAppRef: "app1"}},
		Status:     serverlessdbv1.DBInstanceStatus{Phase: serverlessdbv1.DBInstancePhaseWarmReady},
	}
	ins3 := &serverlessdbv1.DBInstance{
		ObjectMeta: metav1.ObjectMeta{Name: "ins3", Namespace: "ns", Labels: map[string]string{serverlessdbv1.LabelKeyDBResourcePool: "pool1", serverlessdbv1.LabelKeyAppRef: "app2"}},
		Status:     serverlessdbv1.DBInstanceStatus{Phase: serverlessdbv1.DBInstancePhaseRunning},
	}
	cl := newFakeClient(pool, ins1, ins2, ins3)
	r := &DBResourcePoolReconciler{Client: cl}

	err := r.updatePoolStatus(context.Background(), pool)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	got := &serverlessdbv1.DBResourcePool{}
	if err := cl.Get(context.Background(), client.ObjectKey{Namespace: "ns", Name: "pool1"}, got); err != nil {
		t.Fatalf("failed to get pool: %v", err)
	}
	if got.Status.TotalInstances != 3 {
		t.Fatalf("TotalInstances = %d, want 3", got.Status.TotalInstances)
	}
	if got.Status.WarmInstances != 2 {
		t.Fatalf("WarmInstances = %d, want 2", got.Status.WarmInstances)
	}
	if got.Status.AvailableInstances != 1 {
		t.Fatalf("AvailableInstances = %d, want 1", got.Status.AvailableInstances)
	}
	if got.Status.AllocatedInstances != 2 {
		t.Fatalf("AllocatedInstances = %d, want 2", got.Status.AllocatedInstances)
	}
}

func TestReconcileDeletion_HasAllocated_Requeues(t *testing.T) {
	pool := &serverlessdbv1.DBResourcePool{
		ObjectMeta: metav1.ObjectMeta{
			Name:       "pool1",
			Namespace:  "ns",
			Finalizers: []string{serverlessdbv1.PoolFinalizer},
		},
	}
	ins1 := &serverlessdbv1.DBInstance{
		ObjectMeta: metav1.ObjectMeta{Name: "ins1", Namespace: "ns", Labels: map[string]string{
			serverlessdbv1.LabelKeyDBResourcePool: "pool1", serverlessdbv1.LabelKeyAppRef: "app1",
		}},
	}
	cl := newFakeClient(pool, ins1)
	r := &DBResourcePoolReconciler{Client: cl}

	result, err := r.reconcileDeletion(context.Background(), pool)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if result.RequeueAfter != requeueInterval {
		t.Fatalf("RequeueAfter = %v, want %v", result.RequeueAfter, requeueInterval)
	}

	if err := cl.Get(context.Background(), client.ObjectKey{Namespace: "ns", Name: "ins1"}, &serverlessdbv1.DBInstance{}); err != nil {
		t.Fatalf("instance should not be deleted when allocated: %v", err)
	}
}

func TestReconcileDeletion_NoAllocated_DeletesAndRemovesFinalizer(t *testing.T) {
	pool := &serverlessdbv1.DBResourcePool{
		ObjectMeta: metav1.ObjectMeta{
			Name:       "pool1",
			Namespace:  "ns",
			Finalizers: []string{serverlessdbv1.PoolFinalizer},
		},
	}
	ins1 := &serverlessdbv1.DBInstance{
		ObjectMeta: metav1.ObjectMeta{Name: "ins1", Namespace: "ns", Labels: map[string]string{
			serverlessdbv1.LabelKeyDBResourcePool: "pool1",
		}},
	}
	cl := newFakeClient(pool, ins1)
	r := &DBResourcePoolReconciler{Client: cl}

	result, err := r.reconcileDeletion(context.Background(), pool)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if result.RequeueAfter != 0 {
		t.Fatalf("RequeueAfter = %v, want 0", result.RequeueAfter)
	}

	if err := cl.Get(context.Background(), client.ObjectKey{Namespace: "ns", Name: "ins1"}, &serverlessdbv1.DBInstance{}); err == nil {
		t.Fatalf("instance should be deleted")
	}

	got := &serverlessdbv1.DBResourcePool{}
	if err := cl.Get(context.Background(), client.ObjectKey{Namespace: "ns", Name: "pool1"}, got); err != nil {
		t.Fatalf("failed to get pool: %v", err)
	}
	if len(got.Finalizers) != 0 {
		t.Fatalf("finalizer should be removed, got %v", got.Finalizers)
	}
}

func TestReconcileDeletion_NoFinalizer_NoOp(t *testing.T) {
	pool := &serverlessdbv1.DBResourcePool{
		ObjectMeta: metav1.ObjectMeta{Name: "pool1", Namespace: "ns"},
	}
	cl := newFakeClient(pool)
	r := &DBResourcePoolReconciler{Client: cl}

	result, err := r.reconcileDeletion(context.Background(), pool)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if result.RequeueAfter != 0 {
		t.Fatalf("RequeueAfter = %v, want 0", result.RequeueAfter)
	}
}

var _ = ctrl.Log

func TestApplyResize_Success_ClearsTrigger_PersistsLastResizeSpec(t *testing.T) {
	specJSON := `[{"name":"compute","resources":{"cpu":"2000m","memory":"2Gi","cpu_limit":"4","memory_limit":"4Gi"}}]`
	r, instance, pod := resizeTestSetup(specJSON)

	r.applyResize(context.Background(), instance, pod)

	// trigger annotation should be cleared
	if _, ok := instance.Annotations[serverlessdbv1.AnnotationKeyResizeSpec]; ok {
		t.Fatalf("resize-spec trigger annotation should be cleared after submit")
	}
	// last-resize-spec should persist the latest request
	got, ok := instance.Annotations[serverlessdbv1.AnnotationKeyLastResizeSpec]
	if !ok {
		t.Fatalf("last-resize-spec annotation should be set")
	}
	if got != specJSON {
		t.Fatalf("last-resize-spec = %q, want %q", got, specJSON)
	}
}

func TestApplyResize_ResizeInProgress_NoAnnotation_KeepsScaling(t *testing.T) {
	pod := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{Name: "ins1", Namespace: "ns"},
		Status: corev1.PodStatus{Conditions: []corev1.PodCondition{
			{Type: corev1.PodResizeInProgress, Status: corev1.ConditionTrue},
		}},
	}
	instance := &serverlessdbv1.DBInstance{
		ObjectMeta: metav1.ObjectMeta{Name: "ins1", Namespace: "ns"},
		Status:     serverlessdbv1.DBInstanceStatus{Phase: serverlessdbv1.DBInstancePhaseScaling, PodName: "ins1"},
	}

	r := &DBResourcePoolReconciler{Client: newFakeClient(), Scheme: newTestScheme()}
	r.applyResize(context.Background(), instance, pod)

	// Resize still in progress: phase should stay Scaling, not transition.
	if instance.Status.Phase != serverlessdbv1.DBInstancePhaseScaling {
		t.Fatalf("phase = %q, want Scaling", instance.Status.Phase)
	}
}

func TestApplyResize_ResizeCompleted_TransitionsToRunning(t *testing.T) {
	pod := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{Name: "ins1", Namespace: "ns"},
		Status:     corev1.PodStatus{}, // no resize conditions → completed
	}
	instance := &serverlessdbv1.DBInstance{
		ObjectMeta: metav1.ObjectMeta{Name: "ins1", Namespace: "ns"},
		Status:     serverlessdbv1.DBInstanceStatus{Phase: serverlessdbv1.DBInstancePhaseScaling, PodName: "ins1"},
	}

	r := &DBResourcePoolReconciler{Client: newFakeClient(), Scheme: newTestScheme()}
	r.applyResize(context.Background(), instance, pod)

	if instance.Status.Phase != serverlessdbv1.DBInstancePhaseRunning {
		t.Fatalf("phase = %q, want Running", instance.Status.Phase)
	}
}

func TestSyncInstanceStatuses_UpdatesCurrentResourcesFromPod(t *testing.T) {
	pod := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{Name: "ins1", Namespace: "ns", UID: "uid-1"},
		Status: corev1.PodStatus{
			PodIP: testPodIP1,
			Conditions: []corev1.PodCondition{
				{Type: corev1.PodReady, Status: corev1.ConditionTrue},
			},
		},
		Spec: corev1.PodSpec{
			Containers: []corev1.Container{
				{
					Name: serverlessdbv1.ContainerNameCompute,
					Resources: corev1.ResourceRequirements{
						Requests: corev1.ResourceList{
							corev1.ResourceCPU:    resource.MustParse("500m"),
							corev1.ResourceMemory: resource.MustParse("512Mi"),
						},
					},
				},
				{
					Name: "sidecar",
					Resources: corev1.ResourceRequirements{
						Requests: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("100m")},
					},
				},
			},
		},
	}
	instance := &serverlessdbv1.DBInstance{
		ObjectMeta: metav1.ObjectMeta{Name: "ins1", Namespace: "ns", Labels: map[string]string{serverlessdbv1.LabelKeyDBResourcePool: "pool1"}},
		Status:     serverlessdbv1.DBInstanceStatus{Phase: serverlessdbv1.DBInstancePhaseWakingUp, PodName: "ins1"},
	}

	r, cl := syncStatusReconciler(pod, instance)
	runSyncStatuses(r, instance)

	got := &serverlessdbv1.DBInstance{}
	if err := cl.Get(context.Background(), client.ObjectKey{Namespace: "ns", Name: "ins1"}, got); err != nil {
		t.Fatalf("failed to get instance: %v", err)
	}
	if len(got.Status.CurrentResources) != 2 {
		t.Fatalf("CurrentResources should have 2 containers, got %d", len(got.Status.CurrentResources))
	}
	byName := map[string]serverlessdbv1.InstanceResources{}
	for _, c := range got.Status.CurrentResources {
		byName[c.Name] = c.Resources
	}
	if byName[serverlessdbv1.ContainerNameCompute].Cpu != "500m" {
		t.Fatalf("compute CurrentResources.Cpu = %q, want 500m", byName[serverlessdbv1.ContainerNameCompute].Cpu)
	}
	if byName[serverlessdbv1.ContainerNameCompute].Memory != "512Mi" {
		t.Fatalf("compute CurrentResources.Memory = %q, want 512Mi", byName[serverlessdbv1.ContainerNameCompute].Memory)
	}
	if byName["sidecar"].Cpu != "100m" {
		t.Fatalf("sidecar CurrentResources.Cpu = %q, want 100m", byName["sidecar"].Cpu)
	}
}

func TestSyncInstanceStatuses_AllocatedInstanceWritesAllocatedAtAndConnectionString(t *testing.T) {
	pod := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{Name: "ins1", Namespace: "ns", UID: "uid-1"},
		Status: corev1.PodStatus{
			PodIP: testPodIP1,
			Conditions: []corev1.PodCondition{
				{Type: corev1.PodReady, Status: corev1.ConditionTrue},
			},
		},
	}
	instance := &serverlessdbv1.DBInstance{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "ins1",
			Namespace: "ns",
			Labels: map[string]string{
				serverlessdbv1.LabelKeyDBResourcePool: "pool1",
				serverlessdbv1.LabelKeyAppRef:         "myapp",
			},
		},
		Status: serverlessdbv1.DBInstanceStatus{Phase: serverlessdbv1.DBInstancePhaseWakingUp, PodName: "ins1"},
	}

	r, cl := syncStatusReconciler(pod, instance)
	runSyncStatuses(r, instance)

	got := &serverlessdbv1.DBInstance{}
	if err := cl.Get(context.Background(), client.ObjectKey{Namespace: "ns", Name: "ins1"}, got); err != nil {
		t.Fatalf("failed to get instance: %v", err)
	}
	if got.Status.Phase != serverlessdbv1.DBInstancePhaseRunning {
		t.Fatalf("Phase = %q, want Running", got.Status.Phase)
	}
	if got.Status.AllocatedAt == nil {
		t.Fatalf("AllocatedAt should be set for allocated instance")
	}
	wantConnStr := "myapp.ns.svc.cluster.local:5432"
	if got.Status.ConnectionString != wantConnStr {
		t.Fatalf("ConnectionString = %q, want %q", got.Status.ConnectionString, wantConnStr)
	}
}

func TestSyncInstanceStatuses_AllocatedInstancePreservesExistingAllocatedAt(t *testing.T) {
	pod := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{Name: "ins1", Namespace: "ns", UID: "uid-1"},
		Status: corev1.PodStatus{
			Conditions: []corev1.PodCondition{
				{Type: corev1.PodReady, Status: corev1.ConditionTrue},
			},
		},
	}
	existingAllocatedAt := metav1.Now()
	instance := &serverlessdbv1.DBInstance{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "ins1",
			Namespace: "ns",
			Labels: map[string]string{
				serverlessdbv1.LabelKeyAppRef: "myapp",
			},
		},
		Status: serverlessdbv1.DBInstanceStatus{
			Phase:       serverlessdbv1.DBInstancePhaseWakingUp,
			PodName:     "ins1",
			AllocatedAt: &existingAllocatedAt,
		},
	}

	r, cl := syncStatusReconciler(pod, instance)
	runSyncStatuses(r, instance)

	got := &serverlessdbv1.DBInstance{}
	if err := cl.Get(context.Background(), client.ObjectKey{Namespace: "ns", Name: "ins1"}, got); err != nil {
		t.Fatalf("failed to get instance: %v", err)
	}
	if got.Status.AllocatedAt == nil {
		t.Fatalf("AllocatedAt should be preserved, got nil")
	}
	if got.Status.AllocatedAt.Unix() != existingAllocatedAt.Unix() {
		t.Fatalf("AllocatedAt should be preserved, got %v, want %v", got.Status.AllocatedAt.Unix(), existingAllocatedAt.Unix())
	}
}

func TestApplyResize_ActuallyChangesPodResources(t *testing.T) {
	specJSON := `[{"name":"compute","resources":{"cpu":"2000m","memory":"2Gi","cpu_limit":"4","memory_limit":"4Gi"}}]`
	r, instance, pod := resizeTestSetup(specJSON)

	r.applyResize(context.Background(), instance, pod)

	got := &corev1.Pod{}
	cl := r.Client
	if err := cl.Get(context.Background(), client.ObjectKey{Namespace: "ns", Name: "ins1"}, got); err != nil {
		t.Fatalf("failed to get pod: %v", err)
	}
	c := computeContainer(got)
	if c == nil {
		t.Fatalf("compute container not found")
	}
	if gotCPU := c.Resources.Requests[corev1.ResourceCPU]; gotCPU.String() != "2" {
		t.Fatalf("Requests CPU = %q, want 2 (resize not applied)", gotCPU.String())
	}
	if gotMem := c.Resources.Requests[corev1.ResourceMemory]; gotMem.String() != "2Gi" {
		t.Fatalf("Requests Memory = %q, want 2Gi", gotMem.String())
	}
	if gotCPU := c.Resources.Limits[corev1.ResourceCPU]; gotCPU.String() != "4" {
		t.Fatalf("Limits CPU = %q, want 4", gotCPU.String())
	}
	if gotMem := c.Resources.Limits[corev1.ResourceMemory]; gotMem.String() != "4Gi" {
		t.Fatalf("Limits Memory = %q, want 4Gi", gotMem.String())
	}
}