package integration

import (
	"context"
	"crypto/tls"
	"fmt"
	"math/rand"
	"net"
	"os"
	"path/filepath"
	"reflect"
	"strings"
	"testing"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	config2 "github.com/tidbcloud/config-manager/pkg/config"
	clusterconfig2 "github.com/tidbcloud/config-manager/pkg/controllers/clusterconfig"
	admissionv1beta1 "k8s.io/api/admission/v1beta1"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	apiruntime "k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/yaml"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"
	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
	"k8s.io/utils/ptr"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/envtest"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
	"sigs.k8s.io/controller-runtime/pkg/webhook"

	"github.com/tidbcloud/infra-provider/pkg/scheme"

	"github.com/tidbcloud/config-manager/pkg/webhooks"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	awsfake "github.com/tidbcloud/infra-provider/pkg/providers/aws/fake"
)

const (
	kubeApiQPS       = 65536
	CustomReconciler = "my-reconciler"
)

// only on node 1 (synchronized)
var testEnv *envtest.Environment

// on all nodes
var k8sClient client.Client
var k8sConfig *rest.Config
var (
	testNamespacePrefix = "test-"
)

var (
	configRevisionGCPeriod = time.Second * 2
	configRevisionTTL      = time.Second * 5
)

// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
func TestAPIs(t *testing.T) {
	RegisterFailHandler(Fail)
	rand.Seed(time.Now().UnixNano())
	RunSpecs(t, "Controllers Integration Suite")
}

var awsClient *awsfake.MockClient
var commentsAppender *fakeCommentsAppender

var _ = SynchronizedBeforeSuite(func() []byte {
	// runs in node 1 only (synchronized)
	// setup a singleton api-server and all necessary business controllers and mock controllers
	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

	By("bootstrapping test environment")

	config2.InitConfig("./../../local/config.yaml")
	testEnv = &envtest.Environment{
		CRDDirectoryPaths: []string{
			filepath.Join("../../../infra-api", "config/crd/bases"),
		},
		ErrorIfCRDPathMissing: true,
		WebhookInstallOptions: envtest.WebhookInstallOptions{
			Paths: []string{
				filepath.Join("../../../infra-api", "config/webhook"),
			},
		},
		ControlPlaneStopTimeout:  time.Minute * 5,
		ControlPlaneStartTimeout: time.Minute * 5,
	}

	cfg, err := testEnv.Start()
	Expect(err).NotTo(HaveOccurred())
	Expect(cfg).NotTo(BeNil())
	cfg.QPS = kubeApiQPS
	cfg.Burst = kubeApiQPS

	data, err := GenerateKubeConfiguration("", cfg)
	Expect(err).NotTo(HaveOccurred())
	fname, err := filepath.Abs("./kubeconfig.tmp")
	Expect(err).NotTo(HaveOccurred())
	err = os.WriteFile(fname, data, 0644) // #nosec G306
	Expect(err).NotTo(HaveOccurred())
	logf.Log.Info("write test kubeconfig", "filename", fname)

	k8sClient = newClient(cfg)
	Expect(k8sClient).NotTo(BeNil())
	k8sConfig = cfg

	// ref https://onsi.github.io/ginkgo/#integrating-with-gomock
	// currently it will panic if the mock call is not expected
	awsClient = awsfake.NewMockClient(GinkgoT())

	webhookInstallOptions := &testEnv.WebhookInstallOptions
	k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{
		Scheme: scheme.Scheme,
		WebhookServer: webhook.NewServer(webhook.Options{
			Host:    webhookInstallOptions.LocalServingHost,
			Port:    webhookInstallOptions.LocalServingPort,
			CertDir: webhookInstallOptions.LocalServingCertDir,
		}),
		LeaderElection: false,
		Metrics: metricsserver.Options{
			BindAddress: "0",
		},
	})
	Expect(err).ToNot(HaveOccurred())

	//ctrlConfig := &cloudv1.InfraAPIConfig{
	//	Gardener: cloudv1.Gardener{
	//		GardenerProjectName: "test",
	//	},
	//	Server: cloudv1.Server{
	//		ManifestDirectoryPrefix: getManifestPrefix(),
	//	},
	//	DNS: cloudv1.DNSConfig{
	//		PublicHostedZoneID:     "test",
	//		PublicHostedZoneDomain: "tidb-cloud.com",
	//	},
	//}
	//ctrlConfig.Default()

	//p, err := predicate.LabelSelectorPredicate(ctrlConfig.Predicate)
	//Expect(err).ToNot(HaveOccurred())

	//ns := &corev1.Namespace{
	//	ObjectMeta: metav1.ObjectMeta{
	//		Name: gardenerEnv.GetShootNamespace(),
	//	},
	//}
	//err = k8sClient.Create(context.Background(), ns)
	//Expect(err).ToNot(HaveOccurred())
	//err = providertype.NewReconciler(k8sManager, consts.ProviderTypeAWS).SetupWithManager(k8sManager, p)
	//Expect(err).ToNot(HaveOccurred())
	//err = providertype.NewReconciler(k8sManager, consts.ProviderTypeGCP).SetupWithManager(k8sManager, p)
	//Expect(err).ToNot(HaveOccurred())

	err = ApplyObjectFromDir(k8sClient, filepath.Join("../../pkg/gitops-v2/testdata"))
	Expect(err).ToNot(HaveOccurred())
	Expect(err).ToNot(HaveOccurred())
	infraNs := &corev1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			Name: "infra",
		},
	}
	err = k8sClient.Create(context.TODO(), infraNs)
	Expect(err).ToNot(HaveOccurred())
	//we will crete the configmap in infra-cd project
	//err = ApplyObjectFromDir(k8sClient, filepath.Join("..", "..", "manifests", "prom"))
	//Expect(err).ToNot(HaveOccurred())

	commentsAppender = &fakeCommentsAppender{
		client:        k8sClient,
		nextRevisions: make(map[string]string),
		updateCnt:     make(map[string]int),
	}
	err = clusterconfig2.NewClusterConfigReconciler(k8sManager,
		ctrl.Log.WithName("controllers").WithName("ClusterConfig"),
		clusterconfig2.WithGithubClient(commentsAppender),
	).SetupWithManager(k8sManager)
	Expect(err).ToNot(HaveOccurred())
	err = clusterconfig2.NewGarbageCollector(k8sManager, ctrl.Log.WithName("controllers").WithName("ConfigRevisionGCer"),
		configRevisionGCPeriod, configRevisionTTL, testNamespacePrefix).SetupWithManager(k8sManager)
	Expect(err).ToNot(HaveOccurred())

	// register webhooks
	k8sManager.GetWebhookServer().Register("/mutate-cloud-pingcap-com-v1-cluster-revision", &webhook.Admission{Handler: webhooks.NewClusterRevisionMutator(k8sManager)})
	err = cloudv1.RegisterWebhooks(k8sManager)
	Expect(err).ToNot(HaveOccurred())

	ctx := ctrl.SetupSignalHandler()
	go func() {
		defer GinkgoRecover()
		err = k8sManager.Start(ctx)
		Expect(err).ToNot(HaveOccurred())
	}()
	//go func() {
	//	defer GinkgoRecover()
	//	<-k8sManager.Elected()
	//	err = gardenerEnv.Start(ctx)
	//	Expect(err).ToNot(HaveOccurred())
	//}()

	// wait for the webhook server to get ready
	dialer := &net.Dialer{Timeout: time.Second}
	addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort)
	Eventually(func() error {
		conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true}) // #nosec G402
		if err != nil {
			return err
		}
		conn.Close()
		return nil
	}).Should(Succeed())

	err = initProvider(k8sClient)
	Expect(err).NotTo(HaveOccurred())

	return data
}, func(kubeconfigData []byte) {
	// runs on all node
	// setup the k8s and gardener clients for each node
	clientCfg, err := clientcmd.NewClientConfigFromBytes(kubeconfigData)
	Expect(err).To(Succeed())
	cfg, err := clientCfg.ClientConfig()
	Expect(err).To(Succeed())
	cfg.QPS = kubeApiQPS
	cfg.Burst = kubeApiQPS
	if k8sConfig == nil {
		k8sConfig = cfg
	}
	if k8sClient == nil {
		k8sClient = newClient(k8sConfig)
	}
})

func newClient(cfg *rest.Config) client.Client {
	err := cloudv1.AddToScheme(scheme.Scheme)
	Expect(err).NotTo(HaveOccurred())
	err = admissionv1beta1.AddToScheme(scheme.Scheme)
	Expect(err).NotTo(HaveOccurred())

	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
	Expect(err).NotTo(HaveOccurred())
	return k8sClient
}

var _ = SynchronizedAfterSuite(func() {
	// runs on all nodes
	fmt.Printf("ginkgo node %d exit", GinkgoParallelProcess())
}, func() {
	// runs in node 1 only (synchronized)
	_ = cleanProvider(k8sClient)

	By("tearing down the test environment")
	err := testEnv.Stop()
	Expect(err).NotTo(HaveOccurred())
	if awsClient != nil {
		awsClient.Ctrl.Finish()
	}
})

//func getManifestPrefix() string {
//	_, b, _, _ := runtime.Caller(0)
//	dir := filepath.Join(filepath.Dir(b), "../../testbin")
//	return dir
//}

var providerTypeTPL = &cloudv1.ProviderType{
	ObjectMeta: metav1.ObjectMeta{
		Name: "name",
	},
	Spec: cloudv1.ProviderTypeSpec{
		Capabilities: nil,
	},
}

var storage = resource.MustParse("1000Gi")
var providerTPL = &cloudv1.Provider{
	ObjectMeta: metav1.ObjectMeta{
		Name: "name",
	},
	Spec: cloudv1.ProviderSpec{
		Type: "name",
		Regions: []cloudv1.Region{
			{
				Name:  "test-region",
				Zones: []string{"z-a", "z-b", "z-c"},
			},
		},
		InstanceTypes: []cloudv1.InstanceType{
			{
				Name:   "compute",
				Arch:   consts.AMD64ArchName,
				CPU:    resource.MustParse("8"),
				Memory: resource.MustParse("16Gi"),
			},
			{
				Name:       "storage",
				Arch:       consts.AMD64ArchName,
				CPU:        resource.MustParse("8"),
				Memory:     resource.MustParse("32Gi"),
				MinStorage: &storage,
				MaxStorage: &storage,
			},
			{
				Name:   "test",
				Arch:   consts.AMD64ArchName,
				CPU:    resource.MustParse("1"),
				Memory: resource.MustParse("2Gi"),
			},
			{
				Name:   "i3.2xlarge",
				Arch:   consts.AMD64ArchName,
				CPU:    resource.MustParse("1"),
				Memory: resource.MustParse("2Gi"),
			},
			{
				Name:   "r5.2xlarge",
				Arch:   consts.AMD64ArchName,
				CPU:    resource.MustParse("8"),
				Memory: resource.MustParse("16Gi"),
			},
			{
				Name:   "c5.9xlarge",
				Arch:   consts.AMD64ArchName,
				CPU:    resource.MustParse("36"),
				Memory: resource.MustParse("72Gi"),
			},
		},
	},
}

func initProvider(client client.Client) error {
	ctx := context.Background()

	regions := []cloudv1.Region{
		{
			Name:  "us-east-2",
			Zones: []string{"z1", "z2", "z3"},
		},
	}

	tp := providerTypeTPL.DeepCopy()
	tp.Name = consts.ProviderTypeEKS
	err := client.Create(ctx, tp)
	if err != nil {
		return err
	}

	tp = providerTypeTPL.DeepCopy()
	tp.Name = consts.ProviderTypeGKE
	err = client.Create(ctx, tp)
	if err != nil {
		return err
	}

	p := providerTPL.DeepCopy()
	p.Name = consts.ProviderTypeEKS
	p.Spec.Type = consts.ProviderTypeEKS
	p.Spec.Regions = append(p.Spec.Regions, regions...)
	spec := &cloudv1.GardenerProviderSpec{
		TypeMeta: metav1.TypeMeta{
			Kind:       reflect.TypeOf(cloudv1.GardenerProviderSpec{}).Name(),
			APIVersion: cloudv1.GroupVersion.String(),
		},
		EnableGracefulShutdownWebhook: ptr.To(false),
		AWS: cloudv1.AwsConfig{
			AccountID:              "9527",
			PublicHostedZoneDomain: "aws.pingcap.com",
			CrossplaneProvider:     "provider-aws",
		},
		DataFlowAgent: cloudv1.DataFlowAgentConfig{Image: "test image"},
	}
	p.Spec.ForProvider = &apiruntime.RawExtension{Object: spec}

	err = client.Create(ctx, p)
	if err != nil {
		return err
	}

	p = providerTPL.DeepCopy()
	p.Name = consts.ProviderTypeGKE
	p.Spec.Type = consts.ProviderTypeGKE
	p.Spec.Regions = regions
	spec = &cloudv1.GardenerProviderSpec{
		TypeMeta: metav1.TypeMeta{
			Kind:       reflect.TypeOf(cloudv1.GardenerProviderSpec{}).Name(),
			APIVersion: cloudv1.GroupVersion.String(),
		},
		EnableGracefulShutdownWebhook: ptr.To(true),
		AWS: cloudv1.AwsConfig{
			AccountID:          "9527",
			CrossplaneProvider: "provider-aws",
		},
		GCP: cloudv1.GcpConfig{
			ProjectID:              "test-project",
			CertManagerRole:        "aws:dfdfdf/certrole",
			PublicHostedZoneDomain: "gcp.pingcap.com",
			CrossplaneProvider:     "provider-gcp",
		},
		DataFlowAgent: cloudv1.DataFlowAgentConfig{Image: "test image"},
	}
	p.Spec.ForProvider = &apiruntime.RawExtension{Object: spec}
	err = client.Create(ctx, p)
	if err != nil {
		return err
	}

	return nil
}

func cleanProvider(client client.Client) error {
	var err error
	ctx := context.Background()

	// Delete ProviderType
	tp := new(cloudv1.ProviderType)
	err = client.Get(ctx, types.NamespacedName{Name: "aws"}, tp)
	if err != nil {
		return err
	}
	err = client.Delete(ctx, tp)
	if err != nil {
		return err
	}

	err = client.Get(ctx, types.NamespacedName{Name: "gcp"}, tp)
	if err != nil {
		return err
	}
	err = client.Delete(ctx, tp)
	if err != nil {
		return err
	}

	// Delete Provider
	p := new(cloudv1.Provider)
	err = client.Get(ctx, types.NamespacedName{Name: "aws"}, p)
	if err != nil {
		return err
	}
	err = client.Delete(ctx, p)
	if err != nil {
		return err
	}

	err = client.Get(ctx, types.NamespacedName{Name: "gcp"}, p)
	if err != nil {
		return err
	}
	err = client.Delete(ctx, p)
	if err != nil {
		return err
	}

	return nil
}

// change from https://github.com/kubernetes/client-go/issues/711#issuecomment-730112049
func GenerateKubeConfiguration(namespace string, cfg *rest.Config) ([]byte, error) {
	if namespace == "" {
		namespace = "kube-system"
	}

	clusters := make(map[string]*clientcmdapi.Cluster)
	clusters["default-cluster"] = &clientcmdapi.Cluster{
		Server:                   cfg.Host,
		CertificateAuthorityData: cfg.CAData,
	}

	contexts := make(map[string]*clientcmdapi.Context)
	contexts["default-context"] = &clientcmdapi.Context{
		Cluster:   "default-cluster",
		Namespace: namespace,
		AuthInfo:  "default-context",
	}

	authinfos := make(map[string]*clientcmdapi.AuthInfo)
	authinfos["default-context"] = &clientcmdapi.AuthInfo{
		// try to cover all auth strategies that aren't plugins
		ClientCertificateData: cfg.CertData,
		ClientKeyData:         cfg.KeyData,
		Token:                 cfg.BearerToken,
		Username:              cfg.Username,
		Password:              cfg.Password,
	}

	clientConfig := clientcmdapi.Config{
		Kind:           "Config",
		APIVersion:     "v1",
		Clusters:       clusters,
		Contexts:       contexts,
		CurrentContext: "default-context",
		AuthInfos:      authinfos,
	}

	data, err := clientcmd.Write(clientConfig)
	return data, err
}

func ApplyObjectFromDir(client client.Client, dir string) error {
	infos, err := os.ReadDir(dir)
	if err != nil {
		return err
	}
	for _, f := range infos {
		data, err := os.ReadFile(filepath.Join(dir, f.Name()))
		if err != nil {
			return err
		}
		obj := &unstructured.Unstructured{}
		err = yaml.Unmarshal(data, obj)
		if err != nil {
			return err
		}
		err = client.Create(context.TODO(), obj)
		if err != nil {
			return err
		}
	}
	return nil
}

func prepareNetwork(namespace string, ctx context.Context) (*cloudv1.Network, client.Client, *cloudv1.GardenerNetworkStatus) {
	var (
		shootClient  client.Client
		gardenStatus *cloudv1.GardenerNetworkStatus
		err          error
	)

	ns := &corev1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			Name: namespace,
		},
	}
	err = k8sClient.Create(ctx, ns)
	if err != nil && !apierrors.IsAlreadyExists(err) {
		Expect(err).To(Succeed())
	}

	nt := &cloudv1.Network{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "network-" + cloudv1.RandomString(5),
			Namespace: ns.Name,
			Labels: map[string]string{
				"cloud.pingcap.com/project": cloudv1.RandomString(5),
			},
		},
		Spec: cloudv1.NetworkSpec{
			Provider: "eks",
			Region:   "us-east-2",
			CIDR:     "10.251.0.0/16",
		},
	}
	Expect(k8sClient.Create(ctx, nt)).To(Succeed())

	nt.Status.Phase = cloudv1.NetworkPhaseNormal
	Expect(k8sClient.Status().Update(ctx, nt)).To(Succeed())
	//Eventually(func() error {
	//	gardenStatus, err = networkNormal(ctx, nt, true)
	//	if err != nil {
	//		return err
	//	}
	//	shootCtx, err := gardenerEnv.GetShootContext(ctx, gardenStatus.ShootName)
	//	if err != nil {
	//		return err
	//	}
	//	shootClient = shootCtx.Manager.GetClient()
	//	return nil
	//}, time.Second*120, time.Second*1).Should(Succeed())

	return nt, shootClient, gardenStatus
}

func newCluster(network *cloudv1.Network, tenantID, clusterID, instanceType string) *cloudv1.Cluster {
	c := &cloudv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      fmt.Sprintf("%d", rand.Int()),
			Namespace: network.Namespace,
			Labels: map[string]string{
				cloudv1.ReconcilerLabelKey: "enable-gitops",
				cloudv1.ProjectTagKey:      strings.SplitN(network.Namespace, "-", 2)[1],
			},
			Annotations: map[string]string{},
		},
		Spec: cloudv1.ClusterSpec{
			NetworkName: network.Name,
			Cluster: cloudv1.ClusterTopology{
				TiDB: cloudv1.TiDB{
					Replicas:     2,
					InstanceType: instanceType,
				},
				TiKV: cloudv1.TiKV{
					Replicas:     3,
					InstanceType: instanceType,
					StorageSize:  resource.MustParse("1000Gi"),
				},
				PD: cloudv1.PD{
					Replicas:     3,
					InstanceType: instanceType,
					StorageSize:  resource.MustParse("10Gi"),
				},
				TiFlash: cloudv1.TiFlash{
					Replicas:     2,
					InstanceType: instanceType,
					StorageSize:  resource.MustParse("1000Gi"),
				},
			},
			SqlEndpoints: cloudv1.SqlEndpoints{
				Public: cloudv1.Service{
					Enabled: ptr.To(true),
				},
			},
		},
	}
	if tenantID != "" {
		c.Labels[cloudv1.TenantTagKey] = tenantID
	}
	if clusterID != "" {
		c.Labels[cloudv1.ClusterTagKey] = clusterID
	}
	return c
}

func genNamespace() string {
	return testNamespacePrefix + cloudv1.RandomString(5)
}

//func newClusterReconciler(mgr ctrl.Manager) error {
//	p, err := predicate.LabelSelectorPredicate(
//		metav1.LabelSelector{MatchLabels: map[string]string{
//			cloudv1.ProviderTypeLabel: cloudv1.ProviderTypeEKS,
//		}},
//	)
//	rootConfig4eks := pkgconfig.RootConfig{
//		Predicate: metav1.LabelSelector{
//			MatchLabels: map[string]string{
//				cloudv1.ProviderTypeLabel: consts.ProviderTypeEKS,
//			},
//		},
//		ControllerConfig: pkgconfig.ControllerConfig{
//			TaskEvents: pkgconfig.TaskEvents{
//				TopicID: "", // disable taskEvents
//			},
//			FullManage: pkgconfig.FullManage{
//				Info: account.Info{
//					NetworkInfo: account.NetworkInfo{
//						ServiceRoleName:      "fake-ServiceRoleName",
//						AssumeRoleARN:        "fake-AssumeRoleARN",
//						AssumeRoleExternalID: "fake-AssumeRoleExternalID",
//					},
//				},
//			},
//			AccountScheduler: pkgconfig.AcctSchedConfig{},
//			Network:          pkgconfig.NetworkConfig{},
//		},
//		Hibernation: pkgconfig.Hibernation{
//			Enabled: false, // disable hibernation
//		},
//	}
//	configManager := gitopssdk.NewConfigManager(mgr.GetClient(), ctrl.Log.WithName("GitOps-sdk"), pkgconfig.PreConfigKeys, false)
//	accountInfoGetter := &rootConfig4eks.FullManage.Info
//
//	r := &cluster.ClusterReconciler{
//		Client:         mgr.GetClient(),
//		Manager:        networkManager,
//		Cache:          mgr.GetCache(),
//		GitOps:         &gitopsv2.Gitops{Cli: mgr.GetClient()},
//		ConfigManager:  configManager,
//		Scheme:         mgr.GetScheme(),
//		Config:         rootConfig4eks,
//		Logger:         ctrl.Log.WithName("controllers").WithName("Cluster"),
//		AcctInfoGetter: accountInfoGetter,
//		O11yApi:        nil,
//		AWSCli:         awsCli,
//		GCPCli:         gcpCli,
//		EventRecorder:  mgr.GetEventRecorderFor("cluster-controller"),
//		TopicCli:       topicCli,
//	}
//	if r.SetupWithManager(mgr, p); err != nil {
//		return err
//	}
//	return nil
//}
