/*
Copyright 2017 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 initialization

import (
	"reflect"
	"strings"
	"testing"

	"k8s.io/api/admissionregistration/v1alpha1"
	"k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/meta"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apiserver/pkg/admission"
	"k8s.io/apiserver/pkg/authorization/authorizer"
)

func newInitializer(name string, rules ...v1alpha1.Rule) *v1alpha1.InitializerConfiguration {
	return addInitializer(&v1alpha1.InitializerConfiguration{}, name, rules...)
}

func addInitializer(base *v1alpha1.InitializerConfiguration, name string, rules ...v1alpha1.Rule) *v1alpha1.InitializerConfiguration {
	base.Initializers = append(base.Initializers, v1alpha1.Initializer{
		Name:  name,
		Rules: rules,
	})
	return base
}

func TestFindInitializers(t *testing.T) {
	type args struct {
		initializers *v1alpha1.InitializerConfiguration
		gvr          schema.GroupVersionResource
	}
	tests := []struct {
		name string
		args args
		want []string
	}{
		{
			name: "empty",
			args: args{
				gvr:          schema.GroupVersionResource{},
				initializers: newInitializer("1"),
			},
		},
		{
			name: "everything",
			args: args{
				gvr:          schema.GroupVersionResource{},
				initializers: newInitializer("1", v1alpha1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*"}}),
			},
			want: []string{"1"},
		},
		{
			name: "empty group",
			args: args{
				gvr:          schema.GroupVersionResource{},
				initializers: newInitializer("1", v1alpha1.Rule{APIGroups: []string{""}, APIVersions: []string{"*"}, Resources: []string{"*"}}),
			},
			want: []string{"1"},
		},
		{
			name: "pod",
			args: args{
				gvr: schema.GroupVersionResource{Resource: "pods"},
				initializers: addInitializer(
					newInitializer("1", v1alpha1.Rule{APIGroups: []string{""}, APIVersions: []string{"*"}, Resources: []string{"pods"}}),
					"2", v1alpha1.Rule{APIGroups: []string{""}, APIVersions: []string{"*"}, Resources: []string{"pods"}},
				),
			},
			want: []string{"1", "2"},
		},
		{
			name: "multiple matches",
			args: args{
				gvr:          schema.GroupVersionResource{Resource: "pods"},
				initializers: newInitializer("1", v1alpha1.Rule{APIGroups: []string{""}, APIVersions: []string{"*"}, Resources: []string{"pods"}}),
			},
			want: []string{"1"},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := findInitializers(tt.args.initializers, tt.args.gvr); !reflect.DeepEqual(got, tt.want) {
				t.Errorf("findInitializers() = %v, want %v", got, tt.want)
			}
		})
	}
}

type fakeAuthorizer struct {
	accept bool
}

func (f *fakeAuthorizer) Authorize(a authorizer.Attributes) (authorizer.Decision, string, error) {
	if f.accept {
		return authorizer.DecisionAllow, "", nil
	}
	return authorizer.DecisionNoOpinion, "denied", nil
}

func TestAdmitUpdate(t *testing.T) {
	tests := []struct {
		name             string
		oldInitializers  *metav1.Initializers
		newInitializers  *metav1.Initializers
		verifyUpdatedObj func(runtime.Object) (pass bool, reason string)
		err              string
	}{
		{
			name:            "updates on initialized resources are allowed",
			oldInitializers: nil,
			newInitializers: nil,
			err:             "",
		},
		{
			name:            "updates on initialized resources are allowed",
			oldInitializers: &metav1.Initializers{Pending: []metav1.Initializer{{Name: "init.k8s.io"}}},
			newInitializers: &metav1.Initializers{},
			verifyUpdatedObj: func(obj runtime.Object) (bool, string) {
				accessor, err := meta.Accessor(obj)
				if err != nil {
					return false, "cannot get accessor"
				}
				if accessor.GetInitializers() != nil {
					return false, "expect nil initializers"
				}
				return true, ""
			},
			err: "",
		},
		{
			name:            "initializers may not be set once initialized",
			oldInitializers: nil,
			newInitializers: &metav1.Initializers{Pending: []metav1.Initializer{{Name: "init.k8s.io"}}},
			err:             "field is immutable once initialization has completed",
		},
		{
			name:            "empty initializer list is treated as nil initializer",
			oldInitializers: nil,
			newInitializers: &metav1.Initializers{},
			verifyUpdatedObj: func(obj runtime.Object) (bool, string) {
				accessor, err := meta.Accessor(obj)
				if err != nil {
					return false, "cannot get accessor"
				}
				if accessor.GetInitializers() != nil {
					return false, "expect nil initializers"
				}
				return true, ""
			},
			err: "",
		},
	}

	plugin := initializer{
		config:     nil,
		authorizer: &fakeAuthorizer{true},
	}
	for _, tc := range tests {
		oldObj := &v1.Pod{}
		oldObj.Initializers = tc.oldInitializers
		newObj := &v1.Pod{}
		newObj.Initializers = tc.newInitializers
		a := admission.NewAttributesRecord(newObj, oldObj, schema.GroupVersionKind{}, "", "foo", schema.GroupVersionResource{}, "", admission.Update, false, nil)
		err := plugin.Admit(a)
		switch {
		case tc.err == "" && err != nil:
			t.Errorf("%q: unexpected error: %v", tc.name, err)
		case tc.err != "" && err == nil:
			t.Errorf("%q: unexpected no error, expected %s", tc.name, tc.err)
		case tc.err != "" && err != nil && !strings.Contains(err.Error(), tc.err):
			t.Errorf("%q: expected %s, got %v", tc.name, tc.err, err)
		}
	}

}
