package e2e

import (
	"fmt"
	"strings"
	"sync"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	perrs "github.com/pingcap/errors"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"

	"github.com/tidbcloud/infra-api/pkg/consts"

	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
)

var _ = PDescribe("simplify cluster tier test", func() {
	timoutMin := 45
	clusterPerNet := 10 //one network has 10 clusters
	awsProvider := newProvider(consts.ProviderTypeAWS)
	gcpProvider := newProvider(consts.ProviderTypeGCP)

	BeforeEach(func() {
		Expect(infraCli.Get(ctx, types.NamespacedName{Namespace: awsProvider.Namespace, Name: awsProvider.Name}, awsProvider)).Should(Succeed())
		Expect(len(awsProvider.Spec.Regions) > 0).Should(BeTrue())
		Expect(len(awsProvider.Spec.InstanceTypes) > 0).Should(BeTrue())

		Expect(infraCli.Get(ctx, types.NamespacedName{Namespace: gcpProvider.Namespace, Name: gcpProvider.Name}, gcpProvider)).Should(Succeed())
		Expect(len(gcpProvider.Spec.Regions) > 0).Should(BeTrue())
		Expect(len(gcpProvider.Spec.InstanceTypes) > 0).Should(BeTrue())
	})

	It("Should create aws cluster successfully", func() {
		if len(awsProvider.Spec.StorageClasses) == 0 {
			logger.Infow("Provider has no StorageClasses")
			return
		}
		errors := checkCreateAllCluster(awsProvider, timoutMin, clusterPerNet)
		if len(errors) > 0 {
			for _, err := range errors {
				logger.Info(err.Error())
			}
			Fail("simplify cluster tier failed(aws)!")
		}
	})

	It("Should create gcp cluster successfully", func() {
		if len(gcpProvider.Spec.StorageClasses) == 0 {
			logger.Infow("Provider has no StorageClasses")
			return
		}
		errors := checkCreateAllCluster(gcpProvider, timoutMin, clusterPerNet)
		if len(errors) > 0 {
			for _, err := range errors {
				logger.Info(err.Error())
			}
			Fail("simplify cluster tier failed(gcp)!")
		}
	})

	Describe("storageClass is wrong", func() {
		Describe("aws", func() {
			It("tikv storageClass is wrong", func() {
				if len(awsProvider.Spec.StorageClasses) == 0 {
					logger.Infow("Provider has no StorageClasses")
					return
				}
				createWrongStorageClass("tikv", e2eEnv.AwsNetwork.Name, e2eEnv.Config.AWS.InstanceType)
			})
			It("tiflash storageClass is wrong", func() {
				if len(awsProvider.Spec.StorageClasses) == 0 {
					logger.Infow("Provider has no StorageClasses")
					return
				}
				createWrongStorageClass("tiflash", e2eEnv.AwsNetwork.Name, e2eEnv.Config.AWS.InstanceType)
			})
			It("pd storageClass is wrong", func() {
				if len(awsProvider.Spec.StorageClasses) == 0 {
					logger.Infow("Provider has no StorageClasses")
					return
				}
				createWrongStorageClass("pd", e2eEnv.AwsNetwork.Name, e2eEnv.Config.AWS.InstanceType)
			})
		})

		Describe("gcp", func() {
			It("tikv storageClass is wrong", func() {
				if len(gcpProvider.Spec.StorageClasses) == 0 {
					logger.Infow("Provider has no StorageClasses")
					return
				}
				createWrongStorageClass("tikv", e2eEnv.GcpNetwork.Name, e2eEnv.Config.GCP.InstanceType)
			})
			It("tiflash storageClass is wrong", func() {
				if len(gcpProvider.Spec.StorageClasses) == 0 {
					logger.Infow("Provider has no StorageClasses")
					return
				}
				createWrongStorageClass("tiflash", e2eEnv.GcpNetwork.Name, e2eEnv.Config.GCP.InstanceType)
			})
			It("pd storageClass is wrong", func() {
				if len(gcpProvider.Spec.StorageClasses) == 0 {
					logger.Infow("Provider has no StorageClasses")
					return
				}
				createWrongStorageClass("pd", e2eEnv.GcpNetwork.Name, e2eEnv.Config.GCP.InstanceType)
			})
		})
	})

	Describe("instanceType is wrong", func() {
		Describe("aws", func() {
			It("tidb instanceType is wrong", func() {
				createWrongInstanceType("tidb", e2eEnv.AwsNetwork.Name, e2eEnv.Config.AWS.InstanceType)
			})
			It("tikv instanceType is wrong", func() {
				createWrongInstanceType("tikv", e2eEnv.AwsNetwork.Name, e2eEnv.Config.AWS.InstanceType)
			})
			It("tiflash instanceType is wrong", func() {
				createWrongInstanceType("tiflash", e2eEnv.AwsNetwork.Name, e2eEnv.Config.AWS.InstanceType)
			})
			It("pd instanceType is wrong", func() {
				createWrongInstanceType("pd", e2eEnv.AwsNetwork.Name, e2eEnv.Config.AWS.InstanceType)
			})
		})
		Describe("gcp", func() {
			It("tidb instanceType is wrong", func() {
				createWrongInstanceType("tidb", e2eEnv.GcpNetwork.Name, e2eEnv.Config.GCP.InstanceType)
			})
			It("tikv instanceType is wrong", func() {
				createWrongInstanceType("tikv", e2eEnv.GcpNetwork.Name, e2eEnv.Config.GCP.InstanceType)
			})
			It("tiflash instanceType is wrong", func() {
				createWrongInstanceType("tiflash", e2eEnv.GcpNetwork.Name, e2eEnv.Config.GCP.InstanceType)
			})
			It("pd instanceType is wrong", func() {
				createWrongInstanceType("pd", e2eEnv.GcpNetwork.Name, e2eEnv.Config.GCP.InstanceType)
			})
		})
	})
})

func createWrongInstanceType(component, networkName, instanceType string) {
	cluster := newCommonCluster(networkName, instanceType)
	switch component {
	case "tidb":
		cluster.Spec.Cluster.TiDB.InstanceType = "e2e"
	case "tikv":
		cluster.Spec.Cluster.TiKV.InstanceType = "e2e"
	case "pd":
		cluster.Spec.Cluster.PD.InstanceType = "e2e"
	case "tiflash":
		cluster.Spec.Cluster.TiFlash.InstanceType = "e2e"
	}
	err := infraCli.Create(ctx, cluster)
	Expect(err).NotTo(BeNil())
	statusErr, ok := err.(*errors.StatusError)
	Expect(ok).Should(BeTrue())
	Expect(statusErr.ErrStatus.Status).Should(Equal("Failure"))
	Expect(statusErr.ErrStatus.Reason).Should(Equal(metav1.StatusReasonInvalid))
}

func createWrongStorageClass(component, networkName, instanceType string) {
	cluster := newCommonCluster(networkName, instanceType)
	switch component {
	case "tikv":
		cluster.Spec.Cluster.TiKV.StorageClassName = "e2e"
	case "pd":
		cluster.Spec.Cluster.PD.StorageClassName = "e2e"
	case "tiflash":
		cluster.Spec.Cluster.TiFlash.StorageClassName = "e2e"
	}
	err := infraCli.Create(ctx, cluster)
	Expect(err).NotTo(BeNil())
	statusErr, ok := err.(*errors.StatusError)
	Expect(ok).Should(BeTrue())
	Expect(statusErr.ErrStatus.Status).Should(Equal("Failure"))
	Expect(statusErr.ErrStatus.Reason).Should(Equal(metav1.StatusReasonInvalid))
}

func checkCreateAllCluster(provider *cloudv1.Provider, timeout, clusterPerNet int) []error {
	var errs []error

	storageClasses := provider.Spec.StorageClasses
	pdStorageClasses := make([]cloudv1.StorageClass, 0)
	tikvStorageClasses := make([]cloudv1.StorageClass, 0)
	tiflashStorageClasses := make([]cloudv1.StorageClass, 0)

	for _, storageClass := range storageClasses {
		component := strings.Split(storageClass.Name, "-")[0]
		if component == "pd" {
			pdStorageClasses = append(pdStorageClasses, storageClass)
		}
		if component == "tikv" {
			tikvStorageClasses = append(tikvStorageClasses, storageClass)
		}
		if component == "tiflash" {
			tiflashStorageClasses = append(tiflashStorageClasses, storageClass)
		}
	}

	By("create network groups")
	num := len(provider.Spec.InstanceTypes)*len(pdStorageClasses)*len(tikvStorageClasses)*len(tiflashStorageClasses)/clusterPerNet + 1
	totalNum := len(provider.Spec.Regions) * num
	networks := make(map[string][]*cloudv1.Network)
	var wgNet sync.WaitGroup
	for _, region := range provider.Spec.Regions {
		for i := 0; i < num; i++ {
			project := fmt.Sprintf("%d", time.Now().UnixNano())
			network := newCommonNetwork(region.Name, project, provider.Name)
			index := totalNum - 1
			logger.Infow("start create network", "index", index, "region", region)
			Expect(infraCli.Create(ctx, network)).Should(Succeed())
			wgNet.Add(1)
			go func(index int, network *cloudv1.Network, wgNet *sync.WaitGroup) {
				defer GinkgoRecover()
				defer wgNet.Done()

				for j := 0; j < timeout; j++ {
					err := infraCli.Get(ctx, types.NamespacedName{Namespace: network.Namespace, Name: network.Name}, network)
					if err != nil {
						logger.Errorw("error inspecting network status",
							"index", index,
							"region", network.Spec.Region,
							"network", network.Name,
							"error", err.Error())
						errs = append(errs, err)
						return
					}
					if network.Status.Phase != cloudv1.NetworkPhaseNormal {
						logger.Infow("wait network ready",
							"index", index,
							"region", network.Spec.Region,
							"network", network.Name,
							"current phase", network.Status.Phase)
						time.Sleep(time.Minute)
						if j == timeout-1 {
							errs = append(errs, perrs.New("create cluster failed:"+"region "+network.Spec.Region+",network "+network.Name))
						}
						continue
					}
					networks[network.Spec.Region] = append(networks[network.Spec.Region], network)
					break
				}
			}(index, network, &wgNet)
			totalNum--
		}
	}
	wgNet.Wait()
	if len(errs) > 0 {
		return errs
	}

	By("create clusters")
	totalNum = len(provider.Spec.Regions) * len(provider.Spec.InstanceTypes) * len(pdStorageClasses) * len(tikvStorageClasses) * len(tiflashStorageClasses)
	var wg sync.WaitGroup
	for _, region := range provider.Spec.Regions {
		for _, instanceType := range provider.Spec.InstanceTypes {
			if !contains(instanceType.Regions, region.Name) {
				continue
			}
			switch instanceType.Name {
			case "g1-small":
				logger.Infow("this type cannot set up tikv,skip", "instanceType", instanceType.Name)
				continue
			case "t3a.small":
				logger.Infow("this type cannot set up tiflash,skip", "instanceType", instanceType.Name)
				continue
			}

			TiDB := cloudv1.TiDB{
				Replicas:     1,
				InstanceType: instanceType.Name,
			}
			for _, tikvStorageClass := range tikvStorageClasses {
				TiKV := cloudv1.TiKV{
					Replicas:         3,
					InstanceType:     instanceType.Name,
					StorageSize:      resource.MustParse("50Gi"),
					StorageClassName: tikvStorageClass.Name,
				}
				for _, pdStorageClass := range pdStorageClasses {
					PD := cloudv1.PD{
						Replicas:         3,
						InstanceType:     instanceType.Name,
						StorageSize:      resource.MustParse("10Gi"),
						StorageClassName: pdStorageClass.Name,
					}
					for _, tiflashStorageClass := range tiflashStorageClasses {
						TiFlash := cloudv1.TiFlash{
							Replicas:         1,
							InstanceType:     instanceType.Name,
							StorageSize:      resource.MustParse("50Gi"),
							StorageClassName: tiflashStorageClass.Name,
						}
						index := totalNum - 1
						cluster := newCommonCluster(networks[region.Name][index%num].Name, instanceType.Name)
						cluster.Spec.Cluster = cloudv1.ClusterTopology{
							TiDB:    TiDB,
							TiKV:    TiKV,
							TiFlash: TiFlash,
							PD:      PD,
						}
						wg.Add(1)

						go func(cluster *cloudv1.Cluster, network *cloudv1.Network, index int, wg *sync.WaitGroup) {
							defer GinkgoRecover()
							defer wg.Done()

							logger.Infow("start to create cluster",
								"index", index,
								"cluster", cluster.Name,
								"region", network.Spec.Region,
								"instanceType", cluster.Spec.Cluster.TiDB.InstanceType,
								"tikv", cluster.Spec.Cluster.TiKV.StorageClassName,
								"pd", cluster.Spec.Cluster.PD.StorageClassName,
								"tiflash", cluster.Spec.Cluster.TiFlash.StorageClassName)
							Expect(infraCli.Create(ctx, cluster)).Should(Succeed())
							for i := 0; i < timeout; i++ {
								err := infraCli.Get(ctx, types.NamespacedName{Namespace: cluster.Namespace, Name: cluster.Name}, cluster)
								if err != nil {
									logger.Errorw("error inspecting cluster status",
										"cluster", cluster.Name,
										"region", network.Spec.Region,
										"instanceType", cluster.Spec.Cluster.TiDB.InstanceType,
										"tikv", cluster.Spec.Cluster.TiKV.StorageClassName,
										"pd", cluster.Spec.Cluster.PD.StorageClassName,
										"tiflash", cluster.Spec.Cluster.TiFlash.StorageClassName)
									errs = append(errs, err)
								}
								if cluster.Status.Phase != cloudv1.ClusterPhaseRunning {
									logger.Infow("wait cluster ready",
										"cluster", cluster.Name,
										"index", index,
										"current phase", cluster.Status.Phase)
									time.Sleep(time.Minute)
									if i == timeout-1 {
										errs = append(errs, perrs.New("create cluster failed:"+
											" cluster "+cluster.Name+
											",region "+network.Spec.Region+
											",instanceType "+cluster.Spec.Cluster.TiDB.InstanceType+
											",tikv "+cluster.Spec.Cluster.TiKV.StorageClassName+
											",pd "+cluster.Spec.Cluster.PD.StorageClassName+
											",tiflash "+cluster.Spec.Cluster.TiFlash.StorageClassName))
									}
									continue
								}
								logger.Infow("create cluster success",
									"index", index,
									"cluster", cluster.Name,
									"region", network.Spec.Region,
									"instanceType", cluster.Spec.Cluster.TiDB.InstanceType,
									"tikv", cluster.Spec.Cluster.TiKV.StorageClassName,
									"pd", cluster.Spec.Cluster.PD.StorageClassName,
									"tiflash", cluster.Spec.Cluster.TiFlash.StorageClassName)
								break
							}

							Expect(infraCli.Delete(ctx, cluster)).Should(Succeed())
							for i := 0; i < timeout; i++ {
								err := infraCli.Get(ctx, types.NamespacedName{Namespace: cluster.Namespace, Name: cluster.Name}, cluster)
								if err != nil {
									if errors.IsNotFound(err) {
										break
									}
									logger.Errorw("error inspecting cluster status", "index", index, "cluster", cluster.Name, "error", err.Error())
									errs = append(errs, err)
								}
								logger.Infow("wait cluster to be cleaned", "index", index, "cluster", cluster.Name)
								time.Sleep(time.Minute)
								if i == timeout-1 {
									errs = append(errs, perrs.New("clear cluster failed, clusterName"+cluster.Name))
								}
							}
						}(cluster, networks[region.Name][index%num], index, &wg)

						totalNum--
					}
				}
			}
		}
	}
	wg.Wait()
	return errs
}

func contains(s []string, str string) bool {
	for _, v := range s {
		if v == str {
			return true
		}
	}

	return false
}
