package e2e

import (
	"context"
	"errors"
	"flag"
	"fmt"
	"os"
	"strconv"
	"strings"
	"testing"

	awsconfig "github.com/aws/aws-sdk-go-v2/config"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"github.com/tidbcloud/aws-shared-provider/pkg/scheme"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-api/pkg/k8s"
	"github.com/tidbcloud/pkgv2/gormutil"
	infra "github.com/tidbcloud/tidb-management-service/api/serverless_infra"
	infraclient "github.com/tidbcloud/tidb-management-service/api/serverless_infra/client"
	inframysql "github.com/tidbcloud/tidb-management-service/api/serverless_infra/client/mysql"
	"github.com/tidbcloud/tidb-management-service/api/serverless_infra/meta"
	"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"
)

var namespacePrefix string
var managerNs string

var namespace string
var namespace2 string

var infraClusterTable string
var kubeconfig string
var kubecontext string
var infraClusterCli infraclient.Client
var infraCli client.Client

var logger *zap.SugaredLogger
var kubernetesLabel string
var awsRegion string
var vipTidbPool 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-f01 -timeout 60m
func TestMain(m *testing.M) {
	flags := flag.CommandLine
	flags.StringVar(&infraClusterTable, "infra-cluster-table", "", "infra cluster table in dynamodb")
	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(&managerNs, "manager-ns", "", "the namespace manager run in")
	flags.StringVar(&vipTidbPool, "vip-tidb-pool", "", "the tidb pool for vip")
	flags.StringVar(&awsRegion, "aws-region", os.Getenv("AWS_REGION"), "region of aws")
	flags.StringVar(
		&kubernetesLabel,
		"kube-label",
		"",
		"specify tagsKubernetes value directly when create resources",
	)

	RegisterFailHandler(Fail)
	os.Exit(m.Run())
}

func initClient() error {
	awsCfg, err := awsconfig.LoadDefaultConfig(context.TODO())
	if err != nil {
		return err
	}
	awsCfg.Region = awsRegion
	// The RDS credentials and connection info are set as environment variables in the GitHub Actions workflow.
	user := os.Getenv("RDS_USERNAME")
	password := os.Getenv("RDS_PASSWORD")
	dbname := os.Getenv("RDS_DBNAME")
	portStr := os.Getenv("RDS_PORT")
	dbHost := os.Getenv("RDS_CONNECT_HOST")
	if dbHost == "" {
		dbHost = "127.0.0.1"
	}

	port, err := strconv.ParseUint(portStr, 10, 16)
	if err != nil {
		return fmt.Errorf("invalid port number in RDS_PORT: %w", err)
	}
	connCfg := &gormutil.Config{
		User:     user,
		Host:     dbHost,
		Database: dbname,
		Password: password,
		Port:     uint16(port),
	}

	indexField2name := map[string]string{
		"status.zone_redundant_enabled-index": "cluster_statuses:zone_redundant_enabled",
		"status.keyspace_token-index":         "clusters:keyspace_token",
		"namespace-index":                     "clusters:namespace",
	}
	infraClusterCli, err = inframysql.NewMySQLClient(connCfg, "E2E", kubernetesLabel, indexField2name, 0, false)
	if err != nil {
		return err
	}

	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 kubernetesLabel == "" {
		fmt.Println("must set --kube-label")
		flags.Usage()
		os.Exit(-1)
	}
	if infraClusterTable == "" {
		fmt.Println("must set --infra-cluster-table")
		flags.Usage()
		os.Exit(-1)
	}
	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)
	}

	RunSpecs(t, "API E2E Suite")
}

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: NewE2EAnno(nil, kubernetesLabel),
		},
	}
	ctx := context.Background()
	_, err := k8s.CreateIgnoreExists(ctx, infraCli, ns)
	Expect(err).To(Succeed())
	ns = &corev1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			Name:   namespace2,
			Labels: NewE2EAnno(nil, kubernetesLabel),
		},
	}
	_, err = k8s.CreateIgnoreExists(ctx, infraCli, ns)
	Expect(err).To(Succeed())

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

	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),
		); err != nil {
			errs = multierr.Append(errs, err)
		}

		clusterKeys := make([]meta.ObjectKey, 0)
		err := infraClusterCli.GetIndex(ctx, namespace, "namespace-index", &clusterKeys)
		if err != nil && !errors.Is(err, infraclient.ErrNotFound) {
			errs = multierr.Append(errs, err)
		} else {
			for _, clusterKey := range clusterKeys {
				cluster := &infra.Cluster{ObjectMeta: meta.ObjectMeta{Namespace: clusterKey.Namespace, Name: clusterKey.Name}}
				err = infraClusterCli.Get(ctx, cluster)
				if err != nil {
					errs = multierr.Append(errs, err)
					continue
				}
				err = infraClusterCli.Delete(ctx, cluster)
				if err != nil {
					errs = multierr.Append(errs, err)
				}
			}
		}

		if err := infraCli.DeleteAllOf(
			ctx, &cloudv1.Branch{},
			client.InNamespace(namespace),
		); 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()
})

func e2eResourceLabels() 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
	}

	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
}
