/*
Copyright 2026.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package resources

import (
	"encoding/json"
	"slices"
	"testing"

	corev1 "k8s.io/api/core/v1"
	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	lmcachev1alpha1 "github.com/LMCache/LMCache/api/v1alpha1"
)

const (
	testEngineName = "test-engine"
	testNamespace  = "default"
)

// --- helpers ---

func ptr[T any](v T) *T { return &v }

func minimalEngine() *lmcachev1alpha1.LMCacheEngine {
	return &lmcachev1alpha1.LMCacheEngine{
		ObjectMeta: metav1.ObjectMeta{
			Name:      testEngineName,
			Namespace: testNamespace,
		},
		Spec: lmcachev1alpha1.LMCacheEngineSpec{
			L1: lmcachev1alpha1.L1BackendSpec{SizeGB: 10},
		},
	}
}

// ===========================
// Deref helpers
// ===========================

func TestDerefInt32(t *testing.T) {
	if v := derefInt32(ptr(int32(42)), 0); v != 42 {
		t.Fatalf("expected 42, got %d", v)
	}
	if v := derefInt32(nil, 99); v != 99 {
		t.Fatalf("expected 99, got %d", v)
	}
}

func TestDerefString(t *testing.T) {
	if v := derefString(ptr("hello"), "x"); v != "hello" {
		t.Fatalf("expected hello, got %s", v)
	}
	if v := derefString(nil, testNamespace); v != testNamespace {
		t.Fatalf("expected default, got %s", v)
	}
}

func TestDerefBool(t *testing.T) {
	if v := derefBool(ptr(true), false); !v {
		t.Fatal("expected true")
	}
	if v := derefBool(nil, true); !v {
		t.Fatal("expected true (default)")
	}
	if v := derefBool(nil, false); v {
		t.Fatal("expected false (default)")
	}
}

func TestDerefFloat64(t *testing.T) {
	if v := derefFloat64(ptr(3.14), 0); v != 3.14 {
		t.Fatalf("expected 3.14, got %f", v)
	}
	if v := derefFloat64(nil, 2.71); v != 2.71 {
		t.Fatalf("expected 2.71, got %f", v)
	}
}

// ===========================
// Labels
// ===========================

func TestSelectorLabels(t *testing.T) {
	labels := SelectorLabels("my-cache")
	expected := map[string]string{
		"app.kubernetes.io/name":       "lmcache",
		"app.kubernetes.io/instance":   "my-cache",
		"app.kubernetes.io/managed-by": "lmcache-operator",
	}
	if len(labels) != len(expected) {
		t.Fatalf("expected %d labels, got %d", len(expected), len(labels))
	}
	for k, v := range expected {
		if labels[k] != v {
			t.Errorf("label %s: expected %s, got %s", k, v, labels[k])
		}
	}
}

func TestStandardLabels(t *testing.T) {
	labels := StandardLabels("my-cache")
	if labels["app.kubernetes.io/component"] != "cache-engine" {
		t.Fatal("missing component label")
	}
	// Should include selector labels too
	if labels["app.kubernetes.io/instance"] != "my-cache" {
		t.Fatal("missing instance label")
	}
}

func TestMergeLabels(t *testing.T) {
	a := map[string]string{"k1": "v1", "k2": "v2"}
	b := map[string]string{"k2": "override", "k3": "v3"}
	merged := MergeLabels(a, b)

	if merged["k1"] != "v1" {
		t.Error("k1 should be v1")
	}
	if merged["k2"] != "override" {
		t.Error("k2 should be overridden to 'override'")
	}
	if merged["k3"] != "v3" {
		t.Error("k3 should be v3")
	}
}

func TestMergeLabels_Empty(t *testing.T) {
	merged := MergeLabels()
	if len(merged) != 0 {
		t.Fatal("expected empty map")
	}
}

func TestMergeLabels_NilMaps(t *testing.T) {
	merged := MergeLabels(nil, map[string]string{"k": "v"}, nil)
	if merged["k"] != "v" {
		t.Fatal("expected k=v")
	}
}

// ===========================
// ComputeResources
// ===========================

func TestComputeResources_AutoComputed(t *testing.T) {
	spec := &lmcachev1alpha1.LMCacheEngineSpec{
		L1: lmcachev1alpha1.L1BackendSpec{SizeGB: 10},
	}
	res := ComputeResources(spec)

	// memReq = ceil(10+5) = 15Gi
	expectedReq := resource.MustParse("15Gi")
	if !res.Requests.Memory().Equal(expectedReq) {
		t.Fatalf("expected memory request 15Gi, got %s", res.Requests.Memory())
	}

	// memLim = ceil(15*1.5) = 23Gi
	expectedLim := resource.MustParse("23Gi")
	if !res.Limits.Memory().Equal(expectedLim) {
		t.Fatalf("expected memory limit 23Gi, got %s", res.Limits.Memory())
	}

	// CPU request = 4
	expectedCPU := resource.MustParse("4")
	if !res.Requests.Cpu().Equal(expectedCPU) {
		t.Fatalf("expected cpu request 4, got %s", res.Requests.Cpu())
	}
}

func TestComputeResources_FractionalSizeGB(t *testing.T) {
	spec := &lmcachev1alpha1.LMCacheEngineSpec{
		L1: lmcachev1alpha1.L1BackendSpec{SizeGB: 0.5},
	}
	res := ComputeResources(spec)

	// memReq = ceil(0.5+5) = 6Gi
	expectedReq := resource.MustParse("6Gi")
	if !res.Requests.Memory().Equal(expectedReq) {
		t.Fatalf("expected memory request 6Gi, got %s", res.Requests.Memory())
	}

	// memLim = ceil(6*1.5) = 9Gi
	expectedLim := resource.MustParse("9Gi")
	if !res.Limits.Memory().Equal(expectedLim) {
		t.Fatalf("expected memory limit 9Gi, got %s", res.Limits.Memory())
	}
}

func TestComputeResources_Override(t *testing.T) {
	override := &corev1.ResourceRequirements{
		Requests: corev1.ResourceList{
			corev1.ResourceMemory: resource.MustParse("1Gi"),
		},
	}
	spec := &lmcachev1alpha1.LMCacheEngineSpec{
		L1:                lmcachev1alpha1.L1BackendSpec{SizeGB: 100},
		ResourceOverrides: override,
	}
	res := ComputeResources(spec)
	if !res.Requests.Memory().Equal(resource.MustParse("1Gi")) {
		t.Fatalf("expected override to apply, got %s", res.Requests.Memory())
	}
}

// ===========================
// BuildContainerArgs
// ===========================

func TestBuildContainerArgs_Defaults(t *testing.T) {
	spec := &lmcachev1alpha1.LMCacheEngineSpec{
		L1: lmcachev1alpha1.L1BackendSpec{SizeGB: 10},
	}
	args := BuildContainerArgs(spec)

	assertArg(t, args, "--host", "0.0.0.0")
	assertArg(t, args, "--port", "5555")
	assertArg(t, args, "--l1-size-gb", "10.0")
	assertArg(t, args, "--chunk-size", "256")
	assertArg(t, args, "--max-workers", "1")
	assertArg(t, args, "--hash-algorithm", "blake3")
	assertArg(t, args, "--eviction-policy", "LRU")
	assertArg(t, args, "--eviction-trigger-watermark", "0.80")
	assertArg(t, args, "--eviction-ratio", "0.20")
	assertArg(t, args, "--prometheus-port", "9090")
	assertNoArg(t, args, "--disable-prometheus")
}

func TestBuildContainerArgs_CustomServer(t *testing.T) {
	spec := &lmcachev1alpha1.LMCacheEngineSpec{
		L1: lmcachev1alpha1.L1BackendSpec{SizeGB: 5},
		Server: &lmcachev1alpha1.ServerSpec{
			Port:          ptr(int32(8080)),
			ChunkSize:     ptr(int32(512)),
			MaxWorkers:    ptr(int32(4)),
			HashAlgorithm: ptr("sha256_cbor"),
		},
	}
	args := BuildContainerArgs(spec)

	assertArg(t, args, "--port", "8080")
	assertArg(t, args, "--chunk-size", "512")
	assertArg(t, args, "--max-workers", "4")
	assertArg(t, args, "--hash-algorithm", "sha256_cbor")
}

func TestBuildContainerArgs_CustomEviction(t *testing.T) {
	spec := &lmcachev1alpha1.LMCacheEngineSpec{
		L1: lmcachev1alpha1.L1BackendSpec{SizeGB: 10},
		Eviction: &lmcachev1alpha1.EvictionSpec{
			Policy:           ptr("LRU"),
			TriggerWatermark: ptr(0.9),
			EvictionRatio:    ptr(0.3),
		},
	}
	args := BuildContainerArgs(spec)

	assertArg(t, args, "--eviction-policy", "LRU")
	assertArg(t, args, "--eviction-trigger-watermark", "0.90")
	assertArg(t, args, "--eviction-ratio", "0.30")
}

func TestBuildContainerArgs_PrometheusDisabled(t *testing.T) {
	spec := &lmcachev1alpha1.LMCacheEngineSpec{
		L1: lmcachev1alpha1.L1BackendSpec{SizeGB: 10},
		Prometheus: &lmcachev1alpha1.PrometheusSpec{
			Enabled: ptr(false),
		},
	}
	args := BuildContainerArgs(spec)

	assertHasArg(t, args, "--disable-prometheus")
	assertNoArg(t, args, "--prometheus-port")
}

func TestBuildContainerArgs_CustomPrometheusPort(t *testing.T) {
	spec := &lmcachev1alpha1.LMCacheEngineSpec{
		L1: lmcachev1alpha1.L1BackendSpec{SizeGB: 10},
		Prometheus: &lmcachev1alpha1.PrometheusSpec{
			Enabled: ptr(true),
			Port:    ptr(int32(8888)),
		},
	}
	args := BuildContainerArgs(spec)

	assertArg(t, args, "--prometheus-port", "8888")
}

func TestBuildContainerArgs_L2Backends(t *testing.T) {
	spec := &lmcachev1alpha1.LMCacheEngineSpec{
		L1: lmcachev1alpha1.L1BackendSpec{SizeGB: 10},
		L2Backends: []lmcachev1alpha1.L2BackendSpec{
			{
				Type: "redis",
				Config: map[string]apiextensionsv1.JSON{
					"host": {Raw: []byte(`"localhost"`)},
					"port": {Raw: []byte(`6379`)},
				},
			},
		},
	}
	args := BuildContainerArgs(spec)

	// Find the --l2-adapter flag
	var l2JSON string
	for i, a := range args {
		if a == "--l2-adapter" && i+1 < len(args) {
			l2JSON = args[i+1]
			break
		}
	}
	if l2JSON == "" {
		t.Fatal("expected --l2-adapter flag")
	}

	var parsed map[string]any
	if err := json.Unmarshal([]byte(l2JSON), &parsed); err != nil {
		t.Fatalf("failed to parse L2 JSON: %v", err)
	}
	if parsed["type"] != "redis" {
		t.Fatalf("expected type=redis, got %v", parsed["type"])
	}
	if parsed["host"] != "localhost" {
		t.Fatalf("expected host=localhost, got %v", parsed["host"])
	}
	// JSON numbers are float64
	if parsed["port"] != float64(6379) {
		t.Fatalf("expected port=6379, got %v", parsed["port"])
	}
}

func TestBuildContainerArgs_MultipleL2Backends(t *testing.T) {
	spec := &lmcachev1alpha1.LMCacheEngineSpec{
		L1: lmcachev1alpha1.L1BackendSpec{SizeGB: 10},
		L2Backends: []lmcachev1alpha1.L2BackendSpec{
			{Type: "mock"},
			{Type: "disk"},
		},
	}
	args := BuildContainerArgs(spec)

	count := 0
	for _, a := range args {
		if a == "--l2-adapter" {
			count++
		}
	}
	if count != 2 {
		t.Fatalf("expected 2 --l2-adapter flags, got %d", count)
	}
}

func TestBuildContainerArgs_ExtraArgs(t *testing.T) {
	spec := &lmcachev1alpha1.LMCacheEngineSpec{
		L1:        lmcachev1alpha1.L1BackendSpec{SizeGB: 10},
		ExtraArgs: []string{"--l1-init-size-gb", "5", "--custom-flag", "value"},
	}
	args := BuildContainerArgs(spec)

	// extraArgs should be appended at the end
	assertArg(t, args, "--l1-init-size-gb", "5")
	assertArg(t, args, "--custom-flag", "value")
}

// ===========================
// BuildDaemonSet
// ===========================

func TestBuildDaemonSet_Minimal(t *testing.T) {
	engine := minimalEngine()
	ds := BuildDaemonSet(engine)

	if ds.Name != testEngineName {
		t.Fatalf("expected name test-engine, got %s", ds.Name)
	}
	if ds.Namespace != testNamespace {
		t.Fatalf("expected namespace default, got %s", ds.Namespace)
	}

	// Should have selector labels
	sel := ds.Spec.Selector.MatchLabels
	if sel["app.kubernetes.io/instance"] != testEngineName {
		t.Fatal("missing instance selector label")
	}

	// Should NOT have HostNetwork (uses Service with internalTrafficPolicy=Local instead)
	if ds.Spec.Template.Spec.HostNetwork {
		t.Fatal("expected HostNetwork=false")
	}

	// Should have HostIPC=true (required for CUDA IPC)
	if !ds.Spec.Template.Spec.HostIPC {
		t.Fatal("expected HostIPC=true")
	}

	// Should have exactly 1 container
	if len(ds.Spec.Template.Spec.Containers) != 1 {
		t.Fatalf("expected 1 container, got %d", len(ds.Spec.Template.Spec.Containers))
	}

	c := ds.Spec.Template.Spec.Containers[0]
	if c.Name != "lmcache" {
		t.Fatalf("expected container name lmcache, got %s", c.Name)
	}
	if c.Image != "lmcache/vllm-openai:latest" {
		t.Fatalf("expected default image, got %s", c.Image)
	}
	if c.ImagePullPolicy != corev1.PullIfNotPresent {
		t.Fatalf("expected PullIfNotPresent, got %s", c.ImagePullPolicy)
	}

	// Should have probes
	if c.StartupProbe == nil {
		t.Fatal("missing startup probe")
	}
	if c.LivenessProbe == nil {
		t.Fatal("missing liveness probe")
	}
	if c.ReadinessProbe == nil {
		t.Fatal("missing readiness probe")
	}

	// Should NOT have emptyDir /dev/shm volume (hostIPC provides host's /dev/shm)
	for _, v := range ds.Spec.Template.Spec.Volumes {
		if v.Name == "dshm" {
			t.Fatal("dshm emptyDir volume should not be present — it shadows host /dev/shm and breaks CUDA IPC")
		}
	}

	// Should have no volumes by default (no user-specified volumes)
	if len(ds.Spec.Template.Spec.Volumes) != 0 {
		t.Fatalf("expected 0 volumes, got %d", len(ds.Spec.Template.Spec.Volumes))
	}

	// Should have no volume mounts by default
	if len(c.VolumeMounts) != 0 {
		t.Fatalf("expected 0 volume mounts, got %d", len(c.VolumeMounts))
	}

	// Should have LMCACHE_LOG_LEVEL env var
	foundLogLevel := false
	foundVisibleDevices := false
	for _, e := range c.Env {
		if e.Name == "LMCACHE_LOG_LEVEL" && e.Value == "INFO" {
			foundLogLevel = true
		}
		if e.Name == "NVIDIA_VISIBLE_DEVICES" && e.Value == "all" {
			foundVisibleDevices = true
		}
	}
	if !foundLogLevel {
		t.Fatal("missing LMCACHE_LOG_LEVEL env var")
	}
	if !foundVisibleDevices {
		t.Fatal("missing NVIDIA_VISIBLE_DEVICES env var")
	}

	// Should have 2 ports (server + metrics) by default
	if len(c.Ports) != 2 {
		t.Fatalf("expected 2 container ports, got %d", len(c.Ports))
	}
}

func TestBuildDaemonSet_CustomImage(t *testing.T) {
	engine := minimalEngine()
	engine.Spec.Image = &lmcachev1alpha1.ImageSpec{
		Repository: ptr("custom/image"),
		Tag:        ptr("v1.0"),
		PullPolicy: ptr("Always"),
	}

	ds := BuildDaemonSet(engine)
	c := ds.Spec.Template.Spec.Containers[0]

	if c.Image != "custom/image:v1.0" {
		t.Fatalf("expected custom/image:v1.0, got %s", c.Image)
	}
	if c.ImagePullPolicy != corev1.PullAlways {
		t.Fatalf("expected PullAlways, got %s", c.ImagePullPolicy)
	}
}

func TestBuildDaemonSet_NeverPullPolicy(t *testing.T) {
	engine := minimalEngine()
	engine.Spec.Image = &lmcachev1alpha1.ImageSpec{
		PullPolicy: ptr("Never"),
	}

	ds := BuildDaemonSet(engine)
	c := ds.Spec.Template.Spec.Containers[0]
	if c.ImagePullPolicy != corev1.PullNever {
		t.Fatalf("expected PullNever, got %s", c.ImagePullPolicy)
	}
}

func TestBuildDaemonSet_PrometheusDisabled(t *testing.T) {
	engine := minimalEngine()
	engine.Spec.Prometheus = &lmcachev1alpha1.PrometheusSpec{
		Enabled: ptr(false),
	}

	ds := BuildDaemonSet(engine)
	c := ds.Spec.Template.Spec.Containers[0]

	// Should only have server port, no metrics port
	if len(c.Ports) != 1 {
		t.Fatalf("expected 1 container port, got %d", len(c.Ports))
	}
	if c.Ports[0].Name != "server" {
		t.Fatalf("expected port name 'server', got %s", c.Ports[0].Name)
	}
}

func TestBuildDaemonSet_CustomEnvAndVolumes(t *testing.T) {
	engine := minimalEngine()
	engine.Spec.Env = []corev1.EnvVar{
		{Name: "CUSTOM_VAR", Value: "custom_value"},
	}
	engine.Spec.Volumes = []corev1.Volume{
		{Name: "extra-vol", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}},
	}
	engine.Spec.VolumeMounts = []corev1.VolumeMount{
		{Name: "extra-vol", MountPath: "/extra"},
	}

	ds := BuildDaemonSet(engine)
	c := ds.Spec.Template.Spec.Containers[0]

	// Should have LMCACHE_LOG_LEVEL + NVIDIA_VISIBLE_DEVICES + CUSTOM_VAR
	if len(c.Env) != 3 {
		t.Fatalf("expected 3 env vars, got %d", len(c.Env))
	}

	// Should have only user-specified extra-vol (no built-in dshm)
	if len(ds.Spec.Template.Spec.Volumes) != 1 {
		t.Fatalf("expected 1 volume, got %d", len(ds.Spec.Template.Spec.Volumes))
	}

	// Should have only user-specified extra mount
	if len(c.VolumeMounts) != 1 {
		t.Fatalf("expected 1 volume mount, got %d", len(c.VolumeMounts))
	}
}

func TestBuildDaemonSet_NodeSelectorAndTolerations(t *testing.T) {
	engine := minimalEngine()
	engine.Spec.NodeSelector = map[string]string{"nvidia.com/gpu.present": "true"}
	engine.Spec.Tolerations = []corev1.Toleration{
		{Key: "gpu", Operator: corev1.TolerationOpExists, Effect: corev1.TaintEffectNoSchedule},
	}
	engine.Spec.ServiceAccountName = "my-sa"
	engine.Spec.PriorityClassName = "high-priority"

	ds := BuildDaemonSet(engine)
	podSpec := ds.Spec.Template.Spec

	if podSpec.NodeSelector["nvidia.com/gpu.present"] != "true" {
		t.Fatal("missing node selector")
	}
	if len(podSpec.Tolerations) != 1 {
		t.Fatal("missing tolerations")
	}
	if podSpec.ServiceAccountName != "my-sa" {
		t.Fatalf("expected SA my-sa, got %s", podSpec.ServiceAccountName)
	}
	if podSpec.PriorityClassName != "high-priority" {
		t.Fatalf("expected priority class high-priority, got %s", podSpec.PriorityClassName)
	}
}

func TestBuildDaemonSet_PodLabelsAndAnnotations(t *testing.T) {
	engine := minimalEngine()
	engine.Spec.PodLabels = map[string]string{"custom": "label"}
	engine.Spec.PodAnnotations = map[string]string{"anno": "value"}

	ds := BuildDaemonSet(engine)

	if ds.Spec.Template.Labels["custom"] != "label" {
		t.Fatal("missing custom pod label")
	}
	if ds.Spec.Template.Annotations["anno"] != "value" {
		t.Fatal("missing custom pod annotation")
	}
}

// ===========================
// BuildLookupService
// ===========================

func TestBuildLookupService_Default(t *testing.T) {
	engine := minimalEngine()
	svc := BuildLookupService(engine)

	if svc.Name != testEngineName {
		t.Fatalf("expected name test-engine, got %s", svc.Name)
	}
	if svc.Namespace != testNamespace {
		t.Fatalf("expected namespace default, got %s", svc.Namespace)
	}
	// Should NOT be headless — needs a ClusterIP for kube-proxy routing
	if svc.Spec.ClusterIP == corev1.ClusterIPNone {
		t.Fatal("lookup service should not be headless")
	}
	// Should have internalTrafficPolicy=Local
	if svc.Spec.InternalTrafficPolicy == nil || *svc.Spec.InternalTrafficPolicy != corev1.ServiceInternalTrafficPolicyLocal {
		t.Fatal("expected internalTrafficPolicy=Local")
	}
	if len(svc.Spec.Ports) != 1 {
		t.Fatalf("expected 1 port, got %d", len(svc.Spec.Ports))
	}
	if svc.Spec.Ports[0].Port != 5555 {
		t.Fatalf("expected port 5555, got %d", svc.Spec.Ports[0].Port)
	}
	if svc.Spec.Ports[0].Name != "server" {
		t.Fatalf("expected port name server, got %s", svc.Spec.Ports[0].Name)
	}

	sel := svc.Spec.Selector
	if sel["app.kubernetes.io/instance"] != testEngineName {
		t.Fatal("missing instance selector")
	}
}

func TestBuildLookupService_CustomPort(t *testing.T) {
	engine := minimalEngine()
	engine.Spec.Server = &lmcachev1alpha1.ServerSpec{
		Port: ptr(int32(8080)),
	}
	svc := BuildLookupService(engine)

	if svc.Spec.Ports[0].Port != 8080 {
		t.Fatalf("expected port 8080, got %d", svc.Spec.Ports[0].Port)
	}
}

// ===========================
// BuildMetricsService
// ===========================

func TestBuildMetricsService_Default(t *testing.T) {
	engine := minimalEngine()
	svc := BuildMetricsService(engine)

	if svc.Name != testEngineName+"-metrics" {
		t.Fatalf("expected name test-engine-metrics, got %s", svc.Name)
	}
	if svc.Namespace != testNamespace {
		t.Fatalf("expected namespace default, got %s", svc.Namespace)
	}
	if svc.Spec.ClusterIP != corev1.ClusterIPNone {
		t.Fatal("expected headless service (ClusterIP=None)")
	}
	if len(svc.Spec.Ports) != 1 {
		t.Fatalf("expected 1 port, got %d", len(svc.Spec.Ports))
	}
	if svc.Spec.Ports[0].Port != 9090 {
		t.Fatalf("expected port 9090, got %d", svc.Spec.Ports[0].Port)
	}
	if svc.Spec.Ports[0].Name != "metrics" {
		t.Fatalf("expected port name metrics, got %s", svc.Spec.Ports[0].Name)
	}

	// Selector should match SelectorLabels
	sel := svc.Spec.Selector
	if sel["app.kubernetes.io/instance"] != testEngineName {
		t.Fatal("missing instance selector")
	}
}

func TestBuildMetricsService_CustomPort(t *testing.T) {
	engine := minimalEngine()
	engine.Spec.Prometheus = &lmcachev1alpha1.PrometheusSpec{
		Port: ptr(int32(8888)),
	}
	svc := BuildMetricsService(engine)

	if svc.Spec.Ports[0].Port != 8888 {
		t.Fatalf("expected port 8888, got %d", svc.Spec.Ports[0].Port)
	}
}

// ===========================
// BuildConnectionConfigMap
// ===========================

func TestBuildConnectionConfigMap_Default(t *testing.T) {
	engine := minimalEngine()
	cm := BuildConnectionConfigMap(engine)

	if cm.Name != "test-engine-connection" {
		t.Fatalf("expected name test-engine-connection, got %s", cm.Name)
	}
	if cm.Namespace != testNamespace {
		t.Fatalf("expected namespace default, got %s", cm.Namespace)
	}

	jsonStr, ok := cm.Data["kv-transfer-config.json"]
	if !ok {
		t.Fatal("missing kv-transfer-config.json key")
	}

	var config map[string]any
	if err := json.Unmarshal([]byte(jsonStr), &config); err != nil {
		t.Fatalf("invalid JSON: %v", err)
	}

	if config["kv_connector"] != "LMCacheMPConnector" {
		t.Fatalf("expected kv_connector=LMCacheMPConnector, got %v", config["kv_connector"])
	}
	if config["kv_role"] != "kv_both" {
		t.Fatalf("expected kv_role=kv_both, got %v", config["kv_role"])
	}

	extra := config["kv_connector_extra_config"].(map[string]any)
	if extra["lmcache.mp.host"] != "tcp://test-engine.default.svc.cluster.local" {
		t.Fatalf("expected service DNS host, got %v", extra["lmcache.mp.host"])
	}
	if extra["lmcache.mp.port"] != "5555" {
		t.Fatalf("expected port 5555, got %v", extra["lmcache.mp.port"])
	}
}

func TestBuildConnectionConfigMap_CustomPort(t *testing.T) {
	engine := minimalEngine()
	engine.Spec.Server = &lmcachev1alpha1.ServerSpec{
		Port: ptr(int32(8080)),
	}
	cm := BuildConnectionConfigMap(engine)

	jsonStr := cm.Data["kv-transfer-config.json"]
	var config map[string]any
	if err := json.Unmarshal([]byte(jsonStr), &config); err != nil {
		t.Fatalf("invalid JSON: %v", err)
	}
	extra := config["kv_connector_extra_config"].(map[string]any)
	if extra["lmcache.mp.port"] != "8080" {
		t.Fatalf("expected port 8080, got %v", extra["lmcache.mp.port"])
	}
}

// ===========================
// ServiceMonitor
// ===========================

func TestServiceMonitorEnabled(t *testing.T) {
	tests := []struct {
		name string
		spec lmcachev1alpha1.LMCacheEngineSpec
		want bool
	}{
		{
			name: "nil prometheus",
			spec: lmcachev1alpha1.LMCacheEngineSpec{L1: lmcachev1alpha1.L1BackendSpec{SizeGB: 10}},
			want: false,
		},
		{
			name: "nil service monitor",
			spec: lmcachev1alpha1.LMCacheEngineSpec{
				L1:         lmcachev1alpha1.L1BackendSpec{SizeGB: 10},
				Prometheus: &lmcachev1alpha1.PrometheusSpec{},
			},
			want: false,
		},
		{
			name: "disabled",
			spec: lmcachev1alpha1.LMCacheEngineSpec{
				L1: lmcachev1alpha1.L1BackendSpec{SizeGB: 10},
				Prometheus: &lmcachev1alpha1.PrometheusSpec{
					ServiceMonitor: &lmcachev1alpha1.ServiceMonitorSpec{Enabled: ptr(false)},
				},
			},
			want: false,
		},
		{
			name: "enabled",
			spec: lmcachev1alpha1.LMCacheEngineSpec{
				L1: lmcachev1alpha1.L1BackendSpec{SizeGB: 10},
				Prometheus: &lmcachev1alpha1.PrometheusSpec{
					ServiceMonitor: &lmcachev1alpha1.ServiceMonitorSpec{Enabled: ptr(true)},
				},
			},
			want: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := ServiceMonitorEnabled(&tt.spec)
			if got != tt.want {
				t.Fatalf("expected %v, got %v", tt.want, got)
			}
		})
	}
}

func TestBuildServiceMonitor(t *testing.T) {
	engine := minimalEngine()
	engine.Spec.Prometheus = &lmcachev1alpha1.PrometheusSpec{
		ServiceMonitor: &lmcachev1alpha1.ServiceMonitorSpec{
			Enabled:  ptr(true),
			Interval: ptr("15s"),
			Labels:   map[string]string{"release": "prom"},
		},
	}

	sm := BuildServiceMonitor(engine)

	if sm.Name != testEngineName {
		t.Fatalf("expected name test-engine, got %s", sm.Name)
	}
	if sm.Namespace != testNamespace {
		t.Fatalf("expected namespace default, got %s", sm.Namespace)
	}

	// Labels should include standard + custom
	if sm.Labels["release"] != "prom" {
		t.Fatal("missing custom label 'release'")
	}
	if sm.Labels["app.kubernetes.io/instance"] != testEngineName {
		t.Fatal("missing standard label")
	}

	// Endpoints
	if len(sm.Spec.Endpoints) != 1 {
		t.Fatalf("expected 1 endpoint, got %d", len(sm.Spec.Endpoints))
	}
	if string(sm.Spec.Endpoints[0].Interval) != "15s" {
		t.Fatalf("expected interval 15s, got %s", sm.Spec.Endpoints[0].Interval)
	}
	if sm.Spec.Endpoints[0].Port != "metrics" {
		t.Fatalf("expected port name metrics, got %s", sm.Spec.Endpoints[0].Port)
	}

	// Selector
	if sm.Spec.Selector.MatchLabels["app.kubernetes.io/instance"] != testEngineName {
		t.Fatal("missing selector label")
	}
}

// ===========================
// Test helpers
// ===========================

func assertArg(t *testing.T, args []string, flag, value string) {
	t.Helper()
	for i, a := range args {
		if a == flag {
			if i+1 < len(args) && args[i+1] == value {
				return
			}
			t.Fatalf("flag %s found but value is %s, expected %s", flag, args[i+1], value)
		}
	}
	t.Fatalf("flag %s not found in args: %v", flag, args)
}

func assertHasArg(t *testing.T, args []string, flag string) {
	t.Helper()
	if !slices.Contains(args, flag) {
		t.Fatalf("flag %s not found in args: %v", flag, args)
	}
}

func assertNoArg(t *testing.T, args []string, flag string) {
	t.Helper()
	for _, a := range args {
		if a == flag {
			t.Fatalf("flag %s should not be present in args", flag)
		}
	}
}
