package integration

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

	awskmsv1alpha1 "github.com/crossplane/provider-aws/apis/kms/v1alpha1"
	certmanagerv1alpha2 "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha2"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"github.com/pingcap/errors"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	"github.com/tidbcloud/infra-api/pkg/pingcap/label"

	"github.com/tidbcloud/infra-api/pkg/gardener/node_controller"

	"github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-api/controllers/cluster"
	"github.com/tidbcloud/infra-api/pkg/consts"
	"github.com/tidbcloud/infra-api/pkg/gardener"
	gardencorev1beta1 "github.com/tidbcloud/infra-api/pkg/gardener/core/apis/v1beta1"
)

var _ = Describe("Cluster controller", func() {
	BeforeEach(func() {
	})
	AfterEach(func() {
	})

	// Define utility constants for object names and testing timeouts/durations and intervals.
	const (
		timeout   = time.Second * 120
		interval  = time.Second * 1
		namespace = "cluster-test"
	)

	Context("When reconcile cluster", func() {
		It("Should reconcile AWS cluster", func() {
			ctx := context.Background()
			ns := &corev1.Namespace{
				ObjectMeta: metav1.ObjectMeta{
					Name: namespace,
				},
			}
			Expect(k8sClient.Create(ctx, ns)).To(Succeed())
			nt := &cloudv1.Network{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "network-" + cloudv1.RandomString(5),
					Namespace: namespace,
				},
				Spec: cloudv1.NetworkSpec{
					Provider: "aws",
					Region:   "us-east-2",
					CIDR:     "10.251.0.0/16",
				},
			}
			Expect(k8sClient.Create(ctx, nt)).To(Succeed())

			var shootKey types.NamespacedName
			var shootClient client.Client
			Eventually(func() error {
				gardenStatus, err := networkNormal(ctx, nt, true)
				if err != nil {
					return err
				}
				shootCtx, err := gardenerEnv.GetShootContext(ctx, gardenStatus.ShootName)
				if err != nil {
					return err
				}
				shootClient = shootCtx.Manager.GetClient()
				shootKey = types.NamespacedName{Namespace: gardenerEnv.GetShootNamespace(), Name: gardenStatus.ShootName}
				return nil
			}, timeout, interval).Should(Succeed())

			c := &cloudv1.Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Name:      fmt.Sprintf("%d", rand.Int()),
					Namespace: namespace,
				},
				Spec: cloudv1.ClusterSpec{
					NetworkName: nt.Name,
					Cluster: cloudv1.ClusterTopology{
						TiDB: cloudv1.TiDB{
							Replicas:     2,
							InstanceType: "compute",
						},
						TiKV: cloudv1.TiKV{
							Replicas:     3,
							InstanceType: "storage",
							StorageSize:  resource.MustParse("1000Gi"),
						},
						PD: cloudv1.PD{
							Replicas:     3,
							InstanceType: "compute",
							StorageSize:  resource.MustParse("10Gi"),
						},
						TiFlash: cloudv1.TiFlash{
							Replicas:     2,
							InstanceType: "storage",
							StorageSize:  resource.MustParse("1000Gi"),
						},
					},
					SqlEndpoints: cloudv1.SqlEndpoints{
						Public: cloudv1.Service{
							Enabled: pointer.Bool(true),
						},
					},
				},
			}
			Expect(k8sClient.Create(ctx, c)).To(Succeed())
			By("Should add finalizer to cluster")
			Eventually(func() bool {
				if err := k8sClient.Get(ctx, types.NamespacedName{Namespace: c.Namespace, Name: c.Name}, c); err != nil {
					return false
				}
				return controllerutil.ContainsFinalizer(c, cluster.Finalizer)
			}, timeout, interval).Should(BeTrue())

			By("Should ensure an encryption key")
			key := &awskmsv1alpha1.Key{}
			Eventually(func() error {
				return k8sClient.Get(ctx, types.NamespacedName{Name: c.ClusterScopeName()}, key)
			}, timeout, interval).Should(Succeed(), "key should be created")
			Expect(key.Labels[cloudv1.ProviderTypeLabel]).To(Equal(consts.ProviderTypeAWS))
			Expect(key.Labels[consts.ClusterNameLabelKey]).To(Equal(c.Name))
			Expect(key.Labels[consts.ClusterNamespaceLabelKey]).To(Equal(c.Namespace))

			assertAndComputeNodeGroups := func() map[cloudv1.ComponentType][]gardencorev1beta1.Worker {
				componentToNodeGroups := map[cloudv1.ComponentType][]gardencorev1beta1.Worker{}
				Eventually(func() error {
					shoot := new(gardencorev1beta1.Shoot)
					if err := k8sClient.Get(context.Background(), shootKey, shoot); err != nil {
						return err
					}
					topo := c.Spec.Cluster
					comps := []cloudv1.ClusterComponent{&topo.PD, &topo.TiDB, &topo.TiKV, &topo.TiFlash, &node_controller.AdminTiDB{TiDB: &topo.TiDB}}
					for _, comp := range comps {
						var ngs []gardencorev1beta1.Worker
						// naive replicas and ngs assertion that works for the numbers we selected
						desiredReplicas := comp.GetReplicas() / 3
						if desiredReplicas == 0 {
							desiredReplicas = 1
						}
						desiredNgs := comp.GetReplicas()
						if desiredNgs > 3 {
							desiredNgs = 3
						}
						for _, ng := range shoot.Spec.Provider.Workers {
							if ng.Labels[gardener.NodeGroupSetOwnerKey] == c.PrefixedId() &&
								ng.Labels["gardener.wg.prefix.name"] == fmt.Sprintf("%s-%s", c.PrefixedId(), string(comp.GetComponentType())) {
								if ng.Minimum != int32(desiredReplicas) || ng.Maximum != int32(desiredReplicas) {
									return fmt.Errorf("%s node group with minimum %d, maximum %d", comp.GetComponentType(), ng.Minimum, ng.Maximum)
								}
								ngs = append(ngs, ng)
							}
						}
						if len(ngs) != desiredNgs {
							return fmt.Errorf("node groups for %s mismatch, count: %d, desired: %d", comp.GetComponentType(), len(ngs), desiredNgs)
						}
						componentToNodeGroups[comp.GetComponentType()] = ngs
					}
					return nil
				}, timeout, interval).Should(Succeed())
				return componentToNodeGroups
			}

			By("Should ensure cluster node groups in shoot")
			initialNodeGroups := assertAndComputeNodeGroups()

			By("Should create TiDB cluster in shoot")
			tcKey := types.NamespacedName{
				Namespace: c.PrefixedId(),
				Name:      consts.ClusterPrefix,
			}
			tc := &v1alpha1.TidbCluster{}
			Eventually(func() error {
				return shootClient.Get(ctx, tcKey, tc)
			}, timeout, interval).Should(Succeed(), "TiDB cluster should be created")
			Expect(tc.Spec.TLSCluster.Enabled).To(BeTrue())
			Expect(int(tc.Spec.TiDB.Replicas)).To(Equal(c.Spec.Cluster.TiDB.Replicas))
			Expect(int(tc.Spec.TiKV.Replicas)).To(Equal(c.Spec.Cluster.TiKV.Replicas))
			Expect(int(tc.Spec.PD.Replicas)).To(Equal(c.Spec.Cluster.PD.Replicas))

			certificate := &certmanagerv1alpha2.Certificate{}
			Expect(shootClient.Get(ctx, types.NamespacedName{Namespace: c.PrefixedId(), Name: "db-tidb-server-secret"}, certificate)).
				To(Succeed(), "tls certificate should be created")

			adminTiDBKey := types.NamespacedName{
				Namespace: c.PrefixedId(),
				Name:      "db-tidb-extra-access",
			}
			adminTiDB := &appsv1.Deployment{}
			Eventually(func() error {
				return shootClient.Get(ctx, adminTiDBKey, adminTiDB)
			}, timeout, interval).Should(Succeed(), "admin TiDB should be created")

			Expect(adminTiDB.OwnerReferences).NotTo(BeEmpty())
			Expect(adminTiDB.OwnerReferences[0].Name).To(Equal(tc.Name), "Admin TiDB should be owned by tc")
			Expect(adminTiDB.OwnerReferences[0].Kind).To(Equal(tc.Kind), "Admin TiDB should be owned by tc")

			tidbMonitorKey := types.NamespacedName{
				Namespace: c.PrefixedId(),
				Name:      "db",
			}
			tidbMonitor := &v1alpha1.TidbMonitor{}
			Eventually(func() error {
				return shootClient.Get(ctx, tidbMonitorKey, tidbMonitor)
			}, timeout, interval).Should(Succeed(), "TiDB monitor should be created")
			Expect(tidbMonitor.OwnerReferences).NotTo(BeEmpty())
			Expect(tidbMonitor.OwnerReferences[0].Name).To(Equal(tc.Name), "TiDB monitor should be owned by tc")
			Expect(tidbMonitor.OwnerReferences[0].Kind).To(Equal(tc.Kind), "TiDB monitor should be owned by tc")

			Expect(*tidbMonitor.Spec.Prometheus.Config.ConfigMapRef.Namespace).To(Equal(tidbMonitor.Namespace))
			Expect(tidbMonitor.Spec.Prometheus.Config.ConfigMapRef.Name).To(Equal("external-prometheus-config"), "TiDB monitor should be mount external config")

			externalConfig := &corev1.ConfigMap{}
			Eventually(func() error {
				return shootClient.Get(ctx, types.NamespacedName{
					Namespace: tidbMonitor.Namespace,
					Name:      "external-prometheus-config"}, externalConfig)
			}, timeout, interval).Should(Succeed(), "TiDB monitor external prometheus config should be created")

			config := externalConfig.Data["prometheus-config"]

			var dropConfig = `
  - source_labels: [__name__]
    separator: ;
    regex: tikv_thread_nonvoluntary_context_switches|tikv_thread_voluntary_context_switches|tikv_threads_io_bytes_total
    action: drop
  - source_labels: [__name__,name]
    separator: ;
    regex: tikv_thread_cpu_seconds_total;(tokio|rocksdb).+
    action: drop`
			match := strings.Contains(config, dropConfig)
			Expect(match).To(Equal(true), "Prometheus config should be drop some tikv metrics")

			var dbCDCConfig = `regex: db-cdc`
			Expect(strings.Contains(config, dbCDCConfig)).To(Equal(true), "Prometheus config should be contain db-cdc scrape metric config")

			Eventually(func() error {
				if err := k8sClient.Get(ctx, types.NamespacedName{Namespace: c.Namespace, Name: c.Name}, c); err != nil {
					return err
				}
				if c.Status.Phase != cloudv1.ClusterPhaseRunning {
					return errors.New("wait cluster running")
				}
				return nil
			}, timeout, interval).Should(Succeed(), "Cluster object should become Running phase")

			By("Should scale cluster replicas in shoot")

			By("Should scale out cluster node groups in shoot")
			Eventually(func() error {
				_, err := controllerutil.CreateOrUpdate(ctx, k8sClient, c, func() error {
					c.Spec.Cluster.TiKV.Replicas = 6
					c.Spec.Cluster.TiDB.Replicas = 3
					c.Spec.Cluster.TiFlash.Replicas = 3
					return nil
				})
				return err
			}, timeout, interval).Should(Succeed(), "scale out cluster")
			Eventually(func() bool {
				if err := shootClient.Get(ctx, tcKey, tc); err != nil {
					return false
				}
				return int(tc.Spec.TiDB.Replicas) == c.Spec.Cluster.TiDB.Replicas &&
					int(tc.Spec.TiKV.Replicas) == c.Spec.Cluster.TiKV.Replicas &&
					int(tc.Spec.TiFlash.Replicas) == c.Spec.Cluster.TiFlash.Replicas
			}, timeout, interval).Should(BeTrue(), "TidbCluster should be scaled")

			_ = assertAndComputeNodeGroups()

			By("Should scale in cluster node groups in shoot")
			Eventually(func() error {
				_, err := controllerutil.CreateOrUpdate(ctx, k8sClient, c, func() error {
					c.Spec.Cluster.TiKV.Replicas = 3
					c.Spec.Cluster.TiDB.Replicas = 2
					c.Spec.Cluster.TiFlash.Replicas = 2
					return nil
				})
				return err
			}, timeout, interval).Should(Succeed(), "scale in cluster")
			Eventually(func() bool {
				if err := shootClient.Get(ctx, tcKey, tc); err != nil {
					return false
				}
				return int(tc.Spec.TiDB.Replicas) == c.Spec.Cluster.TiDB.Replicas &&
					int(tc.Spec.TiKV.Replicas) == c.Spec.Cluster.TiKV.Replicas &&
					int(tc.Spec.TiFlash.Replicas) == c.Spec.Cluster.TiFlash.Replicas
			}, timeout, interval).Should(BeTrue(), "TidbCluster should be scaled")
			Expect(tc.Annotations[label.AnnTiKVDeleteSlots]).To(Equal("[3,4,5]"), "TiKV should be evenly scaled-in")

			nodeGroupsAfterScaleIn := assertAndComputeNodeGroups()
			Expect(initialNodeGroups).To(Equal(nodeGroupsAfterScaleIn), "should scale in the newest node groups first")

			By("Should enable private endpoint successfully")
			Eventually(func() error {
				_, err := controllerutil.CreateOrUpdate(ctx, k8sClient, c, func() error {
					c.Spec.SqlEndpoints.Private = &cloudv1.Service{
						Enabled: pointer.Bool(true),
					}
					return nil
				})
				return err
			}, timeout, interval).Should(Succeed(), "call enable private endpoint API")
			Eventually(func() bool {
				err := k8sClient.Get(ctx, types.NamespacedName{Namespace: c.Namespace, Name: c.Name}, c)
				if err != nil {
					return false
				}
				return c.Status.SqlEndpoints.PrivateEndpoint != nil
			}, timeout, interval).Should(BeTrue(), "private endpoint should be enabled")

			By("Should disable endpoints successfully")
			Eventually(func() error {
				_, err := controllerutil.CreateOrUpdate(ctx, k8sClient, c, func() error {
					c.Spec.SqlEndpoints.Private = &cloudv1.Service{
						Enabled: pointer.Bool(false),
					}
					c.Spec.SqlEndpoints.Public = cloudv1.Service{
						Enabled: pointer.Bool(false),
					}
					return nil
				})
				return err
			}, timeout, interval).Should(Succeed(), "call API to disable all endpoints")
			Eventually(func() bool {
				err := k8sClient.Get(ctx, types.NamespacedName{Namespace: c.Namespace, Name: c.Name}, c)
				if err != nil {
					return false
				}
				return c.Status.SqlEndpoints.PrivateEndpoint == nil && c.Status.SqlEndpoints.PublicEndpoint == nil
			}, timeout, interval).Should(BeTrue(), "all endpoints should be disabled")

			By("Should cleanup all resources on cluster deletion")
			err := k8sClient.Delete(ctx, c)
			Expect(err).To(Succeed())

			Eventually(func() bool {
				err := shootClient.Get(ctx, tcKey, tc)
				return err != nil && apierrors.IsNotFound(err)
			}, timeout, interval).Should(BeTrue(), "TiDB cluster should be cleaned")

			Eventually(func() error {
				shoot := new(gardencorev1beta1.Shoot)
				if err := k8sClient.Get(context.Background(), shootKey, shoot); err != nil {
					return err
				}
				for _, ng := range shoot.Spec.Provider.Workers {
					if ng.Labels[gardener.NodeGroupSetOwnerKey] == c.ClusterScopeName() {
						return fmt.Errorf("node group owned by the cluster is not yet cleaned")
					}
				}
				return nil
			}, timeout, interval).Should(Succeed(), "node groups should be cleaned")

			Eventually(func() bool {
				err := k8sClient.Get(ctx, types.NamespacedName{Name: key.Name}, key)
				return err != nil && apierrors.IsNotFound(err)
			}, timeout, interval).Should(BeTrue(), "key should be cleaned")

		})
	})
})
