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

	"github.com/google/go-cmp/cmp"
	fuzz "github.com/google/gofuzz"
	"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
	apiequality "k8s.io/apimachinery/pkg/api/equality"
	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/util/json"
	"k8s.io/client-go/applyconfigurations"
	v1mf "k8s.io/client-go/applyconfigurations/core/v1"

	"k8s.io/kubernetes/pkg/api/legacyscheme"
	api "k8s.io/kubernetes/pkg/apis/core"
)

// TestUnstructuredRoundTripApplyConfigurations converts each known object type through unstructured
// to the apply configuration for that object type, then converts it back to the object type and
// verifies it is unchanged.
func TestUnstructuredRoundTripApplyConfigurations(t *testing.T) {
	for gvk := range legacyscheme.Scheme.AllKnownTypes() {
		if nonRoundTrippableTypes.Has(gvk.Kind) {
			continue
		}
		if gvk.Version == runtime.APIVersionInternal {
			continue
		}
		if builder := applyconfigurations.ForKind(gvk); builder == nil {
			continue
		}

		t.Run(gvk.String(), func(t *testing.T) {
			for i := 0; i < 3; i++ {
				item := fuzzObject(t, gvk)
				builder := applyconfigurations.ForKind(gvk)
				unstructuredRoundTripApplyConfiguration(t, item, builder)
				if t.Failed() {
					break
				}
			}
		})
	}
}

// TestJsonRoundTripApplyConfigurations converts each known object type through JSON to the apply
// configuration for that object type, then converts it back to the object type and verifies it
// is unchanged.
func TestJsonRoundTripApplyConfigurations(t *testing.T) {
	for gvk := range legacyscheme.Scheme.AllKnownTypes() {
		if nonRoundTrippableTypes.Has(gvk.Kind) {
			continue
		}
		if gvk.Version == runtime.APIVersionInternal {
			continue
		}
		if builder := applyconfigurations.ForKind(gvk); builder == nil {
			continue
		}

		t.Run(gvk.String(), func(t *testing.T) {
			for i := 0; i < 3; i++ {
				item := fuzzObject(t, gvk)
				builder := applyconfigurations.ForKind(gvk)
				jsonRoundTripApplyConfiguration(t, item, builder)
				if t.Failed() {
					break
				}

			}
		})
	}
}

func unstructuredRoundTripApplyConfiguration(t *testing.T, item runtime.Object, applyConfig interface{}) {
	u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(item)
	if err != nil {
		t.Errorf("ToUnstructured failed: %v", err)
		return
	}
	err = runtime.DefaultUnstructuredConverter.FromUnstructured(u, applyConfig)
	if err != nil {
		t.Errorf("FromUnstructured failed: %v", err)
		return
	}
	rtObj := reflect.New(reflect.TypeOf(item).Elem()).Interface().(runtime.Object)
	u, err = runtime.DefaultUnstructuredConverter.ToUnstructured(applyConfig)
	if err != nil {
		t.Errorf("ToUnstructured failed: %v", err)
		return
	}
	err = runtime.DefaultUnstructuredConverter.FromUnstructured(u, rtObj)
	if err != nil {
		t.Errorf("FromUnstructured failed: %v", err)
		return
	}
	if !apiequality.Semantic.DeepEqual(item, rtObj) {
		t.Errorf("Object changed, diff: %v", cmp.Diff(item, rtObj))
	}
}

func jsonRoundTripApplyConfiguration(t *testing.T, item runtime.Object, applyConfig interface{}) {

	objData, err := json.Marshal(item)
	if err != nil {
		t.Errorf("json.Marshal failed: %v", err)
		return
	}
	err = json.Unmarshal(objData, applyConfig)
	if err != nil {
		t.Errorf("applyConfig.UnmarshalJSON failed: %v", err)
		return
	}
	rtObj := reflect.New(reflect.TypeOf(item).Elem()).Interface().(runtime.Object)
	applyData, err := json.Marshal(applyConfig)
	if err != nil {
		t.Errorf("applyConfig.MarshalJSON failed: %v", err)
		return
	}
	err = json.Unmarshal(applyData, rtObj)
	if err != nil {
		t.Errorf("json.Unmarshal failed: %v", err)
		return
	}
	if !apiequality.Semantic.DeepEqual(item, rtObj) {
		t.Errorf("Object changed, diff: %v", cmp.Diff(item, rtObj))
	}
}

func fuzzObject(t *testing.T, gvk schema.GroupVersionKind) runtime.Object {
	internalVersion := schema.GroupVersion{Group: gvk.Group, Version: runtime.APIVersionInternal}
	externalVersion := gvk.GroupVersion()
	kind := gvk.Kind

	// We do fuzzing on the internal version of the object, and only then
	// convert to the external version. This is because custom fuzzing
	// function are only supported for internal objects.
	internalObj, err := legacyscheme.Scheme.New(internalVersion.WithKind(kind))
	if err != nil {
		t.Fatalf("Couldn't create internal object %v: %v", kind, err)
	}
	seed := rand.Int63()
	fuzzer.FuzzerFor(FuzzerFuncs, rand.NewSource(seed), legacyscheme.Codecs).
		Funcs(
			// Ensure that InitContainers and their statuses are not generated. This
			// is because in this test we are simply doing json operations, in which
			// those disappear.
			func(s *api.PodSpec, c fuzz.Continue) {
				c.FuzzNoCustom(s)
				s.InitContainers = nil
			},
			func(s *api.PodStatus, c fuzz.Continue) {
				c.FuzzNoCustom(s)
				s.InitContainerStatuses = nil
			},
			// Apply configuration types do not have managed fields, so we exclude
			// them in our fuzz test cases.
			func(s *v1.ObjectMeta, c fuzz.Continue) {
				c.FuzzNoCustom(s)
				s.ManagedFields = nil
				s.SelfLink = ""
			},
		).Fuzz(internalObj)

	item, err := legacyscheme.Scheme.New(externalVersion.WithKind(kind))
	if err != nil {
		t.Fatalf("Couldn't create external object %v: %v", kind, err)
	}
	if err := legacyscheme.Scheme.Convert(internalObj, item, nil); err != nil {
		t.Fatalf("Conversion for %v failed: %v", kind, err)
	}
	return item
}

func BenchmarkApplyConfigurationsFromUnstructured(b *testing.B) {
	items := benchmarkItems(b)
	convertor := runtime.DefaultUnstructuredConverter
	unstr := make([]map[string]interface{}, len(items))
	for i := range items {
		item, err := convertor.ToUnstructured(&items[i])
		if err != nil || item == nil {
			b.Fatalf("unexpected error: %v", err)
		}
		unstr = append(unstr, item)
	}
	size := len(items)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		builder := &v1mf.PodApplyConfiguration{}
		if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstr[i%size], builder); err != nil {
			b.Fatalf("unexpected error: %v", err)
		}
	}
	b.StopTimer()
}

func BenchmarkApplyConfigurationsToUnstructured(b *testing.B) {
	items := benchmarkItems(b)
	convertor := runtime.DefaultUnstructuredConverter
	builders := make([]*v1mf.PodApplyConfiguration, len(items))
	for i := range items {
		item, err := convertor.ToUnstructured(&items[i])
		if err != nil || item == nil {
			b.Fatalf("unexpected error: %v", err)
		}
		builder := &v1mf.PodApplyConfiguration{}
		if err := runtime.DefaultUnstructuredConverter.FromUnstructured(item, builder); err != nil {
			b.Fatalf("unexpected error: %v", err)
		}
		builders[i] = builder
	}
	b.ResetTimer()
	size := len(items)
	for i := 0; i < b.N; i++ {
		builder := builders[i%size]
		if _, err := runtime.DefaultUnstructuredConverter.ToUnstructured(builder); err != nil {
			b.Fatalf("unexpected error: %v", err)
		}
	}
	b.StopTimer()
}
