package tests_test

import (
	"context"
	"crypto/tls"
	"errors"
	"fmt"
	"os/exec"
	"time"

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

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	"kubevirt.io/containerized-data-importer/tests/framework"
)

const (
	annNotAfter  = "auth.openshift.io/certificate-not-after"
	annNotBefore = "auth.openshift.io/certificate-not-before"
)

var _ = Describe("Cert rotation tests", Serial, func() {
	f := framework.NewFramework("certrotation-test")

	Context("with port forward", func() {
		var cmd *exec.Cmd

		AfterEach(func() {
			afterCMD(cmd)
		})

		DescribeTable("check secrets re read", func(serviceName, secretName string) {
			var (
				err      error
				hostPort string
			)
			hostPort, cmd, err = startServicePortForward(f, serviceName)
			Expect(err).ToNot(HaveOccurred())

			var conn *tls.Conn

			Eventually(func() error {
				conn, err = tls.Dial("tcp", hostPort, &tls.Config{
					InsecureSkipVerify: true, //nolint: gosec // It's not production code
				})
				return err
			}, 10*time.Second, 1*time.Second).ShouldNot(HaveOccurred())

			oldExpire := conn.ConnectionState().PeerCertificates[0].NotAfter
			conn.Close()

			rotateCert(f, secretName)

			Eventually(func() error {
				conn, err = tls.Dial("tcp", hostPort, &tls.Config{
					InsecureSkipVerify: true, //nolint: gosec // It's not production code
				})

				if err != nil {
					return err
				}

				defer conn.Close()

				if conn.ConnectionState().PeerCertificates[0].NotAfter.After(oldExpire) {
					return nil
				}

				return fmt.Errorf("Expire time not updated")

			}, 2*time.Minute, 1*time.Second).ShouldNot(HaveOccurred())

		}, Entry("[test_id:3925]apiserver", "cdi-api", "cdi-apiserver-server-cert"),
			Entry("[test_id:3926]uploadproxy", "cdi-uploadproxy", "cdi-uploadproxy-server-cert"))
	})

	DescribeTable("check secrets updated", func(secretName, configMapName string) {
		var oldBundle *corev1.ConfigMap
		oldSecret, err := f.K8sClient.CoreV1().Secrets(f.CdiInstallNs).Get(context.TODO(), secretName, metav1.GetOptions{})
		Expect(err).ToNot(HaveOccurred())

		if configMapName != "" {
			oldBundle, err = f.K8sClient.CoreV1().ConfigMaps(f.CdiInstallNs).Get(context.TODO(), configMapName, metav1.GetOptions{})
			Expect(err).ToNot(HaveOccurred())
		}

		rotateCert(f, secretName)

		Eventually(func() bool {
			updatedSecret, err := f.K8sClient.CoreV1().Secrets(f.CdiInstallNs).Get(context.TODO(), secretName, metav1.GetOptions{})
			Expect(err).ToNot(HaveOccurred())

			return updatedSecret.Annotations[annNotAfter] != oldSecret.Annotations[annNotAfter] &&
				string(updatedSecret.Data["tls.cert"]) != string(oldSecret.Data["tls.crt"]) &&
				string(updatedSecret.Data["tls.key"]) != string(oldSecret.Data["tls.key"])

		}, 60*time.Second, 1*time.Second).Should(BeTrue())

		if configMapName != "" {
			Eventually(func() bool {
				updatedBundle, err := f.K8sClient.CoreV1().ConfigMaps(f.CdiInstallNs).Get(context.TODO(), configMapName, metav1.GetOptions{})
				Expect(err).ToNot(HaveOccurred())

				return updatedBundle.Data["ca-bundle.crt"] != oldBundle.Data["ca-bundle.crt"]

			}, 60*time.Second, 1*time.Second).Should(BeTrue())
		}

	}, // not supported
		//Entry("apiserver ca", "cdi-apiserver-signer", "cdi-apiserver-signer-bundle"),
		// done in test above
		//Entry("apiserver cert", "cdi-apiserver-server-cert", ""),
		Entry("[test_id:3927]uploadproxy ca", "cdi-uploadproxy-signer", "cdi-uploadproxy-signer-bundle"),
		//done in test above
		//Entry("uploadproxy cert", "cdi-uploadproxy-server-cert", ""),
		Entry("[test_id:3928]uploadserver ca", "cdi-uploadserver-signer", "cdi-uploadserver-signer-bundle"),
		Entry("[test_id:3929]uploadserver client ca", "cdi-uploadserver-client-signer", "cdi-uploadserver-client-signer-bundle"),
		Entry("[test_id:3930]uploadserver client cert", "cdi-uploadserver-client-cert", ""),
	)
})

func rotateCert(f *framework.Framework, secretName string) {
	secret, err := f.K8sClient.CoreV1().Secrets(f.CdiInstallNs).Get(context.TODO(), secretName, metav1.GetOptions{})
	Expect(err).ToNot(HaveOccurred())

	nb, ok := secret.Annotations[annNotBefore]
	Expect(ok).To(BeTrue())

	notBefore, err := time.Parse(time.RFC3339, nb)
	Expect(err).ToNot(HaveOccurred())
	Expect(time.Since(notBefore).Seconds()).To(BeNumerically(">", 0))

	newSecret := secret.DeepCopy()
	newSecret.Annotations[annNotAfter] = notBefore.Add(time.Second).Format(time.RFC3339)

	_, err = f.K8sClient.CoreV1().Secrets(f.CdiInstallNs).Update(context.TODO(), newSecret, metav1.UpdateOptions{})
	Expect(err).ToNot(HaveOccurred())
}

func startServicePortForward(f *framework.Framework, serviceName string) (string, *exec.Cmd, error) {
	lp := "18443"
	pm := lp + ":443"
	hostPort := "127.0.0.1:" + lp

	cmd := f.CreateKubectlCommand("-n", f.CdiInstallNs, "port-forward", "svc/"+serviceName, pm)
	err := cmd.Start()
	if err != nil {
		return "", nil, err
	}

	return hostPort, cmd, nil
}

func afterCMD(cmd *exec.Cmd) {
	if cmd != nil {
		ExpectWithOffset(1, cmd.Process.Kill()).Should(Succeed())
		err := cmd.Wait()
		if err != nil {
			t := &exec.ExitError{}
			ExpectWithOffset(1, errors.As(err, &t)).Should(BeTrue())
		}
	}
}
