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

import (
	"reflect"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"

	v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
	"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
	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/util/diff"
	"k8s.io/apimachinery/pkg/util/validation"
)

func TestRestoreObjectMeta(t *testing.T) {
	tests := []struct {
		name          string
		original      map[string]interface{}
		converted     map[string]interface{}
		expected      map[string]interface{}
		expectedError bool
	}{
		{"no converted metadata",
			map[string]interface{}{"metadata": map[string]interface{}{}, "spec": map[string]interface{}{}},
			map[string]interface{}{"spec": map[string]interface{}{}},
			map[string]interface{}{"spec": map[string]interface{}{}},
			true,
		},
		{"invalid converted metadata",
			map[string]interface{}{"metadata": map[string]interface{}{}, "spec": map[string]interface{}{}},
			map[string]interface{}{"metadata": []interface{}{"foo"}},
			map[string]interface{}{"metadata": []interface{}{"foo"}},
			true,
		},
		{"no original metadata",
			map[string]interface{}{"spec": map[string]interface{}{}},
			map[string]interface{}{"metadata": map[string]interface{}{}, "spec": map[string]interface{}{}},
			map[string]interface{}{"metadata": map[string]interface{}{}, "spec": map[string]interface{}{}},
			false,
		},
		{"invalid original metadata",
			map[string]interface{}{"metadata": []interface{}{"foo"}},
			map[string]interface{}{"metadata": map[string]interface{}{}, "spec": map[string]interface{}{}},
			map[string]interface{}{"metadata": []interface{}{"foo"}, "spec": map[string]interface{}{}},
			true,
		},
		{"changed label, annotations and non-label",
			map[string]interface{}{"metadata": map[string]interface{}{
				"foo":         "bar",
				"labels":      map[string]interface{}{"a": "A", "b": "B"},
				"annotations": map[string]interface{}{"a": "1", "b": "2"},
			}, "spec": map[string]interface{}{}},
			map[string]interface{}{"metadata": map[string]interface{}{
				"foo":         "abc",
				"labels":      map[string]interface{}{"a": "AA", "b": "B"},
				"annotations": map[string]interface{}{"a": "1", "b": "22"},
			}, "spec": map[string]interface{}{}},
			map[string]interface{}{"metadata": map[string]interface{}{
				"foo":         "bar",
				"labels":      map[string]interface{}{"a": "AA", "b": "B"},
				"annotations": map[string]interface{}{"a": "1", "b": "22"},
			}, "spec": map[string]interface{}{}},
			false,
		},
		{"added labels and annotations",
			map[string]interface{}{"metadata": map[string]interface{}{
				"foo": "bar",
			}, "spec": map[string]interface{}{}},
			map[string]interface{}{"metadata": map[string]interface{}{
				"foo":         "abc",
				"labels":      map[string]interface{}{"a": "AA", "b": "B"},
				"annotations": map[string]interface{}{"a": "1", "b": "22"},
			}, "spec": map[string]interface{}{}},
			map[string]interface{}{"metadata": map[string]interface{}{
				"foo":         "bar",
				"labels":      map[string]interface{}{"a": "AA", "b": "B"},
				"annotations": map[string]interface{}{"a": "1", "b": "22"},
			}, "spec": map[string]interface{}{}},
			false,
		},
		{"added labels and annotations, with nil before",
			map[string]interface{}{"metadata": map[string]interface{}{
				"foo":         "bar",
				"labels":      nil,
				"annotations": nil,
			}, "spec": map[string]interface{}{}},
			map[string]interface{}{"metadata": map[string]interface{}{
				"foo":         "abc",
				"labels":      map[string]interface{}{"a": "AA", "b": "B"},
				"annotations": map[string]interface{}{"a": "1", "b": "22"},
			}, "spec": map[string]interface{}{}},
			map[string]interface{}{"metadata": map[string]interface{}{
				"foo":         "bar",
				"labels":      map[string]interface{}{"a": "AA", "b": "B"},
				"annotations": map[string]interface{}{"a": "1", "b": "22"},
			}, "spec": map[string]interface{}{}},
			false,
		},
		{"removed labels and annotations",
			map[string]interface{}{"metadata": map[string]interface{}{
				"foo":         "bar",
				"labels":      map[string]interface{}{"a": "AA", "b": "B"},
				"annotations": map[string]interface{}{"a": "1", "b": "22"},
			}, "spec": map[string]interface{}{}},
			map[string]interface{}{"metadata": map[string]interface{}{
				"foo": "abc",
			}, "spec": map[string]interface{}{}},
			map[string]interface{}{"metadata": map[string]interface{}{
				"foo": "bar",
			}, "spec": map[string]interface{}{}},
			false,
		},
		{"nil'ed labels and annotations",
			map[string]interface{}{"metadata": map[string]interface{}{
				"foo":         "bar",
				"labels":      map[string]interface{}{"a": "AA", "b": "B"},
				"annotations": map[string]interface{}{"a": "1", "b": "22"},
			}, "spec": map[string]interface{}{}},
			map[string]interface{}{"metadata": map[string]interface{}{
				"foo":         "abc",
				"labels":      nil,
				"annotations": nil,
			}, "spec": map[string]interface{}{}},
			map[string]interface{}{"metadata": map[string]interface{}{
				"foo": "bar",
			}, "spec": map[string]interface{}{}},
			false,
		},
		{"added labels and annotations",
			map[string]interface{}{"metadata": map[string]interface{}{
				"foo": "bar",
			}, "spec": map[string]interface{}{}},
			map[string]interface{}{"metadata": map[string]interface{}{
				"foo":         "abc",
				"labels":      map[string]interface{}{"a": nil, "b": "B"},
				"annotations": map[string]interface{}{"a": nil, "b": "22"},
			}, "spec": map[string]interface{}{}},
			map[string]interface{}{"metadata": map[string]interface{}{
				"foo": "bar",
			}, "spec": map[string]interface{}{}},
			true,
		},
		{"invalid label key",
			map[string]interface{}{"metadata": map[string]interface{}{}},
			map[string]interface{}{"metadata": map[string]interface{}{"labels": map[string]interface{}{"some/non-qualified/label": "x"}}},
			map[string]interface{}{"metadata": map[string]interface{}{}},
			true,
		},
		{"invalid annotation key",
			map[string]interface{}{"metadata": map[string]interface{}{}},
			map[string]interface{}{"metadata": map[string]interface{}{"labels": map[string]interface{}{"some/non-qualified/label": "x"}}},
			map[string]interface{}{"metadata": map[string]interface{}{}},
			true,
		},
		{"invalid label value",
			map[string]interface{}{"metadata": map[string]interface{}{}},
			map[string]interface{}{"metadata": map[string]interface{}{"labels": map[string]interface{}{"foo": "üäö"}}},
			map[string]interface{}{"metadata": map[string]interface{}{}},
			true,
		},
		{"too big label value",
			map[string]interface{}{"metadata": map[string]interface{}{}},
			map[string]interface{}{"metadata": map[string]interface{}{"labels": map[string]interface{}{"foo": strings.Repeat("x", validation.LabelValueMaxLength+1)}}},
			map[string]interface{}{"metadata": map[string]interface{}{}},
			true,
		},
		{"too big annotation value",
			map[string]interface{}{"metadata": map[string]interface{}{}},
			map[string]interface{}{"metadata": map[string]interface{}{"annotations": map[string]interface{}{"foo": strings.Repeat("x", 256*(1<<10)+1)}}},
			map[string]interface{}{"metadata": map[string]interface{}{}},
			true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if err := restoreObjectMeta(&unstructured.Unstructured{Object: tt.original}, &unstructured.Unstructured{Object: tt.converted}); err == nil && tt.expectedError {
				t.Fatalf("expected error, but didn't get one")
			} else if err != nil && !tt.expectedError {
				t.Fatalf("unexpected error: %v", err)
			}

			if !reflect.DeepEqual(tt.converted, tt.expected) {
				t.Errorf("unexpected result: %s", diff.ObjectDiff(tt.expected, tt.converted))
			}
		})
	}
}

func TestGetObjectsToConvert(t *testing.T) {
	v1Object := &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "foo/v1", "kind": "Widget", "metadata": map[string]interface{}{"name": "myv1"}}}
	v2Object := &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "foo/v2", "kind": "Widget", "metadata": map[string]interface{}{"name": "myv2"}}}
	v3Object := &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "foo/v3", "kind": "Widget", "metadata": map[string]interface{}{"name": "myv3"}}}

	testcases := []struct {
		Name       string
		Object     runtime.Object
		APIVersion string

		ExpectObjects []runtime.RawExtension
	}{
		{
			Name:          "empty list",
			Object:        &unstructured.UnstructuredList{},
			APIVersion:    "foo/v1",
			ExpectObjects: nil,
		},
		{
			Name: "one-item list, in desired version",
			Object: &unstructured.UnstructuredList{
				Items: []unstructured.Unstructured{*v1Object},
			},
			APIVersion:    "foo/v1",
			ExpectObjects: nil,
		},
		{
			Name: "one-item list, not in desired version",
			Object: &unstructured.UnstructuredList{
				Items: []unstructured.Unstructured{*v2Object},
			},
			APIVersion:    "foo/v1",
			ExpectObjects: []runtime.RawExtension{{Object: v2Object}},
		},
		{
			Name: "multi-item list, in desired version",
			Object: &unstructured.UnstructuredList{
				Items: []unstructured.Unstructured{*v1Object, *v1Object, *v1Object},
			},
			APIVersion:    "foo/v1",
			ExpectObjects: nil,
		},
		{
			Name: "multi-item list, mixed versions",
			Object: &unstructured.UnstructuredList{
				Items: []unstructured.Unstructured{*v1Object, *v2Object, *v3Object},
			},
			APIVersion:    "foo/v1",
			ExpectObjects: []runtime.RawExtension{{Object: v2Object}, {Object: v3Object}},
		},
		{
			Name:          "single item, in desired version",
			Object:        v1Object,
			APIVersion:    "foo/v1",
			ExpectObjects: nil,
		},
		{
			Name:          "single item, not in desired version",
			Object:        v2Object,
			APIVersion:    "foo/v1",
			ExpectObjects: []runtime.RawExtension{{Object: v2Object}},
		},
	}
	for _, tc := range testcases {
		t.Run(tc.Name, func(t *testing.T) {
			if objects := getObjectsToConvert(tc.Object, tc.APIVersion); !reflect.DeepEqual(objects, tc.ExpectObjects) {
				t.Errorf("unexpected diff: %s", cmp.Diff(tc.ExpectObjects, objects))
			}
		})
	}
}

func TestCreateConversionReviewObjects(t *testing.T) {
	objects := []runtime.RawExtension{
		{Object: &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "foo/v2", "Kind": "Widget"}}},
	}

	testcases := []struct {
		Name     string
		Versions []string

		ExpectRequest  runtime.Object
		ExpectResponse runtime.Object
		ExpectErr      string
	}{
		{
			Name:      "no supported versions",
			Versions:  []string{"vx"},
			ExpectErr: "no supported conversion review versions",
		},
		{
			Name:     "v1",
			Versions: []string{"v1", "v1beta1", "v2"},
			ExpectRequest: &v1.ConversionReview{
				Request:  &v1.ConversionRequest{UID: "uid", DesiredAPIVersion: "foo/v1", Objects: objects},
				Response: &v1.ConversionResponse{},
			},
			ExpectResponse: &v1.ConversionReview{},
		},
		{
			Name:     "v1beta1",
			Versions: []string{"v1beta1", "v1", "v2"},
			ExpectRequest: &v1beta1.ConversionReview{
				Request:  &v1beta1.ConversionRequest{UID: "uid", DesiredAPIVersion: "foo/v1", Objects: objects},
				Response: &v1beta1.ConversionResponse{},
			},
			ExpectResponse: &v1beta1.ConversionReview{},
		},
	}

	for _, tc := range testcases {
		t.Run(tc.Name, func(t *testing.T) {
			request, response, err := createConversionReviewObjects(tc.Versions, objects, "foo/v1", "uid")

			if err == nil && len(tc.ExpectErr) > 0 {
				t.Errorf("expected error, got none")
			} else if err != nil && len(tc.ExpectErr) == 0 {
				t.Errorf("unexpected error %v", err)
			} else if err != nil && !strings.Contains(err.Error(), tc.ExpectErr) {
				t.Errorf("expected error containing %q, got %v", tc.ExpectErr, err)
			}

			if e, a := tc.ExpectRequest, request; !reflect.DeepEqual(e, a) {
				t.Errorf("unexpected diff: %s", cmp.Diff(e, a))
			}
			if e, a := tc.ExpectResponse, response; !reflect.DeepEqual(e, a) {
				t.Errorf("unexpected diff: %s", cmp.Diff(e, a))
			}
		})
	}
}

func TestGetConvertedObjectsFromResponse(t *testing.T) {
	v1Object := &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "foo/v1", "kind": "Widget", "metadata": map[string]interface{}{"name": "myv1"}}}

	testcases := []struct {
		Name     string
		Response runtime.Object

		ExpectObjects []runtime.RawExtension
		ExpectErr     string
	}{
		{
			Name:      "nil response",
			Response:  nil,
			ExpectErr: "unrecognized response type",
		},
		{
			Name:      "unknown type",
			Response:  &unstructured.Unstructured{},
			ExpectErr: "unrecognized response type",
		},

		{
			Name: "minimal valid v1beta1",
			Response: &v1beta1.ConversionReview{
				// apiVersion/kind were not validated originally, preserve backward compatibility
				Response: &v1beta1.ConversionResponse{
					// uid was not validated originally, preserve backward compatibility
					Result: metav1.Status{Status: metav1.StatusSuccess},
				},
			},
			ExpectObjects: nil,
		},
		{
			Name: "valid v1beta1 with objects",
			Response: &v1beta1.ConversionReview{
				// apiVersion/kind were not validated originally, preserve backward compatibility
				Response: &v1beta1.ConversionResponse{
					// uid was not validated originally, preserve backward compatibility
					Result:           metav1.Status{Status: metav1.StatusSuccess},
					ConvertedObjects: []runtime.RawExtension{{Object: v1Object}},
				},
			},
			ExpectObjects: []runtime.RawExtension{{Object: v1Object}},
		},
		{
			Name: "error v1beta1, empty status",
			Response: &v1beta1.ConversionReview{
				Response: &v1beta1.ConversionResponse{
					Result: metav1.Status{Status: ""},
				},
			},
			ExpectErr: `response.result.status was '', not 'Success'`,
		},
		{
			Name: "error v1beta1, failure status",
			Response: &v1beta1.ConversionReview{
				Response: &v1beta1.ConversionResponse{
					Result: metav1.Status{Status: metav1.StatusFailure},
				},
			},
			ExpectErr: `response.result.status was 'Failure', not 'Success'`,
		},
		{
			Name: "error v1beta1, custom status",
			Response: &v1beta1.ConversionReview{
				Response: &v1beta1.ConversionResponse{
					Result: metav1.Status{Status: metav1.StatusFailure, Message: "some failure message"},
				},
			},
			ExpectErr: `some failure message`,
		},
		{
			Name:      "invalid v1beta1, no response",
			Response:  &v1beta1.ConversionReview{},
			ExpectErr: "no response provided",
		},

		{
			Name: "minimal valid v1",
			Response: &v1.ConversionReview{
				TypeMeta: metav1.TypeMeta{APIVersion: "apiextensions.k8s.io/v1", Kind: "ConversionReview"},
				Response: &v1.ConversionResponse{
					UID:    "uid",
					Result: metav1.Status{Status: metav1.StatusSuccess},
				},
			},
			ExpectObjects: nil,
		},
		{
			Name: "valid v1 with objects",
			Response: &v1.ConversionReview{
				TypeMeta: metav1.TypeMeta{APIVersion: "apiextensions.k8s.io/v1", Kind: "ConversionReview"},
				Response: &v1.ConversionResponse{
					UID:              "uid",
					Result:           metav1.Status{Status: metav1.StatusSuccess},
					ConvertedObjects: []runtime.RawExtension{{Object: v1Object}},
				},
			},
			ExpectObjects: []runtime.RawExtension{{Object: v1Object}},
		},
		{
			Name: "invalid v1, no uid",
			Response: &v1.ConversionReview{
				TypeMeta: metav1.TypeMeta{APIVersion: "apiextensions.k8s.io/v1", Kind: "ConversionReview"},
				Response: &v1.ConversionResponse{
					Result: metav1.Status{Status: metav1.StatusSuccess},
				},
			},
			ExpectErr: `expected response.uid="uid"`,
		},
		{
			Name: "invalid v1, no apiVersion",
			Response: &v1.ConversionReview{
				TypeMeta: metav1.TypeMeta{Kind: "ConversionReview"},
				Response: &v1.ConversionResponse{
					UID:    "uid",
					Result: metav1.Status{Status: metav1.StatusSuccess},
				},
			},
			ExpectErr: `expected webhook response of apiextensions.k8s.io/v1, Kind=ConversionReview`,
		},
		{
			Name: "invalid v1, no kind",
			Response: &v1.ConversionReview{
				TypeMeta: metav1.TypeMeta{APIVersion: "apiextensions.k8s.io/v1"},
				Response: &v1.ConversionResponse{
					UID:    "uid",
					Result: metav1.Status{Status: metav1.StatusSuccess},
				},
			},
			ExpectErr: `expected webhook response of apiextensions.k8s.io/v1, Kind=ConversionReview`,
		},
		{
			Name: "invalid v1, mismatched apiVersion",
			Response: &v1.ConversionReview{
				TypeMeta: metav1.TypeMeta{APIVersion: "apiextensions.k8s.io/v2", Kind: "ConversionReview"},
				Response: &v1.ConversionResponse{
					UID:    "uid",
					Result: metav1.Status{Status: metav1.StatusSuccess},
				},
			},
			ExpectErr: `expected webhook response of apiextensions.k8s.io/v1, Kind=ConversionReview`,
		},
		{
			Name: "invalid v1, mismatched kind",
			Response: &v1.ConversionReview{
				TypeMeta: metav1.TypeMeta{APIVersion: "apiextensions.k8s.io/v1", Kind: "ConversionReview2"},
				Response: &v1.ConversionResponse{
					UID:    "uid",
					Result: metav1.Status{Status: metav1.StatusSuccess},
				},
			},
			ExpectErr: `expected webhook response of apiextensions.k8s.io/v1, Kind=ConversionReview`,
		},
		{
			Name: "error v1, empty status",
			Response: &v1.ConversionReview{
				TypeMeta: metav1.TypeMeta{APIVersion: "apiextensions.k8s.io/v1", Kind: "ConversionReview"},
				Response: &v1.ConversionResponse{
					UID:    "uid",
					Result: metav1.Status{Status: ""},
				},
			},
			ExpectErr: `response.result.status was '', not 'Success'`,
		},
		{
			Name: "error v1, failure status",
			Response: &v1.ConversionReview{
				TypeMeta: metav1.TypeMeta{APIVersion: "apiextensions.k8s.io/v1", Kind: "ConversionReview"},
				Response: &v1.ConversionResponse{
					UID:    "uid",
					Result: metav1.Status{Status: metav1.StatusFailure},
				},
			},
			ExpectErr: `response.result.status was 'Failure', not 'Success'`,
		},
		{
			Name: "error v1, custom status",
			Response: &v1.ConversionReview{
				TypeMeta: metav1.TypeMeta{APIVersion: "apiextensions.k8s.io/v1", Kind: "ConversionReview"},
				Response: &v1.ConversionResponse{
					UID:    "uid",
					Result: metav1.Status{Status: metav1.StatusFailure, Message: "some failure message"},
				},
			},
			ExpectErr: `some failure message`,
		},
		{
			Name: "invalid v1, no response",
			Response: &v1.ConversionReview{
				TypeMeta: metav1.TypeMeta{APIVersion: "apiextensions.k8s.io/v1", Kind: "ConversionReview"},
			},
			ExpectErr: "no response provided",
		},
	}

	for _, tc := range testcases {
		t.Run(tc.Name, func(t *testing.T) {

			objects, err := getConvertedObjectsFromResponse("uid", tc.Response)

			if err == nil && len(tc.ExpectErr) > 0 {
				t.Errorf("expected error, got none")
			} else if err != nil && len(tc.ExpectErr) == 0 {
				t.Errorf("unexpected error %v", err)
			} else if err != nil && !strings.Contains(err.Error(), tc.ExpectErr) {
				t.Errorf("expected error containing %q, got %v", tc.ExpectErr, err)
			}

			if !reflect.DeepEqual(objects, tc.ExpectObjects) {
				t.Errorf("unexpected diff: %s", cmp.Diff(tc.ExpectObjects, objects))
			}

		})
	}
}
