//go:build upgrader_integration
// +build upgrader_integration

package k8sversion_test

import (
	"context"
	"fmt"
	"time"

	"github.com/crossplane-contrib/provider-aws/apis/eks/v1beta1"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	"github.com/onsi/gomega/gstruct"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/eks-provider/controllers/k8sversion/common"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"github.com/tidbcloud/infra-provider/pkg/util"
)

func createNamespace(name string) {
	ns := &corev1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
	}
	Expect(k8sClient.Create(ctx, ns)).Should(Succeed())
}

func buildTestUpgradeOperation(name, ns, networkName string, apply func(*cloudv1.UpgradeOperation)) *cloudv1.UpgradeOperation {
	uo := &cloudv1.UpgradeOperation{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: ns,
		},
		Spec: cloudv1.UpgradeOperationSpec{
			NetworkName: networkName,
		},
	}

	if apply != nil {
		apply(uo)
	}
	return uo
}

func createNetwork(ctx context.Context, name, ns, phase string) *cloudv1.Network {
	network := &cloudv1.Network{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: ns,
		},
		Spec: cloudv1.NetworkSpec{
			Provider: "eks",
			Region:   "us-west-2",
			ForProvider: &runtime.RawExtension{
				Object: &runtime.Unknown{
					TypeMeta: runtime.TypeMeta{
						Kind:       "Unknown",
						APIVersion: schema.GroupVersion{Group: "cloud.pingcap.com", Version: "v1"}.String(),
					},
					Raw: []byte(`{
                   "apiVersion": "cloud.pingcap.com/v1",
                   "kind": "Unknown"
                }`),
				},
			},
		},
	}
	util.SetAnnotation(network, consts.AnnotationKeyDataplaneName, namer.LegacyBuildClusterNameByNetwork(network))
	Expect(k8sClient.Create(ctx, network)).Should(Succeed())

	network.Status.Phase = phase
	Expect(k8sClient.Status().Update(ctx, network)).Should(Succeed())
	return network
}

func createEKS(ctx context.Context, name, specVer, statusVer string) *v1beta1.Cluster {
	eks := &v1beta1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
		Spec: v1beta1.ClusterSpec{
			ForProvider: v1beta1.ClusterParameters{
				Version: pointer.String(specVer),
			},
		},
	}
	Expect(k8sClient.Create(ctx, eks)).Should(Succeed())

	eks.Status.AtProvider.Version = statusVer
	eks.Status.AtProvider.Status = v1beta1.ClusterStatusActive
	Expect(k8sClient.Status().Update(ctx, eks)).Should(Succeed())
	return eks
}

const (
	upgradeOperationName = "test"
	networkName          = "eks-us-west-2"
	originVersion        = "1.22"
	targetVersion        = "1.23"
)

var _ = Describe("EKS Upgrader", func() {
	It("Should NOT upgrade when the schedule is not reached", func() {
		ctx, cancel := context.WithCancel(context.Background())
		defer cancel()

		nsName := "test-schedule-not-reached"
		createNamespace(nsName)

		network := createNetwork(ctx, networkName, nsName, cloudv1.NetworkPhaseNormal)
		eksName := namer.LegacyBuildClusterNameByNetwork(network)
		_ = createEKS(ctx, eksName, originVersion, originVersion)

		steps := []cloudv1.Step{
			{
				Target:  cloudv1.ControlPlane,
				Version: targetVersion,
			},
		}
		// Make the current time is before the schedule
		mockClock.EXPECT().Now().Return(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)).AnyTimes()
		uo := buildTestUpgradeOperation(upgradeOperationName, nsName, network.Name, func(uo *cloudv1.UpgradeOperation) {
			uo.Spec.Schedule = "2024-01-02 00:00:00 +0000 UTC"
			uo.Spec.Steps = steps
		})
		Expect(k8sClient.Create(ctx, uo)).Should(Succeed())

		Eventually(func(g Gomega) {
			uo := &cloudv1.UpgradeOperation{}
			g.Expect(k8sClient.Get(ctx, client.ObjectKey{Name: upgradeOperationName, Namespace: nsName}, uo)).Should(Succeed())
			g.Expect(uo.Status.Phase).Should(Equal(cloudv1.UpgradeOperationPhasePending))
			g.Expect(uo.Status.Reason).Should(Equal(common.PendingReason))
			g.Expect(uo.Status.NextSteps.Size()).Should(Equal(0))
			g.Expect(uo.Status.FinishedSteps.Size()).Should(Equal(0))
			g.Expect(uo.Status.CurrentStep).Should(BeNil())

			// Verify the EKS version is not changed
			eks := &v1beta1.Cluster{}
			g.Expect(k8sClient.Get(ctx, client.ObjectKey{Name: eksName}, eks)).Should(Succeed())
			g.Expect(eks.Spec.ForProvider.Version).To(gstruct.PointTo(Equal(originVersion)))
			g.Expect(eks.Status.AtProvider.Version).Should(Equal(originVersion))
			g.Expect(eks.Status.AtProvider.Status).Should(Equal(v1beta1.ClusterStatusActive))
		}).Within(1 * time.Minute).ProbeEvery(time.Second).Should(Succeed())
	})

	It("Should begin to upgrade when the schedule is reach", func() {
		ctx, cancel := context.WithCancel(context.Background())
		defer cancel()

		nsName := "test-control-plane-upgrade"
		createNamespace(nsName)

		network := createNetwork(ctx, networkName, nsName, cloudv1.NetworkPhaseNormal)
		eksName := namer.LegacyBuildClusterNameByNetwork(network)
		_ = createEKS(ctx, eksName, originVersion, originVersion)

		steps := []cloudv1.Step{
			{
				Target:  cloudv1.ControlPlane,
				Version: targetVersion,
			},
		}
		// Make the current time is after the schedule
		mockClock.EXPECT().Now().Return(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)).AnyTimes()
		uo := buildTestUpgradeOperation(upgradeOperationName, nsName, network.Name, func(uo *cloudv1.UpgradeOperation) {
			uo.Spec.Schedule = "2023-12-31 23:59:00 +0000 UTC"
			uo.Spec.Steps = steps
		})
		Expect(k8sClient.Create(ctx, uo)).Should(Succeed())

		Eventually(func(g Gomega) {
			uo := &cloudv1.UpgradeOperation{}
			g.Expect(k8sClient.Get(ctx, client.ObjectKey{Name: upgradeOperationName, Namespace: nsName}, uo)).Should(Succeed())
			g.Expect(uo.Status.Phase).Should(Equal(cloudv1.UpgradeOperationPhaseRunning))
			g.Expect(uo.Status.Reason).Should(Equal(fmt.Sprintf("Begin to upgrade %s to %s", steps[0].Target, steps[0].Version)))
			g.Expect(uo.Status.NextSteps.Size()).Should(Equal(0))
			g.Expect(uo.Status.FinishedSteps.Size()).Should(Equal(0))
			g.Expect(uo.Status.CurrentStep).Should(Not(BeNil()))
			g.Expect(uo.Status.CurrentStep.Target).Should(Equal(steps[0].Target))
			g.Expect(uo.Status.CurrentStep.Version).Should(Equal(steps[0].Version))
			g.Expect(uo.Status.BeginTime).Should(Not(BeNil()))
			g.Expect(uo.Status.EndTime).Should(BeNil())
		}).Within(1 * time.Minute).ProbeEvery(time.Second).Should(Succeed())
	})

	It("Should NOT reconcile when the UpgradeOperation is paused", func() {
		ctx, cancel := context.WithCancel(context.Background())
		defer cancel()

		nsName := "test-paused"
		createNamespace(nsName)

		network := createNetwork(ctx, networkName, nsName, cloudv1.NetworkPhaseNormal)
		eksName := namer.LegacyBuildClusterNameByNetwork(network)
		_ = createEKS(ctx, eksName, originVersion, originVersion)

		steps := []cloudv1.Step{
			{
				Target:  cloudv1.ControlPlane,
				Version: targetVersion,
			},
		}
		// Make the current time is after the schedule
		mockClock.EXPECT().Now().Return(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)).AnyTimes()
		uo := buildTestUpgradeOperation(upgradeOperationName, nsName, network.Name, func(uo *cloudv1.UpgradeOperation) {
			uo.Spec.Schedule = "2023-12-31 23:59:00 +0000 UTC"
			uo.Spec.Steps = steps
			uo.Spec.Paused = true
		})
		Expect(k8sClient.Create(ctx, uo)).Should(Succeed())

		Consistently(func(g Gomega) {
			uo := &cloudv1.UpgradeOperation{}
			g.Expect(k8sClient.Get(ctx, client.ObjectKey{Name: upgradeOperationName, Namespace: nsName}, uo)).Should(Succeed())
			g.Expect(uo.Status.Phase).Should(Equal(cloudv1.UpgradeOperationPhaseNone))
			g.Expect(uo.Status.NextSteps.Size()).Should(Equal(0))
			g.Expect(uo.Status.FinishedSteps.Size()).Should(Equal(0))
			g.Expect(uo.Status.CurrentStep).Should(BeNil())
			g.Expect(uo.Status.BeginTime).Should(BeNil())
			g.Expect(uo.Status.EndTime).Should(BeNil())
		}).WithTimeout(1 * time.Minute).WithPolling(5 * time.Second).Should(Succeed())

		time.Sleep(1 * time.Minute)

		Expect(k8sClient.Get(ctx, client.ObjectKey{Name: upgradeOperationName, Namespace: nsName}, uo)).Should(Succeed())
		uo.Spec.Paused = false
		Expect(k8sClient.Update(ctx, uo)).Should(Succeed())

		// Should begin to upgrade
		Eventually(func(g Gomega) {
			uo := &cloudv1.UpgradeOperation{}
			g.Expect(k8sClient.Get(ctx, client.ObjectKey{Name: upgradeOperationName, Namespace: nsName}, uo)).Should(Succeed())
			g.Expect(uo.Status.Phase).Should(Equal(cloudv1.UpgradeOperationPhaseRunning))
			g.Expect(uo.Status.Reason).Should(Equal(fmt.Sprintf("Begin to upgrade %s to %s", steps[0].Target, steps[0].Version)))
			g.Expect(uo.Status.NextSteps.Size()).Should(Equal(0))
			g.Expect(uo.Status.FinishedSteps.Size()).Should(Equal(0))
			g.Expect(uo.Status.CurrentStep).Should(Not(BeNil()))
			g.Expect(uo.Status.CurrentStep.Target).Should(Equal(steps[0].Target))
			g.Expect(uo.Status.CurrentStep.Version).Should(Equal(steps[0].Version))
			g.Expect(uo.Status.BeginTime).Should(Not(BeNil()))
			g.Expect(uo.Status.EndTime).Should(BeNil())
		}).Within(5 * time.Minute).ProbeEvery(time.Second).Should(Succeed())
	})

	It("Should delete the UpgradeOperation when the TTL is reached", func() {
		ctx, cancel := context.WithCancel(context.Background())
		defer cancel()

		nsName := "test-ttl"
		createNamespace(nsName)

		network := createNetwork(ctx, networkName, nsName, cloudv1.NetworkPhaseNormal)

		// Make the current time is after the end time
		mockClock.EXPECT().Now().Return(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)).AnyTimes()
		uo := buildTestUpgradeOperation(upgradeOperationName, nsName, network.Name, func(uo *cloudv1.UpgradeOperation) {
			uo.Spec.Schedule = "2023-12-30 00:00:00 +0000 UTC"
			uo.Spec.Steps = cloudv1.NewSteps()
			uo.Spec.TTL = &metav1.Duration{Duration: 1 * time.Hour}
		})
		Expect(k8sClient.Create(ctx, uo)).Should(Succeed())
		uo.Status.BeginTime = &metav1.Time{Time: time.Date(2023, 12, 30, 0, 0, 5, 0, time.UTC)}
		uo.Status.EndTime = &metav1.Time{Time: time.Date(2023, 12, 31, 0, 0, 0, 0, time.UTC)}
		uo.Status.Phase = cloudv1.UpgradeOperationPhaseCompleted
		Expect(k8sClient.Status().Update(ctx, uo)).Should(Succeed())

		Eventually(func(g Gomega) {
			uo := &cloudv1.UpgradeOperation{}
			g.Expect(k8sClient.Get(ctx, client.ObjectKey{Name: upgradeOperationName, Namespace: nsName}, uo)).ShouldNot(Succeed())
		}).Within(3 * time.Minute).ProbeEvery(time.Minute).Should(Succeed())
	})
})
