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

import (
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/validation/field"
	componentbaseconfig "k8s.io/component-base/config"
	"k8s.io/kubernetes/pkg/scheduler/apis/config"
	configv1 "k8s.io/kubernetes/pkg/scheduler/apis/config/v1"
	"k8s.io/utils/ptr"
)

func TestValidateKubeSchedulerConfigurationV1(t *testing.T) {
	podInitialBackoffSeconds := int64(1)
	podMaxBackoffSeconds := int64(1)
	validConfig := &config.KubeSchedulerConfiguration{
		TypeMeta: metav1.TypeMeta{
			APIVersion: configv1.SchemeGroupVersion.String(),
		},
		Parallelism: 8,
		ClientConnection: componentbaseconfig.ClientConnectionConfiguration{
			AcceptContentTypes: "application/json",
			ContentType:        "application/json",
			QPS:                10,
			Burst:              10,
		},
		LeaderElection: componentbaseconfig.LeaderElectionConfiguration{
			ResourceLock:      "leases",
			LeaderElect:       true,
			LeaseDuration:     metav1.Duration{Duration: 30 * time.Second},
			RenewDeadline:     metav1.Duration{Duration: 15 * time.Second},
			RetryPeriod:       metav1.Duration{Duration: 5 * time.Second},
			ResourceNamespace: "name",
			ResourceName:      "name",
		},
		PodInitialBackoffSeconds: podInitialBackoffSeconds,
		PodMaxBackoffSeconds:     podMaxBackoffSeconds,
		Profiles: []config.KubeSchedulerProfile{{
			SchedulerName:            "me",
			PercentageOfNodesToScore: ptr.To[int32](35),
			Plugins: &config.Plugins{
				QueueSort: config.PluginSet{
					Enabled: []config.Plugin{{Name: "CustomSort"}},
				},
				Score: config.PluginSet{
					Disabled: []config.Plugin{{Name: "*"}},
				},
			},
			PluginConfig: []config.PluginConfig{{
				Name: "DefaultPreemption",
				Args: &config.DefaultPreemptionArgs{MinCandidateNodesPercentage: 10, MinCandidateNodesAbsolute: 100},
			}},
		}, {
			SchedulerName:            "other",
			PercentageOfNodesToScore: ptr.To[int32](35),
			Plugins: &config.Plugins{
				QueueSort: config.PluginSet{
					Enabled: []config.Plugin{{Name: "CustomSort"}},
				},
				Bind: config.PluginSet{
					Enabled: []config.Plugin{{Name: "CustomBind"}},
				},
			},
		}},
		Extenders: []config.Extender{{
			PrioritizeVerb: "prioritize",
			Weight:         1,
		}},
	}

	invalidParallelismValue := validConfig.DeepCopy()
	invalidParallelismValue.Parallelism = 0

	resourceNameNotSet := validConfig.DeepCopy()
	resourceNameNotSet.LeaderElection.ResourceName = ""

	resourceNamespaceNotSet := validConfig.DeepCopy()
	resourceNamespaceNotSet.LeaderElection.ResourceNamespace = ""

	resourceLockNotLeases := validConfig.DeepCopy()
	resourceLockNotLeases.LeaderElection.ResourceLock = "configmap"

	enableContentProfilingSetWithoutEnableProfiling := validConfig.DeepCopy()
	enableContentProfilingSetWithoutEnableProfiling.EnableProfiling = false
	enableContentProfilingSetWithoutEnableProfiling.EnableContentionProfiling = true

	percentageOfNodesToScore101 := validConfig.DeepCopy()
	percentageOfNodesToScore101.PercentageOfNodesToScore = ptr.To[int32](101)

	percentageOfNodesToScoreNegative := validConfig.DeepCopy()
	percentageOfNodesToScoreNegative.PercentageOfNodesToScore = ptr.To[int32](-1)

	schedulerNameNotSet := validConfig.DeepCopy()
	schedulerNameNotSet.Profiles[1].SchedulerName = ""

	repeatedSchedulerName := validConfig.DeepCopy()
	repeatedSchedulerName.Profiles[0].SchedulerName = "other"

	profilePercentageOfNodesToScore101 := validConfig.DeepCopy()
	profilePercentageOfNodesToScore101.Profiles[1].PercentageOfNodesToScore = ptr.To[int32](101)

	profilePercentageOfNodesToScoreNegative := validConfig.DeepCopy()
	profilePercentageOfNodesToScoreNegative.Profiles[1].PercentageOfNodesToScore = ptr.To[int32](-1)

	differentQueueSort := validConfig.DeepCopy()
	differentQueueSort.Profiles[1].Plugins.QueueSort.Enabled[0].Name = "AnotherSort"

	oneEmptyQueueSort := validConfig.DeepCopy()
	oneEmptyQueueSort.Profiles[0].Plugins = nil

	extenderNegativeWeight := validConfig.DeepCopy()
	extenderNegativeWeight.Extenders[0].Weight = -1

	invalidNodePercentage := validConfig.DeepCopy()
	invalidNodePercentage.Profiles[0].PluginConfig = []config.PluginConfig{{
		Name: "DefaultPreemption",
		Args: &config.DefaultPreemptionArgs{MinCandidateNodesPercentage: 200, MinCandidateNodesAbsolute: 100},
	}}

	invalidPluginArgs := validConfig.DeepCopy()
	invalidPluginArgs.Profiles[0].PluginConfig = []config.PluginConfig{{
		Name: "DefaultPreemption",
		Args: &config.InterPodAffinityArgs{},
	}}

	duplicatedPluginConfig := validConfig.DeepCopy()
	duplicatedPluginConfig.Profiles[0].PluginConfig = []config.PluginConfig{{
		Name: "config",
	}, {
		Name: "config",
	}}

	mismatchQueueSort := validConfig.DeepCopy()
	mismatchQueueSort.Profiles = []config.KubeSchedulerProfile{{
		SchedulerName: "me",
		Plugins: &config.Plugins{
			QueueSort: config.PluginSet{
				Enabled: []config.Plugin{{Name: "PrioritySort"}},
			},
		},
		PluginConfig: []config.PluginConfig{{
			Name: "PrioritySort",
		}},
	}, {
		SchedulerName: "other",
		Plugins: &config.Plugins{
			QueueSort: config.PluginSet{
				Enabled: []config.Plugin{{Name: "CustomSort"}},
			},
		},
		PluginConfig: []config.PluginConfig{{
			Name: "CustomSort",
		}},
	}}

	extenderDuplicateManagedResource := validConfig.DeepCopy()
	extenderDuplicateManagedResource.Extenders[0].ManagedResources = []config.ExtenderManagedResource{
		{Name: "example.com/foo", IgnoredByScheduler: false},
		{Name: "example.com/foo", IgnoredByScheduler: false},
	}

	extenderDuplicateBind := validConfig.DeepCopy()
	extenderDuplicateBind.Extenders[0].BindVerb = "foo"
	extenderDuplicateBind.Extenders = append(extenderDuplicateBind.Extenders, config.Extender{
		PrioritizeVerb: "prioritize",
		BindVerb:       "bar",
		Weight:         1,
	})

	validPlugins := validConfig.DeepCopy()
	validPlugins.Profiles[0].Plugins.Score.Enabled = append(validPlugins.Profiles[0].Plugins.Score.Enabled, config.Plugin{Name: "PodTopologySpread", Weight: 2})

	invalidPlugins := validConfig.DeepCopy()
	invalidPlugins.Profiles[0].Plugins.Score.Enabled = append(invalidPlugins.Profiles[0].Plugins.Score.Enabled, config.Plugin{Name: "AzureDiskLimits"})
	invalidPlugins.Profiles[0].Plugins.Score.Enabled = append(invalidPlugins.Profiles[0].Plugins.Score.Enabled, config.Plugin{Name: "CinderLimits"})
	invalidPlugins.Profiles[0].Plugins.Score.Enabled = append(invalidPlugins.Profiles[0].Plugins.Score.Enabled, config.Plugin{Name: "EBSLimits"})
	invalidPlugins.Profiles[0].Plugins.Score.Enabled = append(invalidPlugins.Profiles[0].Plugins.Score.Enabled, config.Plugin{Name: "GCEPDLimits"})

	scenarios := map[string]struct {
		config   *config.KubeSchedulerConfiguration
		wantErrs field.ErrorList
	}{
		"good": {
			config: validConfig,
		},
		"bad-parallelism-invalid-value": {
			config: invalidParallelismValue,
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "parallelism",
				},
			},
		},
		"bad-resource-name-not-set": {
			config: resourceNameNotSet,
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeRequired,
					Field: "leaderElection.resourceName",
				},
			},
		},
		"bad-resource-namespace-not-set": {
			config: resourceNamespaceNotSet,
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeRequired,
					Field: "leaderElection.resourceNamespace",
				},
			},
		},
		"bad-resource-lock-not-leases": {
			config: resourceLockNotLeases,
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "leaderElection.resourceLock",
				},
			},
		},
		"bad-percentage-of-nodes-to-score": {
			config: percentageOfNodesToScore101,
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "percentageOfNodesToScore",
				},
			},
		},
		"negative-percentage-of-nodes-to-score": {
			config: percentageOfNodesToScoreNegative,
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "percentageOfNodesToScore",
				},
			},
		},
		"scheduler-name-not-set": {
			config: schedulerNameNotSet,
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeRequired,
					Field: "profiles[1].schedulerName",
				},
			},
		},
		"repeated-scheduler-name": {
			config: repeatedSchedulerName,
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeDuplicate,
					Field: "profiles[1].schedulerName",
				},
			},
		},
		"greater-than-100-profile-percentage-of-nodes-to-score": {
			config: profilePercentageOfNodesToScore101,
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "profiles[1].percentageOfNodesToScore",
				},
			},
		},
		"negative-profile-percentage-of-nodes-to-score": {
			config: profilePercentageOfNodesToScoreNegative,
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "profiles[1].percentageOfNodesToScore",
				},
			},
		},
		"different-queue-sort": {
			config: differentQueueSort,
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "profiles[1].plugins.queueSort",
				},
			},
		},
		"one-empty-queue-sort": {
			config: oneEmptyQueueSort,
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "profiles[1].plugins.queueSort",
				},
			},
		},
		"extender-negative-weight": {
			config: extenderNegativeWeight,
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "extenders[0].weight",
				},
			},
		},
		"extender-duplicate-managed-resources": {
			config: extenderDuplicateManagedResource,
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "extenders[0].managedResources[1].name",
				},
			},
		},
		"extender-duplicate-bind": {
			config: extenderDuplicateBind,
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "extenders",
				},
			},
		},
		"invalid-node-percentage": {
			config: invalidNodePercentage,
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "profiles[0].pluginConfig[0].args.minCandidateNodesPercentage",
				},
			},
		},
		"invalid-plugin-args": {
			config: invalidPluginArgs,
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "profiles[0].pluginConfig[0].args",
				},
			},
		},
		"duplicated-plugin-config": {
			config: duplicatedPluginConfig,
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeDuplicate,
					Field: "profiles[0].pluginConfig[1]",
				},
			},
		},
		"mismatch-queue-sort": {
			config: mismatchQueueSort,
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "profiles[1].plugins.queueSort",
				},
			},
		},
		"invalid-plugins": {
			config: invalidPlugins,
			wantErrs: field.ErrorList{
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "profiles[0].plugins.score.enabled[0]",
				},
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "profiles[0].plugins.score.enabled[1]",
				},
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "profiles[0].plugins.score.enabled[2]",
				},
				&field.Error{
					Type:  field.ErrorTypeInvalid,
					Field: "profiles[0].plugins.score.enabled[3]",
				},
			},
		},
		"valid-plugins": {
			config: validPlugins,
		},
	}

	for name, scenario := range scenarios {
		t.Run(name, func(t *testing.T) {
			errs := ValidateKubeSchedulerConfiguration(scenario.config)
			diff := cmp.Diff(scenario.wantErrs.ToAggregate(), errs, ignoreBadValueDetail)
			if diff != "" {
				t.Errorf("KubeSchedulerConfiguration returned err (-want,+got):\n%s", diff)
			}
		})
	}
}
