/*
Copyright 2018 The Kubernetes Authors.

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 framework

import (
	"fmt"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
	"k8s.io/apimachinery/pkg/util/sets"

	v1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	utilfeature "k8s.io/apiserver/pkg/util/feature"
	featuregatetesting "k8s.io/component-base/featuregate/testing"
	ndf "k8s.io/component-helpers/nodedeclaredfeatures"
	ndftesting "k8s.io/component-helpers/nodedeclaredfeatures/testing"
	"k8s.io/klog/v2"
	fwk "k8s.io/kube-scheduler/framework"
	"k8s.io/kubernetes/pkg/features"
	st "k8s.io/kubernetes/pkg/scheduler/testing"
	schedutil "k8s.io/kubernetes/pkg/scheduler/util"
	"k8s.io/kubernetes/test/utils/ktesting"
	"k8s.io/kubernetes/test/utils/ktesting/initoption"
)

var nodeInfoCmpOpts = []cmp.Option{
	cmp.AllowUnexported(NodeInfo{}, PodInfo{}, fwk.PodResource{}),
}

func TestNewResource(t *testing.T) {
	tests := []struct {
		name         string
		resourceList v1.ResourceList
		expected     *Resource
	}{
		{
			name:         "empty resource",
			resourceList: map[v1.ResourceName]resource.Quantity{},
			expected:     &Resource{},
		},
		{
			name: "complex resource",
			resourceList: map[v1.ResourceName]resource.Quantity{
				v1.ResourceCPU:                      *resource.NewScaledQuantity(4, -3),
				v1.ResourceMemory:                   *resource.NewQuantity(2000, resource.BinarySI),
				v1.ResourcePods:                     *resource.NewQuantity(80, resource.BinarySI),
				v1.ResourceEphemeralStorage:         *resource.NewQuantity(5000, resource.BinarySI),
				"scalar.test/" + "scalar1":          *resource.NewQuantity(1, resource.DecimalSI),
				v1.ResourceHugePagesPrefix + "test": *resource.NewQuantity(2, resource.BinarySI),
			},
			expected: &Resource{
				MilliCPU:         4,
				Memory:           2000,
				EphemeralStorage: 5000,
				AllowedPodNumber: 80,
				ScalarResources:  map[v1.ResourceName]int64{"scalar.test/scalar1": 1, "hugepages-test": 2},
			},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			r := NewResource(test.resourceList)
			if diff := cmp.Diff(test.expected, r); diff != "" {
				t.Errorf("Unexpected resource (-want, +got):\n%s", diff)
			}
		})
	}
}

func TestResourceClone(t *testing.T) {
	tests := []struct {
		resource *Resource
		expected *Resource
	}{
		{
			resource: &Resource{},
			expected: &Resource{},
		},
		{
			resource: &Resource{
				MilliCPU:         4,
				Memory:           2000,
				EphemeralStorage: 5000,
				AllowedPodNumber: 80,
				ScalarResources:  map[v1.ResourceName]int64{"scalar.test/scalar1": 1, "hugepages-test": 2},
			},
			expected: &Resource{
				MilliCPU:         4,
				Memory:           2000,
				EphemeralStorage: 5000,
				AllowedPodNumber: 80,
				ScalarResources:  map[v1.ResourceName]int64{"scalar.test/scalar1": 1, "hugepages-test": 2},
			},
		},
	}

	for i, test := range tests {
		t.Run(fmt.Sprintf("case_%d", i), func(t *testing.T) {
			r := test.resource.Clone()
			// Modify the field to check if the result is a clone of the origin one.
			test.resource.MilliCPU += 1000
			if diff := cmp.Diff(test.expected, r); diff != "" {
				t.Errorf("Unexpected resource (-want, +got):\n%s", diff)
			}
		})
	}
}

func TestResourceAddScalar(t *testing.T) {
	tests := []struct {
		resource       *Resource
		scalarName     v1.ResourceName
		scalarQuantity int64
		expected       *Resource
	}{
		{
			resource:       &Resource{},
			scalarName:     "scalar1",
			scalarQuantity: 100,
			expected: &Resource{
				ScalarResources: map[v1.ResourceName]int64{"scalar1": 100},
			},
		},
		{
			resource: &Resource{
				MilliCPU:         4,
				Memory:           2000,
				EphemeralStorage: 5000,
				AllowedPodNumber: 80,
				ScalarResources:  map[v1.ResourceName]int64{"hugepages-test": 2},
			},
			scalarName:     "scalar2",
			scalarQuantity: 200,
			expected: &Resource{
				MilliCPU:         4,
				Memory:           2000,
				EphemeralStorage: 5000,
				AllowedPodNumber: 80,
				ScalarResources:  map[v1.ResourceName]int64{"hugepages-test": 2, "scalar2": 200},
			},
		},
	}

	for _, test := range tests {
		t.Run(string(test.scalarName), func(t *testing.T) {
			test.resource.AddScalar(test.scalarName, test.scalarQuantity)
			if diff := cmp.Diff(test.expected, test.resource); diff != "" {
				t.Errorf("Unexpected resource (-want, +got):\n%s", diff)
			}
		})
	}
}

func TestSetMaxResource(t *testing.T) {
	tests := []struct {
		resource     *Resource
		resourceList v1.ResourceList
		expected     *Resource
	}{
		{
			resource: &Resource{},
			resourceList: map[v1.ResourceName]resource.Quantity{
				v1.ResourceCPU:              *resource.NewScaledQuantity(4, -3),
				v1.ResourceMemory:           *resource.NewQuantity(2000, resource.BinarySI),
				v1.ResourceEphemeralStorage: *resource.NewQuantity(5000, resource.BinarySI),
			},
			expected: &Resource{
				MilliCPU:         4,
				Memory:           2000,
				EphemeralStorage: 5000,
			},
		},
		{
			resource: &Resource{
				MilliCPU:         4,
				Memory:           4000,
				EphemeralStorage: 5000,
				ScalarResources:  map[v1.ResourceName]int64{"scalar.test/scalar1": 1, "hugepages-test": 2},
			},
			resourceList: map[v1.ResourceName]resource.Quantity{
				v1.ResourceCPU:                      *resource.NewScaledQuantity(4, -3),
				v1.ResourceMemory:                   *resource.NewQuantity(2000, resource.BinarySI),
				v1.ResourceEphemeralStorage:         *resource.NewQuantity(7000, resource.BinarySI),
				"scalar.test/scalar1":               *resource.NewQuantity(4, resource.DecimalSI),
				v1.ResourceHugePagesPrefix + "test": *resource.NewQuantity(5, resource.BinarySI),
			},
			expected: &Resource{
				MilliCPU:         4,
				Memory:           4000,
				EphemeralStorage: 7000,
				ScalarResources:  map[v1.ResourceName]int64{"scalar.test/scalar1": 4, "hugepages-test": 5},
			},
		},
	}

	for i, test := range tests {
		t.Run(fmt.Sprintf("case_%d", i), func(t *testing.T) {
			test.resource.SetMaxResource(test.resourceList)
			if diff := cmp.Diff(test.expected, test.resource); diff != "" {
				t.Errorf("Unexpected resource (-want, +got):\n%s", diff)
			}
		})
	}
}

func TestNewNodeInfo(t *testing.T) {
	nodeName := "test-node"
	pods := []*v1.Pod{
		st.MakePod().UID("test-1").Namespace("node_info_cache_test").Name("test-1").Node(nodeName).
			Containers([]v1.Container{st.MakeContainer().ResourceRequests(map[v1.ResourceName]string{
				v1.ResourceCPU:    "100m",
				v1.ResourceMemory: "500",
			}).ContainerPort([]v1.ContainerPort{{
				HostIP:   "127.0.0.1",
				HostPort: 80,
				Protocol: "TCP",
			}}).Obj()}).
			Obj(),

		st.MakePod().UID("test-2").Namespace("node_info_cache_test").Name("test-2").Node(nodeName).
			Containers([]v1.Container{st.MakeContainer().ResourceRequests(map[v1.ResourceName]string{
				v1.ResourceCPU:    "200m",
				v1.ResourceMemory: "1Ki",
			}).ContainerPort([]v1.ContainerPort{{
				HostIP:   "127.0.0.1",
				HostPort: 8080,
				Protocol: "TCP",
			}}).Obj()}).
			Obj(),
	}

	expected := &NodeInfo{
		Requested: &Resource{
			MilliCPU:         300,
			Memory:           1524,
			EphemeralStorage: 0,
			AllowedPodNumber: 0,
			ScalarResources:  map[v1.ResourceName]int64(nil),
		},
		NonZeroRequested: &Resource{
			MilliCPU:         300,
			Memory:           1524,
			EphemeralStorage: 0,
			AllowedPodNumber: 0,
			ScalarResources:  map[v1.ResourceName]int64(nil),
		},
		Allocatable: &Resource{},
		Generation:  2,
		UsedPorts: fwk.HostPortInfo{
			"127.0.0.1": map[fwk.ProtocolPort]struct{}{
				{Protocol: "TCP", Port: 80}:   {},
				{Protocol: "TCP", Port: 8080}: {},
			},
		},
		ImageStates:                   map[string]*fwk.ImageStateSummary{},
		PVCRefCounts:                  map[string]int{},
		NodeAllocatableDRAClaimStates: map[types.NamespacedName]*fwk.NodeAllocatableDRAClaimState{},
		Pods: []fwk.PodInfo{
			&PodInfo{
				Pod: &v1.Pod{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: "node_info_cache_test",
						Name:      "test-1",
						UID:       types.UID("test-1"),
					},
					Spec: v1.PodSpec{
						Containers: []v1.Container{
							{
								Resources: v1.ResourceRequirements{
									Requests: v1.ResourceList{
										v1.ResourceCPU:    resource.MustParse("100m"),
										v1.ResourceMemory: resource.MustParse("500"),
									},
								},
								Ports: []v1.ContainerPort{
									{
										HostIP:   "127.0.0.1",
										HostPort: 80,
										Protocol: "TCP",
									},
								},
							},
						},
						NodeName: nodeName,
					},
				},
				cachedResource: &fwk.PodResource{
					Resource: &Resource{
						MilliCPU: 100,
						Memory:   500,
					},
					Non0CPU: 100,
					Non0Mem: 500,
				},
			},
			&PodInfo{
				Pod: &v1.Pod{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: "node_info_cache_test",
						Name:      "test-2",
						UID:       types.UID("test-2"),
					},
					Spec: v1.PodSpec{
						Containers: []v1.Container{
							{
								Resources: v1.ResourceRequirements{
									Requests: v1.ResourceList{
										v1.ResourceCPU:    resource.MustParse("200m"),
										v1.ResourceMemory: resource.MustParse("1Ki"),
									},
								},
								Ports: []v1.ContainerPort{
									{
										HostIP:   "127.0.0.1",
										HostPort: 8080,
										Protocol: "TCP",
									},
								},
							},
						},
						NodeName: nodeName,
					},
				},
				cachedResource: &fwk.PodResource{
					Resource: &Resource{
						MilliCPU: 200,
						Memory:   1024,
					},
					Non0CPU: 200,
					Non0Mem: 1024,
				},
			},
		},
	}

	gen := generation
	ni := NewNodeInfo(pods...)
	if ni.Generation <= gen {
		t.Errorf("Generation is not incremented. previous: %v, current: %v", gen, ni.Generation)
	}
	expected.Generation = ni.Generation
	if diff := cmp.Diff(expected, ni, nodeInfoCmpOpts...); diff != "" {
		t.Errorf("Unexpected NodeInfo (-want, +got):\n%s", diff)
	}
}

func TestNodeInfoClone(t *testing.T) {
	nodeName := "test-node"
	declaredFeatureSet := ndf.NewFeatureMapper([]string{"A", "B", "C"}).MustMapSorted([]string{"A", "C"})

	tests := []struct {
		nodeInfo *NodeInfo
		expected *NodeInfo
	}{
		{
			nodeInfo: &NodeInfo{
				Requested:        &Resource{},
				NonZeroRequested: &Resource{},
				Allocatable:      &Resource{},
				Generation:       2,
				UsedPorts: fwk.HostPortInfo{
					"127.0.0.1": map[fwk.ProtocolPort]struct{}{
						{Protocol: "TCP", Port: 80}:   {},
						{Protocol: "TCP", Port: 8080}: {},
					},
				},
				ImageStates:                   map[string]*fwk.ImageStateSummary{},
				PVCRefCounts:                  map[string]int{},
				NodeAllocatableDRAClaimStates: map[types.NamespacedName]*fwk.NodeAllocatableDRAClaimState{},
				Pods: []fwk.PodInfo{
					&PodInfo{
						Pod: &v1.Pod{
							ObjectMeta: metav1.ObjectMeta{
								Namespace: "node_info_cache_test",
								Name:      "test-1",
								UID:       types.UID("test-1"),
							},
							Spec: v1.PodSpec{
								Containers: []v1.Container{
									{
										Resources: v1.ResourceRequirements{
											Requests: v1.ResourceList{
												v1.ResourceCPU:    resource.MustParse("100m"),
												v1.ResourceMemory: resource.MustParse("500"),
											},
										},
										Ports: []v1.ContainerPort{
											{
												HostIP:   "127.0.0.1",
												HostPort: 80,
												Protocol: "TCP",
											},
										},
									},
								},
								NodeName: nodeName,
							},
						},
						cachedResource: &fwk.PodResource{
							Resource: &Resource{
								MilliCPU: 100,
								Memory:   500,
							},
							Non0CPU: 100,
							Non0Mem: 500,
						},
					},
					&PodInfo{
						Pod: &v1.Pod{
							ObjectMeta: metav1.ObjectMeta{
								Namespace: "node_info_cache_test",
								Name:      "test-2",
								UID:       types.UID("test-2"),
							},
							Spec: v1.PodSpec{
								Containers: []v1.Container{
									{
										Resources: v1.ResourceRequirements{
											Requests: v1.ResourceList{
												v1.ResourceCPU:    resource.MustParse("200m"),
												v1.ResourceMemory: resource.MustParse("1Ki"),
											},
										},
										Ports: []v1.ContainerPort{
											{
												HostIP:   "127.0.0.1",
												HostPort: 8080,
												Protocol: "TCP",
											},
										},
									},
								},
								NodeName: nodeName,
							},
						},
						cachedResource: &fwk.PodResource{
							Resource: &Resource{
								MilliCPU: 200,
								Memory:   1024,
							},
							Non0CPU: 200,
							Non0Mem: 1024,
						},
					},
				},
			},
			expected: &NodeInfo{
				Requested:        &Resource{},
				NonZeroRequested: &Resource{},
				Allocatable:      &Resource{},
				Generation:       2,
				UsedPorts: fwk.HostPortInfo{
					"127.0.0.1": map[fwk.ProtocolPort]struct{}{
						{Protocol: "TCP", Port: 80}:   {},
						{Protocol: "TCP", Port: 8080}: {},
					},
				},
				ImageStates:                   map[string]*fwk.ImageStateSummary{},
				PVCRefCounts:                  map[string]int{},
				NodeAllocatableDRAClaimStates: map[types.NamespacedName]*fwk.NodeAllocatableDRAClaimState{},
				Pods: []fwk.PodInfo{
					&PodInfo{
						Pod: &v1.Pod{
							ObjectMeta: metav1.ObjectMeta{
								Namespace: "node_info_cache_test",
								Name:      "test-1",
								UID:       types.UID("test-1"),
							},
							Spec: v1.PodSpec{
								Containers: []v1.Container{
									{
										Resources: v1.ResourceRequirements{
											Requests: v1.ResourceList{
												v1.ResourceCPU:    resource.MustParse("100m"),
												v1.ResourceMemory: resource.MustParse("500"),
											},
										},
										Ports: []v1.ContainerPort{
											{
												HostIP:   "127.0.0.1",
												HostPort: 80,
												Protocol: "TCP",
											},
										},
									},
								},
								NodeName: nodeName,
							},
						},
						cachedResource: &fwk.PodResource{
							Resource: &Resource{
								MilliCPU: 100,
								Memory:   500,
							},
							Non0CPU: 100,
							Non0Mem: 500,
						},
					},
					&PodInfo{
						Pod: &v1.Pod{
							ObjectMeta: metav1.ObjectMeta{
								Namespace: "node_info_cache_test",
								Name:      "test-2",
								UID:       types.UID("test-2"),
							},
							Spec: v1.PodSpec{
								Containers: []v1.Container{
									{
										Resources: v1.ResourceRequirements{
											Requests: v1.ResourceList{
												v1.ResourceCPU:    resource.MustParse("200m"),
												v1.ResourceMemory: resource.MustParse("1Ki"),
											},
										},
										Ports: []v1.ContainerPort{
											{
												HostIP:   "127.0.0.1",
												HostPort: 8080,
												Protocol: "TCP",
											},
										},
									},
								},
								NodeName: nodeName,
							},
						},
						cachedResource: &fwk.PodResource{
							Resource: &Resource{
								MilliCPU: 200,
								Memory:   1024,
							},
							Non0CPU: 200,
							Non0Mem: 1024,
						},
					},
				},
			},
		},
		{
			nodeInfo: &NodeInfo{
				Requested:                     &Resource{},
				NonZeroRequested:              &Resource{},
				Allocatable:                   &Resource{},
				Generation:                    3,
				UsedPorts:                     fwk.HostPortInfo{},
				ImageStates:                   map[string]*fwk.ImageStateSummary{},
				PVCRefCounts:                  map[string]int{},
				DeclaredFeatures:              declaredFeatureSet.Clone(),
				NodeAllocatableDRAClaimStates: map[types.NamespacedName]*fwk.NodeAllocatableDRAClaimState{},
			},
			expected: &NodeInfo{
				Requested:                     &Resource{},
				NonZeroRequested:              &Resource{},
				Allocatable:                   &Resource{},
				Generation:                    3,
				UsedPorts:                     fwk.HostPortInfo{},
				ImageStates:                   map[string]*fwk.ImageStateSummary{},
				PVCRefCounts:                  map[string]int{},
				DeclaredFeatures:              declaredFeatureSet,
				NodeAllocatableDRAClaimStates: map[types.NamespacedName]*fwk.NodeAllocatableDRAClaimState{},
			},
		},
		{
			nodeInfo: &NodeInfo{
				Requested:        &Resource{},
				NonZeroRequested: &Resource{},
				Allocatable:      &Resource{},
				Generation:       3,
				UsedPorts:        fwk.HostPortInfo{},
				ImageStates:      map[string]*fwk.ImageStateSummary{},
				PVCRefCounts:     map[string]int{},
				DeclaredFeatures: declaredFeatureSet.Clone(),
				NodeAllocatableDRAClaimStates: map[types.NamespacedName]*fwk.NodeAllocatableDRAClaimState{
					{Name: "claim1", Namespace: "default"}: {ConsumerPods: sets.New[types.UID]("pod1uid", "pod2uid")},
				},
			},
			expected: &NodeInfo{
				Requested:        &Resource{},
				NonZeroRequested: &Resource{},
				Allocatable:      &Resource{},
				Generation:       3,
				UsedPorts:        fwk.HostPortInfo{},
				ImageStates:      map[string]*fwk.ImageStateSummary{},
				PVCRefCounts:     map[string]int{},
				DeclaredFeatures: declaredFeatureSet,
				NodeAllocatableDRAClaimStates: map[types.NamespacedName]*fwk.NodeAllocatableDRAClaimState{
					{Name: "claim1", Namespace: "default"}: {ConsumerPods: sets.New[types.UID]("pod1uid", "pod2uid")},
				},
			},
		},
	}

	for i, test := range tests {
		t.Run(fmt.Sprintf("case_%d", i), func(t *testing.T) {
			ni := test.nodeInfo.Snapshot()
			// Modify the field to check if the result is a clone of the origin one.
			test.nodeInfo.Generation += 10
			test.nodeInfo.UsedPorts.Remove("127.0.0.1", "TCP", 80)
			if diff := cmp.Diff(test.expected, ni, nodeInfoCmpOpts...); diff != "" {
				t.Errorf("Unexpected NodeInfo (-want, +got):\n%s", diff)
			}
		})
	}
}

func TestNodeInfoAddPod(t *testing.T) {
	nodeName := "test-node"
	pods := []*v1.Pod{
		{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: "node_info_cache_test",
				Name:      "test-1",
				UID:       types.UID("test-1"),
			},
			Spec: v1.PodSpec{
				Containers: []v1.Container{
					{
						Resources: v1.ResourceRequirements{
							Requests: v1.ResourceList{
								v1.ResourceCPU:    resource.MustParse("100m"),
								v1.ResourceMemory: resource.MustParse("500"),
							},
						},
						Ports: []v1.ContainerPort{
							{
								HostIP:   "127.0.0.1",
								HostPort: 80,
								Protocol: "TCP",
							},
						},
					},
				},
				NodeName: nodeName,
				Overhead: v1.ResourceList{
					v1.ResourceCPU: resource.MustParse("500m"),
				},
				Volumes: []v1.Volume{
					{
						VolumeSource: v1.VolumeSource{
							PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{
								ClaimName: "pvc-1",
							},
						},
					},
				},
			},
		},
		{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: "node_info_cache_test",
				Name:      "test-2",
				UID:       types.UID("test-2"),
			},
			Spec: v1.PodSpec{
				Containers: []v1.Container{
					{
						Resources: v1.ResourceRequirements{
							Requests: v1.ResourceList{
								v1.ResourceCPU: resource.MustParse("200m"),
							},
						},
						Ports: []v1.ContainerPort{
							{
								HostIP:   "127.0.0.1",
								HostPort: 8080,
								Protocol: "TCP",
							},
						},
					},
				},
				NodeName: nodeName,
				Overhead: v1.ResourceList{
					v1.ResourceCPU:    resource.MustParse("500m"),
					v1.ResourceMemory: resource.MustParse("500"),
				},
				Volumes: []v1.Volume{
					{
						VolumeSource: v1.VolumeSource{
							PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{
								ClaimName: "pvc-1",
							},
						},
					},
				},
			},
		},
		{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: "node_info_cache_test",
				Name:      "test-3",
				UID:       types.UID("test-3"),
			},
			Spec: v1.PodSpec{
				Containers: []v1.Container{
					{
						Resources: v1.ResourceRequirements{
							Requests: v1.ResourceList{
								v1.ResourceCPU: resource.MustParse("200m"),
							},
						},
						Ports: []v1.ContainerPort{
							{
								HostIP:   "127.0.0.1",
								HostPort: 8080,
								Protocol: "TCP",
							},
						},
					},
				},
				InitContainers: []v1.Container{
					{
						Resources: v1.ResourceRequirements{
							Requests: v1.ResourceList{
								v1.ResourceCPU:    resource.MustParse("500m"),
								v1.ResourceMemory: resource.MustParse("200Mi"),
							},
						},
					},
				},
				NodeName: nodeName,
				Overhead: v1.ResourceList{
					v1.ResourceCPU:    resource.MustParse("500m"),
					v1.ResourceMemory: resource.MustParse("500"),
				},
				Volumes: []v1.Volume{
					{
						VolumeSource: v1.VolumeSource{
							PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{
								ClaimName: "pvc-2",
							},
						},
					},
				},
			},
		},
	}
	expected := &NodeInfo{
		node: &v1.Node{
			ObjectMeta: metav1.ObjectMeta{
				Name: "test-node",
			},
		},
		Requested: &Resource{
			MilliCPU:         2300,
			Memory:           209716700, //1500 + 200MB in initContainers
			EphemeralStorage: 0,
			AllowedPodNumber: 0,
			ScalarResources:  map[v1.ResourceName]int64(nil),
		},
		NonZeroRequested: &Resource{
			MilliCPU:         2300,
			Memory:           419431900, //200MB(initContainers) + 200MB(default memory value) + 1500 specified in requests/overhead
			EphemeralStorage: 0,
			AllowedPodNumber: 0,
			ScalarResources:  map[v1.ResourceName]int64(nil),
		},
		Allocatable: &Resource{},
		Generation:  2,
		UsedPorts: fwk.HostPortInfo{
			"127.0.0.1": map[fwk.ProtocolPort]struct{}{
				{Protocol: "TCP", Port: 80}:   {},
				{Protocol: "TCP", Port: 8080}: {},
			},
		},
		ImageStates:                   map[string]*fwk.ImageStateSummary{},
		PVCRefCounts:                  map[string]int{"node_info_cache_test/pvc-1": 2, "node_info_cache_test/pvc-2": 1},
		DeclaredFeatures:              ndf.DefaultFramework.NewFeatureSet(), // Empty FeatureSet.
		NodeAllocatableDRAClaimStates: map[types.NamespacedName]*fwk.NodeAllocatableDRAClaimState{},
		Pods: []fwk.PodInfo{
			&PodInfo{
				Pod: &v1.Pod{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: "node_info_cache_test",
						Name:      "test-1",
						UID:       types.UID("test-1"),
					},
					Spec: v1.PodSpec{
						Containers: []v1.Container{
							{
								Resources: v1.ResourceRequirements{
									Requests: v1.ResourceList{
										v1.ResourceCPU:    resource.MustParse("100m"),
										v1.ResourceMemory: resource.MustParse("500"),
									},
								},
								Ports: []v1.ContainerPort{
									{
										HostIP:   "127.0.0.1",
										HostPort: 80,
										Protocol: "TCP",
									},
								},
							},
						},
						NodeName: nodeName,
						Overhead: v1.ResourceList{
							v1.ResourceCPU: resource.MustParse("500m"),
						},
						Volumes: []v1.Volume{
							{
								VolumeSource: v1.VolumeSource{
									PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{
										ClaimName: "pvc-1",
									},
								},
							},
						},
					},
				},
				cachedResource: &fwk.PodResource{
					Resource: &Resource{
						MilliCPU: 600,
						Memory:   500,
					},
					Non0CPU: 600,
					Non0Mem: 500,
				},
			},
			&PodInfo{
				Pod: &v1.Pod{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: "node_info_cache_test",
						Name:      "test-2",
						UID:       types.UID("test-2"),
					},
					Spec: v1.PodSpec{
						Containers: []v1.Container{
							{
								Resources: v1.ResourceRequirements{
									Requests: v1.ResourceList{
										v1.ResourceCPU: resource.MustParse("200m"),
									},
								},
								Ports: []v1.ContainerPort{
									{
										HostIP:   "127.0.0.1",
										HostPort: 8080,
										Protocol: "TCP",
									},
								},
							},
						},
						NodeName: nodeName,
						Overhead: v1.ResourceList{
							v1.ResourceCPU:    resource.MustParse("500m"),
							v1.ResourceMemory: resource.MustParse("500"),
						},
						Volumes: []v1.Volume{
							{
								VolumeSource: v1.VolumeSource{
									PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{
										ClaimName: "pvc-1",
									},
								},
							},
						},
					},
				},
				cachedResource: &fwk.PodResource{
					Resource: &Resource{
						MilliCPU: 700,
						Memory:   500,
					},
					Non0CPU: 700,
					Non0Mem: schedutil.DefaultMemoryRequest + 500,
				},
			},
			&PodInfo{
				Pod: &v1.Pod{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: "node_info_cache_test",
						Name:      "test-3",
						UID:       types.UID("test-3"),
					},
					Spec: v1.PodSpec{
						Containers: []v1.Container{
							{
								Resources: v1.ResourceRequirements{
									Requests: v1.ResourceList{
										v1.ResourceCPU: resource.MustParse("200m"),
									},
								},
								Ports: []v1.ContainerPort{
									{
										HostIP:   "127.0.0.1",
										HostPort: 8080,
										Protocol: "TCP",
									},
								},
							},
						},
						InitContainers: []v1.Container{
							{
								Resources: v1.ResourceRequirements{
									Requests: v1.ResourceList{
										v1.ResourceCPU:    resource.MustParse("500m"),
										v1.ResourceMemory: resource.MustParse("200Mi"),
									},
								},
							},
						},
						NodeName: nodeName,
						Overhead: v1.ResourceList{
							v1.ResourceCPU:    resource.MustParse("500m"),
							v1.ResourceMemory: resource.MustParse("500"),
						},
						Volumes: []v1.Volume{
							{
								VolumeSource: v1.VolumeSource{
									PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{
										ClaimName: "pvc-2",
									},
								},
							},
						},
					},
				},
				cachedResource: &fwk.PodResource{
					Resource: &Resource{
						MilliCPU: 1000,
						Memory:   schedutil.DefaultMemoryRequest + 500,
					},
					Non0CPU: 1000,
					Non0Mem: schedutil.DefaultMemoryRequest + 500,
				},
			},
		},
	}

	ni := fakeNodeInfo()
	gen := ni.Generation
	for _, pod := range pods {
		ni.AddPod(pod)
		if ni.Generation <= gen {
			t.Errorf("Generation is not incremented. Prev: %v, current: %v", gen, ni.Generation)
		}
		gen = ni.Generation
	}

	expected.Generation = ni.Generation
	if diff := cmp.Diff(expected, ni, nodeInfoCmpOpts...); diff != "" {
		t.Errorf("Unexpected NodeInfo (-want, +got):\n%s", diff)
	}
}

func TestNodeInfoRemovePod(t *testing.T) {
	nodeName := "test-node"
	pods := []*v1.Pod{
		st.MakePod().UID("test-1").Namespace("node_info_cache_test").Name("test-1").Node(nodeName).
			Containers([]v1.Container{st.MakeContainer().ResourceRequests(map[v1.ResourceName]string{
				v1.ResourceCPU:    "100m",
				v1.ResourceMemory: "500",
			}).ContainerPort([]v1.ContainerPort{{
				HostIP:   "127.0.0.1",
				HostPort: 80,
				Protocol: "TCP",
			}}).Obj()}).
			Volumes([]v1.Volume{{VolumeSource: v1.VolumeSource{PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ClaimName: "pvc-1"}}}}).
			Obj(),

		st.MakePod().UID("test-2").Namespace("node_info_cache_test").Name("test-2").Node(nodeName).
			Containers([]v1.Container{st.MakeContainer().ResourceRequests(map[v1.ResourceName]string{
				v1.ResourceCPU:    "200m",
				v1.ResourceMemory: "1Ki",
			}).ContainerPort([]v1.ContainerPort{{
				HostIP:   "127.0.0.1",
				HostPort: 8080,
				Protocol: "TCP",
			}}).Obj()}).
			Obj(),
	}

	// add pod Overhead
	for _, pod := range pods {
		pod.Spec.Overhead = v1.ResourceList{
			v1.ResourceCPU:    resource.MustParse("500m"),
			v1.ResourceMemory: resource.MustParse("500"),
		}
	}

	tests := []struct {
		pod              *v1.Pod
		errExpected      bool
		expectedNodeInfo *NodeInfo
	}{
		{
			pod:         st.MakePod().UID("non-exist").Namespace("node_info_cache_test").Node(nodeName).Obj(),
			errExpected: true,
			expectedNodeInfo: &NodeInfo{
				node: &v1.Node{
					ObjectMeta: metav1.ObjectMeta{
						Name: "test-node",
					},
				},
				Requested: &Resource{
					MilliCPU:         1300,
					Memory:           2524,
					EphemeralStorage: 0,
					AllowedPodNumber: 0,
					ScalarResources:  map[v1.ResourceName]int64(nil),
				},
				NonZeroRequested: &Resource{
					MilliCPU:         1300,
					Memory:           2524,
					EphemeralStorage: 0,
					AllowedPodNumber: 0,
					ScalarResources:  map[v1.ResourceName]int64(nil),
				},
				Allocatable: &Resource{},
				Generation:  2,
				UsedPorts: fwk.HostPortInfo{
					"127.0.0.1": map[fwk.ProtocolPort]struct{}{
						{Protocol: "TCP", Port: 80}:   {},
						{Protocol: "TCP", Port: 8080}: {},
					},
				},
				ImageStates:                   map[string]*fwk.ImageStateSummary{},
				PVCRefCounts:                  map[string]int{"node_info_cache_test/pvc-1": 1},
				DeclaredFeatures:              ndf.DefaultFramework.NewFeatureSet(), // Empty FeatureSet.
				NodeAllocatableDRAClaimStates: map[types.NamespacedName]*fwk.NodeAllocatableDRAClaimState{},
				Pods: []fwk.PodInfo{
					&PodInfo{
						Pod: &v1.Pod{
							ObjectMeta: metav1.ObjectMeta{
								Namespace: "node_info_cache_test",
								Name:      "test-1",
								UID:       types.UID("test-1"),
							},
							Spec: v1.PodSpec{
								Containers: []v1.Container{
									{
										Resources: v1.ResourceRequirements{
											Requests: v1.ResourceList{
												v1.ResourceCPU:    resource.MustParse("100m"),
												v1.ResourceMemory: resource.MustParse("500"),
											},
										},
										Ports: []v1.ContainerPort{
											{
												HostIP:   "127.0.0.1",
												HostPort: 80,
												Protocol: "TCP",
											},
										},
									},
								},
								NodeName: nodeName,
								Overhead: v1.ResourceList{
									v1.ResourceCPU:    resource.MustParse("500m"),
									v1.ResourceMemory: resource.MustParse("500"),
								},
								Volumes: []v1.Volume{
									{
										VolumeSource: v1.VolumeSource{
											PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{
												ClaimName: "pvc-1",
											},
										},
									},
								},
							},
						},
						cachedResource: &fwk.PodResource{
							Resource: &Resource{
								MilliCPU: 600,
								Memory:   1000,
							},
							Non0CPU: 600,
							Non0Mem: 1000,
						},
					},
					&PodInfo{
						Pod: &v1.Pod{
							ObjectMeta: metav1.ObjectMeta{
								Namespace: "node_info_cache_test",
								Name:      "test-2",
								UID:       types.UID("test-2"),
							},
							Spec: v1.PodSpec{
								Containers: []v1.Container{
									{
										Resources: v1.ResourceRequirements{
											Requests: v1.ResourceList{
												v1.ResourceCPU:    resource.MustParse("200m"),
												v1.ResourceMemory: resource.MustParse("1Ki"),
											},
										},
										Ports: []v1.ContainerPort{
											{
												HostIP:   "127.0.0.1",
												HostPort: 8080,
												Protocol: "TCP",
											},
										},
									},
								},
								NodeName: nodeName,
								Overhead: v1.ResourceList{
									v1.ResourceCPU:    resource.MustParse("500m"),
									v1.ResourceMemory: resource.MustParse("500"),
								},
							},
						},
						cachedResource: &fwk.PodResource{
							Resource: &Resource{
								MilliCPU: 700,
								Memory:   1524,
							},
							Non0CPU: 700,
							Non0Mem: 1524,
						},
					},
				},
			},
		},
		{
			pod: &v1.Pod{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: "node_info_cache_test",
					Name:      "test-1",
					UID:       types.UID("test-1"),
				},
				Spec: v1.PodSpec{
					Containers: []v1.Container{
						{
							Resources: v1.ResourceRequirements{
								Requests: v1.ResourceList{
									v1.ResourceCPU:    resource.MustParse("100m"),
									v1.ResourceMemory: resource.MustParse("500"),
								},
							},
							Ports: []v1.ContainerPort{
								{
									HostIP:   "127.0.0.1",
									HostPort: 80,
									Protocol: "TCP",
								},
							},
						},
					},
					NodeName: nodeName,
					Overhead: v1.ResourceList{
						v1.ResourceCPU:    resource.MustParse("500m"),
						v1.ResourceMemory: resource.MustParse("500"),
					},
					Volumes: []v1.Volume{
						{
							VolumeSource: v1.VolumeSource{
								PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{
									ClaimName: "pvc-1",
								},
							},
						},
					},
				},
			},
			errExpected: false,
			expectedNodeInfo: &NodeInfo{
				node: &v1.Node{
					ObjectMeta: metav1.ObjectMeta{
						Name: "test-node",
					},
				},
				Requested: &Resource{
					MilliCPU:         700,
					Memory:           1524,
					EphemeralStorage: 0,
					AllowedPodNumber: 0,
					ScalarResources:  map[v1.ResourceName]int64(nil),
				},
				NonZeroRequested: &Resource{
					MilliCPU:         700,
					Memory:           1524,
					EphemeralStorage: 0,
					AllowedPodNumber: 0,
					ScalarResources:  map[v1.ResourceName]int64(nil),
				},
				Allocatable: &Resource{},
				Generation:  3,
				UsedPorts: fwk.HostPortInfo{
					"127.0.0.1": map[fwk.ProtocolPort]struct{}{
						{Protocol: "TCP", Port: 8080}: {},
					},
				},
				ImageStates:                   map[string]*fwk.ImageStateSummary{},
				PVCRefCounts:                  map[string]int{},
				DeclaredFeatures:              ndf.DefaultFramework.NewFeatureSet(), // Empty FeatureSet.
				NodeAllocatableDRAClaimStates: map[types.NamespacedName]*fwk.NodeAllocatableDRAClaimState{},
				Pods: []fwk.PodInfo{
					&PodInfo{
						Pod: &v1.Pod{
							ObjectMeta: metav1.ObjectMeta{
								Namespace: "node_info_cache_test",
								Name:      "test-2",
								UID:       types.UID("test-2"),
							},
							Spec: v1.PodSpec{
								Containers: []v1.Container{
									{
										Resources: v1.ResourceRequirements{
											Requests: v1.ResourceList{
												v1.ResourceCPU:    resource.MustParse("200m"),
												v1.ResourceMemory: resource.MustParse("1Ki"),
											},
										},
										Ports: []v1.ContainerPort{
											{
												HostIP:   "127.0.0.1",
												HostPort: 8080,
												Protocol: "TCP",
											},
										},
									},
								},
								NodeName: nodeName,
								Overhead: v1.ResourceList{
									v1.ResourceCPU:    resource.MustParse("500m"),
									v1.ResourceMemory: resource.MustParse("500"),
								},
							},
						},
						cachedResource: &fwk.PodResource{
							Resource: &Resource{
								MilliCPU: 700,
								Memory:   1524,
							},
							Non0CPU: 700,
							Non0Mem: 1524,
						},
					},
				},
			},
		},
	}

	for i, test := range tests {
		t.Run(fmt.Sprintf("case_%d", i), func(t *testing.T) {
			logger, _ := ktesting.NewTestContext(t)
			ni := fakeNodeInfo(pods...)

			gen := ni.Generation
			err := ni.RemovePod(logger, test.pod)
			if err != nil {
				if test.errExpected {
					expectedErrorMsg := fmt.Errorf("no corresponding pod %s in pods of node %s", test.pod.Name, ni.Node().Name)
					if expectedErrorMsg == err {
						t.Errorf("expected error: %v, got: %v", expectedErrorMsg, err)
					}
				} else {
					t.Errorf("expected no error, got: %v", err)
				}
			} else {
				if ni.Generation <= gen {
					t.Errorf("Generation is not incremented. Prev: %v, current: %v", gen, ni.Generation)
				}
			}

			test.expectedNodeInfo.Generation = ni.Generation
			if diff := cmp.Diff(test.expectedNodeInfo, ni, nodeInfoCmpOpts...); diff != "" {
				t.Errorf("Unexpected NodeInfo (-want, +got):\n%s", diff)
			}
		})
	}
}

func TestSetNodeDeclaredFeatures(t *testing.T) {
	ndfFramework, _ := ndftesting.NewMockFramework(t, "FeatureA", "FeatureB")
	ndftesting.SetFrameworkDuringTest(t, ndfFramework)
	tests := []struct {
		name               string
		featureGateEnabled bool
		nodeStatus         v1.NodeStatus
		expectedFeatures   []string
	}{
		{
			name:               "Feature gate disabled",
			featureGateEnabled: false,
			nodeStatus: v1.NodeStatus{
				DeclaredFeatures: []string{"FeatureA", "FeatureB"},
			},
			expectedFeatures: nil,
		},
		{
			name:               "Feature gate enabled, node has features",
			featureGateEnabled: true,
			nodeStatus: v1.NodeStatus{
				DeclaredFeatures: []string{"FeatureA", "FeatureB"},
			},
			expectedFeatures: []string{"FeatureA", "FeatureB"},
		},
		{
			name:               "Feature gate enabled, node has no features",
			featureGateEnabled: true,
			nodeStatus: v1.NodeStatus{
				DeclaredFeatures: []string{},
			},
			expectedFeatures: nil,
		},
		{
			name:               "Feature gate enabled, node has an unknown feature",
			featureGateEnabled: true,
			nodeStatus: v1.NodeStatus{
				DeclaredFeatures: []string{"FeatureA", "OtherFeature"},
			},
			expectedFeatures: []string{"FeatureA"},
		},
		{
			name:               "Feature gate enabled, node status has nil features",
			featureGateEnabled: true,
			nodeStatus: v1.NodeStatus{
				DeclaredFeatures: nil,
			},
			expectedFeatures: nil,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.NodeDeclaredFeatures, tt.featureGateEnabled)
			ni := NewNodeInfo()
			node := &v1.Node{
				ObjectMeta: metav1.ObjectMeta{Name: "test-node"},
				Status:     tt.nodeStatus,
			}
			ni.SetNode(node)
			gotFeatures := ni.GetNodeDeclaredFeatures()
			if !tt.featureGateEnabled {
				if !gotFeatures.IsEmpty() {
					got, err := ndfFramework.Unmap(gotFeatures)
					if err != nil {
						t.Fatalf("Failed to unmap features: %v", err)
					}
					t.Errorf("Expected GetNodeDeclaredFeatures() to return nil; got %v", got)
				}
				return
			}
			expected := ndfFramework.MustMapSorted(tt.expectedFeatures)
			if !gotFeatures.Equal(expected) {
				got, err := ndfFramework.Unmap(gotFeatures)
				if err != nil {
					t.Fatalf("Failed to unmap features: %v", err)
				}
				t.Errorf("SetNode() or GetNodeDeclaredFeatures() unexpected result, got: %v, want: %v", got, tt.expectedFeatures)
			}
		})
	}
}

func fakeNodeInfo(pods ...*v1.Pod) *NodeInfo {
	ni := NewNodeInfo(pods...)
	ni.SetNode(&v1.Node{
		ObjectMeta: metav1.ObjectMeta{
			Name: "test-node",
		},
	})
	return ni
}

func TestFitError_Error(t *testing.T) {
	tests := []struct {
		name          string
		pod           *v1.Pod
		numAllNodes   int
		diagnosis     Diagnosis
		wantReasonMsg string
	}{
		{
			name:        "nodes failed Prefilter plugin",
			numAllNodes: 3,
			diagnosis: Diagnosis{
				PreFilterMsg: "Node(s) failed PreFilter plugin FalsePreFilter",
				NodeToStatus: NewNodeToStatus(map[string]*fwk.Status{
					// They're inserted by the framework.
					// We don't include them in the reason message because they'd be just duplicates.
					"node1": fwk.NewStatus(fwk.Unschedulable, "Node(s) failed PreFilter plugin FalsePreFilter"),
					"node2": fwk.NewStatus(fwk.Unschedulable, "Node(s) failed PreFilter plugin FalsePreFilter"),
					"node3": fwk.NewStatus(fwk.Unschedulable, "Node(s) failed PreFilter plugin FalsePreFilter"),
				}, fwk.NewStatus(fwk.UnschedulableAndUnresolvable)),
			},
			wantReasonMsg: "0/3 nodes are available: Node(s) failed PreFilter plugin FalsePreFilter.",
		},
		{
			name:        "nodes failed Prefilter plugin and the preemption also failed",
			numAllNodes: 3,
			diagnosis: Diagnosis{
				PreFilterMsg: "Node(s) failed PreFilter plugin FalsePreFilter",
				NodeToStatus: NewNodeToStatus(map[string]*fwk.Status{
					// They're inserted by the framework.
					// We don't include them in the reason message because they'd be just duplicates.
					"node1": fwk.NewStatus(fwk.Unschedulable, "Node(s) failed PreFilter plugin FalsePreFilter"),
					"node2": fwk.NewStatus(fwk.Unschedulable, "Node(s) failed PreFilter plugin FalsePreFilter"),
					"node3": fwk.NewStatus(fwk.Unschedulable, "Node(s) failed PreFilter plugin FalsePreFilter"),
				}, fwk.NewStatus(fwk.UnschedulableAndUnresolvable)),
				// PostFilterMsg will be included.
				PostFilterMsg: "Error running PostFilter plugin FailedPostFilter",
			},
			wantReasonMsg: "0/3 nodes are available: Node(s) failed PreFilter plugin FalsePreFilter. Error running PostFilter plugin FailedPostFilter",
		},
		{
			name:        "nodes failed one Filter plugin with an empty PostFilterMsg",
			numAllNodes: 3,
			diagnosis: Diagnosis{
				PreFilterMsg: "",
				NodeToStatus: NewNodeToStatus(map[string]*fwk.Status{
					"node1": fwk.NewStatus(fwk.Unschedulable, "Node(s) failed Filter plugin FalseFilter-1"),
					"node2": fwk.NewStatus(fwk.Unschedulable, "Node(s) failed Filter plugin FalseFilter-1"),
					"node3": fwk.NewStatus(fwk.Unschedulable, "Node(s) failed Filter plugin FalseFilter-1"),
				}, fwk.NewStatus(fwk.UnschedulableAndUnresolvable)),
			},
			wantReasonMsg: "0/3 nodes are available: 3 Node(s) failed Filter plugin FalseFilter-1.",
		},
		{
			name:        "nodes failed one Filter plugin with a non-empty PostFilterMsg",
			numAllNodes: 3,
			diagnosis: Diagnosis{
				PreFilterMsg: "",
				NodeToStatus: NewNodeToStatus(map[string]*fwk.Status{
					"node1": fwk.NewStatus(fwk.Unschedulable, "Node(s) failed Filter plugin FalseFilter-1"),
					"node2": fwk.NewStatus(fwk.Unschedulable, "Node(s) failed Filter plugin FalseFilter-1"),
					"node3": fwk.NewStatus(fwk.Unschedulable, "Node(s) failed Filter plugin FalseFilter-1"),
				}, fwk.NewStatus(fwk.UnschedulableAndUnresolvable)),
				PostFilterMsg: "Error running PostFilter plugin FailedPostFilter",
			},
			wantReasonMsg: "0/3 nodes are available: 3 Node(s) failed Filter plugin FalseFilter-1. Error running PostFilter plugin FailedPostFilter",
		},
		{
			name:        "nodes failed two Filter plugins with an empty PostFilterMsg",
			numAllNodes: 3,
			diagnosis: Diagnosis{
				PreFilterMsg: "",
				NodeToStatus: NewNodeToStatus(map[string]*fwk.Status{
					"node1": fwk.NewStatus(fwk.Unschedulable, "Node(s) failed Filter plugin FalseFilter-1"),
					"node2": fwk.NewStatus(fwk.Unschedulable, "Node(s) failed Filter plugin FalseFilter-1"),
					"node3": fwk.NewStatus(fwk.Unschedulable, "Node(s) failed Filter plugin FalseFilter-2"),
				}, fwk.NewStatus(fwk.UnschedulableAndUnresolvable)),
			},
			wantReasonMsg: "0/3 nodes are available: 1 Node(s) failed Filter plugin FalseFilter-2, 2 Node(s) failed Filter plugin FalseFilter-1.",
		},
		{
			name:        "nodes failed two Filter plugins with a non-empty PostFilterMsg",
			numAllNodes: 3,
			diagnosis: Diagnosis{
				PreFilterMsg: "",
				NodeToStatus: NewNodeToStatus(map[string]*fwk.Status{
					"node1": fwk.NewStatus(fwk.Unschedulable, "Node(s) failed Filter plugin FalseFilter-1"),
					"node2": fwk.NewStatus(fwk.Unschedulable, "Node(s) failed Filter plugin FalseFilter-1"),
					"node3": fwk.NewStatus(fwk.Unschedulable, "Node(s) failed Filter plugin FalseFilter-2"),
				}, fwk.NewStatus(fwk.UnschedulableAndUnresolvable)),
				PostFilterMsg: "Error running PostFilter plugin FailedPostFilter",
			},
			wantReasonMsg: "0/3 nodes are available: 1 Node(s) failed Filter plugin FalseFilter-2, 2 Node(s) failed Filter plugin FalseFilter-1. Error running PostFilter plugin FailedPostFilter",
		},
		{
			name:        "failed to Permit on node",
			numAllNodes: 1,
			diagnosis: Diagnosis{
				NodeToStatus: NewNodeToStatus(map[string]*fwk.Status{
					// There should be only one node here.
					"node1": fwk.NewStatus(fwk.Unschedulable, "Node failed Permit plugin Permit-1"),
				}, fwk.NewStatus(fwk.UnschedulableAndUnresolvable)),
			},
			wantReasonMsg: "0/1 nodes are available: 1 Node failed Permit plugin Permit-1.",
		},
		{
			name:        "failed to Reserve on node",
			numAllNodes: 1,
			diagnosis: Diagnosis{
				NodeToStatus: NewNodeToStatus(map[string]*fwk.Status{
					// There should be only one node here.
					"node1": fwk.NewStatus(fwk.Unschedulable, "Node failed Reserve plugin Reserve-1"),
				}, fwk.NewStatus(fwk.UnschedulableAndUnresolvable)),
			},
			wantReasonMsg: "0/1 nodes are available: 1 Node failed Reserve plugin Reserve-1.",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			f := &FitError{
				Pod:         tt.pod,
				NumAllNodes: tt.numAllNodes,
				Diagnosis:   tt.diagnosis,
			}
			if gotReasonMsg := f.Error(); gotReasonMsg != tt.wantReasonMsg {
				t.Errorf("Error() = Got: %v Want: %v", gotReasonMsg, tt.wantReasonMsg)
			}
		})
	}
}

var (
	cpu100m       = resource.MustParse("100m")
	mem200M       = resource.MustParse("200Mi")
	cpu500m       = resource.MustParse("500m")
	mem500M       = resource.MustParse("500Mi")
	cpu700m       = resource.MustParse("700m")
	mem800M       = resource.MustParse("800Mi")
	cpu1000m      = resource.MustParse("1000m")
	cpu1200m      = resource.MustParse("1200m")
	mem1200M      = resource.MustParse("1200Mi")
	restartAlways = v1.ContainerRestartPolicyAlways
)

func TestPodInfoCalculateResources(t *testing.T) {
	testCases := []struct {
		name                                 string
		containers                           []v1.Container
		podResources                         *v1.ResourceRequirements
		podLevelResourcesEnabled             bool
		nodeAllocatableResourcesDRAEnabled   bool
		nodeAllocatableResourceClaimStatuses []v1.NodeAllocatableResourceClaimStatus
		expectedResource                     fwk.PodResource
		initContainers                       []v1.Container
		overhead                             *v1.ResourceList
	}{
		{
			name:       "requestless container",
			containers: []v1.Container{{}},
			expectedResource: fwk.PodResource{
				Resource: &Resource{},
				Non0CPU:  schedutil.DefaultMilliCPURequest,
				Non0Mem:  schedutil.DefaultMemoryRequest,
			},
		},
		{
			name: "1X container with requests",
			containers: []v1.Container{
				{
					Resources: v1.ResourceRequirements{
						Requests: v1.ResourceList{
							v1.ResourceCPU:    cpu500m,
							v1.ResourceMemory: mem500M,
						},
					},
				},
			},
			expectedResource: fwk.PodResource{
				Resource: &Resource{
					MilliCPU: cpu500m.MilliValue(),
					Memory:   mem500M.Value(),
				},
				Non0CPU: cpu500m.MilliValue(),
				Non0Mem: mem500M.Value(),
			},
		},
		{
			name: "2X container with requests",
			containers: []v1.Container{
				{
					Resources: v1.ResourceRequirements{
						Requests: v1.ResourceList{
							v1.ResourceCPU:    cpu500m,
							v1.ResourceMemory: mem500M,
						},
					},
				},
				{
					Resources: v1.ResourceRequirements{
						Requests: v1.ResourceList{
							v1.ResourceCPU:    cpu700m,
							v1.ResourceMemory: mem800M,
						},
					},
				},
			},
			expectedResource: fwk.PodResource{
				Resource: &Resource{
					MilliCPU: cpu500m.MilliValue() + cpu700m.MilliValue(),
					Memory:   mem500M.Value() + mem800M.Value(),
				},
				Non0CPU: cpu500m.MilliValue() + cpu700m.MilliValue(),
				Non0Mem: mem500M.Value() + mem800M.Value(),
			},
		},
		{
			name:                     "1X container and 1X init container with pod-level requests",
			podLevelResourcesEnabled: true,
			initContainers: []v1.Container{
				{
					Resources: v1.ResourceRequirements{
						Requests: v1.ResourceList{
							v1.ResourceCPU:    cpu500m,
							v1.ResourceMemory: mem500M,
						},
					},
				},
			},
			containers: []v1.Container{
				{
					Resources: v1.ResourceRequirements{
						Requests: v1.ResourceList{
							v1.ResourceCPU:    cpu500m,
							v1.ResourceMemory: mem500M,
						},
					},
				},
			},
			podResources: &v1.ResourceRequirements{
				Requests: v1.ResourceList{
					v1.ResourceCPU:    cpu1200m,
					v1.ResourceMemory: mem1200M,
				},
			},
			expectedResource: fwk.PodResource{
				Resource: &Resource{
					MilliCPU: cpu1200m.MilliValue(),
					Memory:   mem1200M.Value(),
				},
				Non0CPU: cpu1200m.MilliValue(),
				Non0Mem: mem1200M.Value(),
			},
		},
		{
			name:                     "1X container and 1X sidecar container with pod-level requests",
			podLevelResourcesEnabled: true,
			initContainers: []v1.Container{
				{
					Resources: v1.ResourceRequirements{
						Requests: v1.ResourceList{
							v1.ResourceCPU:    cpu500m,
							v1.ResourceMemory: mem500M,
						},
					},
					RestartPolicy: &restartAlways,
				},
			},
			containers: []v1.Container{
				{
					Resources: v1.ResourceRequirements{
						Requests: v1.ResourceList{
							v1.ResourceCPU:    cpu500m,
							v1.ResourceMemory: mem500M,
						},
					},
				},
			},
			podResources: &v1.ResourceRequirements{
				Requests: v1.ResourceList{
					v1.ResourceCPU:    cpu1200m,
					v1.ResourceMemory: mem1200M,
				},
			},
			expectedResource: fwk.PodResource{
				Resource: &Resource{
					MilliCPU: cpu1200m.MilliValue(),
					Memory:   mem1200M.Value(),
				},
				Non0CPU: cpu1200m.MilliValue(),
				Non0Mem: mem1200M.Value(),
			},
		},
		{
			name:                     "1X container with pod-level memory requests",
			podLevelResourcesEnabled: true,
			initContainers: []v1.Container{
				{
					Resources: v1.ResourceRequirements{},
				},
			},
			containers: []v1.Container{
				{
					Resources: v1.ResourceRequirements{},
				},
			},
			podResources: &v1.ResourceRequirements{
				Requests: v1.ResourceList{
					v1.ResourceMemory: mem1200M,
				},
			},
			expectedResource: fwk.PodResource{
				Resource: &Resource{
					Memory: mem1200M.Value(),
				},
				Non0CPU: schedutil.DefaultMilliCPURequest,
				Non0Mem: mem1200M.Value(),
			},
		},
		{
			name:                     "1X container with pod-level cpu requests",
			podLevelResourcesEnabled: true,
			initContainers: []v1.Container{
				{
					Resources: v1.ResourceRequirements{},
				},
			},
			containers: []v1.Container{
				{
					Resources: v1.ResourceRequirements{},
				},
			},
			podResources: &v1.ResourceRequirements{
				Requests: v1.ResourceList{
					v1.ResourceCPU: cpu500m,
				},
			},
			expectedResource: fwk.PodResource{
				Resource: &Resource{
					MilliCPU: cpu500m.MilliValue(),
				},
				Non0CPU: cpu500m.MilliValue(),
				Non0Mem: schedutil.DefaultMemoryRequest,
			},
		},
		{
			name:                     "1X container unsupported resources and pod-level supported resources",
			podLevelResourcesEnabled: true,
			initContainers: []v1.Container{
				{
					Resources: v1.ResourceRequirements{
						Requests: v1.ResourceList{
							v1.ResourceEphemeralStorage: mem500M,
						},
					},
				},
			},
			containers: []v1.Container{
				{
					Resources: v1.ResourceRequirements{
						Requests: v1.ResourceList{
							v1.ResourceEphemeralStorage: mem800M,
						},
					},
				},
			},
			podResources: &v1.ResourceRequirements{
				Requests: v1.ResourceList{
					v1.ResourceCPU: cpu500m,
				},
			},
			expectedResource: fwk.PodResource{
				Resource: &Resource{
					MilliCPU:         cpu500m.MilliValue(),
					EphemeralStorage: mem800M.Value(),
				},
				Non0CPU: cpu500m.MilliValue(),
				Non0Mem: schedutil.DefaultMemoryRequest,
			},
		},
		{
			name:                               "DRA gate disabled, with node allocatable resource claim",
			nodeAllocatableResourcesDRAEnabled: false,
			containers: []v1.Container{
				{
					Resources: v1.ResourceRequirements{
						Requests: v1.ResourceList{
							v1.ResourceCPU:    cpu500m,
							v1.ResourceMemory: mem500M,
						},
						// We do not set Pod.Spec.ResourceClaims in this test.
						// We assume the name maps to Pod.Spec.ResourceClaims[].ResourceClaimName.
						Claims: []v1.ResourceClaim{
							{
								Name: "node-allocatable-claim",
							},
						},
					},
				},
			},
			nodeAllocatableResourceClaimStatuses: []v1.NodeAllocatableResourceClaimStatus{
				{
					ResourceClaimName: "node-allocatable-claim",
					Resources: map[v1.ResourceName]resource.Quantity{
						v1.ResourceCPU:    cpu100m,
						v1.ResourceMemory: mem200M,
					},
				},
			},
			expectedResource: fwk.PodResource{
				Resource: &Resource{
					MilliCPU: cpu500m.MilliValue(),
					Memory:   mem500M.Value(),
				},
				Non0CPU: cpu500m.MilliValue(),
				Non0Mem: mem500M.Value(),
			},
		},
		{
			name:                               "container with DRA node allocatable resource claim",
			nodeAllocatableResourcesDRAEnabled: true,
			containers: []v1.Container{
				{
					Resources: v1.ResourceRequirements{
						Requests: v1.ResourceList{
							v1.ResourceCPU:    cpu500m,
							v1.ResourceMemory: mem500M,
						},
						Claims: []v1.ResourceClaim{
							{
								Name: "node-allocatable-claim",
							},
						},
					},
				},
			},
			nodeAllocatableResourceClaimStatuses: []v1.NodeAllocatableResourceClaimStatus{
				{
					ResourceClaimName: "node-allocatable-claim",
					Resources: map[v1.ResourceName]resource.Quantity{
						v1.ResourceCPU:    cpu100m,
						v1.ResourceMemory: mem200M,
					},
				},
			},
			expectedResource: fwk.PodResource{
				Resource: &Resource{
					MilliCPU: cpu500m.MilliValue() + cpu100m.MilliValue(),
					Memory:   mem500M.Value() + mem200M.Value(),
				},
				Non0CPU: cpu500m.MilliValue() + cpu100m.MilliValue(),
				Non0Mem: mem500M.Value() + mem200M.Value(),
			},
		},
		{
			name:                               "Multiple DRA node allocatable resource claims",
			nodeAllocatableResourcesDRAEnabled: true,
			containers: []v1.Container{
				{
					Resources: v1.ResourceRequirements{
						Requests: v1.ResourceList{
							v1.ResourceCPU:    cpu500m,
							v1.ResourceMemory: mem500M,
						},
						Claims: []v1.ResourceClaim{
							{
								Name: "node-allocatable-claim-1",
							},
							{
								Name: "node-allocatable-claim-2",
							},
						},
					},
				},
			},
			nodeAllocatableResourceClaimStatuses: []v1.NodeAllocatableResourceClaimStatus{
				{
					ResourceClaimName: "node-allocatable-claim-1",
					Resources: map[v1.ResourceName]resource.Quantity{
						v1.ResourceCPU:    cpu100m,
						v1.ResourceMemory: mem200M,
					},
				},
				{
					ResourceClaimName: "node-allocatable-claim-2",
					Resources: map[v1.ResourceName]resource.Quantity{
						v1.ResourceCPU: cpu100m,
					},
				},
			},
			expectedResource: fwk.PodResource{
				Resource: &Resource{
					MilliCPU: cpu500m.MilliValue() + cpu100m.MilliValue() + cpu100m.MilliValue(),
					Memory:   mem500M.Value() + mem200M.Value(),
				},
				Non0CPU: cpu500m.MilliValue() + cpu100m.MilliValue() + cpu100m.MilliValue(),
				Non0Mem: mem500M.Value() + mem200M.Value(),
			},
		},
		{
			name:                               "Single DRA claim with multiple resources",
			nodeAllocatableResourcesDRAEnabled: true,
			containers: []v1.Container{
				{
					Resources: v1.ResourceRequirements{
						Requests: v1.ResourceList{
							v1.ResourceCPU:    cpu500m,
							v1.ResourceMemory: mem500M,
						},
						Claims: []v1.ResourceClaim{
							{
								Name: "node-allocatable-claim-1",
							},
						},
					},
				},
			},
			nodeAllocatableResourceClaimStatuses: []v1.NodeAllocatableResourceClaimStatus{
				{
					ResourceClaimName: "node-allocatable-claim-1",
					Resources: map[v1.ResourceName]resource.Quantity{
						v1.ResourceCPU:    cpu1000m,
						v1.ResourceMemory: mem200M,
					},
				},
			},
			expectedResource: fwk.PodResource{
				Resource: &Resource{
					MilliCPU: cpu500m.MilliValue() + cpu1000m.MilliValue(),
					Memory:   mem500M.Value() + mem200M.Value(),
				},
				Non0CPU: cpu500m.MilliValue() + cpu1000m.MilliValue(),
				Non0Mem: mem500M.Value() + mem200M.Value(),
			},
		},
		{
			name:                               "DRA node allocatable Resources with Init Container",
			nodeAllocatableResourcesDRAEnabled: true,
			initContainers: []v1.Container{
				{
					Resources: v1.ResourceRequirements{
						Requests: v1.ResourceList{
							v1.ResourceCPU:    cpu1200m, // Higher than app container
							v1.ResourceMemory: mem500M,
						},
						Claims: []v1.ResourceClaim{
							{
								Name: "node-allocatable-claim-1",
							},
						},
					},
				},
			},
			containers: []v1.Container{
				{
					Resources: v1.ResourceRequirements{
						Requests: v1.ResourceList{
							v1.ResourceCPU:    cpu500m,
							v1.ResourceMemory: mem1200M, // Higher than init container
						},
						Claims: []v1.ResourceClaim{
							{
								Name: "node-allocatable-claim-1",
							},
						},
					},
				},
			},
			nodeAllocatableResourceClaimStatuses: []v1.NodeAllocatableResourceClaimStatus{
				{
					ResourceClaimName: "node-allocatable-claim-1",
					Resources: map[v1.ResourceName]resource.Quantity{
						v1.ResourceCPU:    cpu100m,
						v1.ResourceMemory: mem200M,
					},
				},
			},
			expectedResource: fwk.PodResource{
				Resource: &Resource{
					MilliCPU: cpu1200m.MilliValue() + cpu100m.MilliValue(), // max(cpu500m, cpu1200m) + draCpu
					Memory:   mem1200M.Value() + mem200M.Value(),           // max(mem500M, mem1200M) + draMem
				},
				Non0CPU: cpu1200m.MilliValue() + cpu100m.MilliValue(),
				Non0Mem: mem1200M.Value() + mem200M.Value(),
			},
		},
		{
			name:                               "DRA node allocatable Resources with Pod Level Resources",
			nodeAllocatableResourcesDRAEnabled: true,
			podLevelResourcesEnabled:           true,
			containers: []v1.Container{
				{
					Resources: v1.ResourceRequirements{
						Requests: v1.ResourceList{
							v1.ResourceCPU:    cpu500m,
							v1.ResourceMemory: mem500M,
						},
						Claims: []v1.ResourceClaim{
							{
								Name: "node-allocatable-claim-1",
							},
						},
					},
				},
			},
			podResources: &v1.ResourceRequirements{
				Requests: v1.ResourceList{
					v1.ResourceCPU:    cpu700m,
					v1.ResourceMemory: mem800M,
				},
			},
			nodeAllocatableResourceClaimStatuses: []v1.NodeAllocatableResourceClaimStatus{
				{
					ResourceClaimName: "node-allocatable-claim-1",
					Resources: map[v1.ResourceName]resource.Quantity{
						v1.ResourceCPU:    cpu100m,
						v1.ResourceMemory: mem200M,
					},
				},
			},
			expectedResource: fwk.PodResource{
				Resource: &Resource{
					MilliCPU: cpu700m.MilliValue(), // pod level requests determines the overall footprint
					Memory:   mem800M.Value(),      // pod level requests determines the overall footprint
				},
				Non0CPU: cpu700m.MilliValue(),
				Non0Mem: mem800M.Value(),
			},
		},
		{
			name:                               "DRA node allocatable Resources with Pod Overhead",
			nodeAllocatableResourcesDRAEnabled: true,
			containers: []v1.Container{
				{
					Resources: v1.ResourceRequirements{
						Requests: v1.ResourceList{
							v1.ResourceCPU:    cpu500m,
							v1.ResourceMemory: mem500M,
						},
						Claims: []v1.ResourceClaim{
							{
								Name: "node-allocatable-claim-1",
							},
						},
					},
				},
			},
			nodeAllocatableResourceClaimStatuses: []v1.NodeAllocatableResourceClaimStatus{
				{
					ResourceClaimName: "node-allocatable-claim-1",
					Resources: map[v1.ResourceName]resource.Quantity{
						v1.ResourceCPU:    cpu100m,
						v1.ResourceMemory: mem200M,
					},
				},
			},
			// Pod Overhead
			overhead: &v1.ResourceList{
				v1.ResourceCPU:    cpu100m,
				v1.ResourceMemory: mem200M,
			},
			expectedResource: fwk.PodResource{
				Resource: &Resource{
					MilliCPU: cpu500m.MilliValue() + cpu100m.MilliValue() + cpu100m.MilliValue(), // container + dra + overhead
					Memory:   mem500M.Value() + mem200M.Value() + mem200M.Value(),                // container + dra + overhead
				},
				Non0CPU: cpu500m.MilliValue() + cpu100m.MilliValue() + cpu100m.MilliValue(),
				Non0Mem: mem500M.Value() + mem200M.Value() + mem200M.Value(),
			},
		},
	}
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
				features.PodLevelResources:           tc.podLevelResourcesEnabled,
				features.DRANodeAllocatableResources: tc.nodeAllocatableResourcesDRAEnabled,
			})
			podSpec := v1.PodSpec{
				Resources:      tc.podResources,
				Containers:     tc.containers,
				InitContainers: tc.initContainers,
			}
			if tc.overhead != nil {
				podSpec.Overhead = *tc.overhead
			}
			podInfo := PodInfo{
				Pod: &v1.Pod{
					Spec: podSpec,
					Status: v1.PodStatus{
						NodeAllocatableResourceClaimStatuses: tc.nodeAllocatableResourceClaimStatuses,
					},
				},
			}
			res := podInfo.CalculateResource()
			if diff := cmp.Diff(tc.expectedResource, res, nodeInfoCmpOpts...); diff != "" {
				t.Errorf("Unexpected resource (-want,+got):\n%s", diff)
			}
		})
	}
}

func TestCalculatePodResourcesWithResize(t *testing.T) {
	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.InPlacePodVerticalScaling, true)
	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.NodeDeclaredFeatures, true)
	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.InPlacePodLevelResourcesVerticalScaling, true)

	testpod := v1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: "pod_resize_test",
			Name:      "testpod",
			UID:       types.UID("testpod"),
		},
		Status: v1.PodStatus{
			Phase: v1.PodRunning,
		},
	}

	restartAlways := v1.ContainerRestartPolicyAlways

	preparePodInfo := func(pod v1.Pod,
		podRequests, podStatusResources,
		requests, statusResources,
		initRequests, initStatusResources,
		sidecarRequests, sidecarStatusResources *v1.ResourceList,
		resizeStatus []*v1.PodCondition) PodInfo {

		if podRequests != nil {
			pod.Spec.Resources = &v1.ResourceRequirements{
				Requests: *podRequests,
			}
		}

		if podStatusResources != nil {
			pod.Status.Resources = &v1.ResourceRequirements{
				Requests: *podStatusResources,
			}
		}

		if requests != nil {
			pod.Spec.Containers = append(pod.Spec.Containers,
				v1.Container{
					Name:      "c1",
					Resources: v1.ResourceRequirements{Requests: *requests},
				})
		}
		if statusResources != nil {
			pod.Status.ContainerStatuses = append(pod.Status.ContainerStatuses,
				v1.ContainerStatus{
					Name: "c1",
					Resources: &v1.ResourceRequirements{
						Requests: *statusResources,
					},
				})
		}

		if initRequests != nil {
			pod.Spec.InitContainers = append(pod.Spec.InitContainers,
				v1.Container{
					Name:      "i1",
					Resources: v1.ResourceRequirements{Requests: *initRequests},
				},
			)
		}
		if initStatusResources != nil {
			pod.Status.InitContainerStatuses = append(pod.Status.InitContainerStatuses,
				v1.ContainerStatus{
					Name: "i1",
					Resources: &v1.ResourceRequirements{
						Requests: *initStatusResources,
					},
				})
		}

		if sidecarRequests != nil {
			pod.Spec.InitContainers = append(pod.Spec.InitContainers,
				v1.Container{
					Name:          "s1",
					Resources:     v1.ResourceRequirements{Requests: *sidecarRequests},
					RestartPolicy: &restartAlways,
				},
			)
		}
		if sidecarStatusResources != nil {
			pod.Status.InitContainerStatuses = append(pod.Status.InitContainerStatuses,
				v1.ContainerStatus{
					Name: "s1",
					Resources: &v1.ResourceRequirements{
						Requests: *sidecarStatusResources,
					},
				})
		}

		for _, c := range resizeStatus {
			pod.Status.Conditions = append(pod.Status.Conditions, *c)
		}

		return PodInfo{Pod: &pod}
	}

	tests := []struct {
		name                    string
		podLevelRequests        *v1.ResourceList
		requests                v1.ResourceList
		statusResources         v1.ResourceList
		podLevelStatusResources *v1.ResourceList
		initRequests            *v1.ResourceList
		initStatusResources     *v1.ResourceList
		resizeStatus            []*v1.PodCondition
		sidecarRequests         *v1.ResourceList
		sidecarStatusResources  *v1.ResourceList
		expectedResource        fwk.PodResource
	}{
		{
			name:            "Pod with no pending resize",
			requests:        v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M},
			statusResources: v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M},
			expectedResource: fwk.PodResource{
				Resource: &Resource{
					MilliCPU: cpu500m.MilliValue(),
					Memory:   mem500M.Value(),
				},
				Non0CPU: cpu500m.MilliValue(),
				Non0Mem: mem500M.Value(),
			},
		},
		{
			name:                    "Pod with pod-level resources no pending resize",
			podLevelRequests:        &v1.ResourceList{v1.ResourceCPU: cpu700m, v1.ResourceMemory: mem500M},
			requests:                v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M},
			podLevelStatusResources: &v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M},
			statusResources:         v1.ResourceList{v1.ResourceCPU: cpu700m, v1.ResourceMemory: mem500M},
			expectedResource: fwk.PodResource{
				Resource: &Resource{
					MilliCPU: cpu700m.MilliValue(),
					Memory:   mem500M.Value(),
				},
				Non0CPU: cpu700m.MilliValue(),
				Non0Mem: mem500M.Value(),
			},
		},
		{
			name:            "Pod with resize in progress",
			requests:        v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M},
			statusResources: v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M},
			resizeStatus: []*v1.PodCondition{
				{
					Type:   v1.PodResizeInProgress,
					Status: v1.ConditionTrue,
				},
			},
			expectedResource: fwk.PodResource{
				Resource: &Resource{
					MilliCPU: cpu500m.MilliValue(),
					Memory:   mem500M.Value(),
				},
				Non0CPU: cpu500m.MilliValue(),
				Non0Mem: mem500M.Value(),
			},
		},
		{
			name:                    "Pod with pod-level resources and resize in progress",
			podLevelRequests:        &v1.ResourceList{v1.ResourceCPU: cpu700m, v1.ResourceMemory: mem500M},
			requests:                v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M},
			podLevelStatusResources: &v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M},
			statusResources:         v1.ResourceList{v1.ResourceCPU: cpu700m, v1.ResourceMemory: mem500M},
			resizeStatus: []*v1.PodCondition{
				{
					Type:   v1.PodResizeInProgress,
					Status: v1.ConditionTrue,
				},
			},
			expectedResource: fwk.PodResource{
				Resource: &Resource{
					MilliCPU: cpu700m.MilliValue(),
					Memory:   mem500M.Value(),
				},
				Non0CPU: cpu700m.MilliValue(),
				Non0Mem: mem500M.Value(),
			},
		},
		{
			name:            "Pod with deferred resize",
			requests:        v1.ResourceList{v1.ResourceCPU: cpu700m, v1.ResourceMemory: mem800M},
			statusResources: v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M},
			resizeStatus: []*v1.PodCondition{
				{
					Type:   v1.PodResizePending,
					Status: v1.ConditionTrue,
					Reason: v1.PodReasonDeferred,
				},
			},
			expectedResource: fwk.PodResource{
				Resource: &Resource{
					MilliCPU: cpu700m.MilliValue(),
					Memory:   mem800M.Value(),
				},
				Non0CPU: cpu700m.MilliValue(),
				Non0Mem: mem800M.Value(),
			},
		},
		{
			name:                    "Pod with pod-level resources and with deferred resize",
			podLevelRequests:        &v1.ResourceList{v1.ResourceCPU: cpu700m, v1.ResourceMemory: mem800M},
			requests:                v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M},
			podLevelStatusResources: &v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M},
			statusResources:         v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M},
			resizeStatus: []*v1.PodCondition{
				{
					Type:   v1.PodResizePending,
					Status: v1.ConditionTrue,
					Reason: v1.PodReasonDeferred,
				},
			},
			expectedResource: fwk.PodResource{
				Resource: &Resource{
					MilliCPU: cpu700m.MilliValue(),
					Memory:   mem800M.Value(),
				},
				Non0CPU: cpu700m.MilliValue(),
				Non0Mem: mem800M.Value(),
			},
		},
		{
			name:            "Pod with infeasible resize",
			requests:        v1.ResourceList{v1.ResourceCPU: cpu700m, v1.ResourceMemory: mem800M},
			statusResources: v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M},
			resizeStatus: []*v1.PodCondition{
				{
					Type:   v1.PodResizePending,
					Status: v1.ConditionTrue,
					Reason: v1.PodReasonInfeasible,
				},
			},
			expectedResource: fwk.PodResource{
				Resource: &Resource{
					MilliCPU: cpu500m.MilliValue(),
					Memory:   mem500M.Value(),
				},
				Non0CPU: cpu500m.MilliValue(),
				Non0Mem: mem500M.Value(),
			},
		},
		{
			name:                "Pod with init container and no pending resize",
			requests:            v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M},
			statusResources:     v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M},
			initRequests:        &v1.ResourceList{v1.ResourceCPU: cpu700m, v1.ResourceMemory: mem800M},
			initStatusResources: &v1.ResourceList{v1.ResourceCPU: cpu700m, v1.ResourceMemory: mem800M},
			expectedResource: fwk.PodResource{
				Resource: &Resource{
					MilliCPU: cpu700m.MilliValue(),
					Memory:   mem800M.Value(),
				},
				Non0CPU: cpu700m.MilliValue(),
				Non0Mem: mem800M.Value(),
			},
		},
		{
			name:                   "Pod with sider container and no pending resize",
			requests:               v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M},
			statusResources:        v1.ResourceList{v1.ResourceCPU: cpu500m, v1.ResourceMemory: mem500M},
			initRequests:           &v1.ResourceList{v1.ResourceCPU: cpu700m, v1.ResourceMemory: mem800M},
			initStatusResources:    &v1.ResourceList{v1.ResourceCPU: cpu700m, v1.ResourceMemory: mem800M},
			sidecarRequests:        &v1.ResourceList{v1.ResourceCPU: cpu700m, v1.ResourceMemory: mem800M},
			sidecarStatusResources: &v1.ResourceList{v1.ResourceCPU: cpu700m, v1.ResourceMemory: mem800M},
			expectedResource: fwk.PodResource{
				Resource: &Resource{
					MilliCPU: cpu500m.MilliValue() + cpu700m.MilliValue(),
					Memory:   mem500M.Value() + mem800M.Value(),
				},
				Non0CPU: cpu500m.MilliValue() + cpu700m.MilliValue(),
				Non0Mem: mem500M.Value() + mem800M.Value(),
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			podInfo := preparePodInfo(*testpod.DeepCopy(),
				tt.podLevelRequests, tt.podLevelStatusResources,
				&tt.requests, &tt.statusResources,
				tt.initRequests, tt.initStatusResources,
				tt.sidecarRequests, tt.sidecarStatusResources,
				tt.resizeStatus)

			res := podInfo.CalculateResource()
			if diff := cmp.Diff(tt.expectedResource, res, nodeInfoCmpOpts...); diff != "" {
				t.Errorf("Unexpected podResource (-want, +got):\n%s", diff)
			}
		})
	}
}

func TestCloudEvent_Match(t *testing.T) {
	testCases := []struct {
		name        string
		event       fwk.ClusterEvent
		comingEvent fwk.ClusterEvent
		wantResult  bool
	}{
		{
			name:        "wildcard event matches with all kinds of coming events",
			event:       fwk.ClusterEvent{Resource: fwk.WildCard, ActionType: fwk.All},
			comingEvent: fwk.ClusterEvent{Resource: fwk.Pod, ActionType: fwk.UpdateNodeLabel},
			wantResult:  true,
		},
		{
			name:        "event with resource = 'Pod' matching with coming events carries same actionType",
			event:       fwk.ClusterEvent{Resource: fwk.Pod, ActionType: fwk.UpdateNodeLabel | fwk.UpdateNodeTaint},
			comingEvent: fwk.ClusterEvent{Resource: fwk.Pod, ActionType: fwk.UpdateNodeLabel},
			wantResult:  true,
		},
		{
			name:        "event with resource = 'Pod' matching with coming events carries unschedulablePod",
			event:       fwk.ClusterEvent{Resource: fwk.Pod, ActionType: fwk.UpdateNodeLabel | fwk.UpdateNodeTaint},
			comingEvent: fwk.ClusterEvent{Resource: unschedulablePod, ActionType: fwk.UpdateNodeLabel},
			wantResult:  true,
		},
		{
			name:        "event with resource = '*' matching with coming events carries same actionType",
			event:       fwk.ClusterEvent{Resource: fwk.WildCard, ActionType: fwk.UpdateNodeLabel},
			comingEvent: fwk.ClusterEvent{Resource: fwk.Pod, ActionType: fwk.UpdateNodeLabel},
			wantResult:  true,
		},
		{
			name:        "event with resource = '*' matching with coming events carries different actionType",
			event:       fwk.ClusterEvent{Resource: fwk.WildCard, ActionType: fwk.UpdateNodeLabel},
			comingEvent: fwk.ClusterEvent{Resource: fwk.Pod, ActionType: fwk.UpdateNodeAllocatable},
			wantResult:  false,
		},
		{
			name:        "event matching with coming events carries '*' resources",
			event:       fwk.ClusterEvent{Resource: fwk.Pod, ActionType: fwk.UpdateNodeLabel},
			comingEvent: fwk.ClusterEvent{Resource: fwk.WildCard, ActionType: fwk.UpdateNodeLabel},
			wantResult:  false,
		},
		{
			name:        "event with resource = '*' matching with coming events carrying a too broad actionType",
			event:       fwk.ClusterEvent{Resource: fwk.WildCard, ActionType: fwk.UpdateNodeLabel},
			comingEvent: fwk.ClusterEvent{Resource: fwk.Pod, ActionType: fwk.Update},
			wantResult:  false,
		},
		{
			name:        "event with resource = '*' matching with coming events carrying a more specific actionType",
			event:       fwk.ClusterEvent{Resource: fwk.WildCard, ActionType: fwk.Update},
			comingEvent: fwk.ClusterEvent{Resource: fwk.Pod, ActionType: fwk.UpdateNodeLabel},
			wantResult:  true,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			got := MatchClusterEvents(tc.event, tc.comingEvent)
			if got != tc.wantResult {
				t.Fatalf("unexpected result")
			}
		})
	}
}

func TestNodeInfoKMetadata(t *testing.T) {
	tCtx := ktesting.Init(t, initoption.BufferLogs(true))
	logger := tCtx.Logger()
	logger.Info("Some NodeInfo slice", "nodes", klog.KObjSlice([]*NodeInfo{nil, {}, {node: &v1.Node{}}, {node: &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "worker"}}}}))

	output := logger.GetSink().(ktesting.Underlier).GetBuffer().String()

	// The initial nil entry gets turned into empty ObjectRef by klog,
	// which becomes an empty string during output formatting.
	if !strings.Contains(output, `Some NodeInfo slice nodes=["","<no node>","","worker"]`) {
		tCtx.Fatalf("unexpected output:\n%s", output)
	}
}

func TestUpdateUsedPorts_PodAdd(t *testing.T) {
	testCases := []struct {
		ports fwk.HostPortInfo
		pod   *v1.Pod
		want  fwk.HostPortInfo
	}{
		{
			ports: fwk.HostPortInfo{},
			pod:   nil,
			want:  fwk.HostPortInfo{},
		},
		{
			ports: fwk.HostPortInfo{
				"0.0.0.0": {
					fwk.ProtocolPort{Protocol: "TCP", Port: 8001}: struct{}{},
				},
			},
			pod: nil,
			want: fwk.HostPortInfo{
				"0.0.0.0": {
					fwk.ProtocolPort{Protocol: "TCP", Port: 8001}: struct{}{},
				},
			},
		},
		{
			ports: fwk.HostPortInfo{},
			pod: st.MakePod().
				ContainerPort([]v1.ContainerPort{
					{
						ContainerPort: 8001,
						Protocol:      v1.ProtocolTCP,
					}}).
				Obj(),
			want: fwk.HostPortInfo{},
		},
		{
			ports: fwk.HostPortInfo{},
			pod: st.MakePod().
				ContainerPort([]v1.ContainerPort{
					{
						ContainerPort: 8001,
						HostPort:      8001,
						Protocol:      v1.ProtocolTCP,
					}}).
				Obj(),
			want: fwk.HostPortInfo{
				"0.0.0.0": {
					fwk.ProtocolPort{Protocol: "TCP", Port: 8001}: struct{}{},
				},
			},
		},
		{
			ports: fwk.HostPortInfo{
				"0.0.0.0": {
					fwk.ProtocolPort{Protocol: "TCP", Port: 8001}: struct{}{},
				},
			},
			pod: st.MakePod().
				ContainerPort([]v1.ContainerPort{
					{
						ContainerPort: 8002,
						HostPort:      8002,
						Protocol:      v1.ProtocolTCP,
					}}).
				Obj(),
			want: fwk.HostPortInfo{
				"0.0.0.0": {
					fwk.ProtocolPort{Protocol: "TCP", Port: 8001}: struct{}{},
					fwk.ProtocolPort{Protocol: "TCP", Port: 8002}: struct{}{},
				},
			},
		},
		{
			ports: fwk.HostPortInfo{},
			pod: st.MakePod().
				InitContainerPort(false /* sidecar */, []v1.ContainerPort{
					{
						ContainerPort: 8001,
						HostPort:      8001,
						Protocol:      v1.ProtocolTCP,
					}}).
				Obj(),
			want: fwk.HostPortInfo{},
		},
		{
			ports: fwk.HostPortInfo{
				"0.0.0.0": {
					fwk.ProtocolPort{Protocol: "TCP", Port: 8001}: struct{}{},
				},
			},
			pod: st.MakePod().
				InitContainerPort(false /* sidecar */, []v1.ContainerPort{
					{
						ContainerPort: 8002,
						HostPort:      8002,
						Protocol:      v1.ProtocolTCP,
					}}).
				Obj(),
			want: fwk.HostPortInfo{
				"0.0.0.0": {
					fwk.ProtocolPort{Protocol: "TCP", Port: 8001}: struct{}{},
				},
			},
		},
		{
			ports: fwk.HostPortInfo{},
			pod: st.MakePod().
				InitContainerPort(true /* sidecar */, []v1.ContainerPort{
					{
						ContainerPort: 8001,
						HostPort:      8001,
						Protocol:      v1.ProtocolTCP,
					}}).
				Obj(),
			want: fwk.HostPortInfo{
				"0.0.0.0": {
					fwk.ProtocolPort{Protocol: "TCP", Port: 8001}: struct{}{},
				},
			},
		},
		{
			ports: fwk.HostPortInfo{
				"0.0.0.0": {
					fwk.ProtocolPort{Protocol: "TCP", Port: 8001}: struct{}{},
				},
			},
			pod: st.MakePod().
				InitContainerPort(true /* sidecar */, []v1.ContainerPort{
					{
						ContainerPort: 8001,
						HostPort:      8001,
						Protocol:      v1.ProtocolTCP,
					}}).
				Obj(),
			want: fwk.HostPortInfo{
				"0.0.0.0": {
					fwk.ProtocolPort{Protocol: "TCP", Port: 8001}: struct{}{},
				},
			},
		},
		{
			ports: fwk.HostPortInfo{
				"0.0.0.0": {
					fwk.ProtocolPort{Protocol: "TCP", Port: 8001}: struct{}{},
				},
			},
			pod: st.MakePod().
				InitContainerPort(true /* sidecar */, []v1.ContainerPort{
					{
						ContainerPort: 8002,
						HostPort:      8002,
						Protocol:      v1.ProtocolTCP,
					}}).
				Obj(),
			want: fwk.HostPortInfo{
				"0.0.0.0": {
					fwk.ProtocolPort{Protocol: "TCP", Port: 8001}: struct{}{},
					fwk.ProtocolPort{Protocol: "TCP", Port: 8002}: struct{}{},
				},
			},
		},
	}
	for _, tc := range testCases {
		ni := NodeInfo{UsedPorts: tc.ports}
		ni.updateUsedPorts(tc.pod, true)
		if diff := cmp.Diff(tc.want, ni.UsedPorts); diff != "" {
			t.Errorf("updateUsedPorts() unexpected diff (-want, +got):\n%s", diff)
		}
	}
}

func TestUpdateUsedPorts_PodRemove(t *testing.T) {
	testCases := []struct {
		ports fwk.HostPortInfo
		pod   *v1.Pod
		want  fwk.HostPortInfo
	}{
		{
			ports: fwk.HostPortInfo{},
			pod:   nil,
			want:  fwk.HostPortInfo{},
		},
		{
			ports: fwk.HostPortInfo{
				"0.0.0.0": {
					fwk.ProtocolPort{Protocol: "TCP", Port: 8001}: struct{}{},
				},
			},
			pod: nil,
			want: fwk.HostPortInfo{
				"0.0.0.0": {
					fwk.ProtocolPort{Protocol: "TCP", Port: 8001}: struct{}{},
				},
			},
		},
		{
			ports: fwk.HostPortInfo{},
			pod: st.MakePod().
				ContainerPort([]v1.ContainerPort{
					{
						ContainerPort: 8001,
						Protocol:      v1.ProtocolTCP,
					}}).
				Obj(),
			want: fwk.HostPortInfo{},
		},
		{
			ports: fwk.HostPortInfo{
				"0.0.0.0": {
					fwk.ProtocolPort{Protocol: "TCP", Port: 8001}: struct{}{},
				},
			},
			pod: st.MakePod().
				ContainerPort([]v1.ContainerPort{
					{
						ContainerPort: 8001,
						HostPort:      8001,
						Protocol:      v1.ProtocolTCP,
					}}).
				Obj(),
			want: fwk.HostPortInfo{},
		},
		{
			ports: fwk.HostPortInfo{
				"0.0.0.0": {
					fwk.ProtocolPort{Protocol: "TCP", Port: 8001}: struct{}{},
				},
			},
			pod: st.MakePod().
				ContainerPort([]v1.ContainerPort{
					{
						ContainerPort: 8002,
						HostPort:      8002,
						Protocol:      v1.ProtocolTCP,
					}}).
				Obj(),
			want: fwk.HostPortInfo{
				"0.0.0.0": {
					fwk.ProtocolPort{Protocol: "TCP", Port: 8001}: struct{}{},
				},
			},
		},
		{
			ports: fwk.HostPortInfo{
				"0.0.0.0": {
					fwk.ProtocolPort{Protocol: "TCP", Port: 8001}: struct{}{},
					fwk.ProtocolPort{Protocol: "TCP", Port: 8002}: struct{}{},
				},
			},
			pod: st.MakePod().
				ContainerPort([]v1.ContainerPort{
					{
						ContainerPort: 8002,
						HostPort:      8002,
						Protocol:      v1.ProtocolTCP,
					}}).
				Obj(),
			want: fwk.HostPortInfo{
				"0.0.0.0": {
					fwk.ProtocolPort{Protocol: "TCP", Port: 8001}: struct{}{},
				},
			},
		},
		{
			ports: fwk.HostPortInfo{
				"0.0.0.0": {
					fwk.ProtocolPort{Protocol: "TCP", Port: 8001}: struct{}{},
				},
			},
			pod: st.MakePod().
				InitContainerPort(false /* sidecar */, []v1.ContainerPort{
					{
						ContainerPort: 8001,
						HostPort:      8001,
						Protocol:      v1.ProtocolTCP,
					}}).
				Obj(),
			want: fwk.HostPortInfo{
				"0.0.0.0": {
					fwk.ProtocolPort{Protocol: "TCP", Port: 8001}: struct{}{},
				},
			},
		},
		{
			ports: fwk.HostPortInfo{
				"0.0.0.0": {
					fwk.ProtocolPort{Protocol: "TCP", Port: 8001}: struct{}{},
				},
			},
			pod: st.MakePod().
				InitContainerPort(true /* sidecar */, []v1.ContainerPort{
					{
						ContainerPort: 8001,
						HostPort:      8001,
						Protocol:      v1.ProtocolTCP,
					}}).
				Obj(),
			want: fwk.HostPortInfo{},
		},
		{
			ports: fwk.HostPortInfo{
				"0.0.0.0": {
					fwk.ProtocolPort{Protocol: "TCP", Port: 8001}: struct{}{},
				},
			},
			pod: st.MakePod().
				InitContainerPort(true /* sidecar */, []v1.ContainerPort{
					{
						ContainerPort: 8002,
						HostPort:      8002,
						Protocol:      v1.ProtocolTCP,
					}}).
				Obj(),
			want: fwk.HostPortInfo{
				"0.0.0.0": {
					fwk.ProtocolPort{Protocol: "TCP", Port: 8001}: struct{}{},
				},
			},
		},
		{
			ports: fwk.HostPortInfo{
				"0.0.0.0": {
					fwk.ProtocolPort{Protocol: "TCP", Port: 8001}: struct{}{},
					fwk.ProtocolPort{Protocol: "TCP", Port: 8002}: struct{}{},
				},
			},
			pod: st.MakePod().
				InitContainerPort(true /* sidecar */, []v1.ContainerPort{
					{
						ContainerPort: 8002,
						HostPort:      8002,
						Protocol:      v1.ProtocolTCP,
					}}).
				Obj(),
			want: fwk.HostPortInfo{
				"0.0.0.0": {
					fwk.ProtocolPort{Protocol: "TCP", Port: 8001}: struct{}{},
				},
			},
		},
	}
	for _, tc := range testCases {
		ni := NodeInfo{UsedPorts: tc.ports}
		ni.updateUsedPorts(tc.pod, false)
		if diff := cmp.Diff(tc.want, ni.UsedPorts); diff != "" {
			t.Errorf("updateUsedPorts() unexpected diff (-want, +got):\n%s", diff)
		}
	}
}

func TestQueuedPodInfo_UpdateInvalidatesSignature(t *testing.T) {
	pod1 := st.MakePod().Name("pod1").Label("version", "1").Obj()
	pod2 := pod1.DeepCopy()
	pod2.Labels["version"] = "2"

	podInfo, _ := NewPodInfo(pod1)
	queuedPodInfo := &QueuedPodInfo{
		PodInfo:      podInfo,
		PodSignature: fwk.PodSignature("sig-1"),
	}

	err := queuedPodInfo.Update(pod2)
	if err != nil {
		t.Fatalf("Update failed: %v", err)
	}

	if queuedPodInfo.PodSignature != nil {
		t.Errorf("Expected signature to be nil after Update, got '%s'", string(queuedPodInfo.PodSignature))
	}
}

func TestUpdateNodeAllocatableDRAClaimState(t *testing.T) {
	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DRANodeAllocatableResources, true)
	claim1NamespacedName := types.NamespacedName{
		Name:      "node-allocatable-claim-1",
		Namespace: "default",
	}
	claim2NamespacedName := types.NamespacedName{
		Name:      "node-allocatable-claim-2",
		Namespace: "default",
	}
	tests := []struct {
		name          string
		initialState  map[types.NamespacedName]*fwk.NodeAllocatableDRAClaimState
		pod           *v1.Pod
		sign          int64
		expectedState map[types.NamespacedName]*fwk.NodeAllocatableDRAClaimState
	}{
		{
			name:         "Add pod with single claim",
			initialState: map[types.NamespacedName]*fwk.NodeAllocatableDRAClaimState{},
			pod: &v1.Pod{
				ObjectMeta: metav1.ObjectMeta{UID: "pod1-uid", Namespace: "default"},
				Status: v1.PodStatus{
					NodeAllocatableResourceClaimStatuses: []v1.NodeAllocatableResourceClaimStatus{
						{ResourceClaimName: "node-allocatable-claim-1"},
					},
				},
			},
			sign: 1,
			expectedState: map[types.NamespacedName]*fwk.NodeAllocatableDRAClaimState{
				claim1NamespacedName: {ConsumerPods: sets.New[types.UID]("pod1-uid")},
			},
		},
		{
			name:         "Add pod with multiple claims",
			initialState: map[types.NamespacedName]*fwk.NodeAllocatableDRAClaimState{},
			pod: &v1.Pod{
				ObjectMeta: metav1.ObjectMeta{UID: "pod1-uid", Namespace: "default"},
				Status: v1.PodStatus{
					NodeAllocatableResourceClaimStatuses: []v1.NodeAllocatableResourceClaimStatus{
						{
							ResourceClaimName: "node-allocatable-claim-1",
						},
						{
							ResourceClaimName: "node-allocatable-claim-2",
						},
					},
				},
			},
			sign: 1,
			expectedState: map[types.NamespacedName]*fwk.NodeAllocatableDRAClaimState{
				claim1NamespacedName: {ConsumerPods: sets.New[types.UID]("pod1-uid")},
				claim2NamespacedName: {ConsumerPods: sets.New[types.UID]("pod1-uid")},
			},
		},
		{
			name: "Add multiple pods with the same claim",
			initialState: map[types.NamespacedName]*fwk.NodeAllocatableDRAClaimState{
				claim1NamespacedName: {ConsumerPods: sets.New[types.UID]("pod1-uid")},
			},
			pod: &v1.Pod{
				ObjectMeta: metav1.ObjectMeta{UID: "pod2-uid", Namespace: "default"},
				Status: v1.PodStatus{
					NodeAllocatableResourceClaimStatuses: []v1.NodeAllocatableResourceClaimStatus{
						{ResourceClaimName: "node-allocatable-claim-1"},
					},
				},
			},
			sign: 1,
			expectedState: map[types.NamespacedName]*fwk.NodeAllocatableDRAClaimState{
				claim1NamespacedName: {ConsumerPods: sets.New[types.UID]("pod1-uid", "pod2-uid")},
			},
		},
		{
			name: "Add multiple pods with different claims",
			initialState: map[types.NamespacedName]*fwk.NodeAllocatableDRAClaimState{
				claim1NamespacedName: {ConsumerPods: sets.New[types.UID]("pod1-uid")},
			},
			pod: &v1.Pod{
				ObjectMeta: metav1.ObjectMeta{UID: "pod2-uid", Namespace: "default"},
				Status: v1.PodStatus{
					NodeAllocatableResourceClaimStatuses: []v1.NodeAllocatableResourceClaimStatus{
						{
							ResourceClaimName: "node-allocatable-claim-2",
						},
					},
				},
			},
			sign: 1,
			expectedState: map[types.NamespacedName]*fwk.NodeAllocatableDRAClaimState{
				claim1NamespacedName: {ConsumerPods: sets.New[types.UID]("pod1-uid")},
				claim2NamespacedName: {ConsumerPods: sets.New[types.UID]("pod2-uid")},
			},
		},
		{
			name: "Remove pod with single claim",
			initialState: map[types.NamespacedName]*fwk.NodeAllocatableDRAClaimState{
				claim1NamespacedName: {ConsumerPods: sets.New[types.UID]("pod1-uid")},
			},
			pod: &v1.Pod{
				ObjectMeta: metav1.ObjectMeta{UID: "pod1-uid", Namespace: "default"},
				Status: v1.PodStatus{
					NodeAllocatableResourceClaimStatuses: []v1.NodeAllocatableResourceClaimStatus{
						{
							ResourceClaimName: "node-allocatable-claim-1",
						},
					},
				},
			},
			sign:          -1,
			expectedState: map[types.NamespacedName]*fwk.NodeAllocatableDRAClaimState{},
		},
		{
			name: "Remove pod with shared claim",
			initialState: map[types.NamespacedName]*fwk.NodeAllocatableDRAClaimState{
				claim1NamespacedName: {ConsumerPods: sets.New[types.UID]("pod1-uid", "pod2-uid")},
				claim2NamespacedName: {ConsumerPods: sets.New[types.UID]("pod2-uid")},
			},
			pod: &v1.Pod{
				ObjectMeta: metav1.ObjectMeta{UID: "pod2-uid", Namespace: "default"},
				Status: v1.PodStatus{
					NodeAllocatableResourceClaimStatuses: []v1.NodeAllocatableResourceClaimStatus{
						{ResourceClaimName: "node-allocatable-claim-1"},
						{ResourceClaimName: "node-allocatable-claim-2"},
					},
				},
			},
			sign: -1,
			expectedState: map[types.NamespacedName]*fwk.NodeAllocatableDRAClaimState{
				claim1NamespacedName: {ConsumerPods: sets.New[types.UID]("pod1-uid")},
			},
		},
		{
			name: "Add pod with no claims",
			initialState: map[types.NamespacedName]*fwk.NodeAllocatableDRAClaimState{
				claim1NamespacedName: {ConsumerPods: sets.New[types.UID]("pod1-uid")},
			},
			pod: &v1.Pod{
				ObjectMeta: metav1.ObjectMeta{UID: "pod2-uid", Namespace: "default"},
			},
			sign: 1,
			expectedState: map[types.NamespacedName]*fwk.NodeAllocatableDRAClaimState{
				claim1NamespacedName: {ConsumerPods: sets.New[types.UID]("pod1-uid")},
			},
		},
		{
			name: "Remove pod with no claims",
			initialState: map[types.NamespacedName]*fwk.NodeAllocatableDRAClaimState{
				claim1NamespacedName: {ConsumerPods: sets.New[types.UID]("pod1-uid")},
			},
			pod: &v1.Pod{
				ObjectMeta: metav1.ObjectMeta{UID: "pod2-uid", Namespace: "default"},
			},
			sign: -1,
			expectedState: map[types.NamespacedName]*fwk.NodeAllocatableDRAClaimState{
				claim1NamespacedName: {ConsumerPods: sets.New[types.UID]("pod1-uid")},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ni := &NodeInfo{
				NodeAllocatableDRAClaimStates: tt.initialState,
			}
			podInfo, _ := NewPodInfo(tt.pod)
			ni.updateNodeAllocatableDRAClaimState(podInfo, tt.sign)

			if diff := cmp.Diff(tt.expectedState, ni.NodeAllocatableDRAClaimStates, cmp.AllowUnexported(fwk.NodeAllocatableDRAClaimState{})); diff != "" {
				t.Errorf("updateNodeAllocatableDRAClaimState() returned diff (-want +got):\\n%s", diff)
			}
		})
	}
}
