package framework

import (
	"context"
	"flag"
	"os"
	"reflect"
	"sort"
	"strings"

	ec2v1alpha1 "github.com/crossplane/provider-aws/apis/ec2/v1alpha1"
	ec2v1beta1 "github.com/crossplane/provider-aws/apis/ec2/v1beta1"
	lambdav1alpha1 "github.com/crossplane/provider-aws/apis/lambda/v1alpha1"
	"github.com/onsi/ginkgo"
	"github.com/onsi/gomega"
	"github.com/spf13/pflag"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/api/meta"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"

	"github.com/tidbcloud/shared-provider/tests/framework/cleanup"
	"github.com/tidbcloud/shared-provider/tests/framework/env"
)

type suite struct {
	initialized bool
	topNodes    []func()

	selector       labels.Selector
	cleanupHandler cleanup.Handler

	config      *TestConfig
	environment env.Environment
}

var globalSuite = &suite{
	cleanupHandler: cleanup.New(),
	config:         NewDefaultTestConfig(),
}

func format(ls labels.Set) string {
	selector := make([]string, 0, len(ls))
	for key, val := range ls {
		if val == "True" {
			selector = append(selector, key)
		} else {
			selector = append(selector, key+"="+val)
		}
	}
	sort.StringSlice(selector).Sort()
	return strings.Join(selector, ",")
}

func It(ls labels.Set, desc string, fn func(), timeout ...float64) bool {
	desc = "[" + format(ls) + "] " + desc
	if globalSuite.selector.Matches(ls) {
		return ginkgo.It(desc, fn, timeout...)
	} else {
		return ginkgo.PIt(desc, fn)
	}
}

func RunSpecs(t ginkgo.GinkgoTestingT, desc string) {
	log.SetLogger(zap.New(zap.WriteTo(os.Stdout), zap.UseDevMode(true)))
	logger := log.Log.WithName(desc)

	e, err := env.New(&globalSuite.config.Environment)
	if err != nil {
		logger.Error(err, "new environment failed")
		os.Exit(-1)
	}

	s, err := labels.Parse(globalSuite.config.Selector)
	if err != nil {
		logger.Error(err, "invalid selector config")
		os.Exit(-1)
	}

	globalSuite.environment = e
	globalSuite.selector = s
	globalSuite.initialized = true
	for _, fn := range globalSuite.topNodes {
		fn()
	}

	ginkgo.RunSpecs(t, desc)
}

func AddCleanupAction(fn func()) cleanup.Action {
	return globalSuite.cleanupHandler.Add(fn)
}
func RemoveCleanupAction(ac cleanup.Action) {
	globalSuite.cleanupHandler.Remove(ac)
}

func RegisterFlags(fs *pflag.FlagSet) {
	globalSuite.config.RegisterFlags(fs)
}

func CopyGoFlags(fs *pflag.FlagSet, gfs *flag.FlagSet) {
	gfs.VisitAll(func(f *flag.Flag) {
		if f.Name == "ginkgo.noisyPendings" {
			// can't be error so just panic
			if err := f.Value.Set("false"); err != nil {
				panic(err)
			}
		}
		fs.AddGoFlag(f)
	})
}

func registerResources(e env.Environment, c *TestConfig) {
	err := e.Register(env.ClusterInfraAPI, "", env.ResourceTypeCRD, c.InfraAPICRDPaths...)
	gomega.Expect(err).NotTo(gomega.HaveOccurred())
}

func NewE2ELabels(ls map[string]string, base string) map[string]string {
	if ls == nil {
		ls = map[string]string{}
	}

	ls["testing.tidbcloud.com/for"] = "e2e"
	ls["testing.tidbcloud.com/e2e-id"] = base

	return ls
}

func BeforeSuite(_ []byte) {
}

func AfterSuite() {
	ginkgo.By("run cleanup actions")
	globalSuite.cleanupHandler.Run()
}

func SynchronizedBeforeSuite() []byte {
	ctx := context.Background()

	registerResources(globalSuite.environment, globalSuite.config)

	err := globalSuite.environment.Start(ctx)
	gomega.Expect(err).NotTo(gomega.HaveOccurred())

	infraCfg, err := globalSuite.config.Environment.LoadInfraAPIConfig()
	gomega.Expect(err).NotTo(gomega.HaveOccurred())
	infraClient, err := newClient(infraCfg)
	gomega.Expect(err).NotTo(gomega.HaveOccurred())

	cleanGlobalResources(ctx, infraClient,
		&corev1.NamespaceList{},
	)

	cfg, err := globalSuite.config.Environment.LoadUserConfig()
	gomega.Expect(err).NotTo(gomega.HaveOccurred())
	c, err := newClient(cfg)
	gomega.Expect(err).NotTo(gomega.HaveOccurred())

	cloud := globalSuite.config.Cloud

	if cloud == CloudEKS {
		cleanGlobalResources(ctx, c,
			&ec2v1beta1.VPCList{},
			&ec2v1beta1.SubnetList{},
			&ec2v1beta1.SecurityGroupList{},
			&ec2v1alpha1.VPCEndpointList{},
			&lambdav1alpha1.FunctionList{},
		)
	}

	return nil
}

func cleanGlobalResources(ctx context.Context, c client.Client, objs ...client.ObjectList) {
	logger := log.Log.WithName("Cleanup")
	logger.Info("try to clean up")
	for i := range objs {
		logger.Info("try to clean up resource", "index", i, "type", reflect.TypeOf(objs[i]).Elem())
		if err := c.List(ctx, objs[i], client.MatchingLabels(
			NewE2ELabels(nil, globalSuite.config.KubeClusterNameBase),
		)); err != nil {
			logger.Error(err, "can't list resource", "index", i, "e2e-id", globalSuite.config.KubeClusterNameBase)
			continue
		}
		if err := meta.EachListItem(objs[i], func(obj runtime.Object) error {
			cobj := obj.(client.Object)
			logger.Info("try to clean up resource item", "gvk", cobj.GetObjectKind().GroupVersionKind(), "name", cobj.GetName())
			if err := c.Delete(ctx, cobj); err != nil {
				if !errors.IsNotFound(err) {
					logger.Error(err, "can't clean up resource",
						"gvk", cobj.GetObjectKind().GroupVersionKind(),
						"name", cobj.GetName(),
					)
				}
			}

			return nil
		}); err != nil {
			logger.Error(err, "can't delete resource", "index", i, "e2e-id", globalSuite.config.KubeClusterNameBase)
		}
	}
}

func SynchronizedAfterSuite() {
	ctx := context.Background()

	err := globalSuite.environment.Stop(ctx)
	gomega.Expect(err).NotTo(gomega.HaveOccurred())
}
