/*
Copyright 2020 VMware, Inc.
SPDX-License-Identifier: Apache-2.0
*/

package apicerts

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"encoding/pem"
	"errors"
	"testing"
	"time"

	"github.com/sclevine/spec"
	"github.com/sclevine/spec/report"
	"github.com/stretchr/testify/require"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	kubeinformers "k8s.io/client-go/informers"
	kubernetesfake "k8s.io/client-go/kubernetes/fake"
	coretesting "k8s.io/client-go/testing"
	apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
	aggregatorfake "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/fake"

	"github.com/suzerain-io/controller-go"
	"github.com/suzerain-io/placeholder-name/internal/testutil"
	placeholderv1alpha1 "github.com/suzerain-io/placeholder-name/kubernetes/1.19/api/apis/placeholder/v1alpha1"
)

func TestManagerControllerOptions(t *testing.T) {
	spec.Run(t, "options", func(t *testing.T, when spec.G, it spec.S) {
		const installedInNamespace = "some-namespace"

		var r *require.Assertions
		var observableWithInformerOption *testutil.ObservableWithInformerOption
		var observableWithInitialEventOption *testutil.ObservableWithInitialEventOption
		var secretsInformerFilter controller.Filter

		it.Before(func() {
			r = require.New(t)
			observableWithInformerOption = testutil.NewObservableWithInformerOption()
			observableWithInitialEventOption = testutil.NewObservableWithInitialEventOption()
			secretsInformer := kubeinformers.NewSharedInformerFactory(nil, 0).Core().V1().Secrets()
			_ = NewCertsManagerController(
				installedInNamespace,
				nil,
				nil,
				secretsInformer,
				observableWithInformerOption.WithInformer,         // make it possible to observe the behavior of the Filters
				observableWithInitialEventOption.WithInitialEvent, // make it possible to observe the behavior of the initial event
			)
			secretsInformerFilter = observableWithInformerOption.GetFilterForInformer(secretsInformer)
		})

		when("watching Secret objects", func() {
			var subject controller.Filter
			var target, wrongNamespace, wrongName, unrelated *corev1.Secret

			it.Before(func() {
				subject = secretsInformerFilter
				target = &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "api-serving-cert", Namespace: installedInNamespace}}
				wrongNamespace = &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "api-serving-cert", Namespace: "wrong-namespace"}}
				wrongName = &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "wrong-name", Namespace: installedInNamespace}}
				unrelated = &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "wrong-name", Namespace: "wrong-namespace"}}
			})

			when("the target Secret changes", func() {
				it("returns true to trigger the sync method", func() {
					r.True(subject.Add(target))
					r.True(subject.Update(target, unrelated))
					r.True(subject.Update(unrelated, target))
					r.True(subject.Delete(target))
				})
			})

			when("a Secret from another namespace changes", func() {
				it("returns false to avoid triggering the sync method", func() {
					r.False(subject.Add(wrongNamespace))
					r.False(subject.Update(wrongNamespace, unrelated))
					r.False(subject.Update(unrelated, wrongNamespace))
					r.False(subject.Delete(wrongNamespace))
				})
			})

			when("a Secret with a different name changes", func() {
				it("returns false to avoid triggering the sync method", func() {
					r.False(subject.Add(wrongName))
					r.False(subject.Update(wrongName, unrelated))
					r.False(subject.Update(unrelated, wrongName))
					r.False(subject.Delete(wrongName))
				})
			})

			when("a Secret with a different name and a different namespace changes", func() {
				it("returns false to avoid triggering the sync method", func() {
					r.False(subject.Add(unrelated))
					r.False(subject.Update(unrelated, unrelated))
					r.False(subject.Delete(unrelated))
				})
			})
		})

		when("starting up", func() {
			it("asks for an initial event because the Secret may not exist yet and it needs to run anyway", func() {
				r.Equal(controller.Key{
					Namespace: installedInNamespace,
					Name:      "api-serving-cert",
				}, observableWithInitialEventOption.GetInitialEventKey())
			})
		})
	}, spec.Parallel(), spec.Report(report.Terminal{}))
}

func TestManagerControllerSync(t *testing.T) {
	spec.Run(t, "Sync", func(t *testing.T, when spec.G, it spec.S) {
		const installedInNamespace = "some-namespace"

		var r *require.Assertions

		var subject controller.Controller
		var kubeAPIClient *kubernetesfake.Clientset
		var aggregatorAPIClient *aggregatorfake.Clientset
		var kubeInformerClient *kubernetesfake.Clientset
		var kubeInformers kubeinformers.SharedInformerFactory
		var timeoutContext context.Context
		var timeoutContextCancel context.CancelFunc
		var syncContext *controller.Context

		// Defer starting the informers until the last possible moment so that the
		// nested Before's can keep adding things to the informer caches.
		var startInformersAndController = func() {
			// Set this at the last second to allow for injection of server override.
			subject = NewCertsManagerController(
				installedInNamespace,
				kubeAPIClient,
				aggregatorAPIClient,
				kubeInformers.Core().V1().Secrets(),
				controller.WithInformer,
				controller.WithInitialEvent,
			)

			// Set this at the last second to support calling subject.Name().
			syncContext = &controller.Context{
				Context: timeoutContext,
				Name:    subject.Name(),
				Key: controller.Key{
					Namespace: installedInNamespace,
					Name:      "api-serving-cert",
				},
			}

			// Must start informers before calling TestRunSynchronously()
			kubeInformers.Start(timeoutContext.Done())
			controller.TestRunSynchronously(t, subject)
		}

		it.Before(func() {
			r = require.New(t)

			timeoutContext, timeoutContextCancel = context.WithTimeout(context.Background(), time.Second*3)

			kubeInformerClient = kubernetesfake.NewSimpleClientset()
			kubeInformers = kubeinformers.NewSharedInformerFactory(kubeInformerClient, 0)
			kubeAPIClient = kubernetesfake.NewSimpleClientset()
			aggregatorAPIClient = aggregatorfake.NewSimpleClientset()
		})

		it.After(func() {
			timeoutContextCancel()
		})

		when("there is not yet an api-serving-cert Secret in the installation namespace or it was deleted", func() {
			it.Before(func() {
				unrelatedSecret := &corev1.Secret{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "some other secret",
						Namespace: installedInNamespace,
					},
				}
				err := kubeInformerClient.Tracker().Add(unrelatedSecret)
				r.NoError(err)
			})

			when("the APIService exists", func() {
				it.Before(func() {
					apiService := &apiregistrationv1.APIService{
						ObjectMeta: metav1.ObjectMeta{
							Name: placeholderv1alpha1.SchemeGroupVersion.Version + "." + placeholderv1alpha1.GroupName,
						},
						Spec: apiregistrationv1.APIServiceSpec{
							CABundle:        nil,
							VersionPriority: 1234,
						},
					}
					err := aggregatorAPIClient.Tracker().Add(apiService)
					r.NoError(err)
				})

				it("creates the api-serving-cert Secret and updates the APIService's ca bundle", func() {
					startInformersAndController()
					err := controller.TestSync(t, subject, *syncContext)
					r.NoError(err)

					// Check all the relevant fields from the create Secret action
					r.Len(kubeAPIClient.Actions(), 1)
					actualAction := kubeAPIClient.Actions()[0].(coretesting.CreateActionImpl)
					r.Equal(schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"}, actualAction.GetResource())
					r.Equal(installedInNamespace, actualAction.GetNamespace())
					actualSecret := actualAction.GetObject().(*corev1.Secret)
					r.Equal("api-serving-cert", actualSecret.Name)
					r.Equal(installedInNamespace, actualSecret.Namespace)
					actualCACert := actualSecret.StringData["caCertificate"]
					actualPrivateKey := actualSecret.StringData["tlsPrivateKey"]
					actualCertChain := actualSecret.StringData["tlsCertificateChain"]
					r.NotEmpty(actualCACert)
					r.NotEmpty(actualPrivateKey)
					r.NotEmpty(actualCertChain)

					// Validate the created cert using the CA, and also validate the cert's hostname
					roots := x509.NewCertPool()
					ok := roots.AppendCertsFromPEM([]byte(actualCACert))
					r.True(ok)
					block, _ := pem.Decode([]byte(actualCertChain))
					r.NotNil(block)
					parsedCert, err := x509.ParseCertificate(block.Bytes)
					r.NoError(err)
					serviceEndpoint := "placeholder-name-api." + installedInNamespace + ".svc"
					opts := x509.VerifyOptions{
						DNSName: serviceEndpoint,
						Roots:   roots,
					}
					_, err = parsedCert.Verify(opts)
					r.NoError(err)
					r.Contains(parsedCert.DNSNames, serviceEndpoint, "expected an explicit DNS SAN, not just Common Name")

					// Check the created cert's validity bounds
					r.WithinDuration(time.Now(), parsedCert.NotBefore, time.Minute*2)
					r.WithinDuration(time.Now().Add(24*365*time.Hour), parsedCert.NotAfter, time.Minute*2)

					// Check that the private key and cert chain match
					_, err = tls.X509KeyPair([]byte(actualCertChain), []byte(actualPrivateKey))
					r.NoError(err)

					// Make sure we updated the APIService caBundle and left it otherwise unchanged
					r.Len(aggregatorAPIClient.Actions(), 2)
					r.Equal("get", aggregatorAPIClient.Actions()[0].GetVerb())
					expectedAPIServiceName := placeholderv1alpha1.SchemeGroupVersion.Version + "." + placeholderv1alpha1.GroupName
					expectedUpdateAction := coretesting.NewUpdateAction(
						schema.GroupVersionResource{
							Group:    apiregistrationv1.GroupName,
							Version:  "v1",
							Resource: "apiservices",
						},
						"",
						&apiregistrationv1.APIService{
							ObjectMeta: metav1.ObjectMeta{
								Name:      expectedAPIServiceName,
								Namespace: "",
							},
							Spec: apiregistrationv1.APIServiceSpec{
								VersionPriority: 1234, // only the CABundle is updated, this other field is left unchanged
								CABundle:        []byte(actualCACert),
							},
						},
					)
					r.Equal(expectedUpdateAction, aggregatorAPIClient.Actions()[1])
				})

				when("updating the APIService fails", func() {
					it.Before(func() {
						aggregatorAPIClient.PrependReactor(
							"update",
							"apiservices",
							func(_ coretesting.Action) (bool, runtime.Object, error) {
								return true, nil, errors.New("update failed")
							},
						)
					})

					it("returns the update error", func() {
						startInformersAndController()
						err := controller.TestSync(t, subject, *syncContext)
						r.EqualError(err, "could not update the API service: could not update API service: update failed")
					})
				})
			})

			when("the APIService does not exist", func() {
				it.Before(func() {
					unrelatedAPIService := &apiregistrationv1.APIService{
						ObjectMeta: metav1.ObjectMeta{Name: "some other api service"},
						Spec:       apiregistrationv1.APIServiceSpec{},
					}
					err := aggregatorAPIClient.Tracker().Add(unrelatedAPIService)
					r.NoError(err)
				})

				it("returns an error", func() {
					startInformersAndController()
					err := controller.TestSync(t, subject, *syncContext)
					r.Error(err)
					r.Regexp("could not get existing version of API service: .* not found", err.Error())
				})
			})

			when("creating the Secret fails", func() {
				it.Before(func() {
					kubeAPIClient.PrependReactor(
						"create",
						"secrets",
						func(_ coretesting.Action) (bool, runtime.Object, error) {
							return true, nil, errors.New("create failed")
						},
					)
				})

				it("returns the create error and does not update the APIService", func() {
					startInformersAndController()
					err := controller.TestSync(t, subject, *syncContext)
					r.EqualError(err, "could not create secret: create failed")
					r.Empty(aggregatorAPIClient.Actions())
				})
			})
		})

		when("there is an api-serving-cert Secret already in the installation namespace", func() {
			it.Before(func() {
				apiServingCertSecret := &corev1.Secret{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "api-serving-cert",
						Namespace: installedInNamespace,
					},
				}
				err := kubeInformerClient.Tracker().Add(apiServingCertSecret)
				r.NoError(err)
			})

			it("does not need to make any API calls with its API clients", func() {
				startInformersAndController()
				err := controller.TestSync(t, subject, *syncContext)
				r.NoError(err)
				r.Empty(kubeAPIClient.Actions())
				r.Empty(aggregatorAPIClient.Actions())
			})
		})
	}, spec.Parallel(), spec.Report(report.Terminal{}))
}