package controllers

import (
	"context"
	"fmt"
	"os"
	"strconv"
	"testing"
	"time"

	"github.com/tidbcloud/config-service/pkg/central"

	. "github.com/onsi/gomega"
	pingcapv1alpha1 "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"
	operatorconfig "github.com/pingcap/tidb-operator/pkg/apis/util/config"
	operatorversioned "github.com/pingcap/tidb-operator/pkg/client/clientset/versioned"
	pingcapfake "github.com/pingcap/tidb-operator/pkg/client/clientset/versioned/fake"
	"github.com/tidbcloud/config-service/api/v1alpha1"
	"github.com/tidbcloud/config-service/pkg/cluster"
	"github.com/tidbcloud/config-service/pkg/config"
	"github.com/tidbcloud/config-service/pkg/pdapi"
	"github.com/tidbcloud/config-service/pkg/tenant"
	"github.com/tidbcloud/config-service/sdk/api"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client/fake" // nolint
)

var logger = ctrl.Log.WithName("controller_ut")

func TestClusterConfigController_Start(t *testing.T) {
	g := NewGomegaWithT(t)
	pwd, err := os.Getwd()
	g.Expect(err).To(Succeed())
	configPath := pwd + "/../example/preconfig.yaml"
	g.Expect(config.ParseConfig(configPath)).To(Succeed())
	scheme := runtime.NewScheme()
	_ = v1alpha1.AddToScheme(scheme)
	_ = cloudv1.AddToScheme(scheme)
	fakeClient := fake.NewFakeClientWithScheme(scheme, makeCcTestData()...)
	infraAPIClient := fake.NewFakeClientWithScheme(scheme, makeInfraAPITestData()...)
	tl := tenant.NewMockLister([]central.Tenant{
		{
			ID:     1,
			Name:   "pingcap",
			Status: "active",
		},
		{
			ID:     2,
			Name:   "pingcap-test",
			Status: "active",
		},
		{
			ID:   321,
			Name: "pingcap-test",
		},
	})
	empty := pingcapv1alpha1.NewTiDBConfig()
	tidbConfig := empty.DeepCopy()
	tidbConfig.Set("token-limit", 1000)
	// pre config in tidb cluster
	tidbConfig.Set("security.cluster-verify-cn", []string{"TiDB"})
	updateConfig := makeFakeCluster("1", "123", tidbConfig, map[string]interface{}{}, "NORMAL", TiflashConfig())
	dryrunMode := makeFakeCluster("2", "456", empty, map[string]interface{}{}, "NORMAL")
	updateVersion := makeFakeCluster("1", "999", tidbConfig, map[string]interface{}{}, "NORMAL")
	scaling := makeFakeCluster("1", "888", empty, map[string]interface{}{}, "SCALING")
	skipping := makeFakeCluster("321", "654", empty, map[string]interface{}{}, "NORMAL")

	cl := cluster.NewMockLister(map[string][]cluster.Cluster{
		"1":   {updateConfig.cluster, updateVersion.cluster},
		"2":   {dryrunMode.cluster, scaling.cluster},
		"321": {skipping.cluster},
	})

	ccc := ClusterConfigController{
		infraApiCli:             infraAPIClient,
		Client:                  fakeClient,
		Clusters:                cl,
		Tenants:                 tl,
		Log:                     logger,
		Scheme:                  scheme,
		skipEvalNotFoundCluster: true,
	}
	go func() {
		// run forever
		_ = ccc.Start(context.Background())
	}()

	interval := 1 * time.Second
	duration := 5 * time.Second

	g.Consistently(func() map[string]interface{} {
		tc, err := dryrunMode.tcCli.PingcapV1alpha1().TidbClusters("tidb456").Get(context.Background(), "db", metav1.GetOptions{})
		g.Expect(err).To(Succeed())
		return tc.Spec.TiDB.Config.MP
	}, duration, interval).Should(BeEmpty(), "should not update tidb config in dryrun mode")

	g.Consistently(func() map[string]interface{} {
		pdConf, err := dryrunMode.pdApi.GetConfig()
		g.Expect(err).To(Succeed())
		return pdConf
	}, duration, interval).Should(BeEmpty(), "should not update pd config in dryrun mode")

	g.Eventually(func() string {
		tc, err := updateConfig.tcCli.PingcapV1alpha1().TidbClusters("tidb123").Get(context.Background(), "db", metav1.GetOptions{})
		g.Expect(err).To(Succeed())
		if v, ok := tc.Spec.TiDB.Config.Get("token-limit").Interface().(string); ok {
			return v
		}
		return ""
	}, duration, interval).Should(Equal("13107.20MB"), "should update tidb config eventually")

	g.Eventually(func() string {
		tc, err := skipping.tcCli.PingcapV1alpha1().TidbClusters("tidb654").Get(context.Background(), "db", metav1.GetOptions{})
		g.Expect(err).To(Succeed())
		if v, ok := tc.Spec.TiDB.Config.Get("test-removal").Interface().(string); ok {
			return v
		}
		return ""
	}, duration, interval).Should(Equal(""), "should skip test-removal eventually")

	g.Eventually(func() int64 {
		tc, err := updateConfig.tcCli.PingcapV1alpha1().TidbClusters("tidb123").Get(context.Background(), "db", metav1.GetOptions{})
		g.Expect(err).To(Succeed())
		if v, ok := tc.Spec.TiDB.Config.Get("mem-quota-query").Interface().(int64); ok {
			return v
		}
		return 0
	}, duration, interval).Should(Equal(int64(17179869184)), "should compute instance overlay config")

	g.Eventually(func() int64 {
		tc, err := updateConfig.tcCli.PingcapV1alpha1().TidbClusters("tidb123").Get(context.Background(), "db", metav1.GetOptions{})
		g.Expect(err).To(Succeed())
		if v, ok := tc.Spec.TiKV.Config.Get("test-round.max-bytes-per-sec").Interface().(int64); ok {
			return v
		}
		return 0
	}, duration, interval).Should(Equal(int64(6014)), "should update tikv test-round config eventually")

	g.Eventually(func() float64 {
		tc, err := updateConfig.tcCli.PingcapV1alpha1().TidbClusters("tidb123").Get(context.Background(), "db", metav1.GetOptions{})
		g.Expect(err).To(Succeed())
		if v, ok := tc.Spec.TiKV.Config.Get("test-without-round.max-bytes-per-sec").Interface().(float64); ok {
			return v
		}
		return 0
	}, duration, interval).Should(Equal(float64(4.295235931456e+09)), "should update tikv test-without-round config eventually")

	g.Eventually(func() float64 {
		pdConf, err := updateConfig.pdApi.GetConfig()
		g.Expect(err).To(Succeed())
		if s, ok := pdConf["schedule"]; ok {
			if v, ok := s.(map[string]interface{})["max-merge-region-size"]; ok {
				return v.(float64)
			}
		}
		return 0
	}, duration, interval).Should(Equal(float64(6400.5)), "should update pd config with max expression eventually")

	g.Eventually(func() float64 {
		pdConf, err := updateConfig.pdApi.GetConfig()
		g.Expect(err).To(Succeed())
		if s, ok := pdConf["schedule"]; ok {
			if v, ok := s.(map[string]interface{})["min-merge-region-size"]; ok {
				return v.(float64)
			}
		}
		return 0
	}, duration, interval).Should(Equal(float64(131.072)), "should update pd config with min expression eventually")

	g.Eventually(func() string {
		tc, err := updateVersion.tcCli.PingcapV1alpha1().TidbClusters("tidb999").Get(context.Background(), "db", metav1.GetOptions{})
		g.Expect(err).To(Succeed())
		return tc.Spec.Version
	}, duration, interval).Should(Equal("v5.0.0"), "cluster should be upgraded in reconcile")

	g.Consistently(func() map[string]interface{} {
		tc, err := scaling.tcCli.PingcapV1alpha1().TidbClusters("tidb888").Get(context.Background(), "db", metav1.GetOptions{})
		g.Expect(err).To(Succeed())
		return tc.Spec.TiDB.Config.MP
	}, duration, interval).Should(BeEmpty(), "should not update tidb config when there is a scaling operation ongoing")

	g.Consistently(func() map[string]interface{} {
		pdConf, err := scaling.pdApi.GetConfig()
		g.Expect(err).To(Succeed())
		return pdConf
	}, duration, interval).Should(BeEmpty(), "should not update pd config when there is a scaling operation ongoing")

	// Test if `evictLeaderTimeout` is updated.
	g.Eventually(func() string {
		tc, err := updateConfig.tcCli.PingcapV1alpha1().TidbClusters("tidb123").Get(context.Background(), "db", metav1.GetOptions{})
		g.Expect(err).To(Succeed())
		return *(tc.Spec.TiKV.EvictLeaderTimeout)
	}, duration, interval).Should(Equal(RecommendedEvictLeaderTimeout), "TiKV's `evictLeaderTimeout` should be updated")
	g.Eventually(func() string {
		tc, err := updateVersion.tcCli.PingcapV1alpha1().TidbClusters("tidb999").Get(context.Background(), "db", metav1.GetOptions{})
		g.Expect(err).To(Succeed())
		return *(tc.Spec.TiKV.EvictLeaderTimeout)
	}, duration, interval).Should(Equal(RecommendedEvictLeaderTimeout), "TiKV's `evictLeaderTimeout` should be updated")

	// Test Tiflash config update
	g.Eventually(func() int64 {
		tc, err := updateConfig.tcCli.PingcapV1alpha1().TidbClusters("tidb123").Get(context.Background(), "db", metav1.GetOptions{})
		g.Expect(err).To(Succeed())
		if v, ok := tc.Spec.TiFlash.Config.Common.Get("delta_index_cache_size").Interface().(int64); ok {
			return v
		}
		return 0
	}, duration, interval).Should(Equal(int64(100)), "tiflash config should be updated in reconcile")

	expectedProxyCfg := map[string]interface{}{
		"security": map[string]interface{}{
			"cert-allowed-cn": "test",
			"redact-info-log": true,
		},
	}
	g.Eventually(func() map[string]interface{} {
		tc, err := updateConfig.tcCli.PingcapV1alpha1().TidbClusters("tidb123").Get(context.Background(), "db", metav1.GetOptions{})
		g.Expect(err).To(Succeed())

		return tc.Spec.TiFlash.Config.Proxy.MP
	}, duration, interval).Should(Equal(expectedProxyCfg), "tiflash learner config should be updated in reconcile")

	g.Expect(err).To(Succeed())
}

type fakeCluster struct {
	cluster cluster.Cluster
	tcCli   operatorversioned.Interface
	pdApi   pdapi.DynamicPDClient
}

func makeFakeCluster(tenantId, clusterId string,
	tidbConfig *pingcapv1alpha1.TiDBConfigWraper,
	pdConfig map[string]interface{}, status string, opts ...TCOption) *fakeCluster {
	fakeOperatorCli := pingcapfake.NewSimpleClientset(makeTcTestData(clusterId, tidbConfig, opts...)...)
	fakePDCli := pdapi.NewMockPDClient(pdConfig)
	// ignore err in unit test
	cId, _ := strconv.ParseInt(clusterId, 10, 64)
	tenantCluster := central.TenantCluster{
		ID:       cId,
		Provider: "aws",
		TenantComponents: central.TenantComponents{
			PD: central.TenantComponent{
				MachineType: "compute",
				Replicas:    3,
			},
			TiDB: central.TenantComponent{
				MachineType: "compute",
				Replicas:    2,
			},
			TiFlash: central.TenantComponent{
				MachineType: "storage",
				Replicas:    1,
			},
			TiKV: central.TenantComponent{
				MachineType: "storage",
				Replicas:    3,
			},
		},
		Status: status,
	}
	dc := cluster.NewDBaaSCluster(tenantId, tenantCluster, fakeOperatorCli, fakePDCli)
	return &fakeCluster{dc, fakeOperatorCli, fakePDCli}
}

func makeTcTestData(clusterId string, tidbConfig *pingcapv1alpha1.TiDBConfigWraper, opts ...TCOption) []runtime.Object {
	tc := &pingcapv1alpha1.TidbCluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "db",
			Namespace: fmt.Sprintf("tidb%s", clusterId),
		},
		Spec: pingcapv1alpha1.TidbClusterSpec{
			TiDB: &pingcapv1alpha1.TiDBSpec{
				Config: tidbConfig,
			},
			TiKV: &pingcapv1alpha1.TiKVSpec{},
		},
	}
	for _, opt := range opts {
		opt(tc)
	}
	return []runtime.Object{tc}
}

func makeInfraAPITestData() []runtime.Object {
	return []runtime.Object{
		&cloudv1.ProviderList{
			Items: []cloudv1.Provider{
				{
					Spec: cloudv1.ProviderSpec{
						Type: "aws",
						InstanceTypes: []cloudv1.InstanceType{
							{
								Name:   "compute",
								CPU:    resource.MustParse("8"),
								Memory: resource.MustParse("16Gi"),
							},
							{
								Name:   "storage",
								CPU:    resource.MustParse("8"),
								Memory: resource.MustParse("32Gi"),
							},
						},
					},
				},
			},
		},
		&cloudv1.Cluster{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "123",
				Namespace: "nightly-1",
				Labels: map[string]string{
					"cloud.pingcap.com/provider-type": "aws",
					"tags.tidbcloud.com/cluster":      "123",
					"tags.tidbcloud.com/tenant":       "1",
				},
			},
			Spec: cloudv1.ClusterSpec{
				Cluster: cloudv1.ClusterTopology{
					PD: cloudv1.PD{
						InstanceType: "compute",
						Replicas:     3,
					},
					TiDB: cloudv1.TiDB{
						InstanceType: "compute",
						Replicas:     2,
					},
					TiKV: cloudv1.TiKV{
						InstanceType: "storage",
						Replicas:     3,
					},
					TiFlash: cloudv1.TiFlash{
						InstanceType: "storage",
						Replicas:     1,
					},
				},
			},
		},
		&cloudv1.Cluster{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "999",
				Namespace: "nightly-1",
				Labels: map[string]string{
					"cloud.pingcap.com/provider-type": "aws",
					"tags.tidbcloud.com/cluster":      "999",
					"tags.tidbcloud.com/tenant":       "1",
				},
			},
			Spec: cloudv1.ClusterSpec{
				Cluster: cloudv1.ClusterTopology{
					PD: cloudv1.PD{
						InstanceType: "compute",
						Replicas:     3,
					},
					TiDB: cloudv1.TiDB{
						InstanceType: "compute",
						Replicas:     2,
					},
					TiKV: cloudv1.TiKV{
						InstanceType: "storage",
						Replicas:     3,
					},
					TiFlash: cloudv1.TiFlash{
						InstanceType: "storage",
						Replicas:     1,
					},
				},
			},
		},
		&cloudv1.Cluster{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "888",
				Namespace: "nightly-1",
				Labels: map[string]string{
					"cloud.pingcap.com/provider-type": "aws",
					"tags.tidbcloud.com/cluster":      "888",
					"tags.tidbcloud.com/tenant":       "1",
				},
			},
			Spec: cloudv1.ClusterSpec{
				Cluster: cloudv1.ClusterTopology{
					PD: cloudv1.PD{
						InstanceType: "compute",
						Replicas:     3,
					},
					TiDB: cloudv1.TiDB{
						InstanceType: "compute",
						Replicas:     2,
					},
					TiKV: cloudv1.TiKV{
						InstanceType: "storage",
						Replicas:     3,
					},
					TiFlash: cloudv1.TiFlash{
						InstanceType: "storage",
						Replicas:     1,
					},
				},
			},
		},
		&cloudv1.Cluster{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "456",
				Namespace: "nightly-2",
				Labels: map[string]string{
					"cloud.pingcap.com/provider-type": "aws",
					"tags.tidbcloud.com/cluster":      "456",
					"tags.tidbcloud.com/tenant":       "2",
				},
			},
			Spec: cloudv1.ClusterSpec{
				Cluster: cloudv1.ClusterTopology{
					PD: cloudv1.PD{
						InstanceType: "compute",
						Replicas:     3,
					},
					TiDB: cloudv1.TiDB{
						InstanceType: "compute",
						Replicas:     2,
					},
					TiKV: cloudv1.TiKV{
						InstanceType: "storage",
						Replicas:     3,
					},
					TiFlash: cloudv1.TiFlash{
						InstanceType: "storage",
						Replicas:     1,
					},
				},
			},
		},
	}
}

func makeCcTestData() []runtime.Object {
	var baseConfig = v1alpha1.ClusterConfig{
		TypeMeta: metav1.TypeMeta{
			APIVersion: "ops/v1alpha1",
			Kind:       "ClusterConfig",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name: v1alpha1.BaseClusterConfig,
		},
		Spec: v1alpha1.ClusterConfigSpec{
			Version: "v4.0.9",
			ConfigMeta: &v1alpha1.ConfigMeta{
				GitCommitID:   "123456",
				ConfigVersion: 0,
			},
			TiDB: &v1alpha1.Configuration{
				BaseImage: "gcr.io/pingcap-public/dbaas/tidb",
				Config: api.NewTomlConfig(map[string]interface{}{
					"token-limit":  int64(1000),
					"test-removal": "{TiDB.CPU.Limit}",
				}),
			},
			TiKV: &v1alpha1.Configuration{
				BaseImage: "gcr.io/pingcap-public/dbaas/tikv",
				Config: api.NewTomlConfig(map[string]interface{}{
					"abort-on-panic": false,
				}),
			},
			PD: &v1alpha1.Configuration{
				BaseImage: "gcr.io/pingcap-public/dbaas/pd",
				Config: api.NewTomlConfig(map[string]interface{}{
					"schedule": map[string]interface{}{
						"max-merge-region-size": "{ max(PD.Memory.Request *0.008/1024/1024, 6400.5) }",
						"min-merge-region-size": "{ min(PD.Memory.Limit *0.008/1024/1024, 132) }",
					},
				}),
			},
			TiFlash: &v1alpha1.Configuration{
				BaseImage: "gcr.io/pingcap-public/dbaas/tiflash",
				Config: api.NewTomlConfig(map[string]interface{}{
					"delta_index_cache_size": int64(100),
				}),
			},
			TiFlashLearner: &v1alpha1.Configuration{
				BaseImage: "gcr.io/pingcap-public/dbaas/tiflash",
				Config: api.NewTomlConfig(map[string]interface{}{
					"security": map[string]interface{}{
						"redact-info-log": true,
					},
				}),
			},
			UpdatePolicy: v1alpha1.UpdatePolicyImmediate,
		},
	}
	tenantConfig := baseConfig.DeepCopy()
	tenantConfig.Name = baseConfig.Name + "-t123"
	tenantConfig.Labels = map[string]string{v1alpha1.TenentLabel: "1", v1alpha1.ClusterLabel: v1alpha1.AllCluster}

	tenantConfig.Spec.TiKV.Config = api.NewTomlConfig(map[string]interface{}{
		"test-round":         map[string]interface{}{"max-bytes-per-sec": "{ round(PD.CPU.Limit*0.8 + TiFlash.IOPS + TiFlash.CPU.Request)}"},
		"test-without-round": map[string]interface{}{"max-bytes-per-sec": "{ TiKV.Memory.Limit * 0.8/102400 + TiKV.Memory.Request/TiKV.CPU.Limit + 200 }"},
	})

	computeOverlay := v1alpha1.Overlay{
		InstanceType: "aws.compute",
		Config: api.NewTomlConfig(map[string]interface{}{
			"mem-quota-query": "{TiDB.Memory.Limit}",
			"token-limit":     "{string(TiDB.Memory.Limit * 0.8/1024/1024) +\"MB\"}",
		}),
	}
	tenantConfig.Spec.TiDB.Overlays = append(tenantConfig.Spec.TiDB.Overlays, computeOverlay)

	clusterConfig := tenantConfig.DeepCopy()
	clusterConfig.Name = tenantConfig.Name + "-c456"
	clusterConfig.Labels = map[string]string{v1alpha1.TenentLabel: "2", v1alpha1.ClusterLabel: "456"}
	clusterConfig.Spec.TiDB.Config = api.NewTomlConfig(map[string]interface{}{
		"token-limit": int64(3000),
	})
	clusterConfig.Spec.UpdatePolicy = v1alpha1.UpdatePolicyDryrun
	instanceType := "aws.m5.compute"
	overlay := v1alpha1.Overlay{
		InstanceType: instanceType,
		Config: api.NewTomlConfig(map[string]interface{}{
			"token-limit": int64(4000),
		}),
	}
	clusterConfig.Spec.TiDB.Overlays = append(clusterConfig.Spec.TiDB.Overlays, overlay)

	upgradeCC := tenantConfig.DeepCopy()
	upgradeCC.Name = upgradeCC.Name + "-c999"
	upgradeCC.Labels = map[string]string{v1alpha1.TenentLabel: "1", v1alpha1.ClusterLabel: "999"}
	upgradeCC.Spec.Version = "v5.0.0"

	var objs []runtime.Object
	objs = append(objs, &baseConfig, tenantConfig, clusterConfig, upgradeCC)
	return objs
}

type TCOption func(*pingcapv1alpha1.TidbCluster)

// TiflashConfig defines preconfig for tiflash and fiflash learner
func TiflashConfig() TCOption {
	return func(tc *pingcapv1alpha1.TidbCluster) {
		tc.Spec.TiFlash = &pingcapv1alpha1.TiFlashSpec{
			Config: &pingcapv1alpha1.TiFlashConfigWraper{
				Common: &pingcapv1alpha1.TiFlashCommonConfigWraper{
					GenericConfig: operatorconfig.New(map[string]interface{}{
						"security": map[string]interface{}{
							"cert_allowed_cn": "test",
						},
					}),
				},
				Proxy: &pingcapv1alpha1.TiFlashProxyConfigWraper{
					GenericConfig: operatorconfig.New(map[string]interface{}{
						"security": map[string]interface{}{
							"cert-allowed-cn": "test",
						},
					}),
				},
			},
		}
	}
}
