/*
Copyright © The ESO 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

    https://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 clusterpushsecret

import (
	"context"
	"fmt"
	"math/rand"
	"sort"
	"time"

	v1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	crclient "sigs.k8s.io/controller-runtime/pkg/client"

	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
	"github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
	"github.com/external-secrets/external-secrets/pkg/controllers/clusterpushsecret/cpsmetrics"
	ctrlmetrics "github.com/external-secrets/external-secrets/pkg/controllers/metrics"
	"github.com/external-secrets/external-secrets/runtime/testing/fake"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

func init() {
	ctrlmetrics.SetUpLabelNames(false)
	cpsmetrics.SetUpMetrics()
	fakeProvider = fake.New()
	esv1.ForceRegister(fakeProvider, &esv1.SecretStoreProvider{
		Fake: &esv1.FakeProvider{},
	}, esv1.MaintenanceStatusMaintained)
}

var (
	secretName                = "test-secret"
	testPushSecret            = "test-ps"
	newPushSecret             = "new-ps-name"
	defaultKey                = "default-key"
	defaultVal                = "default-value"
	testLabelKey              = "test-label-key"
	testLabelValue            = "test-label-value"
	testAnnotationKey         = "test-annotation-key"
	testAnnotationValue       = "test-annotation-value"
	updateStoreName           = "updated-test-store"
	kubernetesMetadataLabel   = "kubernetes.io/metadata.name"
	noneMatchingAnnotationKey = "no-longer-match-label-key"
	noneMatchingAnnotationVal = "no-longer-match-annotation-value"
	fakeProvider              *fake.Client
	timeout                   = time.Second * 10
	interval                  = time.Millisecond * 250
)

type clusterPushSecretTestCase struct {
	namespaces                []v1.Namespace
	clusterPushSecret         func(namespaces []v1.Namespace) v1alpha1.ClusterPushSecret
	sourceSecret              func(namespaces []v1.Namespace) []v1.Secret
	beforeCheck               func(ctx context.Context, namespaces []v1.Namespace, created v1alpha1.ClusterPushSecret)
	expectedClusterPushSecret func(namespaces []v1.Namespace, created v1alpha1.ClusterPushSecret) v1alpha1.ClusterPushSecret
	expectedPushSecrets       func(namespaces []v1.Namespace, created v1alpha1.ClusterPushSecret) []v1alpha1.PushSecret
}

var _ = Describe("ClusterPushSecret controller", func() {
	defaultClusterPushSecret := func() *v1alpha1.ClusterPushSecret {
		return &v1alpha1.ClusterPushSecret{
			ObjectMeta: metav1.ObjectMeta{
				Name: fmt.Sprintf("test-pes-%s", randString(10)),
			},
			Spec: v1alpha1.ClusterPushSecretSpec{
				PushSecretSpec: v1alpha1.PushSecretSpec{
					RefreshInterval: &metav1.Duration{Duration: time.Hour},
					SecretStoreRefs: []v1alpha1.PushSecretStoreRef{
						{
							Name: "test-store",
							Kind: "SecretStore",
						},
					},
					Selector: v1alpha1.PushSecretSelector{
						Secret: &v1alpha1.PushSecretSecret{
							Name: secretName,
						},
					},
					Data: []v1alpha1.PushSecretData{
						{
							Match:    v1alpha1.PushSecretMatch{},
							Metadata: nil,
						},
					},
				},
			},
		}
	}

	defaultSourceSecret := func(namespaces []v1.Namespace) []v1.Secret {
		result := make([]v1.Secret, 0, len(namespaces))
		for _, s := range namespaces {
			result = append(result, v1.Secret{
				ObjectMeta: metav1.ObjectMeta{
					Name:      secretName,
					Namespace: s.Name,
				},
				Data: map[string][]byte{
					defaultKey: []byte(defaultVal),
				},
			})
		}

		return result
	}

	DescribeTable("When reconciling a ClusterPush Secret",
		func(tc clusterPushSecretTestCase) {
			ctx := context.Background()
			By("creating namespaces")
			namespaces := make([]v1.Namespace, 0, len(tc.namespaces))
			for _, ns := range tc.namespaces {
				err := k8sClient.Create(ctx, &ns)
				Expect(err).ShouldNot(HaveOccurred())
				namespaces = append(namespaces, ns)
			}

			for _, s := range tc.sourceSecret(namespaces) {
				By("creating a source secret")
				err := k8sClient.Create(ctx, &s)
				Expect(err).ShouldNot(HaveOccurred())
			}

			By("creating a cluster push secret")
			pes := tc.clusterPushSecret(tc.namespaces)
			err := k8sClient.Create(ctx, &pes)
			Expect(err).ShouldNot(HaveOccurred())

			By("running before check")
			if tc.beforeCheck != nil {
				tc.beforeCheck(ctx, namespaces, pes)
			}

			// the before check above may have updated the namespaces, so refresh them
			for i, ns := range namespaces {
				err := k8sClient.Get(ctx, types.NamespacedName{Name: ns.Name}, &ns)
				Expect(err).ShouldNot(HaveOccurred())
				namespaces[i] = ns
			}

			By("checking the cluster push secret")
			expectedCPS := tc.expectedClusterPushSecret(namespaces, pes)

			Eventually(func(g Gomega) {
				key := types.NamespacedName{Name: expectedCPS.Name}
				var gotCes v1alpha1.ClusterPushSecret
				err = k8sClient.Get(ctx, key, &gotCes)
				g.Expect(err).ShouldNot(HaveOccurred())

				g.Expect(gotCes.Labels).To(Equal(expectedCPS.Labels))
				g.Expect(gotCes.Annotations).To(Equal(expectedCPS.Annotations))
				g.Expect(gotCes.Spec).To(Equal(expectedCPS.Spec))
				g.Expect(gotCes.Status).To(Equal(expectedCPS.Status))
			}).WithTimeout(timeout).WithPolling(interval).Should(Succeed())

			By("checking the push secrets")
			expectedPSs := tc.expectedPushSecrets(namespaces, pes)

			Eventually(func(g Gomega) {
				var gotESs []v1alpha1.PushSecret //nolint:prealloc // size unknown before loop
				for _, ns := range namespaces {
					var pushSecrets v1alpha1.PushSecretList
					err := k8sClient.List(ctx, &pushSecrets, crclient.InNamespace(ns.Name))
					g.Expect(err).ShouldNot(HaveOccurred())

					gotESs = append(gotESs, pushSecrets.Items...)
				}

				g.Expect(len(gotESs)).Should(Equal(len(expectedPSs)))
				for _, gotES := range gotESs {
					found := false
					for _, expectedPS := range expectedPSs {
						if gotES.Namespace == expectedPS.Namespace && gotES.Name == expectedPS.Name {
							found = true
							g.Expect(gotES.Labels).To(Equal(expectedPS.Labels))
							g.Expect(gotES.Annotations).To(Equal(expectedPS.Annotations))
							g.Expect(gotES.Spec).To(Equal(expectedPS.Spec))
						}
					}
					g.Expect(found).To(Equal(true))
				}
			}).WithTimeout(timeout).WithPolling(interval).Should(Succeed())
		},

		Entry("Should use cluster push secret name if push secret name isn't defined", clusterPushSecretTestCase{
			namespaces: []v1.Namespace{
				{ObjectMeta: metav1.ObjectMeta{Name: randomNamespaceName()}},
			},
			clusterPushSecret: func(namespaces []v1.Namespace) v1alpha1.ClusterPushSecret {
				pes := defaultClusterPushSecret()
				pes.Spec.NamespaceSelectors = []*metav1.LabelSelector{
					{
						MatchLabels: map[string]string{kubernetesMetadataLabel: namespaces[0].Name},
					},
				}
				return *pes
			},
			sourceSecret: defaultSourceSecret,
			expectedClusterPushSecret: func(namespaces []v1.Namespace, created v1alpha1.ClusterPushSecret) v1alpha1.ClusterPushSecret {
				return v1alpha1.ClusterPushSecret{
					ObjectMeta: metav1.ObjectMeta{
						Name: created.Name,
					},
					Spec: created.Spec,
					Status: v1alpha1.ClusterPushSecretStatus{
						PushSecretName:        created.Name,
						ProvisionedNamespaces: []string{namespaces[0].Name},
						Conditions: []v1alpha1.PushSecretStatusCondition{
							{
								Type:   v1alpha1.PushSecretReady,
								Status: v1.ConditionTrue,
							},
						},
					},
				}
			},
			expectedPushSecrets: func(namespaces []v1.Namespace, created v1alpha1.ClusterPushSecret) []v1alpha1.PushSecret {
				return []v1alpha1.PushSecret{
					{
						ObjectMeta: metav1.ObjectMeta{
							Namespace: namespaces[0].Name,
							Name:      created.Name,
						},
						Spec: created.Spec.PushSecretSpec,
					},
				}
			},
		}),
		Entry("Should set push secret name and metadata if the fields are set", clusterPushSecretTestCase{
			namespaces: []v1.Namespace{
				{ObjectMeta: metav1.ObjectMeta{Name: randomNamespaceName()}},
			},
			clusterPushSecret: func(namespaces []v1.Namespace) v1alpha1.ClusterPushSecret {
				pes := defaultClusterPushSecret()
				pes.Spec.NamespaceSelectors = []*metav1.LabelSelector{
					{
						MatchLabels: map[string]string{kubernetesMetadataLabel: namespaces[0].Name},
					},
				}
				pes.Spec.PushSecretName = testPushSecret
				pes.Spec.PushSecretMetadata = v1alpha1.PushSecretMetadata{
					Labels:      map[string]string{"test-label-key1": "test-label-value1", "test-label-key2": "test-label-value2"},
					Annotations: map[string]string{"test-annotation-key1": "test-annotation-value1", "test-annotation-key2": "test-annotation-value2"},
				}
				return *pes
			},
			sourceSecret: defaultSourceSecret,
			expectedClusterPushSecret: func(namespaces []v1.Namespace, created v1alpha1.ClusterPushSecret) v1alpha1.ClusterPushSecret {
				return v1alpha1.ClusterPushSecret{
					ObjectMeta: metav1.ObjectMeta{
						Name: created.Name,
					},
					Spec: created.Spec,
					Status: v1alpha1.ClusterPushSecretStatus{
						PushSecretName:        testPushSecret,
						ProvisionedNamespaces: []string{namespaces[0].Name},
						Conditions: []v1alpha1.PushSecretStatusCondition{
							{
								Type:   v1alpha1.PushSecretReady,
								Status: v1.ConditionTrue,
							},
						},
					},
				}
			},
			expectedPushSecrets: func(namespaces []v1.Namespace, created v1alpha1.ClusterPushSecret) []v1alpha1.PushSecret {
				return []v1alpha1.PushSecret{
					{
						ObjectMeta: metav1.ObjectMeta{
							Namespace:   namespaces[0].Name,
							Name:        testPushSecret,
							Labels:      map[string]string{"test-label-key1": "test-label-value1", "test-label-key2": "test-label-value2"},
							Annotations: map[string]string{"test-annotation-key1": "test-annotation-value1", "test-annotation-key2": "test-annotation-value2"},
						},
						Spec: created.Spec.PushSecretSpec,
					},
				}
			},
		}),
		Entry("Should delete old push secrets if name has changed", clusterPushSecretTestCase{
			namespaces: []v1.Namespace{
				{ObjectMeta: metav1.ObjectMeta{Name: randomNamespaceName()}},
			},
			clusterPushSecret: func(namespaces []v1.Namespace) v1alpha1.ClusterPushSecret {
				pes := defaultClusterPushSecret()
				pes.Spec.NamespaceSelectors = []*metav1.LabelSelector{
					{
						MatchLabels: map[string]string{kubernetesMetadataLabel: namespaces[0].Name},
					},
				}
				pes.Spec.PushSecretName = "old-es-name"
				return *pes
			},
			sourceSecret: defaultSourceSecret,
			beforeCheck: func(ctx context.Context, namespaces []v1.Namespace, created v1alpha1.ClusterPushSecret) {
				// Wait until the push secret is provisioned
				var es v1alpha1.PushSecret
				Eventually(func(g Gomega) {
					key := types.NamespacedName{Namespace: namespaces[0].Name, Name: "old-es-name"}
					g.Expect(k8sClient.Get(ctx, key, &es)).ShouldNot(HaveOccurred())
				}).WithTimeout(timeout).WithPolling(interval).Should(Succeed())

				copied := created.DeepCopy()
				copied.Spec.PushSecretName = newPushSecret
				Expect(k8sClient.Patch(ctx, copied, crclient.MergeFrom(created.DeepCopy()))).ShouldNot(HaveOccurred())
			},
			expectedClusterPushSecret: func(namespaces []v1.Namespace, created v1alpha1.ClusterPushSecret) v1alpha1.ClusterPushSecret {
				updatedSpec := created.Spec.DeepCopy()
				updatedSpec.PushSecretName = newPushSecret

				return v1alpha1.ClusterPushSecret{
					ObjectMeta: metav1.ObjectMeta{
						Name: created.Name,
					},
					Spec: *updatedSpec,
					Status: v1alpha1.ClusterPushSecretStatus{
						PushSecretName:        newPushSecret,
						ProvisionedNamespaces: []string{namespaces[0].Name},
						Conditions: []v1alpha1.PushSecretStatusCondition{
							{
								Type:   v1alpha1.PushSecretReady,
								Status: v1.ConditionTrue,
							},
						},
					},
				}
			},
			expectedPushSecrets: func(namespaces []v1.Namespace, created v1alpha1.ClusterPushSecret) []v1alpha1.PushSecret {
				return []v1alpha1.PushSecret{
					{
						ObjectMeta: metav1.ObjectMeta{
							Namespace: namespaces[0].Name,
							Name:      newPushSecret,
						},
						Spec: created.Spec.PushSecretSpec,
					},
				}
			},
		}),
		Entry("Should update push secret if the fields change", clusterPushSecretTestCase{
			namespaces: []v1.Namespace{
				{ObjectMeta: metav1.ObjectMeta{Name: randomNamespaceName()}},
			},
			clusterPushSecret: func(namespaces []v1.Namespace) v1alpha1.ClusterPushSecret {
				pes := defaultClusterPushSecret()
				pes.Spec.NamespaceSelectors = []*metav1.LabelSelector{
					{
						MatchLabels: map[string]string{kubernetesMetadataLabel: namespaces[0].Name},
					},
				}
				return *pes
			},
			sourceSecret: defaultSourceSecret,
			beforeCheck: func(ctx context.Context, namespaces []v1.Namespace, created v1alpha1.ClusterPushSecret) {
				// Wait until the push secret is provisioned
				var es v1alpha1.PushSecret
				Eventually(func(g Gomega) {
					key := types.NamespacedName{Namespace: namespaces[0].Name, Name: created.Name}
					g.Expect(k8sClient.Get(ctx, key, &es)).ShouldNot(HaveOccurred())
					g.Expect(len(es.Labels)).Should(Equal(0))
					g.Expect(len(es.Annotations)).Should(Equal(0))
					g.Expect(es.Spec).Should(Equal(created.Spec.PushSecretSpec))
				}).WithTimeout(timeout).WithPolling(interval).Should(Succeed())

				copied := created.DeepCopy()
				copied.Spec.PushSecretMetadata = v1alpha1.PushSecretMetadata{
					Labels:      map[string]string{testLabelKey: testLabelValue},
					Annotations: map[string]string{testAnnotationKey: testAnnotationValue},
				}
				copied.Spec.PushSecretSpec.SecretStoreRefs = []v1alpha1.PushSecretStoreRef{
					{
						Name: updateStoreName,
						Kind: "SecretStore",
					},
				}
				Expect(k8sClient.Patch(ctx, copied, crclient.MergeFrom(created.DeepCopy()))).ShouldNot(HaveOccurred())
			},
			expectedClusterPushSecret: func(namespaces []v1.Namespace, created v1alpha1.ClusterPushSecret) v1alpha1.ClusterPushSecret {
				updatedSpec := created.Spec.DeepCopy()
				updatedSpec.PushSecretMetadata = v1alpha1.PushSecretMetadata{
					Labels:      map[string]string{testLabelKey: testLabelValue},
					Annotations: map[string]string{testAnnotationKey: testAnnotationValue},
				}
				updatedSpec.PushSecretSpec.SecretStoreRefs = []v1alpha1.PushSecretStoreRef{
					{
						Name: updateStoreName,
						Kind: "SecretStore",
					},
				}

				return v1alpha1.ClusterPushSecret{
					ObjectMeta: metav1.ObjectMeta{
						Name: created.Name,
					},
					Spec: *updatedSpec,
					Status: v1alpha1.ClusterPushSecretStatus{
						PushSecretName:        created.Name,
						ProvisionedNamespaces: []string{namespaces[0].Name},
						Conditions: []v1alpha1.PushSecretStatusCondition{
							{
								Type:   v1alpha1.PushSecretReady,
								Status: v1.ConditionTrue,
							},
						},
					},
				}
			},
			expectedPushSecrets: func(namespaces []v1.Namespace, created v1alpha1.ClusterPushSecret) []v1alpha1.PushSecret {
				updatedSpec := created.Spec.PushSecretSpec.DeepCopy()
				updatedSpec.SecretStoreRefs = []v1alpha1.PushSecretStoreRef{
					{
						Name: updateStoreName,
						Kind: "SecretStore",
					},
				}

				return []v1alpha1.PushSecret{
					{
						ObjectMeta: metav1.ObjectMeta{
							Namespace:   namespaces[0].Name,
							Name:        created.Name,
							Labels:      map[string]string{testLabelKey: testLabelValue},
							Annotations: map[string]string{testAnnotationKey: testAnnotationValue},
						},
						Spec: *updatedSpec,
					},
				}
			},
		}),
		Entry("Should not overwrite existing push secrets and error out if one is present", clusterPushSecretTestCase{
			namespaces: []v1.Namespace{
				{ObjectMeta: metav1.ObjectMeta{Name: randomNamespaceName()}},
			},
			clusterPushSecret: func(namespaces []v1.Namespace) v1alpha1.ClusterPushSecret {
				pes := defaultClusterPushSecret()
				pes.Spec.NamespaceSelectors = []*metav1.LabelSelector{
					{
						MatchLabels: map[string]string{kubernetesMetadataLabel: namespaces[0].Name},
					},
				}

				es := &v1alpha1.PushSecret{
					ObjectMeta: metav1.ObjectMeta{
						Name:      pes.Name,
						Namespace: namespaces[0].Name,
					},
					Spec: v1alpha1.PushSecretSpec{
						RefreshInterval: &metav1.Duration{Duration: time.Hour},
						SecretStoreRefs: []v1alpha1.PushSecretStoreRef{
							{
								Name: updateStoreName,
							},
						},
						Selector: v1alpha1.PushSecretSelector{
							Secret: &v1alpha1.PushSecretSecret{
								Name: secretName,
							},
						},
					},
				}
				Expect(k8sClient.Create(context.Background(), es)).ShouldNot(HaveOccurred())

				return *pes
			},
			sourceSecret: defaultSourceSecret,
			expectedClusterPushSecret: func(namespaces []v1.Namespace, created v1alpha1.ClusterPushSecret) v1alpha1.ClusterPushSecret {
				return v1alpha1.ClusterPushSecret{
					ObjectMeta: metav1.ObjectMeta{
						Name: created.Name,
					},
					Spec: created.Spec,
					Status: v1alpha1.ClusterPushSecretStatus{
						PushSecretName: created.Name,
						FailedNamespaces: []v1alpha1.ClusterPushSecretNamespaceFailure{
							{
								Namespace: namespaces[0].Name,
								Reason:    "push secret already exists in namespace",
							},
						},
						Conditions: []v1alpha1.PushSecretStatusCondition{
							{
								Type:    v1alpha1.PushSecretReady,
								Status:  v1.ConditionFalse,
								Message: "one or more namespaces failed",
							},
						},
					},
				}
			},
			expectedPushSecrets: func(namespaces []v1.Namespace, created v1alpha1.ClusterPushSecret) []v1alpha1.PushSecret {
				return []v1alpha1.PushSecret{
					{
						ObjectMeta: metav1.ObjectMeta{
							Namespace: namespaces[0].Name,
							Name:      created.Name,
						},
						Spec: v1alpha1.PushSecretSpec{
							RefreshInterval: &metav1.Duration{Duration: time.Hour},
							SecretStoreRefs: []v1alpha1.PushSecretStoreRef{
								{
									Name: updateStoreName,
									Kind: "SecretStore",
								},
							},
							UpdatePolicy: "Replace",
							Selector: v1alpha1.PushSecretSelector{
								Secret: &v1alpha1.PushSecretSecret{
									Name: secretName,
								},
							},
							DeletionPolicy: "None",
						},
					},
				}
			},
		}),
		Entry("Should crate an push secret if one with the same name has been deleted", clusterPushSecretTestCase{
			namespaces: []v1.Namespace{
				{ObjectMeta: metav1.ObjectMeta{Name: randomNamespaceName()}},
			},
			clusterPushSecret: func(namespaces []v1.Namespace) v1alpha1.ClusterPushSecret {
				pes := defaultClusterPushSecret()
				pes.Spec.NamespaceSelectors = []*metav1.LabelSelector{
					{
						MatchLabels: map[string]string{kubernetesMetadataLabel: namespaces[0].Name},
					},
				}

				es := &v1alpha1.PushSecret{
					ObjectMeta: metav1.ObjectMeta{
						Name:      pes.Name,
						Namespace: namespaces[0].Name,
					},
					Spec: v1alpha1.PushSecretSpec{
						RefreshInterval: &metav1.Duration{Duration: time.Hour},
						SecretStoreRefs: []v1alpha1.PushSecretStoreRef{
							{
								Name: updateStoreName,
							},
						},
						Selector: v1alpha1.PushSecretSelector{
							Secret: &v1alpha1.PushSecretSecret{
								Name: secretName,
							},
						},
					},
				}
				Expect(k8sClient.Create(context.Background(), es)).ShouldNot(HaveOccurred())
				return *pes
			},
			sourceSecret: defaultSourceSecret,
			beforeCheck: func(ctx context.Context, namespaces []v1.Namespace, created v1alpha1.ClusterPushSecret) {
				pes := v1alpha1.ClusterPushSecret{}
				Eventually(func(g Gomega) {
					key := types.NamespacedName{Namespace: created.Namespace, Name: created.Name}
					g.Expect(k8sClient.Get(ctx, key, &pes)).ShouldNot(HaveOccurred())
					g.Expect(len(pes.Status.FailedNamespaces)).Should(Equal(1))
				}).WithTimeout(timeout).WithPolling(interval).Should(Succeed())

				es := &v1alpha1.PushSecret{
					ObjectMeta: metav1.ObjectMeta{
						Name:      pes.Name,
						Namespace: namespaces[0].Name,
					},
				}
				Expect(k8sClient.Delete(ctx, es)).ShouldNot(HaveOccurred())
			},
			expectedClusterPushSecret: func(namespaces []v1.Namespace, created v1alpha1.ClusterPushSecret) v1alpha1.ClusterPushSecret {
				return v1alpha1.ClusterPushSecret{
					ObjectMeta: metav1.ObjectMeta{
						Name: created.Name,
					},
					Spec: created.Spec,
					Status: v1alpha1.ClusterPushSecretStatus{
						PushSecretName:        created.Name,
						ProvisionedNamespaces: []string{namespaces[0].Name},
						Conditions: []v1alpha1.PushSecretStatusCondition{
							{
								Type:   v1alpha1.PushSecretReady,
								Status: v1.ConditionTrue,
							},
						},
					},
				}
			},
			expectedPushSecrets: func(namespaces []v1.Namespace, created v1alpha1.ClusterPushSecret) []v1alpha1.PushSecret {
				return []v1alpha1.PushSecret{
					{
						ObjectMeta: metav1.ObjectMeta{
							Namespace: namespaces[0].Name,
							Name:      created.Name,
						},
						Spec: created.Spec.PushSecretSpec,
					},
				}
			},
		}),
		Entry("Should delete push secrets when namespaces no longer match", clusterPushSecretTestCase{
			namespaces: []v1.Namespace{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:   randomNamespaceName(),
						Labels: map[string]string{noneMatchingAnnotationKey: noneMatchingAnnotationVal},
					},
				},
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:   randomNamespaceName(),
						Labels: map[string]string{noneMatchingAnnotationKey: noneMatchingAnnotationVal},
					},
				},
			},
			sourceSecret: defaultSourceSecret,
			clusterPushSecret: func(_ []v1.Namespace) v1alpha1.ClusterPushSecret {
				pes := defaultClusterPushSecret()
				pes.Spec.RefreshInterval = &metav1.Duration{Duration: 100 * time.Millisecond}
				pes.Spec.NamespaceSelectors = []*metav1.LabelSelector{
					{
						MatchLabels: map[string]string{noneMatchingAnnotationKey: noneMatchingAnnotationVal},
					},
				}
				return *pes
			},
			beforeCheck: func(ctx context.Context, namespaces []v1.Namespace, created v1alpha1.ClusterPushSecret) {
				// Wait until the target ESs have been created
				Eventually(func(g Gomega) {
					for _, ns := range namespaces {
						key := types.NamespacedName{Namespace: ns.Name, Name: created.Name}
						g.Expect(k8sClient.Get(ctx, key, &v1alpha1.PushSecret{})).ShouldNot(HaveOccurred())
					}
				}).WithTimeout(timeout).WithPolling(interval).Should(Succeed())

				namespaces[0].Labels = map[string]string{}
				Expect(k8sClient.Update(ctx, &namespaces[0])).ShouldNot(HaveOccurred())
			},
			expectedClusterPushSecret: func(namespaces []v1.Namespace, created v1alpha1.ClusterPushSecret) v1alpha1.ClusterPushSecret {
				return v1alpha1.ClusterPushSecret{
					ObjectMeta: metav1.ObjectMeta{
						Name: created.Name,
					},
					Spec: created.Spec,
					Status: v1alpha1.ClusterPushSecretStatus{
						PushSecretName:        created.Name,
						ProvisionedNamespaces: []string{namespaces[1].Name},
						Conditions: []v1alpha1.PushSecretStatusCondition{
							{
								Type:   v1alpha1.PushSecretReady,
								Status: v1.ConditionTrue,
							},
						},
					},
				}
			},
			expectedPushSecrets: func(namespaces []v1.Namespace, created v1alpha1.ClusterPushSecret) []v1alpha1.PushSecret {
				return []v1alpha1.PushSecret{
					{
						ObjectMeta: metav1.ObjectMeta{
							Namespace: namespaces[1].Name,
							Name:      created.Name,
						},
						Spec: created.Spec.PushSecretSpec,
					},
				}
			},
		}),
		Entry("Should sync with match expression", clusterPushSecretTestCase{
			namespaces: []v1.Namespace{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:   randomNamespaceName(),
						Labels: map[string]string{"prefix": "foo"},
					},
				},
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:   randomNamespaceName(),
						Labels: map[string]string{"prefix": "bar"},
					},
				},
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:   randomNamespaceName(),
						Labels: map[string]string{"prefix": "baz"},
					},
				},
			},
			sourceSecret: defaultSourceSecret,
			clusterPushSecret: func(_ []v1.Namespace) v1alpha1.ClusterPushSecret {
				pes := defaultClusterPushSecret()
				pes.Spec.RefreshInterval = &metav1.Duration{Duration: 100 * time.Millisecond}
				pes.Spec.NamespaceSelectors = []*metav1.LabelSelector{
					{
						MatchExpressions: []metav1.LabelSelectorRequirement{
							{
								Key:      "prefix",
								Operator: metav1.LabelSelectorOpIn,
								Values:   []string{"foo", "bar"}, // "baz" is excluded
							},
						},
					},
				}
				return *pes
			},
			expectedClusterPushSecret: func(namespaces []v1.Namespace, created v1alpha1.ClusterPushSecret) v1alpha1.ClusterPushSecret {
				provisionedNamespaces := []string{namespaces[0].Name, namespaces[1].Name}
				sort.Strings(provisionedNamespaces)
				return v1alpha1.ClusterPushSecret{
					ObjectMeta: metav1.ObjectMeta{
						Name: created.Name,
					},
					Spec: created.Spec,
					Status: v1alpha1.ClusterPushSecretStatus{
						PushSecretName:        created.Name,
						ProvisionedNamespaces: provisionedNamespaces,
						Conditions: []v1alpha1.PushSecretStatusCondition{
							{
								Type:   v1alpha1.PushSecretReady,
								Status: v1.ConditionTrue,
							},
						},
					},
				}
			},
			expectedPushSecrets: func(namespaces []v1.Namespace, created v1alpha1.ClusterPushSecret) []v1alpha1.PushSecret {
				return []v1alpha1.PushSecret{
					{
						ObjectMeta: metav1.ObjectMeta{
							Namespace: namespaces[0].Name,
							Name:      created.Name,
						},
						Spec: created.Spec.PushSecretSpec,
					},
					{
						ObjectMeta: metav1.ObjectMeta{
							Namespace: namespaces[1].Name,
							Name:      created.Name,
						},
						Spec: created.Spec.PushSecretSpec,
					},
				}
			},
		}),
		Entry("Should be ready if no namespace matches", clusterPushSecretTestCase{
			namespaces: []v1.Namespace{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: randomNamespaceName(),
					},
				},
			},
			clusterPushSecret: func(_ []v1.Namespace) v1alpha1.ClusterPushSecret {
				pes := defaultClusterPushSecret()
				pes.Spec.NamespaceSelectors = []*metav1.LabelSelector{
					{
						MatchLabels: map[string]string{kubernetesMetadataLabel: "no-namespace-matches"},
					},
				}
				return *pes
			},
			sourceSecret: defaultSourceSecret,
			expectedClusterPushSecret: func(_ []v1.Namespace, created v1alpha1.ClusterPushSecret) v1alpha1.ClusterPushSecret {
				return v1alpha1.ClusterPushSecret{
					ObjectMeta: metav1.ObjectMeta{
						Name: created.Name,
					},
					Spec: created.Spec,
					Status: v1alpha1.ClusterPushSecretStatus{
						PushSecretName: created.Name,
						Conditions: []v1alpha1.PushSecretStatusCondition{
							{
								Type:   v1alpha1.PushSecretReady,
								Status: v1.ConditionTrue,
							},
						},
					},
				}
			},
			expectedPushSecrets: func([]v1.Namespace, v1alpha1.ClusterPushSecret) []v1alpha1.PushSecret {
				return []v1alpha1.PushSecret{}
			},
		}),
		Entry("Should be ready if namespace is selected via the namespace selectors", clusterPushSecretTestCase{
			namespaces: []v1.Namespace{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "namespace1",
						Labels: map[string]string{
							"key": "value1",
						},
					},
				},
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "namespace2",
						Labels: map[string]string{
							"key": "value2",
						},
					},
				},
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "namespace3",
						Labels: map[string]string{
							"key": "value3",
						},
					},
				},
			},
			clusterPushSecret: func(_ []v1.Namespace) v1alpha1.ClusterPushSecret {
				pes := defaultClusterPushSecret()
				pes.Spec.NamespaceSelectors = []*metav1.LabelSelector{
					{
						MatchLabels: map[string]string{"key": "value1"},
					},
					{
						MatchLabels: map[string]string{"key": "value2"},
					},
				}
				return *pes
			},
			sourceSecret: defaultSourceSecret,
			expectedClusterPushSecret: func(_ []v1.Namespace, created v1alpha1.ClusterPushSecret) v1alpha1.ClusterPushSecret {
				return v1alpha1.ClusterPushSecret{
					ObjectMeta: metav1.ObjectMeta{
						Name: created.Name,
					},
					Spec: created.Spec,
					Status: v1alpha1.ClusterPushSecretStatus{
						PushSecretName: created.Name,
						ProvisionedNamespaces: []string{
							"namespace1",
							"namespace2",
						},
						Conditions: []v1alpha1.PushSecretStatusCondition{
							{
								Type:   v1alpha1.PushSecretReady,
								Status: v1.ConditionTrue,
							},
						},
					},
				}
			},
			expectedPushSecrets: func(_ []v1.Namespace, created v1alpha1.ClusterPushSecret) []v1alpha1.PushSecret {
				return []v1alpha1.PushSecret{
					{
						ObjectMeta: metav1.ObjectMeta{
							Namespace: "namespace1",
							Name:      created.Name,
						},
						Spec: created.Spec.PushSecretSpec,
					},
					{
						ObjectMeta: metav1.ObjectMeta{
							Namespace: "namespace2",
							Name:      created.Name,
						},
						Spec: created.Spec.PushSecretSpec,
					},
				}
			},
		}))
})

var letterRunes = []rune("abcdefghijklmnopqrstuvwxyz")

func randString(n int) string {
	b := make([]rune, n)
	for i := range b {
		b[i] = letterRunes[rand.Intn(len(letterRunes))]
	}
	return string(b)
}

func randomNamespaceName() string {
	return fmt.Sprintf("testns-%s", randString(10))
}
