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

import (
	"context"
	"reflect"
	"testing"
	"time"

	"k8s.io/api/admissionregistration/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apiserver/pkg/admission/plugin/webhook"
	"k8s.io/client-go/informers"
	"k8s.io/client-go/kubernetes/fake"
)

func TestGetValidatingWebhookConfig(t *testing.T) {
	// Build a test client that the admission plugin can use to look up the ValidatingWebhookConfiguration
	client := fake.NewSimpleClientset()
	informerFactory := informers.NewSharedInformerFactory(client, 0)
	stop := make(chan struct{})
	defer close(stop)

	manager := NewValidatingWebhookConfigurationManager(informerFactory)
	informerFactory.Start(stop)
	informerFactory.WaitForCacheSync(stop)

	// no configurations
	if configurations := manager.Webhooks(); len(configurations) != 0 {
		t.Errorf("expected empty webhooks, but got %v", configurations)
	}

	webhookConfiguration := &v1.ValidatingWebhookConfiguration{
		ObjectMeta: metav1.ObjectMeta{Name: "webhook1"},
		Webhooks:   []v1.ValidatingWebhook{{Name: "webhook1.1"}},
	}

	client.
		AdmissionregistrationV1().
		ValidatingWebhookConfigurations().
		Create(context.TODO(), webhookConfiguration, metav1.CreateOptions{})

	// Wait up to 10s for the notification to be delivered.
	// (on my system this takes < 2ms)
	startTime := time.Now()
	configurations := manager.Webhooks()
	for len(configurations) == 0 {
		if time.Since(startTime) > 10*time.Second {
			break
		}
		time.Sleep(time.Millisecond)
		configurations = manager.Webhooks()
	}

	// verify presence
	if len(configurations) == 0 {
		t.Errorf("expected non empty webhooks")
	}
	for i := range configurations {
		h, ok := configurations[i].GetValidatingWebhook()
		if !ok {
			t.Errorf("Expected validating webhook")
			continue
		}
		if !reflect.DeepEqual(h, &webhookConfiguration.Webhooks[i]) {
			t.Errorf("Expected\n%#v\ngot\n%#v", &webhookConfiguration.Webhooks[i], h)
		}
	}
}

// mockCreateValidatingWebhookAccessor is a struct used to compute how many times
// the function webhook.NewValidatingWebhookAccessor is being called when refreshing
// webhookAccessors.
//
// NOTE: Maybe there some testing help that we can import and reuse instead.
type mockCreateValidatingWebhookAccessor struct {
	numberOfCalls int
}

func (mock *mockCreateValidatingWebhookAccessor) calledNTimes() int { return mock.numberOfCalls }
func (mock *mockCreateValidatingWebhookAccessor) resetCounter()     { mock.numberOfCalls = 0 }
func (mock *mockCreateValidatingWebhookAccessor) incrementCounter() { mock.numberOfCalls++ }

func (mock *mockCreateValidatingWebhookAccessor) fn(uid string, configurationName string, h *v1.ValidatingWebhook) webhook.WebhookAccessor {
	mock.incrementCounter()
	return webhook.NewValidatingWebhookAccessor(uid, configurationName, h)
}

func configurationTotalWebhooks(configurations []*v1.ValidatingWebhookConfiguration) int {
	total := 0
	for _, configuration := range configurations {
		total += len(configuration.Webhooks)
	}
	return total
}

func TestGetValidatingWebhookConfigSmartReload(t *testing.T) {
	type args struct {
		createWebhookConfigurations []*v1.ValidatingWebhookConfiguration
		updateWebhookConfigurations []*v1.ValidatingWebhookConfiguration
	}
	tests := []struct {
		name              string
		args              args
		numberOfCreations int
		// number of refreshes are number of times we recrated a webhook accessor
		// instead of pulling from the cache.
		numberOfRefreshes             int
		finalNumberOfWebhookAccessors int
	}{
		{
			name: "no creations and no updates",
			args: args{
				nil,
				nil,
			},
			numberOfCreations:             0,
			numberOfRefreshes:             0,
			finalNumberOfWebhookAccessors: 0,
		},
		{
			name: "create configurations and no updates",
			args: args{
				[]*v1.ValidatingWebhookConfiguration{
					{
						ObjectMeta: metav1.ObjectMeta{Name: "webhook1"},
						Webhooks:   []v1.ValidatingWebhook{{Name: "webhook1.1"}},
					},
					{
						ObjectMeta: metav1.ObjectMeta{Name: "webhook2"},
						Webhooks:   []v1.ValidatingWebhook{{Name: "webhook2.1"}},
					},
				},
				nil,
			},
			numberOfCreations:             2,
			numberOfRefreshes:             0,
			finalNumberOfWebhookAccessors: 2,
		},
		{
			name: "create configurations and update some of them",
			args: args{
				[]*v1.ValidatingWebhookConfiguration{
					{
						ObjectMeta: metav1.ObjectMeta{Name: "webhook3"},
						Webhooks:   []v1.ValidatingWebhook{{Name: "webhook3.1"}},
					},
					{
						ObjectMeta: metav1.ObjectMeta{Name: "webhook4"},
						Webhooks:   []v1.ValidatingWebhook{{Name: "webhook4.1"}},
					},
				},
				[]*v1.ValidatingWebhookConfiguration{
					{
						ObjectMeta: metav1.ObjectMeta{Name: "webhook3"},
						Webhooks:   []v1.ValidatingWebhook{{Name: "webhook3.1-updated"}},
					},
				},
			},
			numberOfCreations:             2,
			numberOfRefreshes:             1,
			finalNumberOfWebhookAccessors: 2,
		},
		{
			name: "create configuration and update moar of them",
			args: args{
				[]*v1.ValidatingWebhookConfiguration{
					{
						ObjectMeta: metav1.ObjectMeta{Name: "webhook5"},
						Webhooks:   []v1.ValidatingWebhook{{Name: "webhook5.1"}, {Name: "webhook5.2"}},
					},
					{
						ObjectMeta: metav1.ObjectMeta{Name: "webhook6"},
						Webhooks:   []v1.ValidatingWebhook{{Name: "webhook6.1"}},
					},
					{
						ObjectMeta: metav1.ObjectMeta{Name: "webhook7"},
						Webhooks:   []v1.ValidatingWebhook{{Name: "webhook7.1"}, {Name: "webhook7.1"}},
					},
				},
				[]*v1.ValidatingWebhookConfiguration{
					{
						ObjectMeta: metav1.ObjectMeta{Name: "webhook5"},
						Webhooks:   []v1.ValidatingWebhook{{Name: "webhook5.1-updated"}},
					},
					{
						ObjectMeta: metav1.ObjectMeta{Name: "webhook7"},
						Webhooks:   []v1.ValidatingWebhook{{Name: "webhook7.1-updated"}, {Name: "webhook7.2-updated"}, {Name: "webhook7.3"}},
					},
				},
			},
			numberOfCreations:             5,
			numberOfRefreshes:             4,
			finalNumberOfWebhookAccessors: 5,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			client := fake.NewSimpleClientset()
			informerFactory := informers.NewSharedInformerFactory(client, 0)
			stop := make(chan struct{})
			defer close(stop)
			manager := NewValidatingWebhookConfigurationManager(informerFactory)
			managerStructPtr := manager.(*validatingWebhookConfigurationManager)
			fakeWebhookAccessorCreator := &mockCreateValidatingWebhookAccessor{}
			managerStructPtr.createValidatingWebhookAccessor = fakeWebhookAccessorCreator.fn
			informerFactory.Start(stop)
			informerFactory.WaitForCacheSync(stop)

			// Create webhooks
			for _, configurations := range tt.args.createWebhookConfigurations {
				client.
					AdmissionregistrationV1().
					ValidatingWebhookConfigurations().
					Create(context.TODO(), configurations, metav1.CreateOptions{})
			}
			// TODO use channels to wait for manager.createValidatingWebhookAccessor
			// to be called instead of using time.Sleep
			time.Sleep(1 * time.Second)
			webhooks := manager.Webhooks()
			if configurationTotalWebhooks(tt.args.createWebhookConfigurations) != len(webhooks) {
				t.Errorf("Expected number of webhooks %d received %d",
					configurationTotalWebhooks(tt.args.createWebhookConfigurations),
					len(webhooks),
				)
			}
			// assert creations
			if tt.numberOfCreations != fakeWebhookAccessorCreator.calledNTimes() {
				t.Errorf(
					"Expected number of creations %d received %d",
					tt.numberOfCreations, fakeWebhookAccessorCreator.calledNTimes(),
				)
			}

			// reset mock counter
			fakeWebhookAccessorCreator.resetCounter()

			// Update webhooks
			for _, configurations := range tt.args.updateWebhookConfigurations {
				client.
					AdmissionregistrationV1().
					ValidatingWebhookConfigurations().
					Update(context.TODO(), configurations, metav1.UpdateOptions{})
			}
			// TODO use channels to wait for manager.createValidatingWebhookAccessor
			// to be called instead of using time.Sleep
			time.Sleep(1 * time.Second)
			webhooks = manager.Webhooks()
			if tt.finalNumberOfWebhookAccessors != len(webhooks) {
				t.Errorf("Expected final number of webhooks %d received %d",
					tt.finalNumberOfWebhookAccessors,
					len(webhooks),
				)
			}

			// assert updates
			if tt.numberOfRefreshes != fakeWebhookAccessorCreator.calledNTimes() {
				t.Errorf(
					"Expected number of refreshes %d received %d",
					tt.numberOfRefreshes, fakeWebhookAccessorCreator.calledNTimes(),
				)
			}
			// reset mock counter for the next test cases
			fakeWebhookAccessorCreator.resetCounter()
		})
	}
}
