/*
Copyright 2020 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 validation

import (
	"fmt"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	v1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/errors"
	"k8s.io/apimachinery/pkg/util/validation/field"
	"k8s.io/apiserver/pkg/util/feature"
	"k8s.io/component-base/featuregate"
	featuregatetesting "k8s.io/component-base/featuregate/testing"
	"k8s.io/kubernetes/pkg/features"
	"k8s.io/kubernetes/pkg/scheduler/apis/config"
)

var (
	ignoreBadValueDetail = cmpopts.IgnoreFields(field.Error{}, "BadValue", "Detail")
)

func TestValidateDefaultPreemptionArgs(t *testing.T) {
	cases := map[string]struct {
		args     config.DefaultPreemptionArgs
		wantErrs field.ErrorList
	}{
		"valid args (default)": {
			args: config.DefaultPreemptionArgs{
				MinCandidateNodesPercentage: 10,
				MinCandidateNodesAbsolute:   100,
			},
		},
		"negative minCandidateNodesPercentage": {
			args: config.DefaultPreemptionArgs{
				MinCandidateNodesPercentage: -1,
				MinCandidateNodesAbsolute:   100,
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "minCandidateNodesPercentage",
				},
			},
		},
		"minCandidateNodesPercentage over 100": {
			args: config.DefaultPreemptionArgs{
				MinCandidateNodesPercentage: 900,
				MinCandidateNodesAbsolute:   100,
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "minCandidateNodesPercentage",
				},
			},
		},
		"negative minCandidateNodesAbsolute": {
			args: config.DefaultPreemptionArgs{
				MinCandidateNodesPercentage: 20,
				MinCandidateNodesAbsolute:   -1,
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "minCandidateNodesAbsolute",
				},
			},
		},
		"all zero": {
			args: config.DefaultPreemptionArgs{
				MinCandidateNodesPercentage: 0,
				MinCandidateNodesAbsolute:   0,
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "minCandidateNodesPercentage",
				}, &field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "minCandidateNodesAbsolute",
				},
			},
		},
		"both negative": {
			args: config.DefaultPreemptionArgs{
				MinCandidateNodesPercentage: -1,
				MinCandidateNodesAbsolute:   -1,
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "minCandidateNodesPercentage",
				}, &field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "minCandidateNodesAbsolute",
				},
			},
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			err := ValidateDefaultPreemptionArgs(nil, &tc.args)
			if diff := cmp.Diff(tc.wantErrs.ToAggregate(), err, ignoreBadValueDetail); diff != "" {
				t.Errorf("ValidateDefaultPreemptionArgs returned err (-want,+got):\n%s", diff)
			}
		})
	}
}

func TestValidateInterPodAffinityArgs(t *testing.T) {
	cases := map[string]struct {
		args    config.InterPodAffinityArgs
		wantErr error
	}{
		"valid args": {
			args: config.InterPodAffinityArgs{
				HardPodAffinityWeight: 10,
			},
		},
		"hardPodAffinityWeight less than min": {
			args: config.InterPodAffinityArgs{
				HardPodAffinityWeight: -1,
			},
			wantErr: &field.Error{
				Type:  field.ErrorTypeInvalid,
				Field: "hardPodAffinityWeight",
			},
		},
		"hardPodAffinityWeight more than max": {
			args: config.InterPodAffinityArgs{
				HardPodAffinityWeight: 101,
			},
			wantErr: &field.Error{
				Type:  field.ErrorTypeInvalid,
				Field: "hardPodAffinityWeight",
			},
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			err := ValidateInterPodAffinityArgs(nil, &tc.args)
			if diff := cmp.Diff(tc.wantErr, err, ignoreBadValueDetail); diff != "" {
				t.Errorf("ValidateInterPodAffinityArgs returned err (-want,+got):\n%s", diff)
			}
		})
	}
}

func TestValidateNodeLabelArgs(t *testing.T) {
	cases := map[string]struct {
		args     config.NodeLabelArgs
		wantErrs field.ErrorList
	}{
		"valid config": {
			args: config.NodeLabelArgs{
				PresentLabels:           []string{"present"},
				AbsentLabels:            []string{"absent"},
				PresentLabelsPreference: []string{"present-preference"},
				AbsentLabelsPreference:  []string{"absent-preference"},
			},
		},
		"labels conflict": {
			args: config.NodeLabelArgs{
				PresentLabels: []string{"label"},
				AbsentLabels:  []string{"label"},
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "presentLabels[0]",
				},
			},
		},
		"multiple labels conflict": {
			args: config.NodeLabelArgs{
				PresentLabels: []string{"label", "label3"},
				AbsentLabels:  []string{"label", "label2", "label3"},
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "presentLabels[0]",
				},
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "presentLabels[1]",
				},
			},
		},
		"labels preference conflict": {
			args: config.NodeLabelArgs{
				PresentLabelsPreference: []string{"label"},
				AbsentLabelsPreference:  []string{"label"},
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "presentLabelsPreference[0]",
				},
			},
		},
		"multiple labels preference conflict": {
			args: config.NodeLabelArgs{
				PresentLabelsPreference: []string{"label", "label3"},
				AbsentLabelsPreference:  []string{"label", "label2", "label3"},
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "presentLabelsPreference[0]",
				},
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "presentLabelsPreference[1]",
				},
			},
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			err := ValidateNodeLabelArgs(nil, &tc.args)
			if diff := cmp.Diff(tc.wantErrs.ToAggregate(), err, ignoreBadValueDetail); diff != "" {
				t.Errorf("ValidateNodeLabelArgs returned err (-want,+got):\n%s", diff)
			}
		})
	}
}

func TestValidatePodTopologySpreadArgs(t *testing.T) {
	cases := map[string]struct {
		args     *config.PodTopologySpreadArgs
		wantErrs field.ErrorList
	}{
		"valid config": {
			args: &config.PodTopologySpreadArgs{
				DefaultConstraints: []v1.TopologySpreadConstraint{
					{
						MaxSkew:           1,
						TopologyKey:       "node",
						WhenUnsatisfiable: v1.DoNotSchedule,
					},
					{
						MaxSkew:           2,
						TopologyKey:       "zone",
						WhenUnsatisfiable: v1.ScheduleAnyway,
					},
				},
				DefaultingType: config.ListDefaulting,
			},
		},
		"maxSkew less than zero": {
			args: &config.PodTopologySpreadArgs{
				DefaultConstraints: []v1.TopologySpreadConstraint{
					{
						MaxSkew:           -1,
						TopologyKey:       "node",
						WhenUnsatisfiable: v1.DoNotSchedule,
					},
				},
				DefaultingType: config.ListDefaulting,
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "defaultConstraints[0].maxSkew",
				},
			},
		},
		"empty topology key": {
			args: &config.PodTopologySpreadArgs{
				DefaultConstraints: []v1.TopologySpreadConstraint{
					{
						MaxSkew:           1,
						TopologyKey:       "",
						WhenUnsatisfiable: v1.DoNotSchedule,
					},
				},
				DefaultingType: config.ListDefaulting,
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeRequired,
					Field: "defaultConstraints[0].topologyKey",
				},
			},
		},
		"whenUnsatisfiable is empty": {
			args: &config.PodTopologySpreadArgs{
				DefaultConstraints: []v1.TopologySpreadConstraint{
					{
						MaxSkew:           1,
						TopologyKey:       "node",
						WhenUnsatisfiable: "",
					},
				},
				DefaultingType: config.ListDefaulting,
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeRequired,
					Field: "defaultConstraints[0].whenUnsatisfiable",
				},
			},
		},
		"whenUnsatisfiable contains unsupported action": {
			args: &config.PodTopologySpreadArgs{
				DefaultConstraints: []v1.TopologySpreadConstraint{
					{
						MaxSkew:           1,
						TopologyKey:       "node",
						WhenUnsatisfiable: "unknown action",
					},
				},
				DefaultingType: config.ListDefaulting,
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeNotSupported,
					Field: "defaultConstraints[0].whenUnsatisfiable",
				},
			},
		},
		"duplicated constraints": {
			args: &config.PodTopologySpreadArgs{
				DefaultConstraints: []v1.TopologySpreadConstraint{
					{
						MaxSkew:           1,
						TopologyKey:       "node",
						WhenUnsatisfiable: v1.DoNotSchedule,
					},
					{
						MaxSkew:           2,
						TopologyKey:       "node",
						WhenUnsatisfiable: v1.DoNotSchedule,
					},
				},
				DefaultingType: config.ListDefaulting,
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeDuplicate,
					Field: "defaultConstraints[1]",
				},
			},
		},
		"label selector present": {
			args: &config.PodTopologySpreadArgs{
				DefaultConstraints: []v1.TopologySpreadConstraint{
					{
						MaxSkew:           1,
						TopologyKey:       "key",
						WhenUnsatisfiable: v1.DoNotSchedule,
						LabelSelector: &metav1.LabelSelector{
							MatchLabels: map[string]string{
								"a": "b",
							},
						},
					},
				},
				DefaultingType: config.ListDefaulting,
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeForbidden,
					Field: "defaultConstraints[0].labelSelector",
				},
			},
		},
		"list default constraints, no constraints": {
			args: &config.PodTopologySpreadArgs{
				DefaultingType: config.ListDefaulting,
			},
		},
		"system default constraints": {
			args: &config.PodTopologySpreadArgs{
				DefaultingType: config.SystemDefaulting,
			},
		},
		"wrong constraints": {
			args: &config.PodTopologySpreadArgs{
				DefaultingType: "unknown",
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeNotSupported,
					Field: "defaultingType",
				},
			},
		},
		"system default constraints, but has constraints": {
			args: &config.PodTopologySpreadArgs{
				DefaultConstraints: []v1.TopologySpreadConstraint{
					{
						MaxSkew:           1,
						TopologyKey:       "key",
						WhenUnsatisfiable: v1.DoNotSchedule,
					},
				},
				DefaultingType: config.SystemDefaulting,
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "defaultingType",
				},
			},
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			err := ValidatePodTopologySpreadArgs(nil, tc.args)
			if diff := cmp.Diff(tc.wantErrs.ToAggregate(), err, ignoreBadValueDetail); diff != "" {
				t.Errorf("ValidatePodTopologySpreadArgs returned err (-want,+got):\n%s", diff)
			}
		})
	}
}

func TestValidateRequestedToCapacityRatioArgs(t *testing.T) {
	cases := map[string]struct {
		args     config.RequestedToCapacityRatioArgs
		wantErrs field.ErrorList
	}{
		"valid config": {
			args: config.RequestedToCapacityRatioArgs{
				Shape: []config.UtilizationShapePoint{
					{
						Utilization: 20,
						Score:       5,
					},
					{
						Utilization: 30,
						Score:       3,
					},
					{
						Utilization: 50,
						Score:       2,
					},
				},
				Resources: []config.ResourceSpec{
					{
						Name:   "custom-resource",
						Weight: 5,
					},
				},
			},
		},
		"no shape points": {
			args: config.RequestedToCapacityRatioArgs{
				Shape: []config.UtilizationShapePoint{},
				Resources: []config.ResourceSpec{
					{
						Name:   "custom",
						Weight: 5,
					},
				},
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeRequired,
					Field: "shape",
				},
			},
		},
		"utilization less than min": {
			args: config.RequestedToCapacityRatioArgs{
				Shape: []config.UtilizationShapePoint{
					{
						Utilization: -10,
						Score:       3,
					},
					{
						Utilization: 10,
						Score:       2,
					},
				},
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "shape[0].utilization",
				},
			},
		},
		"utilization greater than max": {
			args: config.RequestedToCapacityRatioArgs{
				Shape: []config.UtilizationShapePoint{
					{
						Utilization: 10,
						Score:       3,
					},
					{
						Utilization: 110,
						Score:       2,
					},
				},
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "shape[1].utilization",
				},
			},
		},
		"utilization values in non-increasing order": {
			args: config.RequestedToCapacityRatioArgs{
				Shape: []config.UtilizationShapePoint{
					{
						Utilization: 30,
						Score:       3,
					},
					{
						Utilization: 20,
						Score:       2,
					},
					{
						Utilization: 10,
						Score:       1,
					},
				},
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "shape[1].utilization",
				},
			},
		},
		"duplicated utilization values": {
			args: config.RequestedToCapacityRatioArgs{
				Shape: []config.UtilizationShapePoint{
					{
						Utilization: 10,
						Score:       3,
					},
					{
						Utilization: 20,
						Score:       2,
					},
					{
						Utilization: 20,
						Score:       1,
					},
				},
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "shape[2].utilization",
				},
			},
		},
		"score less than min": {
			args: config.RequestedToCapacityRatioArgs{
				Shape: []config.UtilizationShapePoint{
					{
						Utilization: 10,
						Score:       -1,
					},
					{
						Utilization: 20,
						Score:       2,
					},
				},
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "shape[0].score",
				},
			},
		},
		"score greater than max": {
			args: config.RequestedToCapacityRatioArgs{
				Shape: []config.UtilizationShapePoint{
					{
						Utilization: 10,
						Score:       3,
					},
					{
						Utilization: 20,
						Score:       11,
					},
				},
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "shape[1].score",
				},
			},
		},
		"resources weight less than 1": {
			args: config.RequestedToCapacityRatioArgs{
				Shape: []config.UtilizationShapePoint{
					{
						Utilization: 10,
						Score:       1,
					},
				},
				Resources: []config.ResourceSpec{
					{
						Name:   "custom",
						Weight: 0,
					},
				},
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "resources[0].weight",
				},
			},
		},
		"multiple errors": {
			args: config.RequestedToCapacityRatioArgs{
				Shape: []config.UtilizationShapePoint{
					{
						Utilization: 20,
						Score:       -1,
					},
					{
						Utilization: 10,
						Score:       2,
					},
				},
				Resources: []config.ResourceSpec{
					{
						Name:   "custom",
						Weight: 0,
					},
				},
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "shape[1].utilization",
				},
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "shape[0].score",
				},
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "resources[0].weight",
				},
			},
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			err := ValidateRequestedToCapacityRatioArgs(nil, &tc.args)
			if diff := cmp.Diff(tc.wantErrs.ToAggregate(), err, ignoreBadValueDetail); diff != "" {
				t.Errorf("ValidateRequestedToCapacityRatioArgs returned err (-want,+got):\n%s", diff)
			}
		})
	}
}

func TestValidateNodeResourcesLeastAllocatedArgs(t *testing.T) {
	cases := map[string]struct {
		args     *config.NodeResourcesLeastAllocatedArgs
		wantErrs field.ErrorList
	}{
		"valid config": {
			args: &config.NodeResourcesLeastAllocatedArgs{
				Resources: []config.ResourceSpec{
					{
						Name:   "cpu",
						Weight: 50,
					},
					{
						Name:   "memory",
						Weight: 30,
					},
				},
			},
		},
		"weight less than min": {
			args: &config.NodeResourcesLeastAllocatedArgs{
				Resources: []config.ResourceSpec{
					{
						Name:   "cpu",
						Weight: 0,
					},
				},
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "resources[0].weight",
				},
			},
		},
		"weight more than max": {
			args: &config.NodeResourcesLeastAllocatedArgs{
				Resources: []config.ResourceSpec{
					{
						Name:   "memory",
						Weight: 101,
					},
				},
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "resources[0].weight",
				},
			},
		},
		"multiple error": {
			args: &config.NodeResourcesLeastAllocatedArgs{
				Resources: []config.ResourceSpec{
					{
						Name:   "memory",
						Weight: 0,
					},
					{
						Name:   "cpu",
						Weight: 101,
					},
				},
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "resources[0].weight",
				},
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "resources[1].weight",
				},
			},
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			err := ValidateNodeResourcesLeastAllocatedArgs(nil, tc.args)
			if diff := cmp.Diff(tc.wantErrs.ToAggregate(), err, ignoreBadValueDetail); diff != "" {
				t.Errorf("ValidateNodeResourcesLeastAllocatedArgs returned err (-want,+got):\n%s", diff)
			}
		})
	}
}

func TestValidateNodeResourcesMostAllocatedArgs(t *testing.T) {
	cases := map[string]struct {
		args     *config.NodeResourcesMostAllocatedArgs
		wantErrs field.ErrorList
	}{
		"valid config": {
			args: &config.NodeResourcesMostAllocatedArgs{
				Resources: []config.ResourceSpec{
					{
						Name:   "cpu",
						Weight: 70,
					},
					{
						Name:   "memory",
						Weight: 40,
					},
				},
			},
		},
		"weight less than min": {
			args: &config.NodeResourcesMostAllocatedArgs{
				Resources: []config.ResourceSpec{
					{
						Name:   "cpu",
						Weight: -1,
					},
				},
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "resources[0].weight",
				},
			},
		},
		"weight more than max": {
			args: &config.NodeResourcesMostAllocatedArgs{
				Resources: []config.ResourceSpec{
					{
						Name:   "memory",
						Weight: 110,
					},
				},
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "resources[0].weight",
				},
			},
		},
		"multiple error": {
			args: &config.NodeResourcesMostAllocatedArgs{
				Resources: []config.ResourceSpec{
					{
						Name:   "memory",
						Weight: -1,
					},
					{
						Name:   "cpu",
						Weight: 110,
					},
				},
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "resources[0].weight",
				},
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "resources[1].weight",
				},
			},
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			err := ValidateNodeResourcesMostAllocatedArgs(nil, tc.args)
			if diff := cmp.Diff(tc.wantErrs.ToAggregate(), err, ignoreBadValueDetail); diff != "" {
				t.Errorf("ValidateNodeResourcesLeastAllocatedArgs returned err (-want,+got):\n%s", diff)
			}
		})
	}
}

func TestValidateNodeResourcesBalancedAllocationArgs(t *testing.T) {
	cases := map[string]struct {
		args     *config.NodeResourcesBalancedAllocationArgs
		wantErrs field.ErrorList
	}{
		"valid config": {
			args: &config.NodeResourcesBalancedAllocationArgs{
				Resources: []config.ResourceSpec{
					{
						Name:   "cpu",
						Weight: 1,
					},
					{
						Name:   "memory",
						Weight: 1,
					},
				},
			},
		},
		"invalid config": {
			args: &config.NodeResourcesBalancedAllocationArgs{
				Resources: []config.ResourceSpec{
					{
						Name:   "cpu",
						Weight: 2,
					},
					{
						Name:   "memory",
						Weight: 1,
					},
				},
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "resources[0].weight",
				},
			},
		},
		"repeated resources": {
			args: &config.NodeResourcesBalancedAllocationArgs{
				Resources: []config.ResourceSpec{
					{
						Name:   "cpu",
						Weight: 1,
					},
					{
						Name:   "cpu",
						Weight: 1,
					},
				},
			},
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeDuplicate,
					Field: "resources[1].name",
				},
			},
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			err := ValidateNodeResourcesBalancedAllocationArgs(nil, tc.args)
			if diff := cmp.Diff(tc.wantErrs.ToAggregate(), err, ignoreBadValueDetail); diff != "" {
				t.Errorf("ValidateNodeResourcesBalancedAllocationArgs returned err (-want,+got):\n%s", diff)
			}
		})
	}
}

func TestValidateNodeAffinityArgs(t *testing.T) {
	cases := []struct {
		name    string
		args    config.NodeAffinityArgs
		wantErr error
	}{
		{
			name: "empty",
		},
		{
			name: "valid added affinity",
			args: config.NodeAffinityArgs{
				AddedAffinity: &v1.NodeAffinity{
					RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{
						NodeSelectorTerms: []v1.NodeSelectorTerm{
							{
								MatchExpressions: []v1.NodeSelectorRequirement{
									{
										Key:      "label-1",
										Operator: v1.NodeSelectorOpIn,
										Values:   []string{"label-1-val"},
									},
								},
							},
						},
					},
					PreferredDuringSchedulingIgnoredDuringExecution: []v1.PreferredSchedulingTerm{
						{
							Weight: 1,
							Preference: v1.NodeSelectorTerm{
								MatchFields: []v1.NodeSelectorRequirement{
									{
										Key:      "metadata.name",
										Operator: v1.NodeSelectorOpIn,
										Values:   []string{"node-1"},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			name: "invalid added affinity",
			args: config.NodeAffinityArgs{
				AddedAffinity: &v1.NodeAffinity{
					RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{
						NodeSelectorTerms: []v1.NodeSelectorTerm{
							{
								MatchExpressions: []v1.NodeSelectorRequirement{
									{
										Key:      "invalid/label/key",
										Operator: v1.NodeSelectorOpIn,
										Values:   []string{"label-1-val"},
									},
								},
							},
						},
					},
					PreferredDuringSchedulingIgnoredDuringExecution: []v1.PreferredSchedulingTerm{
						{
							Weight: 1,
							Preference: v1.NodeSelectorTerm{
								MatchFields: []v1.NodeSelectorRequirement{
									{
										Key:      "metadata.name",
										Operator: v1.NodeSelectorOpIn,
										Values:   []string{"node-1", "node-2"},
									},
								},
							},
						},
					},
				},
			},
			wantErr: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "addedAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].key",
				},
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "addedAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].matchFields[0].values",
				},
			}.ToAggregate(),
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			err := ValidateNodeAffinityArgs(nil, &tc.args)
			if diff := cmp.Diff(tc.wantErr, err, ignoreBadValueDetail); diff != "" {
				t.Errorf("ValidatedNodeAffinityArgs returned err (-want,+got):\n%s", diff)
			}
		})
	}
}

func TestValidateVolumeBindingArgs(t *testing.T) {
	cases := []struct {
		name     string
		args     config.VolumeBindingArgs
		features map[featuregate.Feature]bool
		wantErr  error
	}{
		{
			name: "zero is a valid config",
			args: config.VolumeBindingArgs{
				BindTimeoutSeconds: 0,
			},
		},
		{
			name: "positive value is valid config",
			args: config.VolumeBindingArgs{
				BindTimeoutSeconds: 10,
			},
		},
		{
			name: "negative value is invalid config ",
			args: config.VolumeBindingArgs{
				BindTimeoutSeconds: -10,
			},
			wantErr: errors.NewAggregate([]error{&field.Error{
				Type:     field.ErrorTypeInvalid,
				Field:    "bindTimeoutSeconds",
				BadValue: int64(-10),
				Detail:   "invalid BindTimeoutSeconds, should not be a negative value",
			}}),
		},
		{
			name: "[VolumeCapacityPriority=off] shape should be nil when the feature is off",
			features: map[featuregate.Feature]bool{
				features.VolumeCapacityPriority: false,
			},
			args: config.VolumeBindingArgs{
				BindTimeoutSeconds: 10,
				Shape:              nil,
			},
		},
		{
			name: "[VolumeCapacityPriority=off] error if the shape is not nil when the feature is off",
			features: map[featuregate.Feature]bool{
				features.VolumeCapacityPriority: false,
			},
			args: config.VolumeBindingArgs{
				BindTimeoutSeconds: 10,
				Shape: []config.UtilizationShapePoint{
					{Utilization: 1, Score: 1},
					{Utilization: 3, Score: 3},
				},
			},
			wantErr: errors.NewAggregate([]error{&field.Error{
				Type:  field.ErrorTypeInvalid,
				Field: "shape",
			}}),
		},
		{
			name: "[VolumeCapacityPriority=on] shape should not be empty",
			features: map[featuregate.Feature]bool{
				features.VolumeCapacityPriority: true,
			},
			args: config.VolumeBindingArgs{
				BindTimeoutSeconds: 10,
				Shape:              []config.UtilizationShapePoint{},
			},
			wantErr: errors.NewAggregate([]error{&field.Error{
				Type:  field.ErrorTypeRequired,
				Field: "shape",
			}}),
		},
		{
			name: "[VolumeCapacityPriority=on] shape points must be sorted in increasing order",
			features: map[featuregate.Feature]bool{
				features.VolumeCapacityPriority: true,
			},
			args: config.VolumeBindingArgs{
				BindTimeoutSeconds: 10,
				Shape: []config.UtilizationShapePoint{
					{Utilization: 3, Score: 3},
					{Utilization: 1, Score: 1},
				},
			},
			wantErr: errors.NewAggregate([]error{&field.Error{
				Type:   field.ErrorTypeInvalid,
				Field:  "shape[1].utilization",
				Detail: "Invalid value: 1: utilization values must be sorted in increasing order",
			}}),
		},
		{
			name: "[VolumeCapacityPriority=on] shape point: invalid utilization and score",
			features: map[featuregate.Feature]bool{
				features.VolumeCapacityPriority: true,
			},
			args: config.VolumeBindingArgs{
				BindTimeoutSeconds: 10,
				Shape: []config.UtilizationShapePoint{
					{Utilization: -1, Score: 1},
					{Utilization: 10, Score: -1},
					{Utilization: 20, Score: 11},
					{Utilization: 101, Score: 1},
				},
			},
			wantErr: errors.NewAggregate([]error{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "shape[0].utilization",
				},
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "shape[1].score",
				},
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "shape[2].score",
				},
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "shape[3].utilization",
				},
			}),
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			for k, v := range tc.features {
				defer featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, k, v)()
			}
			err := ValidateVolumeBindingArgs(nil, &tc.args)
			if diff := cmp.Diff(tc.wantErr, err, ignoreBadValueDetail); diff != "" {
				t.Errorf("ValidateVolumeBindingArgs returned err (-want,+got):\n%s", diff)
			}
		})
	}
}

func TestValidateFitArgs(t *testing.T) {
	defaultScoringStrategy := &config.ScoringStrategy{
		Type: config.LeastAllocated,
		Resources: []config.ResourceSpec{
			{Name: "cpu", Weight: 1},
			{Name: "memory", Weight: 1},
		},
	}
	argsTest := []struct {
		name   string
		args   config.NodeResourcesFitArgs
		expect string
	}{
		{
			name: "IgnoredResources: too long value",
			args: config.NodeResourcesFitArgs{
				IgnoredResources: []string{fmt.Sprintf("longvalue%s", strings.Repeat("a", 64))},
				ScoringStrategy:  defaultScoringStrategy,
			},
			expect: "name part must be no more than 63 characters",
		},
		{
			name: "IgnoredResources: name is empty",
			args: config.NodeResourcesFitArgs{
				IgnoredResources: []string{"example.com/"},
				ScoringStrategy:  defaultScoringStrategy,
			},
			expect: "name part must be non-empty",
		},
		{
			name: "IgnoredResources: name has too many slash",
			args: config.NodeResourcesFitArgs{
				IgnoredResources: []string{"example.com/aaa/bbb"},
				ScoringStrategy:  defaultScoringStrategy,
			},
			expect: "a qualified name must consist of alphanumeric characters",
		},
		{
			name: "IgnoredResources: valid args",
			args: config.NodeResourcesFitArgs{
				IgnoredResources: []string{"example.com"},
				ScoringStrategy:  defaultScoringStrategy,
			},
		},
		{
			name: "IgnoredResourceGroups: valid args ",
			args: config.NodeResourcesFitArgs{
				IgnoredResourceGroups: []string{"example.com"},
				ScoringStrategy:       defaultScoringStrategy,
			},
		},
		{
			name: "IgnoredResourceGroups: illegal args",
			args: config.NodeResourcesFitArgs{
				IgnoredResourceGroups: []string{"example.com/"},
				ScoringStrategy:       defaultScoringStrategy,
			},
			expect: "name part must be non-empty",
		},
		{
			name: "IgnoredResourceGroups: name is too long",
			args: config.NodeResourcesFitArgs{
				IgnoredResourceGroups: []string{strings.Repeat("a", 64)},
				ScoringStrategy:       defaultScoringStrategy,
			},
			expect: "name part must be no more than 63 characters",
		},
		{
			name: "IgnoredResourceGroups: name cannot be contain slash",
			args: config.NodeResourcesFitArgs{
				IgnoredResourceGroups: []string{"example.com/aa"},
				ScoringStrategy:       defaultScoringStrategy,
			},
			expect: "resource group name can't contain '/'",
		},
		{
			name:   "ScoringStrategy: field is required",
			args:   config.NodeResourcesFitArgs{},
			expect: "ScoringStrategy field is required",
		},
	}

	for _, test := range argsTest {
		t.Run(test.name, func(t *testing.T) {
			if err := ValidateNodeResourcesFitArgs(nil, &test.args); err != nil && (!strings.Contains(err.Error(), test.expect)) {
				t.Errorf("case[%v]: error details do not include %v", test.name, err)
			}
		})
	}
}
