/*
Copyright 2018 The Kubernetes Authors.
Copyright 2020 Authors of Arktos - file modified.

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 unstructured_test

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

	"github.com/stretchr/testify/assert"
	"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
	"k8s.io/apimachinery/pkg/api/equality"
	metafuzzer "k8s.io/apimachinery/pkg/apis/meta/fuzzer"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/serializer"
	"k8s.io/apimachinery/pkg/util/diff"
)

func TestNilUnstructuredContent(t *testing.T) {
	var u unstructured.Unstructured
	uCopy := u.DeepCopy()
	content := u.UnstructuredContent()
	expContent := make(map[string]interface{})
	assert.EqualValues(t, expContent, content)
	assert.Equal(t, uCopy, &u)
}

// TestUnstructuredMetadataRoundTrip checks that metadata accessors
// correctly set the metadata for unstructured objects.
// First, it fuzzes an empty ObjectMeta and sets this value as the metadata for an unstructured object.
// Next, it uses metadata accessor methods to set these fuzzed values to another unstructured object.
// Finally, it checks that both the unstructured objects are equal.
func TestUnstructuredMetadataRoundTrip(t *testing.T) {
	scheme := runtime.NewScheme()
	codecs := serializer.NewCodecFactory(scheme)
	seed := rand.Int63()
	fuzzer := fuzzer.FuzzerFor(metafuzzer.Funcs, rand.NewSource(seed), codecs)

	N := 1000
	for i := 0; i < N; i++ {
		u := &unstructured.Unstructured{Object: map[string]interface{}{}}
		uCopy := u.DeepCopy()
		metadata := &metav1.ObjectMeta{}
		fuzzer.Fuzz(metadata)

		if err := setObjectMeta(u, metadata); err != nil {
			t.Fatalf("unexpected error setting fuzzed ObjectMeta: %v", err)
		}
		setObjectMetaUsingAccessors(u, uCopy)

		// TODO: remove this special casing when creationTimestamp becomes a pointer.
		// Right now, creationTimestamp is a struct (metav1.Time) so omitempty holds no meaning for it.
		// However, the current behaviour is to remove the field if it holds an empty struct.
		// This special casing exists here because custom marshallers for metav1.Time marshal
		// an empty value to "null", which gets converted to nil when converting to an unstructured map by "ToUnstructured".
		if err := unstructured.SetNestedField(uCopy.UnstructuredContent(), nil, "metadata", "creationTimestamp"); err != nil {
			t.Fatalf("unexpected error setting creationTimestamp as nil: %v", err)
		}

		if !equality.Semantic.DeepEqual(u, uCopy) {
			t.Errorf("diff: %v", diff.ObjectReflectDiff(u, uCopy))
		}
	}
}

// TestUnstructuredMetadataOmitempty checks that ObjectMeta omitempty
// semantics are enforced for unstructured objects.
// The fuzzing test above should catch these cases but this is here just to be safe.
// Example: the metadata.clusterName field has the omitempty json tag
// so if it is set to it's zero value (""), it should be removed from the metadata map.
func TestUnstructuredMetadataOmitempty(t *testing.T) {
	scheme := runtime.NewScheme()
	codecs := serializer.NewCodecFactory(scheme)
	seed := rand.Int63()
	fuzzer := fuzzer.FuzzerFor(metafuzzer.Funcs, rand.NewSource(seed), codecs)

	// fuzz to make sure we don't miss any function calls below
	u := &unstructured.Unstructured{Object: map[string]interface{}{}}
	metadata := &metav1.ObjectMeta{}
	fuzzer.Fuzz(metadata)
	if err := setObjectMeta(u, metadata); err != nil {
		t.Fatalf("unexpected error setting fuzzed ObjectMeta: %v", err)
	}

	// set zero values for all fields in metadata explicitly
	// to check that omitempty fields having zero values are never set
	u.SetName("")
	u.SetGenerateName("")
	u.SetTenant("")
	u.SetNamespace("")
	u.SetSelfLink("")
	u.SetUID("")
	u.SetHashKey(-1)
	u.SetResourceVersion("")
	u.SetGeneration(0)
	u.SetCreationTimestamp(metav1.Time{})
	u.SetDeletionTimestamp(nil)
	u.SetDeletionGracePeriodSeconds(nil)
	u.SetLabels(nil)
	u.SetAnnotations(nil)
	u.SetOwnerReferences(nil)
	u.SetInitializers(nil)
	u.SetFinalizers(nil)
	u.SetClusterName("")
	u.SetManagedFields(nil)

	gotMetadata, _, err := unstructured.NestedFieldNoCopy(u.UnstructuredContent(), "metadata")
	if err != nil {
		t.Error(err)
	}
	emptyMetadata := make(map[string]interface{})

	if !reflect.DeepEqual(gotMetadata, emptyMetadata) {
		t.Errorf("expected %v, got %v", emptyMetadata, gotMetadata)
	}
}

func setObjectMeta(u *unstructured.Unstructured, objectMeta *metav1.ObjectMeta) error {
	if objectMeta == nil {
		unstructured.RemoveNestedField(u.UnstructuredContent(), "metadata")
		return nil
	}
	metadata, err := runtime.DefaultUnstructuredConverter.ToUnstructured(objectMeta)
	if err != nil {
		return err
	}
	u.UnstructuredContent()["metadata"] = metadata
	return nil
}

func setObjectMetaUsingAccessors(u, uCopy *unstructured.Unstructured) {
	uCopy.SetName(u.GetName())
	uCopy.SetGenerateName(u.GetGenerateName())
	uCopy.SetTenant(u.GetTenant())
	uCopy.SetNamespace(u.GetNamespace())
	uCopy.SetSelfLink(u.GetSelfLink())
	uCopy.SetUID(u.GetUID())
	uCopy.SetHashKey(u.GetHashKey())
	uCopy.SetResourceVersion(u.GetResourceVersion())
	uCopy.SetGeneration(u.GetGeneration())
	uCopy.SetCreationTimestamp(u.GetCreationTimestamp())
	uCopy.SetDeletionTimestamp(u.GetDeletionTimestamp())
	uCopy.SetDeletionGracePeriodSeconds(u.GetDeletionGracePeriodSeconds())
	uCopy.SetLabels(u.GetLabels())
	uCopy.SetAnnotations(u.GetAnnotations())
	uCopy.SetOwnerReferences(u.GetOwnerReferences())
	uCopy.SetInitializers(u.GetInitializers())
	uCopy.SetFinalizers(u.GetFinalizers())
	uCopy.SetClusterName(u.GetClusterName())
	uCopy.SetManagedFields(u.GetManagedFields())
}
