/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 controllers

import (
	"context"
	"crypto/md5"
	"fmt"
	solrv1beta1 "github.com/apache/solr-operator/api/v1beta1"
	"github.com/apache/solr-operator/controllers/util"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

var _ = FDescribe("SolrCloud controller - Backup Repositories", func() {
	var (
		solrCloud *solrv1beta1.SolrCloud
	)

	BeforeEach(func() {
		solrCloud = &solrv1beta1.SolrCloud{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "foo",
				Namespace: "default",
			},
			Spec: solrv1beta1.SolrCloudSpec{},
		}
	})

	JustBeforeEach(func(ctx context.Context) {
		By("creating the SolrCloud")
		Expect(k8sClient.Create(ctx, solrCloud)).To(Succeed())

		By("defaulting the missing SolrCloud values")
		expectSolrCloudWithChecks(ctx, solrCloud, func(g Gomega, found *solrv1beta1.SolrCloud) {
			g.Expect(found.WithDefaults(logger)).To(BeFalse(), "The SolrCloud spec should not need to be defaulted eventually")
		})
	})

	AfterEach(func(ctx context.Context) {
		cleanupTest(ctx, solrCloud)
	})

	FContext("S3 Repository", func() {
		BeforeEach(func() {
			solrCloud.Spec = solrv1beta1.SolrCloudSpec{
				ZookeeperRef: &solrv1beta1.ZookeeperRef{
					ConnectionInfo: &solrv1beta1.ZookeeperConnectionInfo{
						InternalConnectionString: "host:7271",
					},
				},
				CustomSolrKubeOptions: solrv1beta1.CustomSolrKubeOptions{
					PodOptions: &solrv1beta1.PodOptions{
						EnvVariables: extraVars,
						Volumes:      extraVolumes,
						Annotations:  testPodAnnotations,
					},
				},
				BackupRepositories: []solrv1beta1.SolrBackupRepository{
					{
						Name: "test-repo",
						S3: &solrv1beta1.S3Repository{
							Region: "test-region",
							Bucket: "test-bucket",
						},
					},
				},
			}
		})
		FIt("has the correct resources", func(ctx context.Context) {
			By("testing the Solr ConfigMap")
			configMap := expectConfigMap(ctx, solrCloud, solrCloud.ConfigMapName(), map[string]string{"solr.xml": util.GenerateSolrXMLStringForCloud(solrCloud)})

			By("testing the Solr StatefulSet with explicit volumes and envVars before adding S3Repo credentials")
			// Make sure envVars and Volumes are correct be
			statefulSet := expectStatefulSet(ctx, solrCloud, solrCloud.StatefulSetName())

			// Annotations for the solrxml configMap
			solrXmlMd5 := fmt.Sprintf("%x", md5.Sum([]byte(configMap.Data[util.SolrXmlFile])))
			Expect(statefulSet.Spec.Template.Annotations).To(Equal(util.MergeLabelsOrAnnotations(testPodAnnotations, map[string]string{
				"solr.apache.org/solrXmlMd5":          solrXmlMd5,
				util.SolrBackupRepositoriesAnnotation: "test-repo",
				util.ServiceTypeAnnotation:            util.HeadlessServiceType,
			})), "Incorrect pod annotations")

			// Env Variable Tests
			expectedEnvVars := map[string]string{
				"ZK_HOST":             "host:7271/",
				"SOLR_HOST":           "$(POD_NAME)." + solrCloud.HeadlessServiceName() + "." + solrCloud.Namespace,
				"SOLR_PORT":           "8983",
				"SOLR_NODE_PORT":      "8983",
				"SOLR_PORT_ADVERTISE": "8983",
				"SOLR_OPTS":           "-DhostPort=$(SOLR_NODE_PORT)",
			}
			foundEnv := statefulSet.Spec.Template.Spec.Containers[0].Env

			// First test the extraVars, then the default Solr Operator vars
			Expect(foundEnv[len(foundEnv)-3:len(foundEnv)-1]).To(Equal(extraVars), "Extra Env Vars are not the same as the ones provided in podOptions")
			testPodEnvVariables(expectedEnvVars, append(foundEnv[:len(foundEnv)-3], foundEnv[len(foundEnv)-1]))

			// Check Volumes
			extraVolumes[0].DefaultContainerMount.Name = extraVolumes[0].Name
			Expect(statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts).To(HaveLen(len(extraVolumes)+1), "Container has wrong number of volumeMounts")
			Expect(statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts[1]).To(Equal(*extraVolumes[0].DefaultContainerMount), "Additional Volume from podOptions not mounted into container properly.")
			Expect(statefulSet.Spec.Template.Spec.Volumes).To(HaveLen(len(extraVolumes)+3), "Pod has wrong number of volumes")
			Expect(statefulSet.Spec.Template.Spec.Volumes[3].Name).To(Equal(extraVolumes[0].Name), "Additional Volume from podOptions not loaded into pod properly.")
			Expect(statefulSet.Spec.Template.Spec.Volumes[3].VolumeSource).To(Equal(extraVolumes[0].Source), "Additional Volume from podOptions not loaded into pod properly.")

			By("adding credentials to the S3 repository (envVars)")
			s3Credentials := &solrv1beta1.S3Credentials{
				AccessKeyIdSecret: &corev1.SecretKeySelector{
					LocalObjectReference: corev1.LocalObjectReference{Name: "aws-secret-1"},
					Key:                  "accessKeyId",
				},
				SecretAccessKeySecret: &corev1.SecretKeySelector{
					LocalObjectReference: corev1.LocalObjectReference{Name: "aws-secret-2"},
					Key:                  "secretAccessKey",
				},
				SessionTokenSecret: &corev1.SecretKeySelector{
					LocalObjectReference: corev1.LocalObjectReference{Name: "aws-secret-3"},
					Key:                  "sessionToken",
				},
			}
			foundSolrCloud := expectSolrCloudWithChecks(ctx, solrCloud, func(g Gomega, found *solrv1beta1.SolrCloud) {
				found.Spec.BackupRepositories[0].S3.Credentials = s3Credentials
				g.Expect(k8sClient.Update(ctx, found)).To(Succeed(), "Change the s3 credentials for the SolrCloud")
			})

			By("testing the Solr StatefulSet after adding S3Repo envVar credentials")
			// Make sure envVars and Volumes are correct be
			statefulSet = expectStatefulSetWithChecks(ctx, foundSolrCloud, foundSolrCloud.StatefulSetName(), func(g Gomega, found *appsv1.StatefulSet) {
				// Annotations for the solrxml configMap
				g.Expect(found.Spec.Template.Annotations).To(HaveKeyWithValue(util.SolrXmlMd5Annotation, fmt.Sprintf("%x", md5.Sum([]byte(configMap.Data[util.SolrXmlFile])))), "Wrong solr.xml MD5 annotation in the pod template!")

				// Env Variable Tests
				expectedEnvVars := map[string]string{
					"ZK_HOST":             "host:7271/",
					"SOLR_HOST":           "$(POD_NAME)." + foundSolrCloud.HeadlessServiceName() + "." + foundSolrCloud.Namespace,
					"SOLR_PORT":           "8983",
					"SOLR_NODE_PORT":      "8983",
					"SOLR_PORT_ADVERTISE": "8983",
					"SOLR_LOG_LEVEL":      "INFO",
					"SOLR_OPTS":           "-DhostPort=$(SOLR_NODE_PORT)",
				}
				foundEnv := found.Spec.Template.Spec.Containers[0].Env

				// First test the extraVars, then the default Solr Operator vars, then the S3 vars
				g.Expect(foundEnv[len(foundEnv)-3:len(foundEnv)-1]).To(Equal(extraVars), "Extra Env Vars are not the same as the ones provided in podOptions")
				testPodEnvVariablesWithGomega(g, expectedEnvVars, append(append([]corev1.EnvVar{}, foundEnv[:len(foundEnv)-6]...), foundEnv[len(foundEnv)-1]))
				g.Expect(foundEnv[len(foundEnv)-6:len(foundEnv)-3]).To(Equal([]corev1.EnvVar{
					{
						Name:      "AWS_ACCESS_KEY_ID",
						ValueFrom: &corev1.EnvVarSource{SecretKeyRef: s3Credentials.AccessKeyIdSecret},
					},
					{
						Name:      "AWS_SECRET_ACCESS_KEY",
						ValueFrom: &corev1.EnvVarSource{SecretKeyRef: s3Credentials.SecretAccessKeySecret},
					},
					{
						Name:      "AWS_SESSION_TOKEN",
						ValueFrom: &corev1.EnvVarSource{SecretKeyRef: s3Credentials.SessionTokenSecret},
					},
				}), "Wrong envVars for the S3 Credentials")

				// Check that no additional volumes have been added
				extraVolumes[0].DefaultContainerMount.Name = extraVolumes[0].Name
				g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts).To(HaveLen(len(extraVolumes)+1), "Container has wrong number of volumeMounts")
				g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts[1]).To(Equal(*extraVolumes[0].DefaultContainerMount), "Additional Volume from podOptions not mounted into container properly.")
				g.Expect(found.Spec.Template.Spec.Volumes).To(HaveLen(len(extraVolumes)+3), "Pod has wrong number of volumes")
				g.Expect(found.Spec.Template.Spec.Volumes[3].Name).To(Equal(extraVolumes[0].Name), "Additional Volume from podOptions not loaded into pod properly.")
				g.Expect(found.Spec.Template.Spec.Volumes[3].VolumeSource).To(Equal(extraVolumes[0].Source), "Additional Volume from podOptions not loaded into pod properly.")
			})

			By("adding credentials to the S3 repository (envVars & credentials file)")
			s3Credentials.CredentialsFileSecret = &corev1.SecretKeySelector{
				LocalObjectReference: corev1.LocalObjectReference{Name: "aws-credentials"},
				Key:                  "credentials-file",
			}
			foundSolrCloud = expectSolrCloudWithChecks(ctx, solrCloud, func(g Gomega, found *solrv1beta1.SolrCloud) {
				found.Spec.BackupRepositories[0].S3.Credentials = s3Credentials
				g.Expect(k8sClient.Update(ctx, found)).To(Succeed(), "Change the s3 credentials for the SolrCloud")
			})

			By("testing the Solr StatefulSet after adding S3Repo envVar credentials & credentials file")
			// Make sure envVars and Volumes are correct be
			statefulSet = expectStatefulSetWithChecks(ctx, foundSolrCloud, foundSolrCloud.StatefulSetName(), func(g Gomega, found *appsv1.StatefulSet) {
				// Annotations for the solrxml configMap
				g.Expect(found.Spec.Template.Annotations).To(HaveKeyWithValue(util.SolrXmlMd5Annotation, fmt.Sprintf("%x", md5.Sum([]byte(configMap.Data[util.SolrXmlFile])))), "Wrong solr.xml MD5 annotation in the pod template!")

				// Env Variable Tests
				expectedEnvVars := map[string]string{
					"ZK_HOST":             "host:7271/",
					"SOLR_HOST":           "$(POD_NAME)." + foundSolrCloud.HeadlessServiceName() + "." + foundSolrCloud.Namespace,
					"SOLR_PORT":           "8983",
					"SOLR_NODE_PORT":      "8983",
					"SOLR_PORT_ADVERTISE": "8983",
					"SOLR_LOG_LEVEL":      "INFO",
					"SOLR_OPTS":           "-DhostPort=$(SOLR_NODE_PORT)",
				}
				foundEnv := found.Spec.Template.Spec.Containers[0].Env

				// First test the extraVars, then the default Solr Operator vars, then the S3 vars
				g.Expect(foundEnv[len(foundEnv)-3:len(foundEnv)-1]).To(Equal(extraVars), "Extra Env Vars are not the same as the ones provided in podOptions")
				testPodEnvVariablesWithGomega(g, expectedEnvVars, append(append([]corev1.EnvVar{}, foundEnv[:len(foundEnv)-7]...), foundEnv[len(foundEnv)-1]))
				g.Expect(foundEnv[len(foundEnv)-7:len(foundEnv)-3]).To(Equal([]corev1.EnvVar{
					{
						Name:      "AWS_ACCESS_KEY_ID",
						ValueFrom: &corev1.EnvVarSource{SecretKeyRef: s3Credentials.AccessKeyIdSecret},
					},
					{
						Name:      "AWS_SECRET_ACCESS_KEY",
						ValueFrom: &corev1.EnvVarSource{SecretKeyRef: s3Credentials.SecretAccessKeySecret},
					},
					{
						Name:      "AWS_SESSION_TOKEN",
						ValueFrom: &corev1.EnvVarSource{SecretKeyRef: s3Credentials.SessionTokenSecret},
					},
					{
						Name:  "AWS_SHARED_CREDENTIALS_FILE",
						Value: "/var/solr/data/backup-restore/test-repo/s3credential/credentials",
					},
				}), "Wrong envVars for the S3 Credentials")

				// Check that no additional volumes have been added
				extraVolumes[0].DefaultContainerMount.Name = extraVolumes[0].Name
				g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts).To(HaveLen(len(extraVolumes)+2), "Container has wrong number of volumeMounts")
				g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts[1].Name).To(Equal("backup-repository-test-repo"), "S3Credentials file volumeMount has wrong name.")
				g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts[1].MountPath).To(Equal("/var/solr/data/backup-restore/test-repo/s3credential"), "S3Credentials file volumeMount has wrong mount path.")
				g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts[1].ReadOnly).To(BeTrue(), "S3Credentials file volumeMount must be read-only.")
				g.Expect(found.Spec.Template.Spec.Containers[0].VolumeMounts[2]).To(Equal(*extraVolumes[0].DefaultContainerMount), "Additional Volume from podOptions not mounted into container properly.")
				g.Expect(found.Spec.Template.Spec.Volumes).To(HaveLen(len(extraVolumes)+4), "Pod has wrong number of volumes")
				g.Expect(found.Spec.Template.Spec.Volumes[3].Name).To(Equal("backup-repository-test-repo"), "S3Credentials file volume has wrong name.")
				g.Expect(found.Spec.Template.Spec.Volumes[3].VolumeSource.Secret).To(Not(BeNil()), "S3Credentials file has to be loaded via a secret volume.")
				g.Expect(found.Spec.Template.Spec.Volumes[3].VolumeSource.Secret.SecretName).To(Equal(s3Credentials.CredentialsFileSecret.Name), "S3Credentials file is loaded into the pod using the wrong secret name.")
				g.Expect(found.Spec.Template.Spec.Volumes[3].VolumeSource.Secret.Items).To(Equal(
					[]corev1.KeyToPath{{Key: s3Credentials.CredentialsFileSecret.Key, Path: util.S3CredentialFileName}}), "S3Credentials file pod volume has the wrong items.")
				g.Expect(found.Spec.Template.Spec.Volumes[3].VolumeSource.Secret.DefaultMode).To(BeEquivalentTo(&util.SecretReadOnlyPermissions), "S3Credentials file pod volume has the wrong default mode.")
				g.Expect(found.Spec.Template.Spec.Volumes[4].Name).To(Equal(extraVolumes[0].Name), "Additional Volume from podOptions not loaded into pod properly.")
				g.Expect(found.Spec.Template.Spec.Volumes[4].VolumeSource).To(Equal(extraVolumes[0].Source), "Additional Volume from podOptions not loaded into pod properly.")
			})

			By("adding extra options to the S3 repository")
			s3Credentials.CredentialsFileSecret = &corev1.SecretKeySelector{
				LocalObjectReference: corev1.LocalObjectReference{Name: "aws-credentials"},
				Key:                  "credentials-file",
			}
			foundSolrCloud = expectSolrCloudWithChecks(ctx, solrCloud, func(g Gomega, found *solrv1beta1.SolrCloud) {
				found.Spec.BackupRepositories[0].S3.Endpoint = "https://test-endpoint:3223"
				g.Expect(k8sClient.Update(ctx, found)).To(Succeed(), "Change the s3 endpoint for the SolrCloud")
			})

			By("checking the solr.xml configMap is updated with the new S3 options, and a solrCloud rolling update follows")
			newConfigMap := expectConfigMap(ctx, foundSolrCloud, foundSolrCloud.ConfigMapName(), map[string]string{"solr.xml": util.GenerateSolrXMLStringForCloud(foundSolrCloud)})

			updateSolrXmlMd5 := fmt.Sprintf("%x", md5.Sum([]byte(newConfigMap.Data[util.SolrXmlFile])))
			Expect(updateSolrXmlMd5).To(Not(Equal(solrXmlMd5)), "New configMap hash should not equal initial configMap hash")
			expectStatefulSetWithChecks(ctx, solrCloud, solrCloud.StatefulSetName(), func(g Gomega, found *appsv1.StatefulSet) {
				g.Expect(found.Spec.Template.Annotations).To(HaveKeyWithValue(util.SolrXmlMd5Annotation, updateSolrXmlMd5), "Custom solr.xml MD5 annotation should be updated on the pod template.")
			})
		})
	})

	FContext("Multiple Repositories - Annotations", func() {
		BeforeEach(func() {
			solrCloud.Spec = solrv1beta1.SolrCloudSpec{
				ZookeeperRef: &solrv1beta1.ZookeeperRef{
					ConnectionInfo: &solrv1beta1.ZookeeperConnectionInfo{
						InternalConnectionString: "host:7271",
					},
				},
				CustomSolrKubeOptions: solrv1beta1.CustomSolrKubeOptions{
					PodOptions: &solrv1beta1.PodOptions{
						EnvVariables: extraVars,
						Volumes:      extraVolumes,
					},
				},
				BackupRepositories: []solrv1beta1.SolrBackupRepository{
					{
						Name: "test-repo",
						S3: &solrv1beta1.S3Repository{
							Region: "test-region",
							Bucket: "test-bucket",
						},
					},
					{
						Name: "another",
						S3: &solrv1beta1.S3Repository{
							Region: "test-region-2",
							Bucket: "test-bucket-2",
						},
					},
				},
			}
		})
		FIt("has the correct resources", func(ctx context.Context) {
			By("testing the Solr ConfigMap")
			configMap := expectConfigMap(ctx, solrCloud, solrCloud.ConfigMapName(), map[string]string{"solr.xml": util.GenerateSolrXMLStringForCloud(solrCloud)})

			By("testing the Solr StatefulSet with explicit volumes and envVars before adding S3Repo credentials")
			// Make sure envVars and Volumes are correct be
			statefulSet := expectStatefulSet(ctx, solrCloud, solrCloud.StatefulSetName())

			// Annotations for the solrxml configMap
			Expect(statefulSet.Spec.Template.Annotations).To(Equal(map[string]string{
				"solr.apache.org/solrXmlMd5":          fmt.Sprintf("%x", md5.Sum([]byte(configMap.Data["solr.xml"]))),
				util.SolrBackupRepositoriesAnnotation: "another,test-repo",
				util.ServiceTypeAnnotation:            util.HeadlessServiceType,
			}), "Incorrect pod annotations")
		})
	})
})
