package e2e

import (
	"context"
	"flag"
	"fmt"
	"math/rand"
	"os"
	"strings"
	"testing"
	"time"

	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-api/pkg/k8s"
	"go.uber.org/multierr"
	"go.uber.org/zap"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	restclient "k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/envtest/printer"

	"github.com/tidbcloud/shared-provider/pkg/scheme"
	"github.com/tidbcloud/shared-provider/pkg/utils/labels"
)

var namespacePrefix string

var namespace string
var namespace2 string

var kubeconfig string
var kubecontext string
var infraCli client.Client

var logger *zap.SugaredLogger
var awsNetwork *cloudv1.Network
var awsNetwork2 *cloudv1.Network
var kubernetesLabel string
var awsRegion string

var eksKubeconfig string
var eksKubecontext string
var eksClient client.Client

// To test just against the API:
// go test -kube-config=./infra-api.kubeconfig -timeout 60m
//
// Te test against a specify manager, specify kube-label to set the tags.tidbcloud.com/kubernetes label value:
// go test -kube-config=./infra-api.kubeconfig -kube-label=dev-us-east-1-s01 -timeout 60m
func TestMain(m *testing.M) {
	flags := flag.CommandLine
	flags.StringVar(&kubeconfig, "kube-config", os.Getenv("KUBECONFIG"), "the kubeconfig path to access infra apiserver")
	flags.StringVar(&kubecontext, "kube-context", "", "context of kubeconfig, default use current context")
	flags.StringVar(&eksKubeconfig, "eks-kube-config", "", "the target kubeconfig path to deploy tidb clusters")
	flags.StringVar(&eksKubecontext, "eks-kube-context", "", "context of kubeconfig, default use current context")
	flags.StringVar(&namespacePrefix, "namespace", "shared-e2e", "the namespace prefix to run e2e test")
	flags.StringVar(&awsRegion, "aws-region", os.Getenv("AWS_REGION"), "region of aws")
	flags.StringVar(
		&kubernetesLabel,
		"kube-label",
		"",
		"specify tags.tidbcloud.com/kubernetes value directly when create resources",
	)

	RegisterFailHandler(Fail)
	rand.Seed(time.Now().UnixNano())
	os.Exit(m.Run())
}

func initClient() error {
	restConfig, err := loadKubeRestConfig(kubeconfig, kubecontext)
	if err != nil {
		return err
	}

	infraCli, err = client.New(restConfig, client.Options{
		Scheme: scheme.Scheme,
	})
	if err != nil {
		return err
	}

	logger.Info("eks kubeconfig: ", eksKubeconfig)
	if eksKubeconfig != "" {
		logger.Info("init eks client")
		restConfig, err := loadKubeRestConfig(eksKubeconfig, eksKubecontext)
		if err != nil {
			return err
		}
		eksClient, err = client.New(restConfig, client.Options{
			Scheme: scheme.Scheme,
		})
		if err != nil {
			return err
		}
	}

	return nil
}

func TestE2E(t *testing.T) {
	flags := flag.CommandLine
	if awsRegion == "" {
		fmt.Println("must set -aws-region")
		flags.Usage()
		os.Exit(-1)
	}

	err := initClient()
	if err != nil {
		logger.Error("failed to init client ", err)
		os.Exit(-1)
	}

	RunSpecsWithDefaultAndCustomReporters(t,
		"E2E Suite",
		[]Reporter{printer.NewlineReporter{}})
}

func init() {
	baseLog, err := zap.NewDevelopment()
	if err != nil {
		panic(err)
	}
	logger = baseLog.Sugar()
}

var _ = SynchronizedBeforeSuite(func() []byte {
	// run synchronized
	// randomize a namespace name to avoid collision
	namespace = fmt.Sprintf("%s-%s", namespacePrefix, cloudv1.RandomString(6))
	namespace2 = namespace + "-2"

	ns := &corev1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			Name: namespace,
			Labels: map[string]string{
				"testing.tidbcloud.com/for":    "e2e",
				"testing.tidbcloud.com/e2e-id": kubernetesLabel,
			},
		},
	}
	ctx := context.Background()
	_, err := k8s.CreateIgnoreExists(ctx, infraCli, ns)
	Expect(err).To(Succeed())
	ns = &corev1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			Name: namespace2,
			Labels: map[string]string{
				"testing.tidbcloud.com/for":    "e2e",
				"testing.tidbcloud.com/e2e-id": kubernetesLabel,
			},
		},
	}
	_, err = k8s.CreateIgnoreExists(ctx, infraCli, ns)
	Expect(err).To(Succeed())

	awsNetwork = newCommonAWSNetwork(awsRegion)
	awsNetwork.Name = "ns1-test"
	_, err = k8s.CreateIgnoreExists(ctx, infraCli, awsNetwork)
	Expect(err).Should(Succeed(), "network %+v", awsNetwork)
	waitNetworkReady(ctx, awsNetwork, 25*time.Minute, 10*time.Second)

	awsNetwork2 = newCommonAWSNetwork(awsRegion)
	awsNetwork2.Name = "ns2-test"
	awsNetwork2.Namespace = namespace2
	_, err = k8s.CreateIgnoreExists(ctx, infraCli, awsNetwork2)
	Expect(err).Should(Succeed(), "network %+v", awsNetwork2)
	waitNetworkReady(ctx, awsNetwork2, 25*time.Minute, 10*time.Second)

	return []byte(namespace + ";" + namespace2)
}, func(fromNode1 []byte) {
	ar := strings.Split(string(fromNode1), ";")
	namespace = ar[0]
	namespace2 = ar[1]

	awsNetwork = newCommonAWSNetwork(awsRegion)
	awsNetwork.Name = "ns1-test"

	awsNetwork2 = newCommonAWSNetwork(awsRegion)
	awsNetwork2.Name = "ns2-test"
	awsNetwork2.Namespace = namespace2

	logger.Infow("run E2E tests with namespace", "namespace", namespace, "namespace2", namespace2)
})

var _ = SynchronizedAfterSuite(func() {
	// run on all nodes
	_ = logger.Sync()
}, func() {
	// run synchronized
	// cleanup all resources to avoid resource leak
	var errs error
	for _, namespace := range []string{namespace, namespace2} {
		ctx := context.Background()
		if err := infraCli.DeleteAllOf(
			ctx,
			&cloudv1.Import{},
			client.InNamespace(namespace),
			client.MatchingLabels(e2eResourceLabels("")),
		); err != nil {
			errs = multierr.Append(errs, err)
		}

		if err := infraCli.DeleteAllOf(
			ctx,
			&cloudv1.Restore{},
			client.InNamespace(namespace),
			client.MatchingLabels(e2eResourceLabels("")),
		); err != nil {
			errs = multierr.Append(errs, err)
		}

		if err := infraCli.DeleteAllOf(
			ctx, &cloudv1.Backup{},
			client.InNamespace(namespace),
			client.MatchingLabels(e2eResourceLabels("")),
		); err != nil {
			errs = multierr.Append(errs, err)
		}

		if err := infraCli.DeleteAllOf(
			ctx, &cloudv1.NetworkPeer{},
			client.InNamespace(namespace),
			client.MatchingLabels(e2eResourceLabels("")),
		); err != nil {
			errs = multierr.Append(errs, err)
		}

		if err := infraCli.DeleteAllOf(
			ctx, &cloudv1.Cluster{},
			client.InNamespace(namespace),
			client.MatchingLabels(e2eResourceLabels("")),
		); err != nil {
			errs = multierr.Append(errs, err)
		}

		if err := infraCli.DeleteAllOf(
			ctx, &cloudv1.Network{},
			client.InNamespace(namespace),
			client.MatchingLabels(e2eResourceLabels("")),
		); err != nil {
			errs = multierr.Append(errs, err)
		}

		// cleanup namespace
		Expect(infraCli.Delete(ctx, &corev1.Namespace{
			ObjectMeta: metav1.ObjectMeta{Name: namespace},
		})).To(Succeed())
	}
	Expect(errs).To(Succeed())
	_ = logger.Sync()
})

// empty projectID will not add the label key-value pair.
func e2eResourceLabels(projectID string) map[string]string {
	mp := map[string]string{
		"cloud.pingcap.com/usage": "shared-e2e",
		"managed-by":              "shared-e2e-suite",
	}
	if kubernetesLabel != "" {
		mp["tags.tidbcloud.com/kubernetes"] = kubernetesLabel
	}
	if projectID != "" {
		mp[labels.LabelKeyProject] = projectID
	}

	return mp
}

func loadKubeRestConfig(kubeconfig string, context string) (*restclient.Config, error) {
	override := &clientcmd.ConfigOverrides{}
	if context != "" {
		override.CurrentContext = context
	}

	cfg, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
		&clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfig},
		override,
	).ClientConfig()

	return cfg, err
}
