package integration

import (
	"context"
	"errors"
	"time"

	"k8s.io/apimachinery/pkg/api/resource"
	"k8s.io/utils/pointer"
	logf "sigs.k8s.io/controller-runtime/pkg/log"

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

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"

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

var _ = Describe("Hibernation controller", func() {
	const (
		timeout  = time.Second * 120
		interval = time.Second * 1
	)

	Context("When shoot hibernation controller running", func() {
		It("Should reconcile shoot", func() {
			By("Create a namespace for hibernation e2e suite")
			ctx := context.Background()
			ns := &corev1.Namespace{
				ObjectMeta: metav1.ObjectMeta{
					GenerateName: "hibernation-" + cloudv1.RandomString(5),
				},
			}
			Expect(k8sClient.Create(ctx, ns)).To(Succeed())

			By("Create a sample network")
			nt := &cloudv1.Network{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "network-" + cloudv1.RandomString(5),
					Namespace: ns.Name,
					Labels: map[string]string{
						HibernationReconciler: "true",
						cloudv1.ProjectLabel:  cloudv1.RandomString(5),
					},
				},
				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
			By("Sample network to ready")
			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())

			By("No cluster over idle threshold")
			By("Check k8s event")
			Eventually(func() bool {
				es := new(corev1.EventList)
				err := k8sClient.List(ctx, es, client.InNamespace(nt.Namespace))
				if err != nil {
					return false
				}
				var (
					receiveCleanProxyLoadbalancer = false
					receiveHibernationSuccessful  = false
				)
				for _, e := range es.Items {
					if e.Reason == "CleanProxyLoadbalancer" {
						receiveCleanProxyLoadbalancer = true
					}
					if e.Reason == "HibernationSuccessful" {
						receiveHibernationSuccessful = true
					}
				}
				if receiveCleanProxyLoadbalancer && receiveHibernationSuccessful {
					return true
				}
				return false
			}, timeout, interval).Should(BeTrue(), "Should receive proxy load balancer deleted and hibernation successful event")

			By("Ensure network status to hibernated")
			Eventually(func() bool {
				n := new(cloudv1.Network)
				err := k8sClient.Get(ctx, types.NamespacedName{
					Name:      nt.Name,
					Namespace: nt.Namespace,
				}, n)
				if err != nil {
					return false
				}
				if n.Status.Phase == cloudv1.NetworkPhaseHibernated {
					return true
				}
				return false
			}, timeout, interval).Should(BeTrue(), "Network status should be hibernated")

			By("Create a cluster in network")
			c := sampleCluster(nt.Namespace, nt.Name)
			Expect(k8sClient.Create(ctx, c)).To(Succeed())
			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")

			shoot := new(gardencorev1beta1.Shoot)
			Eventually(func() bool {
				// rebuild shoot context
				shootCtx, err := gardenerEnv.GetShootContext(ctx, shootKey.Name)
				if err != nil {
					return false
				}
				shootClient = shootCtx.Manager.GetClient()
				err = shootClient.Get(ctx, shootKey, shoot)
				if err != nil {
					return false
				}
				if !gardener.EnableHibernation(shoot) {
					return true
				}
				return false
			}, timeout, interval).Should(BeTrue(), "Shoot should be awake")

			By("Check k8s event")
			Eventually(func() bool {
				es := new(corev1.EventList)
				err := k8sClient.List(ctx, es, client.InNamespace(nt.Namespace))
				if err != nil {
					return false
				}
				for _, e := range es.Items {
					if e.Reason == "AwakeShoot" {
						return true
					}
				}
				return false
			}, timeout, interval).Should(BeTrue(), "Should receive awake shoot event")

			By("Check proxy service")
			Eventually(func() bool {
				svc := &corev1.Service{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "dbaas-proxy",
						Namespace: "tidb-admin",
					},
				}
				err := shootClient.Get(ctx, types.NamespacedName{
					Name:      svc.Name,
					Namespace: svc.Namespace,
				}, svc)
				if err != nil {
					return false
				}
				return true
			}, timeout, interval).Should(BeTrue(), "Proxy service should be recreated")

			By("Ensure network status will update to normal")
			Eventually(func() bool {
				// Set onlyShoot to false, network controller will update network status to normal if reconcile successful
				_, err := networkNormal(ctx, nt, false)
				if err != nil {
					logf.Log.Error(err, "networkNormal")
					return false
				}

				n := new(cloudv1.Network)
				err = k8sClient.Get(ctx, types.NamespacedName{
					Name:      nt.Name,
					Namespace: nt.Namespace,
				}, n)
				if err != nil {
					logf.Log.Error(err, "get network")
					return false
				}
				if n.Status.Phase == cloudv1.NetworkPhaseNormal {
					return true
				}
				return false
			}, timeout, interval).Should(BeTrue(), "Network status should be normal")

		})
	})
})

func sampleCluster(namespace, network string) *cloudv1.Cluster {
	return &cloudv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "cluster-" + cloudv1.RandomString(5),
			Namespace: namespace,
		},
		Spec: cloudv1.ClusterSpec{
			NetworkName: network,
			Cluster: cloudv1.ClusterTopology{
				TiDB: cloudv1.TiDB{
					Replicas:     1,
					InstanceType: "compute",
				},
				TiKV: cloudv1.TiKV{
					Replicas:     3,
					InstanceType: "storage",
					StorageSize:  resource.MustParse("1000Gi"),
				},
				PD: cloudv1.PD{
					Replicas:     1,
					InstanceType: "compute",
					StorageSize:  resource.MustParse("10Gi"),
				},
			},
			SqlEndpoints: cloudv1.SqlEndpoints{
				Public: cloudv1.Service{
					Enabled: pointer.Bool(true),
				},
			},
		},
	}
}
