/*
Copyright 2016 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 testing

import (
	"math/rand"
	"reflect"
	"sort"
	"testing"

	fuzz "github.com/google/gofuzz"

	apiv1 "k8s.io/api/core/v1"
	extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
	"k8s.io/apimachinery/pkg/api/apitesting/roundtrip"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/util/diff"
	"k8s.io/kubernetes/pkg/api/legacyscheme"
)

type orderedGroupVersionKinds []schema.GroupVersionKind

func (o orderedGroupVersionKinds) Len() int      { return len(o) }
func (o orderedGroupVersionKinds) Swap(i, j int) { o[i], o[j] = o[j], o[i] }
func (o orderedGroupVersionKinds) Less(i, j int) bool {
	return o[i].String() < o[j].String()
}

// TODO: add a reflexive test that verifies that all SetDefaults functions are registered
func TestDefaulting(t *testing.T) {
	// these are the known types with defaulters - you must add to this list if you add a top level defaulter
	typesWithDefaulting := map[schema.GroupVersionKind]struct{}{
		{Group: "", Version: "v1", Kind: "ConfigMap"}:                                             {},
		{Group: "", Version: "v1", Kind: "ConfigMapList"}:                                         {},
		{Group: "", Version: "v1", Kind: "Endpoints"}:                                             {},
		{Group: "", Version: "v1", Kind: "EndpointsList"}:                                         {},
		{Group: "", Version: "v1", Kind: "EphemeralContainers"}:                                   {},
		{Group: "", Version: "v1", Kind: "Namespace"}:                                             {},
		{Group: "", Version: "v1", Kind: "NamespaceList"}:                                         {},
		{Group: "", Version: "v1", Kind: "Node"}:                                                  {},
		{Group: "", Version: "v1", Kind: "NodeList"}:                                              {},
		{Group: "", Version: "v1", Kind: "PersistentVolume"}:                                      {},
		{Group: "", Version: "v1", Kind: "PersistentVolumeList"}:                                  {},
		{Group: "", Version: "v1", Kind: "PersistentVolumeClaim"}:                                 {},
		{Group: "", Version: "v1", Kind: "PersistentVolumeClaimList"}:                             {},
		{Group: "", Version: "v1", Kind: "Pod"}:                                                   {},
		{Group: "", Version: "v1", Kind: "PodList"}:                                               {},
		{Group: "", Version: "v1", Kind: "PodTemplate"}:                                           {},
		{Group: "", Version: "v1", Kind: "PodTemplateList"}:                                       {},
		{Group: "", Version: "v1", Kind: "ReplicationController"}:                                 {},
		{Group: "", Version: "v1", Kind: "ReplicationControllerList"}:                             {},
		{Group: "", Version: "v1", Kind: "Secret"}:                                                {},
		{Group: "", Version: "v1", Kind: "SecretList"}:                                            {},
		{Group: "", Version: "v1", Kind: "Service"}:                                               {},
		{Group: "", Version: "v1", Kind: "ServiceList"}:                                           {},
		{Group: "apps", Version: "v1beta1", Kind: "StatefulSet"}:                                  {},
		{Group: "apps", Version: "v1beta1", Kind: "StatefulSetList"}:                              {},
		{Group: "apps", Version: "v1beta2", Kind: "StatefulSet"}:                                  {},
		{Group: "apps", Version: "v1beta2", Kind: "StatefulSetList"}:                              {},
		{Group: "apps", Version: "v1", Kind: "StatefulSet"}:                                       {},
		{Group: "apps", Version: "v1", Kind: "StatefulSetList"}:                                   {},
		{Group: "autoscaling", Version: "v1", Kind: "HorizontalPodAutoscaler"}:                    {},
		{Group: "autoscaling", Version: "v1", Kind: "HorizontalPodAutoscalerList"}:                {},
		{Group: "autoscaling", Version: "v2beta1", Kind: "HorizontalPodAutoscaler"}:               {},
		{Group: "autoscaling", Version: "v2beta1", Kind: "HorizontalPodAutoscalerList"}:           {},
		{Group: "batch", Version: "v1", Kind: "Job"}:                                              {},
		{Group: "batch", Version: "v1", Kind: "JobList"}:                                          {},
		{Group: "batch", Version: "v1beta1", Kind: "CronJob"}:                                     {},
		{Group: "batch", Version: "v1beta1", Kind: "CronJobList"}:                                 {},
		{Group: "batch", Version: "v1beta1", Kind: "JobTemplate"}:                                 {},
		{Group: "batch", Version: "v2alpha1", Kind: "CronJob"}:                                    {},
		{Group: "batch", Version: "v2alpha1", Kind: "CronJobList"}:                                {},
		{Group: "batch", Version: "v2alpha1", Kind: "JobTemplate"}:                                {},
		{Group: "certificates.k8s.io", Version: "v1beta1", Kind: "CertificateSigningRequest"}:     {},
		{Group: "certificates.k8s.io", Version: "v1beta1", Kind: "CertificateSigningRequestList"}: {},
		{Group: "discovery.k8s.io", Version: "v1alpha1", Kind: "EndpointSlice"}:                   {},
		{Group: "discovery.k8s.io", Version: "v1alpha1", Kind: "EndpointSliceList"}:               {},
		{Group: "kubeadm.k8s.io", Version: "v1alpha1", Kind: "MasterConfiguration"}:               {},
		// This object contains only int fields which currently breaks the defaulting test because
		// it's pretty stupid. Once we add non integer fields, we should uncomment this.
		// {Group: "kubeadm.k8s.io", Version: "v1alpha1", Kind: "NodeConfiguration"}:                 {},
		{Group: "extensions", Version: "v1beta1", Kind: "DaemonSet"}:                                            {},
		{Group: "extensions", Version: "v1beta1", Kind: "DaemonSetList"}:                                        {},
		{Group: "apps", Version: "v1beta2", Kind: "DaemonSet"}:                                                  {},
		{Group: "apps", Version: "v1beta2", Kind: "DaemonSetList"}:                                              {},
		{Group: "apps", Version: "v1", Kind: "DaemonSet"}:                                                       {},
		{Group: "apps", Version: "v1", Kind: "DaemonSetList"}:                                                   {},
		{Group: "extensions", Version: "v1beta1", Kind: "Deployment"}:                                           {},
		{Group: "extensions", Version: "v1beta1", Kind: "DeploymentList"}:                                       {},
		{Group: "apps", Version: "v1beta1", Kind: "Deployment"}:                                                 {},
		{Group: "apps", Version: "v1beta1", Kind: "DeploymentList"}:                                             {},
		{Group: "apps", Version: "v1beta2", Kind: "Deployment"}:                                                 {},
		{Group: "apps", Version: "v1beta2", Kind: "DeploymentList"}:                                             {},
		{Group: "apps", Version: "v1", Kind: "Deployment"}:                                                      {},
		{Group: "apps", Version: "v1", Kind: "DeploymentList"}:                                                  {},
		{Group: "extensions", Version: "v1beta1", Kind: "PodSecurityPolicy"}:                                    {},
		{Group: "extensions", Version: "v1beta1", Kind: "PodSecurityPolicyList"}:                                {},
		{Group: "apps", Version: "v1beta2", Kind: "ReplicaSet"}:                                                 {},
		{Group: "apps", Version: "v1beta2", Kind: "ReplicaSetList"}:                                             {},
		{Group: "apps", Version: "v1", Kind: "ReplicaSet"}:                                                      {},
		{Group: "apps", Version: "v1", Kind: "ReplicaSetList"}:                                                  {},
		{Group: "extensions", Version: "v1beta1", Kind: "ReplicaSet"}:                                           {},
		{Group: "extensions", Version: "v1beta1", Kind: "ReplicaSetList"}:                                       {},
		{Group: "extensions", Version: "v1beta1", Kind: "NetworkPolicy"}:                                        {},
		{Group: "extensions", Version: "v1beta1", Kind: "NetworkPolicyList"}:                                    {},
		{Group: "policy", Version: "v1beta1", Kind: "PodSecurityPolicy"}:                                        {},
		{Group: "policy", Version: "v1beta1", Kind: "PodSecurityPolicyList"}:                                    {},
		{Group: "rbac.authorization.k8s.io", Version: "v1alpha1", Kind: "ClusterRoleBinding"}:                   {},
		{Group: "rbac.authorization.k8s.io", Version: "v1alpha1", Kind: "ClusterRoleBindingList"}:               {},
		{Group: "rbac.authorization.k8s.io", Version: "v1alpha1", Kind: "RoleBinding"}:                          {},
		{Group: "rbac.authorization.k8s.io", Version: "v1alpha1", Kind: "RoleBindingList"}:                      {},
		{Group: "rbac.authorization.k8s.io", Version: "v1beta1", Kind: "ClusterRoleBinding"}:                    {},
		{Group: "rbac.authorization.k8s.io", Version: "v1beta1", Kind: "ClusterRoleBindingList"}:                {},
		{Group: "rbac.authorization.k8s.io", Version: "v1beta1", Kind: "RoleBinding"}:                           {},
		{Group: "rbac.authorization.k8s.io", Version: "v1beta1", Kind: "RoleBindingList"}:                       {},
		{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRoleBinding"}:                         {},
		{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRoleBindingList"}:                     {},
		{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "RoleBinding"}:                                {},
		{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "RoleBindingList"}:                            {},
		{Group: "settings.k8s.io", Version: "v1alpha1", Kind: "PodPreset"}:                                      {},
		{Group: "settings.k8s.io", Version: "v1alpha1", Kind: "PodPresetList"}:                                  {},
		{Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "ValidatingWebhookConfiguration"}:     {},
		{Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "ValidatingWebhookConfigurationList"}: {},
		{Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "MutatingWebhookConfiguration"}:       {},
		{Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "MutatingWebhookConfigurationList"}:   {},
		{Group: "admissionregistration.k8s.io", Version: "v1", Kind: "ValidatingWebhookConfiguration"}:          {},
		{Group: "admissionregistration.k8s.io", Version: "v1", Kind: "ValidatingWebhookConfigurationList"}:      {},
		{Group: "admissionregistration.k8s.io", Version: "v1", Kind: "MutatingWebhookConfiguration"}:            {},
		{Group: "admissionregistration.k8s.io", Version: "v1", Kind: "MutatingWebhookConfigurationList"}:        {},
		{Group: "auditregistration.k8s.io", Version: "v1alpha1", Kind: "AuditSink"}:                             {},
		{Group: "auditregistration.k8s.io", Version: "v1alpha1", Kind: "AuditSinkList"}:                         {},
		{Group: "networking.k8s.io", Version: "v1", Kind: "NetworkPolicy"}:                                      {},
		{Group: "networking.k8s.io", Version: "v1", Kind: "NetworkPolicyList"}:                                  {},
		{Group: "storage.k8s.io", Version: "v1beta1", Kind: "StorageClass"}:                                     {},
		{Group: "storage.k8s.io", Version: "v1beta1", Kind: "StorageClassList"}:                                 {},
		{Group: "storage.k8s.io", Version: "v1beta1", Kind: "CSIDriver"}:                                        {},
		{Group: "storage.k8s.io", Version: "v1beta1", Kind: "CSIDriverList"}:                                    {},
		{Group: "storage.k8s.io", Version: "v1", Kind: "StorageClass"}:                                          {},
		{Group: "storage.k8s.io", Version: "v1", Kind: "StorageClassList"}:                                      {},
		{Group: "storage.k8s.io", Version: "v1", Kind: "VolumeAttachment"}:                                      {},
		{Group: "storage.k8s.io", Version: "v1", Kind: "VolumeAttachmentList"}:                                  {},
		{Group: "storage.k8s.io", Version: "v1beta1", Kind: "VolumeAttachment"}:                                 {},
		{Group: "storage.k8s.io", Version: "v1beta1", Kind: "VolumeAttachmentList"}:                             {},
		{Group: "authentication.k8s.io", Version: "v1", Kind: "TokenRequest"}:                                   {},
	}

	f := fuzz.New().NilChance(.5).NumElements(1, 1).RandSource(rand.NewSource(1))
	f.Funcs(
		func(s *runtime.RawExtension, c fuzz.Continue) {},
		func(s *metav1.LabelSelector, c fuzz.Continue) {
			c.FuzzNoCustom(s)
			s.MatchExpressions = nil // need to fuzz this specially
		},
		func(s *metav1.ListOptions, c fuzz.Continue) {
			c.FuzzNoCustom(s)
			s.LabelSelector = "" // need to fuzz requirement strings specially
			s.FieldSelector = "" // need to fuzz requirement strings specially
		},
		func(s *extensionsv1beta1.ScaleStatus, c fuzz.Continue) {
			c.FuzzNoCustom(s)
			s.TargetSelector = "" // need to fuzz requirement strings specially
		},
	)

	scheme := legacyscheme.Scheme
	var testTypes orderedGroupVersionKinds
	for gvk := range scheme.AllKnownTypes() {
		if gvk.Version == runtime.APIVersionInternal {
			continue
		}
		testTypes = append(testTypes, gvk)
	}
	sort.Sort(testTypes)

	for _, gvk := range testTypes {
		_, expectedChanged := typesWithDefaulting[gvk]
		iter := 0
		changedOnce := false
		for {
			if iter > *roundtrip.FuzzIters {
				if !expectedChanged || changedOnce {
					break
				}
				if iter > 300 {
					t.Errorf("expected %s to trigger defaulting due to fuzzing", gvk)
					break
				}
				// if we expected defaulting, continue looping until the fuzzer gives us one
				// at worst, we will timeout
			}
			iter++

			src, err := scheme.New(gvk)
			if err != nil {
				t.Fatal(err)
			}
			f.Fuzz(src)

			src.GetObjectKind().SetGroupVersionKind(schema.GroupVersionKind{})

			original := src.DeepCopyObject()

			// get internal
			withDefaults := src.DeepCopyObject()
			scheme.Default(withDefaults.(runtime.Object))

			if !reflect.DeepEqual(original, withDefaults) {
				changedOnce = true
				if !expectedChanged {
					t.Errorf("{Group: \"%s\", Version: \"%s\", Kind: \"%s\"} did not expect defaults to be set - update expected or check defaulter registering: %s", gvk.Group, gvk.Version, gvk.Kind, diff.ObjectReflectDiff(original, withDefaults))
				}
			}
		}
	}
}

func BenchmarkPodDefaulting(b *testing.B) {
	f := fuzz.New().NilChance(.5).NumElements(1, 1).RandSource(rand.NewSource(1))
	items := make([]apiv1.Pod, 100)
	for i := range items {
		f.Fuzz(&items[i])
	}

	scheme := legacyscheme.Scheme
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		pod := &items[i%len(items)]

		scheme.Default(pod)
	}
	b.StopTimer()
}
