package network

import (
	"context"
	"fmt"

	"github.com/onsi/ginkgo"
	"github.com/onsi/gomega"
	"github.com/pingcap/errors"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	rbacv1 "k8s.io/api/rbac/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	apitypes "k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/shared-provider/pkg/scheme"
	"github.com/tidbcloud/shared-provider/pkg/types"
	labelsutil "github.com/tidbcloud/shared-provider/pkg/utils/labels"
	"github.com/tidbcloud/shared-provider/pkg/utils/namespace"
	"github.com/tidbcloud/shared-provider/tests/e2e/common/helper"
	"github.com/tidbcloud/shared-provider/tests/e2e/common/labels"
	"github.com/tidbcloud/shared-provider/tests/e2e/common/wait"
	"github.com/tidbcloud/shared-provider/tests/framework"
)

var _ = ginkgo.Describe("Network", func() {
	f := framework.New()

	framework.It(labels.With(
		labels.TNetwork,
		labels.Feature("Core"),
	), "check ns and network policy exist", func() {
		ctx := context.Background()
		net, _, _ := helper.Create(ctx, f, helper.TypeNetwork, true, "")

		ownedNsName := namespace.GetOwnedNamespaceNameFromNetwork(net)
		ownedNs := corev1.Namespace{}
		err := f.Client.Get(ctx, client.ObjectKey{Name: ownedNsName}, &ownedNs)
		gomega.Expect(err).NotTo(gomega.HaveOccurred())

		// NOTE: Get from server directly will lose TypeMeta
		scheme.Type(net)
		_, matched := labelsutil.MatchOwnerLabels(net, ownedNs.Labels)
		gomega.Expect(matched).To(gomega.BeTrue())
	})

	framework.It(labels.With(
		labels.TNetwork,
	), "check resources of tidb-operator exist", func() {
		ctx := context.Background()
		net, _, _ := helper.Create(ctx, f, helper.TypeNetwork, true, "")

		ownedNsName := namespace.GetOwnedNamespaceNameFromNetwork(net)
		deploy := &appsv1.Deployment{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "tidb-operator-controller-manager",
				Namespace: ownedNsName,
			},
		}

		ginkgo.By("ensure deployment does not exist if no cluster")
		err := f.Client.Get(ctx, client.ObjectKeyFromObject(deploy), deploy)
		gomega.Expect(errors.IsNotFound(err)).To(gomega.BeTrue())

		ginkgo.By("try to create a cluster")
		cluster := helper.CreateCluster(ctx, f, net, false, "")
		err = wait.WaitForObjectExists(ctx, f.Client, deploy, helper.DefaultTimeout)
		gomega.Expect(err).NotTo(gomega.HaveOccurred())

		// TODO: read name from addon dir
		ginkgo.By("check all resource has been created")
		sa := &corev1.ServiceAccount{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: ownedNsName,
				Name:      "tidb-operator-controller-manager",
			},
		}
		err = wait.WaitForObjectExists(ctx, f.Client, sa, helper.DefaultTimeout)
		gomega.Expect(err).NotTo(gomega.HaveOccurred())

		role := &rbacv1.Role{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: ownedNsName,
				Name:      "tidb-operator:controller-manager",
			},
		}
		err = wait.WaitForObjectExists(ctx, f.Client, role, helper.DefaultTimeout)
		gomega.Expect(err).NotTo(gomega.HaveOccurred())

		rb := &rbacv1.RoleBinding{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: ownedNsName,
				Name:      "tidb-operator:controller-manager",
			},
		}
		err = wait.WaitForObjectExists(ctx, f.Client, rb, helper.DefaultTimeout)
		gomega.Expect(err).NotTo(gomega.HaveOccurred())

		bsa := &corev1.ServiceAccount{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: ownedNsName,
				Name:      "tidb-backup-manager",
			},
		}
		err = wait.WaitForObjectExists(ctx, f.Client, bsa, helper.DefaultTimeout)
		gomega.Expect(err).NotTo(gomega.HaveOccurred())

		brole := &rbacv1.Role{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: ownedNsName,
				Name:      "tidb-backup-manager",
			},
		}
		err = wait.WaitForObjectExists(ctx, f.Client, brole, helper.DefaultTimeout)
		gomega.Expect(err).NotTo(gomega.HaveOccurred())

		brb := &rbacv1.RoleBinding{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: ownedNsName,
				Name:      "tidb-backup-manager",
			},
		}
		err = wait.WaitForObjectExists(ctx, f.Client, brb, helper.DefaultTimeout)
		gomega.Expect(err).NotTo(gomega.HaveOccurred())

		ginkgo.By("try to delete the cluster and ensure operator is deleted")
		err = f.InfraClient.Delete(ctx, cluster)
		gomega.Expect(err).NotTo(gomega.HaveOccurred())
		err = wait.WaitForObjectDeleted(ctx, f.Client, deploy, helper.LongTaskTimeout)
		gomega.Expect(err).NotTo(gomega.HaveOccurred())
	})

	framework.It(labels.With(
		labels.TNetwork,
	), "check network deletion", func() {
		ctx := context.Background()
		net, cluster, backup := helper.Create(ctx, f, helper.TypeBackup, true, "")

		err := f.InfraClient.Delete(ctx, net)
		gomega.Expect(err).NotTo(gomega.HaveOccurred())

		err = wait.WaitForNetworkPhase(ctx, f.InfraClient, net, cloudv1.NetworkPhasePendingDelete, helper.DefaultTimeout)
		gomega.Expect(err).NotTo(gomega.HaveOccurred())

		err = f.InfraClient.Delete(ctx, cluster)
		gomega.Expect(err).NotTo(gomega.HaveOccurred())

		err = wait.WaitForObjectDeleted(ctx, f.InfraClient, cluster, helper.LongTaskTimeout)
		gomega.Expect(err).NotTo(gomega.HaveOccurred())

		err = f.InfraClient.Delete(ctx, backup)
		gomega.Expect(err).NotTo(gomega.HaveOccurred())

		err = wait.WaitForObjectDeleted(ctx, f.InfraClient, net, helper.DefaultTimeout)
		gomega.Expect(err).NotTo(gomega.HaveOccurred())
	})

	framework.It(labels.With(
		labels.TNetwork,
		labels.Feature("Reschedule"),
	), "network reschedule", func() {
		ctx := context.Background()

		net, cluster, backup := helper.Create(ctx, f, helper.TypeBackup, true, "")

		ginkgo.By("try to reschedule network")
		reschedule(ctx, f.InfraClient, net, f.KubeClusterName(framework.SuffixAnother))

		ginkgo.By("reschedule failed because of cluster exists")
		err := wait.WaitForNetworkRescheduleFailed(ctx, f.InfraClient, net, "", helper.DefaultTimeout)
		gomega.Expect(err).NotTo(gomega.HaveOccurred())

		ginkgo.By("delete cluster")
		err = f.InfraClient.Delete(ctx, cluster)
		gomega.Expect(err).NotTo(gomega.HaveOccurred())

		ginkgo.By("wait for cluster deleted")
		err = wait.WaitForObjectDeleted(ctx, f.InfraClient, cluster, helper.LongTaskTimeout)
		gomega.Expect(err).NotTo(gomega.HaveOccurred())

		ownedNsName := namespace.GetOwnedNamespaceNameFromNetwork(net)

		ginkgo.By("get old namespace")
		oldNs := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: ownedNsName}}
		err = f.Client.Get(ctx, client.ObjectKeyFromObject(&oldNs), &oldNs)
		gomega.Expect(err).NotTo(gomega.HaveOccurred())

		ginkgo.By("try to reschedule network again")
		reschedule(ctx, f.InfraClient, net, f.KubeClusterName(framework.SuffixAnother))
		gomega.Expect(err).NotTo(gomega.HaveOccurred())

		ginkgo.By("reschedule network successfully")
		err = wait.WaitForNetworkRescheduled(
			ctx,
			f.InfraClient,
			net,
			f.KubeClusterName(framework.SuffixAnother),
			helper.DefaultTimeout,
		)
		gomega.Expect(err).NotTo(gomega.HaveOccurred())

		ginkgo.By("assert old namespace is deleted")
		ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: ownedNsName}}

		err = wait.WaitForCondition(ctx, f.Client, ns, func() bool {
			if ns.Status.Phase != corev1.NamespaceActive {
				return false
			}
			if ns.UID == oldNs.UID {
				return false
			}

			return true
		}, helper.LongTaskTimeout)

		gomega.Expect(err).NotTo(gomega.HaveOccurred())
		gomega.Expect(ns.UID).NotTo(gomega.Equal(oldNs.UID))

		ginkgo.By("check restore is successfully")
		newCluster := helper.CreateCluster(ctx, f, net, true, framework.SuffixAnother)
		helper.CreateRestore(ctx, f, net, newCluster, backup, true, framework.SuffixAnother)
	})

	ginkgo.Context("Compatibility", func() {
		framework.It(labels.With(
			labels.TNetwork,
			labels.Feature("Reschedule"),
			labels.FCompatibility,
			labels.From("release-0.5"),
		), "try to reschedule old backup", func() {

			ctx := context.Background()
			net, cluster, backup := helper.Create(ctx, f, helper.TypeBackup, true, "")

			ginkgo.By("delete cluster")
			err := f.InfraClient.Delete(ctx, cluster)
			gomega.Expect(err).NotTo(gomega.HaveOccurred())
			err = wait.WaitForObjectDeleted(ctx, f.InfraClient, cluster, helper.LongTaskTimeout)
			gomega.Expect(err).NotTo(gomega.HaveOccurred())

			// Upgrade to new manager.
			// NOTE(liubo02): It's not a real upgrade logic, so some display infos maybe wrong.
			// But it should be fine for business logic because all global resources(e.g. aws resources)
			// should be shared cross EKS in one region.
			// And tags.tidbcloud.com should be recoverable when reconciling.
			// TODO(liubo02): Upgrade by a more reasonable way
			ginkgo.By("upgrade manager")
			f.ScheduleTo(ctx, net, framework.SuffixUpgrade)
			f.ScheduleTo(ctx, backup, framework.SuffixUpgrade)

			// cluster fake doesn't exist, so we can simply wait for owned namespace deleted
			ginkgo.By("reschedule network")
			reschedule(ctx, f.InfraClient, net, f.KubeClusterName("fake"))

			ownedNsName := namespace.GetOwnedNamespaceNameFromNetwork(net)
			ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: ownedNsName}}
			err = wait.WaitForObjectDeleted(ctx, f.Client, &ns, helper.LongTaskTimeout)
			gomega.Expect(err).NotTo(gomega.HaveOccurred())

			// schedule back to cluster 1 to verify backup data is not broken
			ginkgo.By("schedule network back")
			f.ScheduleTo(ctx, net, framework.SuffixUpgrade)
			err = wait.WaitForNetworkPhase(ctx, f.InfraClient, net, cloudv1.NetworkPhaseNormal, helper.LongTaskTimeout)
			gomega.Expect(err).NotTo(gomega.HaveOccurred())

			ginkgo.By("check restore is successfully")
			newCluster := helper.CreateCluster(ctx, f, net, true, framework.SuffixUpgrade)
			helper.CreateRestore(ctx, f, net, newCluster, backup, true, framework.SuffixUpgrade)
		})
	})
})

func reschedule(ctx context.Context, c client.Client, net *cloudv1.Network, to string) {
	patch := fmt.Sprintf(
		`{"metadata":{"annotations":{"%s":"%s","%s":null}}}`,
		types.AnnoKeyReschedule,
		to,
		types.AnnoKeyRescheduleFailedReason,
	)

	err := c.Patch(ctx, net, client.RawPatch(apitypes.MergePatchType, []byte(patch)))
	gomega.Expect(err).NotTo(gomega.HaveOccurred())
}
