package framework

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

	"github.com/onsi/ginkgo/v2"
	"github.com/onsi/gomega"
	"github.com/spf13/pflag"
	"github.com/tidbcloud/aws-shared-provider/tests/framework/cleanup"
	"github.com/tidbcloud/aws-shared-provider/tests/framework/env"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/labels"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
)

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(), decorators ...interface{}) bool {
	desc = "[" + format(ls) + "] " + desc
	if globalSuite.selector.Matches(ls) {
		args := make([]interface{}, 0, len(decorators)+1)
		args = append(args, fn)
		args = append(args, decorators...)
		return ginkgo.It(desc, args...)
	} else {
		return ginkgo.PIt(desc, fn)
	}
}

func RunSpecs(t ginkgo.GinkgoTestingT, desc string) {
	log.SetLogger(zap.New(zap.WriteTo(ginkgo.GinkgoWriter), 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()) {
	var ac cleanup.Action
	ginkgo.BeforeEach(func() {
		ac = globalSuite.cleanupHandler.Add(fn)
	})

	ginkgo.AfterEach(func() {
		fn()
		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.ClusterDefault, "", env.ResourceTypeCRD, c.InfraAPICRDPaths...)
	gomega.Expect(err).NotTo(gomega.HaveOccurred())
}

func NewE2EAnno(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 cleanAllInfraNamespaces() {
	cfg, err := globalSuite.config.Environment.LoadUserConfig()
	gomega.Expect(err).NotTo(gomega.HaveOccurred())
	c, err := newClient(cfg)
	gomega.Expect(err).NotTo(gomega.HaveOccurred())

	logger := log.Log.WithName("cleanAllInfraNamespaces")

	ctx := context.Background()
	nl := &corev1.NamespaceList{}

	if err := c.List(ctx, nl, client.MatchingLabels(
		NewE2EAnno(nil, globalSuite.config.KubeClusterNameBase),
	)); err != nil {
		logger.Error(err, "can't list infra namespaces", "e2e-id", globalSuite.config.KubeClusterNameBase)
		return
	}

	for _, ns := range nl.Items {
		logger.Info("try to clean infra namespace", "namespace", ns.Name)
		if err := c.Delete(ctx, &ns); err != nil {
			if !errors.IsNotFound(err) {
				logger.Error(err, "can't clean infra namespace", "namespace", ns.Name)
			}
		}
	}
}

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())

	cleanAllInfraNamespaces()

	return nil
}

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

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