//go:build intergration

package ft

import (
	"context"
	"encoding/json"
	"flag"
	"fmt"
	"net/http"
	"os"
	"testing"
	"time"

	compute "cloud.google.com/go/compute/apiv1"
	container "cloud.google.com/go/container/apiv1"
	awsconfig "github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/ec2"
	"github.com/aws/aws-sdk-go-v2/service/eks"

	ec2v1alpha1 "github.com/crossplane-contrib/provider-aws/apis/ec2/v1alpha1"
	ec2v1beta1 "github.com/crossplane-contrib/provider-aws/apis/ec2/v1beta1"
	"github.com/crossplane-contrib/provider-aws/apis/eks/manualv1alpha1"
	eksv1alpha1 "github.com/crossplane-contrib/provider-aws/apis/eks/v1alpha1"
	eksv1beta1 "github.com/crossplane-contrib/provider-aws/apis/eks/v1beta1"
	awsiamv1alpha1 "github.com/crossplane-contrib/provider-aws/apis/iam/v1alpha1"
	iamv1beta1 "github.com/crossplane-contrib/provider-aws/apis/iam/v1beta1"
	xpawsv1beta1 "github.com/crossplane-contrib/provider-aws/apis/v1beta1"
	gcpv1alpha1 "github.com/crossplane-contrib/provider-gcp/apis/compute/v1alpha1"
	gcpv1beta1 "github.com/crossplane-contrib/provider-gcp/apis/compute/v1beta1"
	containerv1beta1 "github.com/crossplane-contrib/provider-gcp/apis/container/v1beta1"
	containerv1beta2 "github.com/crossplane-contrib/provider-gcp/apis/container/v1beta2"
	gcpiamv1alpha1 "github.com/crossplane-contrib/provider-gcp/apis/iam/v1alpha1"
	gcpstoragev1alpha1 "github.com/crossplane-contrib/provider-gcp/apis/storage/v1alpha1"
	gcpvpcpeeringv1beta1 "github.com/crossplane/provider-gcp/apis/vpcpeering/v1beta1"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/cloud/aws"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"go.uber.org/zap/zapcore"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/constants"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/regionalservercli"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/utils"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/runtime"
	k8serrors "k8s.io/apimachinery/pkg/util/errors"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
)

var (
	testScheme         = runtime.NewScheme()
	configPath         string
	kubeClient         client.Client
	ctx                context.Context
	logger             = ctrl.Log.WithName("ft")
	clusterClient      *regionalservercli.ClusterServiceHTTPClient
	vpcClient          *regionalservercli.VpcServiceHTTPClient
	vpcpeeringClient   *regionalservercli.VpcPeeringServiceHTTPClient
	ec2Client          *ec2.Client
	eksClient          *eks.Client
	awsClient          aws.Client
	gcpContainerClient *container.ClusterManagerClient
	gcpNetworksClient  *compute.NetworksClient
	gcpRoutersClient   *compute.RoutersClient
)

func TestFT(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "regional-server ft Suite")
}

func init() {
	utilruntime.Must(clientgoscheme.AddToScheme(testScheme))
	utilruntime.Must(cloudv1.AddToScheme(testScheme))
	utilruntime.Must(v1alpha1.AddToScheme(testScheme))
	loadAWSScheme()
	loadGCPScheme()
	//+kubebuilder:scaffold:scheme
}

func loadAWSScheme() {
	utilruntime.Must(ec2v1beta1.SchemeBuilder.AddToScheme(testScheme))
	utilruntime.Must(ec2v1alpha1.SchemeBuilder.AddToScheme(testScheme))
	utilruntime.Must(manualv1alpha1.SchemeBuilder.AddToScheme(testScheme))
	utilruntime.Must(awsiamv1alpha1.SchemeBuilder.AddToScheme(testScheme))
	utilruntime.Must(iamv1beta1.SchemeBuilder.AddToScheme(testScheme))
	utilruntime.Must(eksv1beta1.SchemeBuilder.AddToScheme(testScheme))
	utilruntime.Must(eksv1alpha1.SchemeBuilder.AddToScheme(testScheme))
	utilruntime.Must(xpawsv1beta1.SchemeBuilder.AddToScheme(testScheme))
}

// +kubebuilder:rbac:groups=container.gcp.crossplane.io,resources="*",verbs="*"
// +kubebuilder:rbac:groups=iam.gcp.crossplane.io,resources="*",verbs="*"
// +kubebuilder:rbac:groups=gcp.crossplane.io,resources="*",verbs="*"
// +kubebuilder:rbac:groups=storage.gcp.crossplane.io,resources="*",verbs="*"
// +kubebuilder:rbac:groups=vpcpeering.gcp.crossplane.io,resources="*",verbs="*"

func loadGCPScheme() {
	utilruntime.Must(containerv1beta1.SchemeBuilder.AddToScheme(testScheme)) // nodepool is v1beta1
	utilruntime.Must(containerv1beta2.SchemeBuilder.AddToScheme(testScheme)) // cluster is v1beta2
	utilruntime.Must(gcpv1beta1.SchemeBuilder.AddToScheme(testScheme))
	utilruntime.Must(gcpiamv1alpha1.SchemeBuilder.AddToScheme(testScheme))
	utilruntime.Must(gcpv1alpha1.SchemeBuilder.AddToScheme(testScheme))
	utilruntime.Must(gcpvpcpeeringv1beta1.SchemeBuilder.AddToScheme(testScheme))
	utilruntime.Must(gcpstoragev1alpha1.SchemeBuilder.AddToScheme(testScheme))
}

func handleFlags() {
	flag.StringVar(&configPath, "ft-config", "ft-config.yaml", "The path to the ft config.")
	flag.Parse()
}

type FTContext struct {
	Config *Config
	// default random tenant shared by all nodes
	TenantFoo string
	TenantBar string
	// tenants with account binding, each TenantAcc[i] binds to Config.AccountIDList[i]
	TenantAcc []string
	// default random project shared by all nodes
	ProjectFoo string
	ProjectBar string
}

var f FTContext

func TestMain(m *testing.M) {
	handleFlags()
	opts := zap.Options{
		Development: false,
		DestWriter:  GinkgoWriter,
		TimeEncoder: zapcore.RFC3339TimeEncoder,
	}
	ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))

	logger.Info("init regional-server k8s's kubeconfig")
	var kubeConfig *rest.Config
	// creates the out-of-cluster config
	kubeConfig, err := ctrl.GetConfig()
	if err != nil {
		panic(err)
	}
	mapper, err := apiutil.NewDynamicRESTMapper(kubeConfig, http.DefaultClient)
	if err != nil {
		panic(err)
	}
	clientOptions := client.Options{Scheme: testScheme, Mapper: mapper}
	kubeClient, err = client.New(kubeConfig, clientOptions)
	if err != nil {
		panic(err)
	}
	ctx = context.Background()

	os.Exit(m.Run())
}

var _ = SynchronizedBeforeSuite(func() []byte {
	// run on node 1
	config, err := LoadConfig(configPath)
	Expect(err).To(BeNil())
	logger.Info(fmt.Sprintf("ft config from %s", configPath), "config", config)
	f.Config = config
	// f.TenantFoo = "foo"
	// f.TenantBar = "bar"
	f.TenantFoo = fmt.Sprintf("foo-%d", time.Now().UnixNano())
	f.TenantBar = fmt.Sprintf("bar-%d", time.Now().UnixNano())
	tenantAcc := make([]string, len(f.Config.AccountIDList))
	for i := range f.Config.AccountIDList {
		tenantAcc[i] = fmt.Sprintf("%d", time.Now().UnixNano())
		BindAccountTenant(tenantAcc[i], f.Config.AccountIDList[i])
	}
	f.TenantAcc = tenantAcc
	f.ProjectFoo = "foo"
	f.ProjectBar = "bar"
	logger.Info("allocated tenant foo for ft", "tenant", f.TenantFoo)
	logger.Info("allocated tenant bar for ft", "tenant", f.TenantBar)

	// By("Check if vpc exists under test namespace")
	// vpcList := &v1alpha1.VPCList{}
	// Expect(kubeClient.List(ctx, vpcList, client.InNamespace(f.Config.Namespace), client.MatchingLabels(f.Config.ResourceLabels))).To(Succeed())
	// Expect(vpcList.Items).To(BeEmpty())
	buf, err := json.Marshal(f)
	Expect(err).Should(BeNil())
	return buf
}, func(fromNode1 []byte) {
	// run on all nodes
	var ft FTContext
	err := json.Unmarshal(fromNode1, &ft)
	Expect(err).Should(BeNil())
	f = ft

	clusterClient = regionalservercli.NewClusterServiceHTTPClient(f.Config.ServerURL)
	vpcClient = regionalservercli.NewVpcServiceHTTPClient(f.Config.ServerURL)
	vpcpeeringClient = regionalservercli.NewVpcPeeringServiceHTTPClient(f.Config.ServerURL)

	switch f.Config.Provider {
	case "aws":
		cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(f.Config.Region))
		Expect(err).Should(BeNil())
		awsClient = aws.NewClientSet(cfg)
		ec2Client = awsClient.EC2(GetAssumedRole(f.Config.DefaultAccount, f.Config.RoleName))
		eksClient = awsClient.EKS(GetAssumedRole(f.Config.DefaultAccount, f.Config.RoleName))
	case "gcp":
		gcpNetworksClient, err = compute.NewNetworksRESTClient(ctx)
		Expect(err).Should(BeNil())
		gcpRoutersClient, err = compute.NewRoutersRESTClient(ctx)
		Expect(err).Should(BeNil())
		gcpContainerClient, err = container.NewClusterManagerClient(ctx)
		Expect(err).Should(BeNil())
	}
})

var _ = SynchronizedAfterSuite(func() {
	// run on all nodes
}, func() {
	// run on node 1
	By("Delete resources with ft labels, ensure no lingering resources left")
	Expect(kubeClient.DeleteAllOf(ctx, &corev1.ConfigMap{}, client.InNamespace(constants.DataPlaneAccountNamespace), client.MatchingLabels(f.Config.ResourceLabels))).To(Succeed())
	tenants := append(f.TenantAcc, []string{f.TenantFoo, f.TenantBar}...)
	var errList []error
	for _, tenant := range tenants {
		By(fmt.Sprintf("Delete resources of tenant %s", tenant))
		errList = append(errList, kubeClient.DeleteAllOf(ctx, &v1alpha1.APPRoute{}, client.InNamespace(f.Config.Namespace), client.MatchingLabels(f.Config.ResourceLabels), client.HasLabels([]string{utils.GenTenantLabelKey(tenant)})))
		errList = append(errList, kubeClient.DeleteAllOf(ctx, &v1alpha1.Internet{}, client.InNamespace(f.Config.Namespace), client.MatchingLabels(f.Config.ResourceLabels), client.HasLabels([]string{utils.GenTenantLabelKey(tenant)})))
		errList = append(errList, kubeClient.DeleteAllOf(ctx, &v1alpha1.APPBox{}, client.InNamespace(f.Config.Namespace), client.MatchingLabels(f.Config.ResourceLabels), client.HasLabels([]string{utils.GenTenantLabelKey(tenant)})))
		errList = append(errList, kubeClient.DeleteAllOf(ctx, &v1alpha1.VPC{}, client.InNamespace(f.Config.Namespace), client.MatchingLabels(f.Config.ResourceLabels), client.HasLabels([]string{utils.GenTenantLabelKey(tenant)})))
		errList = append(errList, kubeClient.DeleteAllOf(ctx, &v1alpha1.VPCPeering{}, client.InNamespace(f.Config.Namespace), client.MatchingLabels(f.Config.ResourceLabels), client.MatchingLabels{v1alpha1.TenantIDKey: tenant}))
	}
	Expect(k8serrors.NewAggregate(errList)).To(Succeed())

	By("Check all resources are deleted")
	filter := &client.ListOptions{
		Namespace:     f.Config.Namespace,
		LabelSelector: labels.SelectorFromSet(f.Config.ResourceLabels),
	}
	Eventually(func() bool {
		list := new(v1alpha1.VPCList)
		err := kubeClient.List(ctx, list, filter)
		if err != nil {
			logger.Error(err, "list vpc failed")
			return false
		}
		for _, item := range list.Items {
			if _, ok := item.GetLabels()[utils.GenTenantLabelKey(f.TenantFoo)]; ok {
				logger.Info("resource still remains", "vpc", client.ObjectKeyFromObject(&item).String())
				return false
			}
			if _, ok := item.GetLabels()[utils.GenTenantLabelKey(f.TenantBar)]; ok {
				logger.Info("resource still remains", "vpc", client.ObjectKeyFromObject(&item).String())
				return false
			}
		}
		return true
	}, 15*time.Minute, time.Minute).Should(BeTrue(), "all vpc cr under ns %s should be deleted", f.Config.Namespace)
	Eventually(func() bool {
		list := new(v1alpha1.APPBoxList)
		err := kubeClient.List(ctx, list, filter)
		if err != nil {
			logger.Error(err, "list appbox failed")
			return false
		}
		for _, item := range list.Items {
			if _, ok := item.GetLabels()[utils.GenTenantLabelKey(f.TenantFoo)]; ok {
				logger.Info("resource still remains", "appbox", client.ObjectKeyFromObject(&item).String())
				return false
			}
			if _, ok := item.GetLabels()[utils.GenTenantLabelKey(f.TenantBar)]; ok {
				logger.Info("resource still remains", "appbox", client.ObjectKeyFromObject(&item).String())
				return false
			}
		}
		return true
	}, 10*time.Minute, time.Minute).Should(BeTrue(), "all appbox cr under ns %s should be deleted", f.Config.Namespace)
	Eventually(func() bool {
		list := new(v1alpha1.VPCPeeringList)
		err := kubeClient.List(ctx, list, filter)
		if err != nil {
			logger.Error(err, "list vpc failed")
			return false
		}
		for _, item := range list.Items {
			if v, _ := item.GetLabels()[v1alpha1.TenantIDKey]; v == f.TenantFoo {
				logger.Info("resource still remains", "vpcpeering", client.ObjectKeyFromObject(&item).String())
				return false
			}
			if v, _ := item.GetLabels()[v1alpha1.TenantIDKey]; v == f.TenantBar {
				logger.Info("resource still remains", "vpcpeering", client.ObjectKeyFromObject(&item).String())
				return false
			}
		}
		return true
	}, 10*time.Minute, time.Minute).Should(BeTrue(), "all vpcpeering cr under ns %s should be deleted", f.Config.Namespace)
})

func BindAccountTenant(tenant, account string) {
	err := kubeClient.Create(ctx, &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      fmt.Sprintf("tenant-%s", tenant),
			Namespace: constants.DataPlaneAccountNamespace,
			Labels:    f.Config.ResourceLabels,
		},
		Data: map[string]string{
			"default": account,
		},
	})
	Expect(err).Should(BeNil())
}
