package podautoscaler

import (
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/sets"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/observability/agent-control-server/pkg/config"
	"github.com/tidbcloud/observability/agent-control-server/pkg/consts"
)

func TestGroupPods(t *testing.T) {
	now := time.Now()
	startTime := metav1.NewTime(now.Add(-10 * time.Minute))
	recentStartTime := metav1.NewTime(now.Add(-1 * time.Minute))
	metricWindow := 1 * time.Minute

	makePod := func(name string, phase corev1.PodPhase, ready bool, start metav1.Time, lastTransition metav1.Time) *corev1.Pod {
		conditionStatus := corev1.ConditionFalse
		if ready {
			conditionStatus = corev1.ConditionTrue
		}
		return &corev1.Pod{
			ObjectMeta: metav1.ObjectMeta{
				Name:      name,
				Namespace: "default",
			},
			Status: corev1.PodStatus{
				Phase:     phase,
				StartTime: &start,
				Conditions: []corev1.PodCondition{
					{
						Type:               corev1.PodReady,
						Status:             conditionStatus,
						LastTransitionTime: lastTransition,
					},
				},
			},
		}
	}

	makeKey := func(name string) client.ObjectKey {
		return client.ObjectKey{Namespace: "default", Name: name}
	}

	tests := []struct {
		name     string
		pods     []*corev1.Pod
		metrics  PodMetricsInfo
		resource corev1.ResourceName

		expectedReady   int
		expectedUnready sets.Set[client.ObjectKey]
		expectedMissing sets.Set[client.ObjectKey]
		expectedIgnored sets.Set[client.ObjectKey]
	}{
		{
			name:     "mixed scenarios with memory resource (skips cpu logic)",
			resource: corev1.ResourceMemory,
			pods: []*corev1.Pod{
				makePod("ready-pod", corev1.PodRunning, true, startTime, startTime),
				makePod("pending-pod", corev1.PodPending, false, startTime, startTime),
				makePod("missing-metric-pod", corev1.PodRunning, true, startTime, startTime),
				{
					ObjectMeta: metav1.ObjectMeta{Name: "deleted-pod", Namespace: "default", DeletionTimestamp: &metav1.Time{Time: now}},
					Status:     corev1.PodStatus{Phase: corev1.PodRunning},
				},
				{
					ObjectMeta: metav1.ObjectMeta{Name: "failed-pod", Namespace: "default"},
					Status:     corev1.PodStatus{Phase: corev1.PodFailed},
				},
			},
			metrics: PodMetricsInfo{
				makeKey("ready-pod"):   PodMetric{Value: 100},
				makeKey("pending-pod"): PodMetric{Value: 100}, // Even if metric exists, pending is unready
			},
			expectedReady:   1, // ready-pod
			expectedUnready: sets.New(makeKey("pending-pod")),
			expectedMissing: sets.New(makeKey("missing-metric-pod")),
			expectedIgnored: sets.New(makeKey("deleted-pod"), makeKey("failed-pod")),
		},
		{
			name:     "cpu logic - new pod scenarios (within init period)",
			resource: corev1.ResourceCPU,
			pods: []*corev1.Pod{
				// Ready, Metric Fresh -> Ready
				makePod("new-ready-fresh", corev1.PodRunning, true, recentStartTime, recentStartTime),
				// Not Ready -> Unready
				makePod("new-not-ready", corev1.PodRunning, false, recentStartTime, recentStartTime),
				// Ready, Metric Stale (Metric < ReadyTransition + Window) -> Unready
				makePod("new-ready-stale", corev1.PodRunning, true, recentStartTime, metav1.NewTime(now)), // Transitioned just now
			},
			metrics: PodMetricsInfo{
				makeKey("new-ready-fresh"): {Timestamp: now, Window: metricWindow},
				makeKey("new-not-ready"):   {Timestamp: now, Window: metricWindow},
				makeKey("new-ready-stale"): {Timestamp: now.Add(-2 * time.Minute), Window: metricWindow},
			},
			expectedReady:   1, // new-ready-fresh
			expectedUnready: sets.New(makeKey("new-not-ready"), makeKey("new-ready-stale")),
			expectedMissing: sets.New[client.ObjectKey](),
			expectedIgnored: sets.New[client.ObjectKey](),
		},
		{
			name:     "cpu logic - old pod scenarios (after init period)",
			resource: corev1.ResourceCPU,
			pods: []*corev1.Pod{
				// Ready -> Ready
				makePod("old-ready", corev1.PodRunning, true, startTime, startTime),

				// Not Ready, LastTransition < Start + Delay -> Unready (Never ready or initial unready)
				// Start = Now - 10m. Delay = 30s. Limit = Now - 10m + 30s.
				// LastTransition = Now - 10m.
				// Start + Delay (Now - 9m30s) > LastTransition (Now - 10m). -> True. -> Unready.
				makePod("old-not-ready-early-transition", corev1.PodRunning, false, startTime, startTime),

				// Not Ready, LastTransition > Start + Delay -> Ready (Counted as ready even if unready status?)
				// Start = Now - 10m.
				// LastTransition = Now - 1m.
				// Start + Delay (Now - 9m30s) > LastTransition (Now - 1m). -> False.
				// unready = false. -> Ready.
				makePod("old-not-ready-late-transition", corev1.PodRunning, false, startTime, recentStartTime),
			},
			metrics: PodMetricsInfo{
				makeKey("old-ready"):                      {Value: 100},
				makeKey("old-not-ready-early-transition"): {Value: 100},
				makeKey("old-not-ready-late-transition"):  {Value: 100},
			},
			expectedReady:   2, // old-ready, old-not-ready-late-transition
			expectedUnready: sets.New(makeKey("old-not-ready-early-transition")),
			expectedMissing: sets.New[client.ObjectKey](),
			expectedIgnored: sets.New[client.ObjectKey](),
		},
	}

	initPeriod := 5 * time.Minute
	delayDelay := 30 * time.Second
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			readyCount, unready, missing, ignored := groupPods(tt.pods, tt.metrics, tt.resource, initPeriod, delayDelay)
			assert.Equal(t, tt.expectedReady, readyCount)
			assert.True(t, tt.expectedUnready.Equal(unready))
			assert.True(t, tt.expectedMissing.Equal(missing))
			assert.True(t, tt.expectedIgnored.Equal(ignored))
		})
	}
}

func TestCalcActionResult(t *testing.T) {
	tests := []struct {
		name     string
		rets     []RawResult
		expected *ActionResult
	}{
		{
			name: "not enough samples",
			rets: []RawResult{
				{ReplicaCount: 2, Utilization: 60},
				{ReplicaCount: 2, Utilization: 60},
			},
			expected: nil,
		},
		{
			name: "scale up triggered (hits threshold met)",
			rets: []RawResult{
				{ReplicaCount: 2, Utilization: 40}, // ignored (index 0, outside last 3?) No, length is 4. Loop from 3 down to 1.
				{ReplicaCount: 2, Utilization: 60}, // Up
				{ReplicaCount: 2, Utilization: 40}, // No action
				{ReplicaCount: 2, Utilization: 60}, // Up
			},
			// Samples: 3. Length: 4. Indices: 3, 2, 1.
			// rets[3]: 60 >= 50 -> Up.
			// rets[2]: 40 < 50.
			// rets[1]: 60 >= 50 -> Up.
			// Up count: 2. Hits: 2. -> Trigger Up.
			// Avg Up Utilization: (60 + 60) / 2 = 60.
			expected: &ActionResult{
				Action:             consts.ScaleActionUp,
				Hits:               2,
				Samples:            3,
				TargetUtilization:  50,
				AverageUtilization: 60,
			},
		},
		{
			name: "scale down triggered",
			rets: []RawResult{
				{ReplicaCount: 0, Utilization: 10}, // Down (util <= 50/4 = 12)
				{ReplicaCount: 0, Utilization: 10}, // Down
				{ReplicaCount: 0, Utilization: 10}, // Down
			},
			// Samples: 3. Length: 3. Indices: 2, 1, 0.
			// rets[2]: 10 <= 16 -> Down.
			// rets[1]: 10 <= 16 -> Down.
			// rets[0]: 10 <= 16 -> Down.
			// Down count: 3. Hits: 2. -> Trigger Down.
			// Avg Down Util: 10.
			expected: &ActionResult{
				Action:             consts.ScaleActionDown,
				Hits:               3,
				Samples:            3,
				TargetUtilization:  50,
				AverageUtilization: 10,
			},
		},
		{
			name: "no action (hits threshold not met)",
			rets: []RawResult{
				{ReplicaCount: 2, Utilization: 60}, // Up
				{ReplicaCount: 2, Utilization: 40}, // No
				{ReplicaCount: 2, Utilization: 40}, // No
			},
			// Up count: 1. Hits: 2.
			expected: nil,
		},
		{
			name: "conflicting signals (none meet hits)",
			rets: []RawResult{
				{ReplicaCount: 2, Utilization: 60}, // Up
				{ReplicaCount: 0, Utilization: 10}, // Down
				{ReplicaCount: 2, Utilization: 40}, // No
			},
			expected: nil,
		},
	}

	autoscalerConfig := config.AutoScaler{
		Samples:           3,
		Hits:              2,
		TargetUtilization: 50,
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := calcActionResult(autoscalerConfig, tt.rets)
			assert.Equal(t, tt.expected, got)
		})
	}
}

func TestGetResourceReplicasGroup(t *testing.T) {
	makeKey := func(name string) client.ObjectKey {
		return client.ObjectKey{Namespace: "default", Name: name}
	}

	makePod := func(name string, cpuRequest string, phase corev1.PodPhase, ready bool) *corev1.Pod {
		conditionStatus := corev1.ConditionFalse
		if ready {
			conditionStatus = corev1.ConditionTrue
		}
		var req resource.Quantity
		if cpuRequest != "" {
			req = resource.MustParse(cpuRequest)
		}

		return &corev1.Pod{
			ObjectMeta: metav1.ObjectMeta{
				Name:      name,
				Namespace: "default",
			},
			Spec: corev1.PodSpec{
				Containers: []corev1.Container{
					{
						Name: "container",
						Resources: corev1.ResourceRequirements{
							Requests: corev1.ResourceList{
								corev1.ResourceCPU: req,
							},
						},
					},
				},
			},
			Status: corev1.PodStatus{
				Phase:     phase,
				StartTime: &metav1.Time{Time: time.Now().Add(-10 * time.Minute)},
				Conditions: []corev1.PodCondition{
					{
						Type:               corev1.PodReady,
						Status:             conditionStatus,
						LastTransitionTime: metav1.Time{Time: time.Now().Add(-10 * time.Minute)},
					},
				},
			},
		}
	}

	defaultTolerances := DefaultTolerances()
	tests := []struct {
		name              string
		pods              []*corev1.Pod
		metrics           PodMetricsInfo
		currentReplicas   int32
		targetUtilization int32

		expectedReplicas    int32
		expectedUtilization int32
		expectError         bool
	}{
		{
			name: "basic scale up (utilization 200%, target 50%) -> ratio 4.0",
			pods: []*corev1.Pod{
				makePod("pod1", "100m", corev1.PodRunning, true),
			},
			metrics: PodMetricsInfo{
				makeKey("pod1"): {Value: 200}, // 200m usage vs 100m request -> 200% utilization
			},
			currentReplicas:   1,
			targetUtilization: 50, // Target 50%

			// Usage Ratio = 200% / 50% = 4.0
			// New Replicas = ceil(4.0 * 1) = 4
			expectedReplicas:    4,
			expectedUtilization: 200,
			expectError:         false,
		},
		{
			name: "basic scale down (utilization 20%, target 50%) -> ratio 0.4",
			pods: []*corev1.Pod{
				makePod("pod1", "100m", corev1.PodRunning, true),
			},
			metrics: PodMetricsInfo{
				makeKey("pod1"): {Value: 20}, // 20m usage vs 100m request -> 20% utilization
			},
			currentReplicas:   1,
			targetUtilization: 50,

			// Usage Ratio = 20% / 50% = 0.4
			expectedReplicas:    1,
			expectedUtilization: 20,
			expectError:         false,
		},
		{
			name: "no change within tolerance",
			pods: []*corev1.Pod{
				makePod("pod1", "100m", corev1.PodRunning, true),
			},
			metrics: PodMetricsInfo{
				makeKey("pod1"): {Value: 52}, // 52m vs 100m -> 52%. Target 50%. Ratio 1.04.
			},
			currentReplicas:   1,
			targetUtilization: 50,

			// Tolerance is 0.05 (0.95 - 1.05).
			// Ratio 1.04 is within tolerance.
			expectedReplicas:    1,
			expectedUtilization: 52,
			expectError:         false,
		},
		{
			name: "scale up with unready pods (treated as 0 usage)",
			pods: []*corev1.Pod{
				makePod("pod1", "100m", corev1.PodRunning, true),
				makePod("pod2", "100m", corev1.PodRunning, false), // Unready
			},
			metrics: PodMetricsInfo{
				makeKey("pod1"): {Value: 200}, // 200m
				makeKey("pod2"): {Value: 200}, // 200m (should be ignored/zeroed)
			},
			currentReplicas:   2,
			targetUtilization: 50,

			// New Replicas = ceil(2.0 * 2 (len(metrics))) = 4.
			expectedReplicas:    4,
			expectedUtilization: 200, // Returns original utilization
			expectError:         false,
		},
		{
			name: "scale down with missing pods (treated as 100% usage)",
			pods: []*corev1.Pod{
				makePod("pod1", "100m", corev1.PodRunning, true),
				makePod("pod2", "100m", corev1.PodRunning, true), // Missing metric
			},
			metrics: PodMetricsInfo{
				makeKey("pod1"): {Value: 10}, // 10m -> 10%. Target 50%.
			},
			currentReplicas:     2,
			targetUtilization:   50,
			expectedReplicas:    2,
			expectedUtilization: 10, // Returns original utilization
			expectError:         false,
		},
		{
			name: "error: all pods unready/ignored",
			pods: []*corev1.Pod{
				makePod("pod1", "100m", corev1.PodRunning, false),
			},
			metrics: PodMetricsInfo{
				makeKey("pod1"): {Value: 200},
			},
			currentReplicas:   1,
			targetUtilization: 50,
			expectError:       true,
		},
		{
			name: "error: pod missing request",
			pods: []*corev1.Pod{
				{
					ObjectMeta: metav1.ObjectMeta{Name: "pod1", Namespace: "default"},
					Spec: corev1.PodSpec{
						Containers: []corev1.Container{
							{Name: "c1", Resources: corev1.ResourceRequirements{}}, // No requests
						},
					},
					Status: corev1.PodStatus{
						Phase: corev1.PodRunning,
						Conditions: []corev1.PodCondition{
							{Type: corev1.PodReady, Status: corev1.ConditionTrue},
						},
					},
				},
			},
			metrics: PodMetricsInfo{
				makeKey("pod1"): {Value: 100},
			},
			currentReplicas:   1,
			targetUtilization: 50,
			expectError:       true,
		},
		{
			name: "ignore deleted pod",
			pods: []*corev1.Pod{
				makePod("pod1", "100m", corev1.PodRunning, true),
				func() *corev1.Pod {
					p := makePod("pod2", "100m", corev1.PodRunning, true)
					now := metav1.Now()
					p.DeletionTimestamp = &now
					return p
				}(),
			},
			metrics: PodMetricsInfo{
				makeKey("pod1"): {Value: 200},
				makeKey("pod2"): {Value: 200},
			},
			currentReplicas:     2,
			targetUtilization:   50,
			expectedReplicas:    4,
			expectedUtilization: 200,
			expectError:         false,
		},
		{
			name: "ignore failed pod",
			pods: []*corev1.Pod{
				makePod("pod1", "100m", corev1.PodRunning, true),
				makePod("pod2", "100m", corev1.PodFailed, true),
			},
			metrics: PodMetricsInfo{
				makeKey("pod1"): {Value: 200},
				makeKey("pod2"): {Value: 200},
			},
			currentReplicas:     2,
			targetUtilization:   50,
			expectedReplicas:    4,
			expectedUtilization: 200,
			expectError:         false,
		},
		{
			name: "pending pod treated as unready",
			pods: []*corev1.Pod{
				makePod("pod1", "100m", corev1.PodRunning, true),
				makePod("pod2", "100m", corev1.PodPending, true),
			},
			metrics: PodMetricsInfo{
				makeKey("pod1"): {Value: 200},
				makeKey("pod2"): {Value: 200},
			},
			currentReplicas:     2,
			targetUtilization:   50,
			expectedReplicas:    4,
			expectedUtilization: 200,
			expectError:         false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			replicas, utilization, _, err := getResourceReplicasGroup(defaultTolerances, tt.pods, tt.metrics, tt.currentReplicas, tt.targetUtilization, corev1.ResourceCPU)
			if tt.expectError {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
				assert.Equal(t, tt.expectedReplicas, replicas)
				assert.Equal(t, tt.expectedUtilization, utilization)
			}
		})
	}
}

func TestCalcScaleResult(t *testing.T) {
	key := GroupKey{RefName: "test-pod", RefNamespace: "default", Namespace: "default"}
	tests := []struct {
		name         string
		setupQueue   func(*queueManager)
		expectedLen  int
		expectedType string // ActionType is string based on usage
	}{
		{
			name: "not enough samples",
			setupQueue: func(qm *queueManager) {
				qm.Put(key, RawResult{Utilization: 100})
			},
			expectedLen: 0,
		},
		{
			name: "frozen window active",
			setupQueue: func(qm *queueManager) {
				// Put enough samples for UP action
				qm.Put(key, RawResult{Utilization: 100})
				qm.Put(key, RawResult{Utilization: 100})
				qm.Put(key, RawResult{Utilization: 100})
				// Set reset time to now
				q, _ := qm.get(key)
				q.lastReset = time.Now().Unix()
			},
			expectedLen: 0,
		},
		{
			name: "frozen window passed",
			setupQueue: func(qm *queueManager) {
				qm.Put(key, RawResult{Utilization: 100})
				qm.Put(key, RawResult{Utilization: 100})
				qm.Put(key, RawResult{Utilization: 100})
				// Set reset time to past
				q, _ := qm.get(key)
				q.lastReset = time.Now().Add(-6 * time.Minute).Unix()
			},
			expectedLen:  1,
			expectedType: consts.ScaleActionUp,
		},
		{
			name: "no reset timestamp (first time)",
			setupQueue: func(qm *queueManager) {
				qm.Put(key, RawResult{Utilization: 100})
				qm.Put(key, RawResult{Utilization: 100})
				qm.Put(key, RawResult{Utilization: 100})
				// lastReset is 0 by default
			},
			expectedLen:  1,
			expectedType: consts.ScaleActionUp,
		},
	}

	autoscalerConfig := config.AutoScaler{
		FrozenSeconds:     300, // 5 minutes
		Samples:           3,
		Hits:              2,
		TargetUtilization: 50,
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			qm := newQueueManager(10)
			tt.setupQueue(qm)

			results := calcScaleResult(autoscalerConfig, qm, corev1.ResourceCPU)
			assert.Equal(t, tt.expectedLen, len(results))
			if tt.expectedLen > 0 {
				assert.Equal(t, tt.expectedType, results[0].ActionResult.Action)
				assert.Equal(t, key, results[0].GroupKey)
				assert.Equal(t, corev1.ResourceCPU, results[0].Resource)
			}
		})
	}
}

func TestGroupByPodMetrics(t *testing.T) {
	makeKey := func(name string) client.ObjectKey {
		return client.ObjectKey{Namespace: "default", Name: name}
	}

	makePod := func(name string, refNamespace, refName string) *corev1.Pod {
		return &corev1.Pod{
			ObjectMeta: metav1.ObjectMeta{
				Name:      name,
				Namespace: "default",
				Labels: map[string]string{
					consts.AgentKeyRefNamespace: refNamespace,
					consts.AgentKeyRefName:      refName,
				},
			},
		}
	}

	tests := []struct {
		name                 string
		pods                 []*corev1.Pod
		metrics              PodMetricsInfo
		expectedGroupCounts  map[GroupKey]int // count of pods per group
		expectedMetricCounts map[GroupKey]int // count of metrics per group
	}{
		{
			name: "single group valid pods",
			pods: []*corev1.Pod{
				makePod("pod1", "ns1", "agent1"),
				makePod("pod2", "ns1", "agent1"),
			},
			metrics: PodMetricsInfo{
				makeKey("pod1"): {Value: 100},
				makeKey("pod2"): {Value: 200},
			},
			expectedGroupCounts: map[GroupKey]int{
				{Namespace: "default", RefNamespace: "ns1", RefName: "agent1"}: 2,
			},
			expectedMetricCounts: map[GroupKey]int{
				{Namespace: "default", RefNamespace: "ns1", RefName: "agent1"}: 2,
			},
		},
		{
			name: "multiple groups",
			pods: []*corev1.Pod{
				makePod("pod1", "ns1", "agent1"),
				makePod("pod2", "ns2", "agent2"),
			},
			metrics: PodMetricsInfo{
				makeKey("pod1"): {Value: 100},
				makeKey("pod2"): {Value: 200},
			},
			expectedGroupCounts: map[GroupKey]int{
				{Namespace: "default", RefNamespace: "ns1", RefName: "agent1"}: 1,
				{Namespace: "default", RefNamespace: "ns2", RefName: "agent2"}: 1,
			},
			expectedMetricCounts: map[GroupKey]int{
				{Namespace: "default", RefNamespace: "ns1", RefName: "agent1"}: 1,
				{Namespace: "default", RefNamespace: "ns2", RefName: "agent2"}: 1,
			},
		},
		{
			name: "skip pod missing from pods list",
			pods: []*corev1.Pod{
				makePod("pod1", "ns1", "agent1"),
			},
			metrics: PodMetricsInfo{
				makeKey("pod1"): {Value: 100},
				makeKey("pod2"): {Value: 200}, // pod2 not in list
			},
			expectedGroupCounts: map[GroupKey]int{
				{Namespace: "default", RefNamespace: "ns1", RefName: "agent1"}: 1,
			},
			expectedMetricCounts: map[GroupKey]int{
				{Namespace: "default", RefNamespace: "ns1", RefName: "agent1"}: 1,
			},
		},
		{
			name: "include pod without metrics",
			pods: []*corev1.Pod{
				makePod("pod1", "ns1", "agent1"),
				makePod("pod2", "ns1", "agent1"),
			},
			metrics: PodMetricsInfo{
				makeKey("pod1"): {Value: 100},
			},
			expectedGroupCounts: map[GroupKey]int{
				{Namespace: "default", RefNamespace: "ns1", RefName: "agent1"}: 1,
			},
			expectedMetricCounts: map[GroupKey]int{
				{Namespace: "default", RefNamespace: "ns1", RefName: "agent1"}: 1,
			},
		},
		{
			name: "skip pod missing labels",
			pods: []*corev1.Pod{
				makePod("pod1", "ns1", "agent1"),
				makePod("pod2", "", "agent1"), // missing refNamespace
				makePod("pod3", "ns1", ""),    // missing refName
				makePod("pod4", "", ""),       // missing both
			},
			metrics: PodMetricsInfo{
				makeKey("pod1"): {Value: 100},
				makeKey("pod2"): {Value: 200},
				makeKey("pod3"): {Value: 300},
				makeKey("pod4"): {Value: 400},
			},
			expectedGroupCounts: map[GroupKey]int{
				{Namespace: "default", RefNamespace: "ns1", RefName: "agent1"}: 1,
			},
			expectedMetricCounts: map[GroupKey]int{
				{Namespace: "default", RefNamespace: "ns1", RefName: "agent1"}: 1,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			podsGroup, metricsGroup := groupByPodMetrics(tt.pods, tt.metrics)

			assert.Equal(t, len(tt.expectedGroupCounts), len(podsGroup))
			for k, count := range tt.expectedGroupCounts {
				assert.Equal(t, count, len(podsGroup[k]))
			}

			assert.Equal(t, len(tt.expectedMetricCounts), len(metricsGroup))
			for k, count := range tt.expectedMetricCounts {
				assert.Equal(t, count, len(metricsGroup[k]))
			}
		})
	}
}

func TestDoCalculateScaleResult(t *testing.T) {
	key := GroupKey{RefName: "test-pod", RefNamespace: "default", Namespace: "default"}
	autoscalerConfig := config.AutoScaler{
		Samples:           3,
		Hits:              2,
		TargetUtilization: 50,
		FrozenSeconds:     300,
	}

	tests := []struct {
		name             string
		cpuRaw           []int32
		memRaw           []int32
		expectedAction   string
		expectedLen      int
		expectQueueReset bool
	}{
		{
			name:             "CPU Up, Mem Up -> Up",
			cpuRaw:           []int32{100, 100, 100},
			memRaw:           []int32{100, 100, 100},
			expectedAction:   consts.ScaleActionUp,
			expectedLen:      1,
			expectQueueReset: true,
		},
		{
			name:             "CPU Up, Mem Down -> Up",
			cpuRaw:           []int32{100, 100, 100},
			memRaw:           []int32{10, 10, 10},
			expectedAction:   consts.ScaleActionUp,
			expectedLen:      1,
			expectQueueReset: true,
		},
		{
			name:             "CPU Down, Mem Up -> Up",
			cpuRaw:           []int32{10, 10, 10},
			memRaw:           []int32{100, 100, 100},
			expectedAction:   consts.ScaleActionUp,
			expectedLen:      1,
			expectQueueReset: true,
		},
		{
			name:             "CPU Down, Mem Down -> Down (single)",
			cpuRaw:           []int32{10, 10, 10},
			memRaw:           []int32{10, 10, 10},
			expectedAction:   consts.ScaleActionDown,
			expectedLen:      1,
			expectQueueReset: true,
		},
		{
			name:             "CPU None, Mem Down -> None (single down ignored)",
			cpuRaw:           []int32{40, 40, 40},
			memRaw:           []int32{10, 10, 10},
			expectedAction:   "",
			expectedLen:      0,
			expectQueueReset: true,
		},
		{
			name:             "CPU Down, Mem None -> None (single down ignored)",
			cpuRaw:           []int32{10, 10, 10},
			memRaw:           []int32{40, 40, 40},
			expectedAction:   "",
			expectedLen:      0,
			expectQueueReset: true,
		},
		{
			name:             "CPU None, Mem None -> None",
			cpuRaw:           []int32{40, 40, 40},
			memRaw:           []int32{40, 40, 40},
			expectedAction:   "",
			expectedLen:      0,
			expectQueueReset: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			rc := &resourceCalculator{
				cpuq:             newQueueManager(10),
				memq:             newQueueManager(10),
				autoscalerConfig: autoscalerConfig,
			}

			for _, v := range tt.cpuRaw {
				rc.cpuq.Put(key, RawResult{Utilization: v, Pods: 1, Time: time.Now()})
			}
			for _, v := range tt.memRaw {
				rc.memq.Put(key, RawResult{Utilization: v, Pods: 1, Time: time.Now()})
			}

			results := rc.doCalculateScaleResult()
			assert.Equal(t, tt.expectedLen, len(results))
			if tt.expectedLen > 0 {
				assert.Equal(t, tt.expectedAction, results[0].ActionResult.Action)
			}

			if tt.expectQueueReset {
				assert.Empty(t, rc.cpuq.List(key))
				assert.Empty(t, rc.memq.List(key))
			} else {
				assert.Equal(t, len(tt.cpuRaw), len(rc.cpuq.List(key)))
				assert.Equal(t, len(tt.memRaw), len(rc.memq.List(key)))
			}
		})
	}
}

func TestCalculatePodLevelRequests(t *testing.T) {
	tests := []struct {
		name     string
		pod      *corev1.Pod
		resource corev1.ResourceName
		want     int64
		wantErr  bool
	}{
		{
			name: "valid pod-level CPU request",
			pod: &corev1.Pod{
				ObjectMeta: metav1.ObjectMeta{Name: "pod1", Namespace: "default"},
				Spec: corev1.PodSpec{
					Resources: &corev1.ResourceRequirements{
						Requests: corev1.ResourceList{
							corev1.ResourceCPU: resource.MustParse("500m"),
						},
					},
				},
			},
			resource: corev1.ResourceCPU,
			want:     500,
		},
		{
			name: "valid pod-level memory request",
			pod: &corev1.Pod{
				ObjectMeta: metav1.ObjectMeta{Name: "pod1", Namespace: "default"},
				Spec: corev1.PodSpec{
					Resources: &corev1.ResourceRequirements{
						Requests: corev1.ResourceList{
							corev1.ResourceMemory: resource.MustParse("1Gi"),
						},
					},
				},
			},
			resource: corev1.ResourceMemory,
			want:     int64(1073741824000),
		},
		{
			name: "missing resource in pod-level requests",
			pod: &corev1.Pod{
				ObjectMeta: metav1.ObjectMeta{Name: "pod1", Namespace: "default"},
				Spec: corev1.PodSpec{
					Resources: &corev1.ResourceRequirements{
						Requests: corev1.ResourceList{
							corev1.ResourceMemory: resource.MustParse("1Gi"),
						},
					},
				},
			},
			resource: corev1.ResourceCPU,
			wantErr:  true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := calculatePodLevelRequests(tt.pod, tt.resource)
			if tt.wantErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
				assert.Equal(t, tt.want, got)
			}
		})
	}
}

func TestCalculatePodRequestsFromContainers(t *testing.T) {
	alwaysRestart := corev1.ContainerRestartPolicyAlways

	tests := []struct {
		name     string
		pod      *corev1.Pod
		resource corev1.ResourceName
		want     int64
		wantErr  bool
	}{
		{
			name: "sums regular containers",
			pod: &corev1.Pod{
				ObjectMeta: metav1.ObjectMeta{Name: "pod1", Namespace: "default"},
				Spec: corev1.PodSpec{
					Containers: []corev1.Container{
						{Name: "c1", Resources: corev1.ResourceRequirements{
							Requests: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("200m")},
						}},
						{Name: "c2", Resources: corev1.ResourceRequirements{
							Requests: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("300m")},
						}},
					},
				},
			},
			resource: corev1.ResourceCPU,
			want:     500,
		},
		{
			name: "includes restartable init containers",
			pod: &corev1.Pod{
				ObjectMeta: metav1.ObjectMeta{Name: "pod1", Namespace: "default"},
				Spec: corev1.PodSpec{
					Containers: []corev1.Container{
						{Name: "c1", Resources: corev1.ResourceRequirements{
							Requests: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("200m")},
						}},
					},
					InitContainers: []corev1.Container{
						{Name: "sidecar", RestartPolicy: &alwaysRestart, Resources: corev1.ResourceRequirements{
							Requests: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("100m")},
						}},
						{Name: "init-not-sidecar", Resources: corev1.ResourceRequirements{
							Requests: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("50m")},
						}},
					},
				},
			},
			resource: corev1.ResourceCPU,
			want:     300,
		},
		{
			name: "error when container missing request",
			pod: &corev1.Pod{
				ObjectMeta: metav1.ObjectMeta{Name: "pod1", Namespace: "default"},
				Spec: corev1.PodSpec{
					Containers: []corev1.Container{
						{Name: "c1", Resources: corev1.ResourceRequirements{}},
					},
				},
			},
			resource: corev1.ResourceCPU,
			wantErr:  true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := calculatePodRequestsFromContainers(tt.pod, tt.resource)
			if tt.wantErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
				assert.Equal(t, tt.want, got)
			}
		})
	}
}

func TestGetResourceUtilizationRatio(t *testing.T) {
	makeKey := func(name string) client.ObjectKey {
		return client.ObjectKey{Namespace: "default", Name: name}
	}

	tests := []struct {
		name              string
		metrics           PodMetricsInfo
		requests          map[client.ObjectKey]int64
		targetUtilization int32
		wantErr           bool
		wantRatio         float64
		wantUtilization   int32
	}{
		{
			name: "disjoint metrics and requests returns error",
			metrics: PodMetricsInfo{
				makeKey("pod1"): {Value: 100},
			},
			requests:          map[client.ObjectKey]int64{makeKey("pod2"): 200},
			targetUtilization: 50,
			wantErr:           true,
		},
		{
			name: "skips metrics without matching request",
			metrics: PodMetricsInfo{
				makeKey("pod1"): {Value: 100},
				makeKey("pod2"): {Value: 50},
			},
			requests:          map[client.ObjectKey]int64{makeKey("pod1"): 200},
			targetUtilization: 50,
			wantErr:           false,
			wantRatio:         1.0,
			wantUtilization:   50,
		},
		{
			name:              "empty metrics and requests returns error",
			metrics:           PodMetricsInfo{},
			requests:          map[client.ObjectKey]int64{},
			targetUtilization: 80,
			wantErr:           true,
		},
		{
			name: "100 percent utilization matches target",
			metrics: PodMetricsInfo{
				makeKey("pod1"): {Value: 200},
			},
			requests:          map[client.ObjectKey]int64{makeKey("pod1"): 200},
			targetUtilization: 100,
			wantErr:           false,
			wantRatio:         1.0,
			wantUtilization:   100,
		},
		{
			name: "multiple pods aggregate correctly",
			metrics: PodMetricsInfo{
				makeKey("pod1"): {Value: 100},
				makeKey("pod2"): {Value: 200},
			},
			requests:          map[client.ObjectKey]int64{makeKey("pod1"): 200, makeKey("pod2"): 400},
			targetUtilization: 50,
			wantErr:           false,
			wantRatio:         1.0,
			wantUtilization:   50,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ratio, util, _, err := getResourceUtilizationRatio(tt.metrics, tt.requests, tt.targetUtilization)
			if tt.wantErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
				assert.InDelta(t, tt.wantRatio, ratio, 0.01)
				assert.Equal(t, tt.wantUtilization, util)
			}
		})
	}
}

func TestGetPodCondition(t *testing.T) {
	tests := []struct {
		name     string
		status   *corev1.PodStatus
		wantNil  bool
		wantType corev1.PodConditionType
	}{
		{
			name:    "nil status returns nil",
			wantNil: true,
		},
		{
			name: "condition found",
			status: &corev1.PodStatus{
				Conditions: []corev1.PodCondition{
					{Type: corev1.PodScheduled, Status: corev1.ConditionTrue},
					{Type: corev1.PodReady, Status: corev1.ConditionTrue},
				},
			},
			wantType: corev1.PodReady,
		},
		{
			name: "condition not found",
			status: &corev1.PodStatus{
				Conditions: []corev1.PodCondition{
					{Type: corev1.PodScheduled, Status: corev1.ConditionTrue},
				},
			},
			wantNil: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cond := getPodCondition(tt.status, corev1.PodReady)
			if tt.wantNil {
				assert.Nil(t, cond)
			} else {
				assert.NotNil(t, cond)
				assert.Equal(t, tt.wantType, cond.Type)
			}
		})
	}
}

func TestGetPodConditionFromList(t *testing.T) {
	tests := []struct {
		name       string
		conditions []corev1.PodCondition
		wantNil    bool
	}{
		{
			name:    "nil conditions returns nil",
			wantNil: true,
		},
		{
			name: "condition found in list",
			conditions: []corev1.PodCondition{
				{Type: corev1.PodReady, Status: corev1.ConditionTrue},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cond := getPodConditionFromList(tt.conditions, corev1.PodReady)
			if tt.wantNil {
				assert.Nil(t, cond)
			} else {
				assert.NotNil(t, cond)
			}
		})
	}
}
