package fixtures

import (
	"context"
	"fmt"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	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"
	"github.com/tidbcloud/infra-api/test/e2ev2/framework"
)

const (
	backupCompleteTimeout  = 15 * time.Minute
	backupDeleteTimeout    = 5 * time.Minute
	restoreCompleteTimeout = 15 * time.Minute
	restoreDeleteTimeout   = 5 * time.Minute
	importCompleteTimeout  = 30 * time.Minute
)

var errWait = fmt.Errorf("wait for condition met")

var _ = Describe("backup", func() {
	var (
		ctx = context.Background()
		f   = framework.DefaultFramework
		p   = f.Provider
		c   = f.InfraCli
		t   = f.TestContext
		ns  *corev1.Namespace
	)

	BeforeEach(func() {
		ns = &corev1.Namespace{
			ObjectMeta: metav1.ObjectMeta{
				GenerateName: fmt.Sprintf("e2e-%s-", p.Name()),
			},
		}
		f.ExpectNoError(c.Create(ctx, ns), "create namespace")
		f.Log("namespace: %s", ns.Name)
	})

	AfterEach(func() {
		f.ExpectNoError(c.Delete(ctx, ns), "delete namespace")
	})

	It("Backup and restore", func() {
		By("Create network")
		network := p.NewNetwork(ctx)
		network.Namespace = ns.Name
		f.ExpectNoError(c.Create(ctx, network), "create network")
		waitNetworkReady(ctx, network, c, t.NetworkCreateTimeout, t.Interval)
		f.Log("Network is ready")

		By("Connect network")
		_, err := p.ConnectNetwork(ctx, c, network)
		f.ExpectNoError(err, "connect to network")

		By("Create backup cluster")
		backupCluster := p.NewCluster(ctx, network)
		backupCluster.GenerateName = "backup-cluster-"
		f.ExpectNoError(c.Create(ctx, backupCluster), "create backup cluster")

		By("Create restore cluster")
		restoreCluster := p.NewCluster(ctx, network)
		restoreCluster.GenerateName = "restore-cluster-"
		f.ExpectNoError(c.Create(ctx, restoreCluster), "create restore cluster")

		By("Wait backup cluster ready")
		waitClusterReady(ctx, backupCluster, c, t.ClusterCreateTimeout, t.Interval)
		f.Log("Backup cluster is ready")

		By("Wait restore cluster ready")
		waitClusterReady(ctx, restoreCluster, c, t.ClusterCreateTimeout, t.Interval)
		f.Log("Restore cluster is ready")

		By("Init data")
		initSampleData(ctx, f, c, backupCluster, importCompleteTimeout, t.Interval)

		By("Create backup")
		backup := newBackup(ns.Name, backupCluster.Name, network.Labels)
		f.ExpectNoError(c.Create(ctx, backup), "create backup")

		By("Wait backup complete")
		waitBackupComplete(ctx, backup, c, backupCompleteTimeout, t.Interval)
		f.Log("Backup completed. backup.Status.BackupSize: %d", backup.Status.BackupSize)
		Expect(backup.Status.BackupSize > int64(0)).To(BeTrue())
		Expect(backup.Status.BackupTS).NotTo(BeEmpty())
		Expect(backup.Status.BucketName).NotTo(BeEmpty())

		By("Create restore")
		restore := newRestore(ns.Name, restoreCluster.Name, backup.Name, network.Labels)
		f.ExpectNoError(c.Create(ctx, restore))

		By("Wait restore complete")
		waitRestoreComplete(ctx, restore, c, restoreCompleteTimeout, t.Interval)
		f.Log("Restore completed")

		By("Delete backup")
		f.ExpectNoError(c.Delete(ctx, backup), "delete backup")

		By("Delete restore")
		f.ExpectNoError(c.Delete(ctx, restore), "delete restore")

		By("Wait backup deleted")
		waitBackupDeleted(ctx, backup, c, backupDeleteTimeout, t.Interval)

		By("Wait restore deleted")
		waitRestoreDeleted(ctx, restore, c, restoreDeleteTimeout, t.Interval)

		By("Delete backup cluster")
		f.ExpectNoError(c.Delete(ctx, backupCluster), "delete backup cluster")

		By("Delete restore cluster")
		f.ExpectNoError(c.Delete(ctx, restoreCluster), "delete restore cluster")

		By("Wait backup cluster deleted")
		waitClusterDeleted(ctx, backupCluster, c, t.ClusterDeleteTimeout, t.Interval)

		By("Wait restore cluster deleted")
		waitClusterDeleted(ctx, restoreCluster, c, t.ClusterDeleteTimeout, t.Interval)

		By("Delete network")
		f.ExpectNoError(c.Delete(ctx, network), "delete network")

		By("Wait network deleted")
		waitNetworkDeleted(ctx, network, c, t.NetworkDeleteTimeout, t.Interval)
		f.Log("Network deleted")
	})

})

func newBackup(namespace, clusterName string, labels map[string]string) *cloudv1.Backup {
	backup := &cloudv1.Backup{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: namespace,
			Name:      cloudv1.RandomString(6),
			Labels:    labels,
		},
		Spec: cloudv1.BackupSpec{
			ClusterName: clusterName,
		},
	}

	return backup
}

func newRestore(namespace, clusterName, backup string, labels map[string]string) *cloudv1.Restore {
	restore := &cloudv1.Restore{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: namespace,
			Name:      cloudv1.RandomString(6),
			Labels:    labels,
		},
		Spec: cloudv1.RestoreSpec{
			ClusterName: clusterName,
			From: cloudv1.From{
				Backup: backup,
			},
		},
	}

	return restore
}

func waitNetworkReady(ctx context.Context, network *cloudv1.Network, infraCli client.Client, timeout, interval time.Duration) {
	Eventually(func() error {
		err := infraCli.Get(ctx, types.NamespacedName{Namespace: network.Namespace, Name: network.Name}, network)
		if err != nil {
			return err
		}
		if network.Status.Phase != cloudv1.NetworkPhaseNormal {
			return errWait
		}
		return nil
	}, timeout, interval).Should(Succeed(), "Network should be ready")
}

func waitNetworkDeleted(ctx context.Context, network *cloudv1.Network, infraCli client.Client, timeout, interval time.Duration) {
	Eventually(func() error {
		err := infraCli.Get(ctx, types.NamespacedName{Namespace: network.Namespace, Name: network.Name}, network)
		if err != nil && !apierrors.IsNotFound(err) {
			return err
		}
		if apierrors.IsNotFound(err) {
			return nil
		}
		return errWait
	}, timeout, interval).Should(Succeed(), "network should be cleaned up")
}

func waitClusterReady(ctx context.Context, cluster *cloudv1.Cluster, infraCli client.Client, timeout, interval time.Duration) {
	Eventually(func() error {
		err := infraCli.Get(ctx, types.NamespacedName{Namespace: cluster.Namespace, Name: cluster.Name}, cluster)

		if err != nil {
			return err
		}
		if cluster.Status.Phase != cloudv1.ClusterPhaseRunning {
			return errWait
		}
		return nil
	}, timeout, interval).Should(Succeed(), "Cluster should be ready")
}

func waitClusterDeleted(ctx context.Context, cluster *cloudv1.Cluster, infraCli client.Client, timeout, interval time.Duration) {
	Eventually(func() error {
		err := infraCli.Get(ctx, types.NamespacedName{Namespace: cluster.Namespace, Name: cluster.Name}, cluster)
		if err != nil {
			if apierrors.IsNotFound(err) {
				return nil
			}
			return err
		}
		return errWait
	}, timeout, interval).Should(Succeed(), "Cluster should be cleaned up")
}

func waitBackupComplete(ctx context.Context, backup *cloudv1.Backup, infraCli client.Client, timeout, interval time.Duration) {
	Eventually(func() error {
		err := infraCli.Get(ctx, types.NamespacedName{Namespace: backup.Namespace, Name: backup.Name}, backup)
		if err != nil {
			return err
		}
		if backup.Status.Phase != cloudv1.BackupComplete {
			return errWait
		}
		return nil
	}, timeout, interval).Should(Succeed(), "Backup should complete")
}

func waitRestoreComplete(ctx context.Context, restore *cloudv1.Restore, infraCli client.Client, timeout, interval time.Duration) {
	Eventually(func() error {
		err := infraCli.Get(ctx, types.NamespacedName{Namespace: restore.Namespace, Name: restore.Name}, restore)
		if err != nil {
			return err
		}
		if restore.Status.Phase != cloudv1.RestoreComplete {
			return errWait
		}
		return nil
	}, timeout, interval).Should(Succeed(), "Restore should complete")
}

func initSampleData(ctx context.Context, fw *framework.Framework, infraCli client.Client, cluster *cloudv1.Cluster, timeout, interval time.Duration) {
	startTime := time.Now()
	importIns := fw.Provider.NewImport(ctx, cluster)
	fw.ExpectNoError(infraCli.Create(ctx, importIns), "create import")
	fw.Log("Create import: %s", importIns.Name)
	Eventually(func() error {
		err := infraCli.Get(ctx, types.NamespacedName{Namespace: importIns.Namespace, Name: importIns.Name}, importIns)
		if err != nil {
			return err
		}
		if importIns.Status.Phase != cloudv1.ImportCompleted {
			return errWait
		}

		return nil
	}, timeout, interval).Should(Succeed(), fmt.Sprintf("import status to normal within %v", timeout))
	fw.Log("Import used: %s", time.Since(startTime))
	fw.ExpectNoError(infraCli.Delete(ctx, importIns), "delete import")
}

func waitBackupDeleted(ctx context.Context, backup *cloudv1.Backup, infraCli client.Client, timeout, interval time.Duration) {
	Eventually(func() error {
		err := infraCli.Get(ctx, types.NamespacedName{Namespace: backup.Namespace, Name: backup.Name}, backup)
		if err != nil {
			if apierrors.IsNotFound(err) {
				return nil
			}
			return err
		}
		return errWait
	}, timeout, interval).Should(Succeed(), "Backup should be deleted")
}

func waitRestoreDeleted(ctx context.Context, restore *cloudv1.Restore, infraCli client.Client, timeout, interval time.Duration) {
	Eventually(func() error {
		err := infraCli.Get(ctx, types.NamespacedName{Namespace: restore.Namespace, Name: restore.Name}, restore)
		if err != nil {
			if apierrors.IsNotFound(err) {
				return nil
			}
			return err
		}
		return errWait
	}, timeout, interval).Should(Succeed(), "Restore should be deleted")
}
