/*
Copyright 2015 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 versioning

import (
	"fmt"
	"io/ioutil"
	"testing"

	"github.com/stretchr/testify/assert"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
)

func buildUnstructuredDecodable(gvk schema.GroupVersionKind) runtime.Object {
	obj := &unstructured.Unstructured{}
	obj.SetGroupVersionKind(gvk)
	return obj
}

func buildUnstructuredListDecodable(gvk schema.GroupVersionKind) runtime.Object {
	obj := &unstructured.UnstructuredList{}
	obj.SetGroupVersionKind(gvk)
	return obj
}

func TestEncodeUnstructured(t *testing.T) {
	v1GVK := schema.GroupVersionKind{
		Group:   "crispy",
		Version: "v1",
		Kind:    "Noxu",
	}
	v2GVK := schema.GroupVersionKind{
		Group:   "crispy",
		Version: "v2",
		Kind:    "Noxu",
	}
	elseGVK := schema.GroupVersionKind{
		Group:   "crispy2",
		Version: "else",
		Kind:    "Noxu",
	}
	elseUnstructuredDecodable := buildUnstructuredDecodable(elseGVK)
	elseUnstructuredDecodableList := buildUnstructuredListDecodable(elseGVK)
	v1UnstructuredDecodable := buildUnstructuredDecodable(v1GVK)
	v1UnstructuredDecodableList := buildUnstructuredListDecodable(v1GVK)
	v2UnstructuredDecodable := buildUnstructuredDecodable(v2GVK)

	testCases := []struct {
		name          string
		convertor     runtime.ObjectConvertor
		targetVersion runtime.GroupVersioner
		outObj        runtime.Object
		typer         runtime.ObjectTyper

		errFunc     func(error) bool
		expectedObj runtime.Object
	}{
		{
			name: "encode v1 unstructured with v2 encode version",
			typer: &mockTyper{
				gvks: []schema.GroupVersionKind{v1GVK},
			},
			outObj:        v1UnstructuredDecodable,
			targetVersion: v2GVK.GroupVersion(),
			convertor: &checkConvertor{
				obj:          v2UnstructuredDecodable,
				groupVersion: v2GVK.GroupVersion(),
			},
			expectedObj: v2UnstructuredDecodable,
		},
		{
			name: "both typer and conversion are bypassed when unstructured gvk matches encode gvk",
			typer: &mockTyper{
				err: fmt.Errorf("unexpected typer call"),
			},
			outObj:        v1UnstructuredDecodable,
			targetVersion: v1GVK.GroupVersion(),
			convertor: &checkConvertor{
				err: fmt.Errorf("unexpected conversion happened"),
			},
			expectedObj: v1UnstructuredDecodable,
		},
		{
			name:          "encode will fail when unstructured object's gvk and encode gvk mismatches",
			outObj:        elseUnstructuredDecodable,
			targetVersion: v1GVK.GroupVersion(),
			errFunc: func(err error) bool {
				return assert.Equal(t, runtime.NewNotRegisteredGVKErrForTarget("noxu-scheme", elseGVK, v1GVK.GroupVersion()), err)
			},
		},
		{
			name:          "encode with unstructured list's gvk regardless of its elements' gvk",
			outObj:        elseUnstructuredDecodableList,
			targetVersion: elseGVK.GroupVersion(),
		},
		{
			name:          "typer fail to recognize unstructured object gvk will fail the encoding",
			outObj:        elseUnstructuredDecodable,
			targetVersion: v1GVK.GroupVersion(),
			typer: &mockTyper{
				err: fmt.Errorf("invalid obj gvk"),
			},
		},
		{
			name:          "encoding unstructured object without encode version will fallback to typer suggested version",
			targetVersion: v1GVK.GroupVersion(),
			convertor: &checkConvertor{
				obj:          v1UnstructuredDecodableList,
				groupVersion: v1GVK.GroupVersion(),
			},
			outObj: elseUnstructuredDecodable,
			typer: &mockTyper{
				gvks: []schema.GroupVersionKind{v1GVK},
			},
		},
	}
	for _, testCase := range testCases {
		serializer := &mockSerializer{}
		codec := NewCodec(serializer, serializer, testCase.convertor, nil, testCase.typer, nil, testCase.targetVersion, nil, "noxu-scheme")
		err := codec.Encode(testCase.outObj, ioutil.Discard)
		if testCase.errFunc != nil {
			if !testCase.errFunc(err) {
				t.Errorf("%v: failed: %v", testCase.name, err)
			}
			return
		}
		assert.NoError(t, err)
		assert.Equal(t, testCase.expectedObj, serializer.obj)
	}
}

type errNotRecognizedGVK struct {
	failedGVK    schema.GroupVersionKind
	claimingGVKs []schema.GroupVersionKind
}

func (e errNotRecognizedGVK) Error() string {
	return fmt.Sprintf("unrecognized gvk %v, should be one of %v", e.failedGVK, e.claimingGVKs)
}

type mockUnstructuredNopConvertor struct {
	claimingGVKs []schema.GroupVersionKind
}

func (c *mockUnstructuredNopConvertor) recognizeGVK(gvkToCheck schema.GroupVersionKind) error {
	matched := false
	for _, gvk := range c.claimingGVKs {
		if gvk == gvkToCheck {
			matched = true
		}
	}
	if !matched {
		return errNotRecognizedGVK{
			failedGVK:    gvkToCheck,
			claimingGVKs: c.claimingGVKs,
		}
	}
	return nil
}

func (c *mockUnstructuredNopConvertor) Convert(in, out, context interface{}) error {
	inObj := in.(*unstructured.Unstructured)
	outObj := out.(*unstructured.Unstructured)
	if err := c.recognizeGVK(outObj.GroupVersionKind()); err != nil {
		return err
	}
	outGVK := outObj.GetObjectKind().GroupVersionKind()
	*outObj = *inObj.DeepCopy()
	outObj.GetObjectKind().SetGroupVersionKind(outGVK)
	return nil
}

func (c *mockUnstructuredNopConvertor) ConvertToVersion(in runtime.Object, outVersion runtime.GroupVersioner) (runtime.Object, error) {
	out := in.DeepCopyObject()
	targetGVK, matched := outVersion.KindForGroupVersionKinds([]schema.GroupVersionKind{in.GetObjectKind().GroupVersionKind()})
	if !matched {
		return nil, fmt.Errorf("attempt to convert to mismatched gv %v", outVersion)
	}
	if err := c.recognizeGVK(out.GetObjectKind().GroupVersionKind()); err != nil {
		return nil, err
	}
	out.GetObjectKind().SetGroupVersionKind(targetGVK)
	return out, nil
}

func (c *mockUnstructuredNopConvertor) ConvertFieldLabel(gvk schema.GroupVersionKind, label, value string) (string, string, error) {
	return "", "", fmt.Errorf("unexpected call to ConvertFieldLabel")
}

func TestDecodeUnstructured(t *testing.T) {
	internalGVK := schema.GroupVersionKind{
		Group:   "crispy",
		Version: runtime.APIVersionInternal,
		Kind:    "Noxu",
	}
	v1GVK := schema.GroupVersionKind{
		Group:   "crispy",
		Version: "v1",
		Kind:    "Noxu",
	}
	v2GVK := schema.GroupVersionKind{
		Group:   "crispy",
		Version: "v2",
		Kind:    "Noxu",
	}
	internalUnstructuredDecodable := buildUnstructuredDecodable(internalGVK)
	v1UnstructuredDecodable := buildUnstructuredDecodable(v1GVK)
	v2UnstructuredDecodable := buildUnstructuredDecodable(v2GVK)

	testCases := []struct {
		name                    string
		serializer              runtime.Serializer
		convertor               runtime.ObjectConvertor
		suggestedConvertVersion runtime.GroupVersioner
		defaultGVK              *schema.GroupVersionKind
		intoObj                 runtime.Object

		errFunc                     func(error) bool
		expectedGVKOfSerializedData *schema.GroupVersionKind
		expectedOut                 runtime.Object
	}{
		{
			name:       "decode v1 unstructured into non-nil v2 unstructured",
			serializer: &mockSerializer{actual: &v1GVK, obj: v1UnstructuredDecodable},
			convertor: &mockUnstructuredNopConvertor{
				claimingGVKs: []schema.GroupVersionKind{
					v1GVK, v2GVK,
				},
			},
			suggestedConvertVersion:     v2GVK.GroupVersion(),
			intoObj:                     v2UnstructuredDecodable,
			expectedGVKOfSerializedData: &v1GVK,
			expectedOut:                 v2UnstructuredDecodable,
		},
		{
			name:       "decode v1 unstructured into nil object with v2 version",
			serializer: &mockSerializer{actual: &v1GVK, obj: v1UnstructuredDecodable},
			convertor: &mockUnstructuredNopConvertor{
				claimingGVKs: []schema.GroupVersionKind{
					v1GVK, v2GVK,
				},
			},
			suggestedConvertVersion:     v2GVK.GroupVersion(),
			intoObj:                     nil,
			expectedGVKOfSerializedData: &v1GVK,
			expectedOut:                 v2UnstructuredDecodable,
		},
		{
			name:       "decode v1 unstructured into non-nil internal unstructured",
			serializer: &mockSerializer{actual: &v1GVK, obj: v1UnstructuredDecodable},
			convertor: &mockUnstructuredNopConvertor{
				claimingGVKs: []schema.GroupVersionKind{
					v1GVK, v2GVK,
				},
			},
			suggestedConvertVersion: internalGVK.GroupVersion(),
			intoObj:                 internalUnstructuredDecodable,
			errFunc: func(err error) bool {
				notRecognized, ok := err.(errNotRecognizedGVK)
				if !ok {
					return false
				}
				return assert.Equal(t, notRecognized.failedGVK, internalGVK)
			},
		},
		{
			name:       "decode v1 unstructured into nil object with internal version",
			serializer: &mockSerializer{actual: &v1GVK, obj: v1UnstructuredDecodable},
			convertor: &mockUnstructuredNopConvertor{
				claimingGVKs: []schema.GroupVersionKind{
					v1GVK, v2GVK,
				},
			},
			suggestedConvertVersion: internalGVK.GroupVersion(),
			intoObj:                 nil,
			errFunc: func(err error) bool {
				notRecognized, ok := err.(errNotRecognizedGVK)
				if !ok {
					return false
				}
				return assert.Equal(t, notRecognized.failedGVK, internalGVK)
			},
		},
		{
			name:       "skip conversion if serializer returns the same unstructured as into",
			serializer: &mockSerializer{actual: &v1GVK, obj: v1UnstructuredDecodable},
			convertor: &checkConvertor{
				err: fmt.Errorf("unexpected conversion happened"),
			},
			suggestedConvertVersion:     internalGVK.GroupVersion(),
			intoObj:                     v1UnstructuredDecodable,
			expectedGVKOfSerializedData: &v1GVK,
			expectedOut:                 v1UnstructuredDecodable,
		},
		{
			name:       "invalid convert version makes decoding unstructured fail",
			serializer: &mockSerializer{actual: &v1GVK, obj: v1UnstructuredDecodable},
			convertor: &checkConvertor{
				in:           v1UnstructuredDecodable,
				groupVersion: internalGVK.GroupVersion(),
				err:          fmt.Errorf("no matching decode version"),
			},
			suggestedConvertVersion: internalGVK.GroupVersion(),
			errFunc: func(err error) bool {
				return assert.Equal(t, err, fmt.Errorf("no matching decode version"))
			},
		},
	}
	for _, testCase := range testCases {
		codec := NewCodec(testCase.serializer, testCase.serializer, testCase.convertor, nil, nil, nil, nil, testCase.suggestedConvertVersion, "noxu-scheme")
		actualObj, actualSerializedGVK, err := codec.Decode([]byte(`{}`), testCase.defaultGVK, testCase.intoObj)
		if testCase.errFunc != nil {
			if !testCase.errFunc(err) {
				t.Errorf("%v: failed: %v", testCase.name, err)
			}
			return
		}
		assert.NoError(t, err)
		assert.Equal(t, testCase.expectedOut, actualObj, "%v failed", testCase.name)
		assert.Equal(t, testCase.expectedGVKOfSerializedData, actualSerializedGVK, "%v failed", testCase.name)
	}
}
