package configmanager

import (
	"context"
	"encoding/json"
	"fmt"
	"testing"

	. "github.com/onsi/gomega"
	"github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"
	operatorconfig "github.com/pingcap/tidb-operator/pkg/apis/util/config"
	apiequality "k8s.io/apimachinery/pkg/api/equality"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
	logf "sigs.k8s.io/controller-runtime/pkg/log"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/consts"
)

var (
	preConfigKeys = map[cloudv1.ComponentType][]string{
		cloudv1.ComponentPD: {
			"dashboard.public-path-prefix",
			"replication.enable-placement-rules",
			"security.key-path",
		},
		cloudv1.ComponentTiDB: {
			"security.ssl-ca",
			"log.file.max-backups",
		},
		cloudv1.ComponentTiKV: {
			"security.ca-path",
			"security.encryption.data-encryption-method",
			"security.encryption.master-key.type",
		},
		cloudv1.ComponentTiFlash: {
			"security.cert_allowed_cn",
		},
		cloudv1.ComponentTiFlashLearner: {
			"security.cert-allowed-cn",
			"security.encryption.data-encryption-method",
			"security.encryption.master-key.type",
		},
	}
	testValue = "test"
	testlog   = logf.Log.WithName("testing")
)

func TestConfigManager_UpdateConfig(t *testing.T) {
	g := NewGomegaWithT(t)
	scheme := runtime.NewScheme()
	g.Expect(cloudv1.AddToScheme(scheme)).To(Succeed())
	g.Expect(v1alpha1.AddToScheme(scheme)).To(Succeed())
	clusterId := "123"
	rev := &cloudv1.ConfigRevision{
		ObjectMeta: metav1.ObjectMeta{
			Name: fmt.Sprintf("basev6.1.0-1-%s-ae862333", clusterId),
		},
		Spec: cloudv1.ConfigRevisionSpec{
			Version: "v6.1.0",
			TiDB: &cloudv1.Configuration{
				Config: cloudv1.NewTomlConfig(map[string]interface{}{
					"token-limit": int64(1000),
					"security": map[string]interface{}{
						"redact-info-log": true,
					},
				}),
			},
			TiKV: &cloudv1.Configuration{
				Config: cloudv1.NewTomlConfig(map[string]interface{}{}),
			},
			PD: &cloudv1.Configuration{
				Config: cloudv1.NewTomlConfig(map[string]interface{}{
					"security": map[string]interface{}{
						"redact-info-log": true,
					},
				}),
			},
			TiFlash: &cloudv1.Configuration{
				Config: cloudv1.NewTomlConfig(map[string]interface{}{}),
			},
			TiFlashLearner: &cloudv1.Configuration{
				Config: cloudv1.NewTomlConfig(map[string]interface{}{
					"security": map[string]interface{}{
						"redact-info-log": true,
					},
				}),
			},
		},
	}
	c := &cloudv1.Cluster{
		Spec: cloudv1.ClusterSpec{
			ConfigRevisionRef: &cloudv1.Reference{
				Name: rev.Name,
			},
		},
	}

	rev1 := rev.DeepCopy()
	rev1.Name = fmt.Sprintf("basev6.1.0-1-%s-ae862334", clusterId)
	rev1.Spec.PD.Config = cloudv1.NewTomlConfig(map[string]interface{}{
		"security": map[string]interface{}{
			"redact-info-log": true,
		},
		"replication": map[string]interface{}{
			"max-replicas": int64(3),
		},
		"schedule": map[string]interface{}{
			"max-merge-region-size": int64(20),
		},
	})
	c1 := c.DeepCopy()
	c1.Spec.ConfigRevisionRef.Name = rev1.Name

	type testcase struct {
		Name            string
		tidbCluster     *v1alpha1.TidbCluster
		cluster         *cloudv1.Cluster
		rev             *cloudv1.ConfigRevision
		mockPDConfig    map[string]interface{}
		expectedVersion *string
		expectedTiDB    map[string]interface{}
		// the PD config in tc.Spec
		expectedPD map[string]interface{}
		// the PD config from PDClient
		expectedPDCluster      map[string]interface{}
		expectedTiFlash        map[string]interface{}
		expectedTiFlashLearner map[string]interface{}
	}

	testFn := func(tc testcase) {
		fmt.Println(tc.Name)
		infraCli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(tc.rev).Build()
		m := NewConfigManager(infraCli, preConfigKeys, nil)
		tcCli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(tc.tidbCluster).Build()
		fakePDCli := NewMockPDClient(tc.mockPDConfig)
		if tc.cluster.Annotations == nil {
			tc.cluster.Annotations = map[string]string{}
		}
		tc.cluster.Annotations[AnnoSkipEnsureNoUnexpectedConfigChange] = "true"
		g.Expect(m.UpdateConfig(context.Background(), tc.cluster, false, RevFinderBuilder(infraCli), tc.tidbCluster, fakePDCli, ConfigUpdateAllComponents)).To(Succeed())
		g.Expect(tcCli.Update(context.Background(), tc.tidbCluster))
		updated := &v1alpha1.TidbCluster{}
		g.Expect(tcCli.Get(context.TODO(), client.ObjectKeyFromObject(tc.tidbCluster), updated)).To(Succeed())
		g.Expect(tc.expectedVersion == nil || *tc.expectedVersion == updated.Spec.Version).To(BeTrue())
		g.Expect(tc.expectedTiDB == nil || apiequality.Semantic.DeepEqual(tc.expectedTiDB, updated.Spec.TiDB.Config.MP)).To(BeTrue(),
			"expected %+v but get %+v", tc.expectedTiDB, updated.Spec.TiDB.Config.MP)
		g.Expect(tc.expectedPD == nil || apiequality.Semantic.DeepEqual(tc.expectedPD, updated.Spec.PD.Config.MP)).To(BeTrue(),
			"expected %+v but get %+v", tc.expectedPD, updated.Spec.PD.Config.MP)
		conf, _ := fakePDCli.GetConfig(context.TODO())
		g.Expect(tc.expectedPDCluster == nil || apiequality.Semantic.DeepEqual(conf, tc.expectedPDCluster)).To(BeTrue(),
			"expected %+v but get %+v", tc.expectedPDCluster, tc.mockPDConfig)
		g.Expect(tc.expectedTiFlash == nil || apiequality.Semantic.DeepEqual(tc.expectedTiFlash, updated.Spec.TiFlash.Config.Common.MP)).To(BeTrue(),
			func() string { // lazy evaluate
				return fmt.Sprintf("expected %+v but get %+v", tc.expectedTiFlash, updated.Spec.TiFlash.Config.Common.MP)
			})
		g.Expect(tc.expectedTiFlashLearner == nil || apiequality.Semantic.DeepEqual(tc.expectedTiFlashLearner, updated.Spec.TiFlash.Config.Proxy.MP)).To(BeTrue(),
			func() string {
				return fmt.Sprintf("expected %+v actual %+v", tc.expectedTiFlashLearner, updated.Spec.TiFlash.Config.Proxy.MP)
			})
	}

	cases := []testcase{
		{
			Name:            "should not overwrite pre config and update version",
			tidbCluster:     makeTcTestData(clusterId, WithPreConf(), TiFlashPreConfig(true), WithVersion("v6.0.0")),
			cluster:         c,
			rev:             rev,
			mockPDConfig:    map[string]interface{}{},
			expectedVersion: pointer.String("v6.1.0"),
			expectedTiDB: map[string]interface{}{
				"token-limit": int64(1000),
				"security": map[string]interface{}{
					"ssl-ca":          testValue,
					"redact-info-log": true,
				},
				"log": map[string]interface{}{
					"file": map[string]interface{}{
						"max-backups": testValue,
					},
				},
			},
			expectedPD: map[string]interface{}{
				"security": map[string]interface{}{
					"redact-info-log": true,
					"key-path":        testValue,
				},
				"dashboard": map[string]interface{}{
					"public-path-prefix": testValue,
				},
				"replication": map[string]interface{}{
					"enable-placement-rules": testValue,
				},
			},
			expectedTiFlash: map[string]interface{}{
				"security": map[string]interface{}{
					"cert_allowed_cn": testValue,
				},
			},
			expectedTiFlashLearner: map[string]interface{}{
				"security": map[string]interface{}{
					"cert-allowed-cn": testValue,
					"redact-info-log": true,
					"encryption": map[string]interface{}{
						"data-encryption-method": testValue,
						"master-key": map[string]interface{}{
							"type": testValue,
						},
					},
				},
			},
		},
		{
			Name: "should update pd cluster config and pd tc config",
			tidbCluster: makeTcTestData(clusterId, WithPreConf(), PDConfig(map[string]interface{}{
				"security.redact-info-log": false,
			})),
			cluster: c1,
			rev:     rev1,
			mockPDConfig: map[string]interface{}{
				"replication": map[string]interface{}{
					"max-replicas": int64(1),
				},
			},
			expectedPD: map[string]interface{}{
				"security": map[string]interface{}{
					"redact-info-log": true,
					"key-path":        testValue,
				},
				// shouldn't contain cluster-level config that not in the pre-config
				"replication": map[string]interface{}{
					"enable-placement-rules": testValue,
				},
				"dashboard": map[string]interface{}{
					"public-path-prefix": testValue,
				},
			},
			expectedPDCluster: map[string]interface{}{
				"replication": map[string]interface{}{
					"max-replicas": int64(3),
				},
				"schedule": map[string]interface{}{
					"max-merge-region-size": int64(20),
				},
			},
		},
		{
			Name:         "should not change existing tiflash config if tiflashLearner is nil",
			tidbCluster:  makeTcTestData(clusterId, WithPreConf(), TiFlashPreConfig(false)),
			cluster:      c,
			rev:          rev,
			mockPDConfig: map[string]interface{}{},
			expectedTiFlash: map[string]interface{}{
				"security": map[string]interface{}{
					"cert_allowed_cn": testValue,
				},
			},
		},
	}

	for _, c := range cases {
		testFn(c)
	}
}

func makeTcTestData(clusterId string, opts ...TCOption) *v1alpha1.TidbCluster {
	tc := &v1alpha1.TidbCluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "db",
			Namespace: fmt.Sprintf("tidb%s", clusterId),
		},
		Spec: v1alpha1.TidbClusterSpec{
			TiDB: &v1alpha1.TiDBSpec{
				Config: v1alpha1.NewTiDBConfig(),
			},
			PD: &v1alpha1.PDSpec{
				Config: v1alpha1.NewPDConfig(),
			},
			TiKV: &v1alpha1.TiKVSpec{
				Config: v1alpha1.NewTiKVConfig(),
			},
		},
	}
	for _, opt := range opts {
		opt(tc)
	}
	return tc
}

type TCOption func(*v1alpha1.TidbCluster)

func WithPreConf() TCOption {
	return func(tc *v1alpha1.TidbCluster) {
		for c, keys := range preConfigKeys {
			switch c {
			case cloudv1.ComponentTiDB:
				tidbConfig := v1alpha1.NewTiDBConfig()
				for _, k := range keys {
					tidbConfig.Set(k, testValue)
				}
				tc.Spec.TiDB.Config = tidbConfig
			case cloudv1.ComponentTiKV:
				tikvConfig := v1alpha1.NewTiKVConfig()
				for _, k := range keys {
					tikvConfig.Set(k, testValue)
				}
				tc.Spec.TiKV.Config = tikvConfig
			case cloudv1.ComponentPD:
				pdConfig := v1alpha1.NewPDConfig()
				for _, k := range keys {
					pdConfig.Set(k, testValue)
				}
				tc.Spec.PD.Config = pdConfig
			}
		}
	}
}

func WithVersion(version string) TCOption {
	return func(tc *v1alpha1.TidbCluster) {
		tc.Spec.Version = version
	}
}

func PDConfig(m map[string]interface{}) TCOption {
	return func(tc *v1alpha1.TidbCluster) {
		for k, v := range m {
			tc.Spec.PD.Config.Set(k, v)
		}
	}
}

// TiFlashPreConfig defines preconfig for tiflash and tiflash learner
func TiFlashPreConfig(withLearner bool) TCOption {
	return func(tc *v1alpha1.TidbCluster) {
		tc.Spec.TiFlash = &v1alpha1.TiFlashSpec{
			Config: &v1alpha1.TiFlashConfigWraper{
				Common: &v1alpha1.TiFlashCommonConfigWraper{
					GenericConfig: operatorconfig.New(map[string]interface{}{
						"security": map[string]interface{}{
							"cert_allowed_cn": testValue,
						},
					}),
				},
			},
		}
		if withLearner {
			tc.Spec.TiFlash.Config.Proxy = &v1alpha1.TiFlashProxyConfigWraper{
				GenericConfig: operatorconfig.New(map[string]interface{}{
					"security": map[string]interface{}{
						"cert-allowed-cn": testValue,
						"encryption": map[string]interface{}{
							"data-encryption-method": testValue,
							"master-key": map[string]interface{}{
								"type": testValue,
							},
						},
					},
				}),
			}
		}
	}
}

func TestMergeBRConfig(t *testing.T) {
	g := NewGomegaWithT(t)
	br := &v1alpha1.BRConfig{}
	conf := cloudv1.NewTomlConfig(map[string]interface{}{
		"logLevel":    "debug",
		"concurrency": 32,
	})
	configBytes, err := json.Marshal(conf.MP)
	g.Expect(err).To(BeNil())
	g.Expect(MergeBRConfig(br, configBytes)).To(Succeed())
	g.Expect(br.LogLevel).To(Equal("debug"))
	g.Expect(*br.Concurrency).To(Equal(uint32(32)))

}

func Test_configManager_ensureNoUnexpectedConfigChange(t *testing.T) {
	cluster := &cloudv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "cluster1",
			Namespace: "ns1",
			Annotations: map[string]string{
				"placeholder": "placeholder",
			},
		},
	}
	cc1 := &cloudv1.ConfigChange{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "cc1",
			Namespace: "ns1",
		},
		Spec: cloudv1.ConfigChangeSpec{},
		Status: cloudv1.ConfigChangeStatus{
			ChangedComponents: []cloudv1.ComponentType{cloudv1.ComponentTiDB, cloudv1.ComponentPD},
		},
	}
	currentTC := &v1alpha1.TidbCluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "tc1",
			Namespace: "tidbcluster1",
			Annotations: map[string]string{
				"placeholder": "placeholder",
			},
		},
		Spec: v1alpha1.TidbClusterSpec{
			PD: &v1alpha1.PDSpec{
				Config: &v1alpha1.PDConfigWraper{
					GenericConfig: &operatorconfig.GenericConfig{
						MP: map[string]interface{}{
							"a": "b",
						},
					},
				},
				ComponentSpec: v1alpha1.ComponentSpec{
					Version: pointer.String("v7.1.0"),
					Image:   "tag",
				},
			},
		},
	}
	newTC := currentTC.DeepCopy()
	newTC.Spec.PD.Config.MP["a"] = "c"
	newTC.Spec.PD.ComponentSpec.Image = "new-tag"

	tests := []struct {
		name           string
		objectModifier func(cluster *cloudv1.Cluster, currentTC, newTC *v1alpha1.TidbCluster)
		wantErr        bool
	}{
		{
			name: "cluster with skip annotation",
			objectModifier: func(cluster *cloudv1.Cluster, currentTC, newTC *v1alpha1.TidbCluster) {
				cluster.Annotations[AnnoSkipEnsureNoUnexpectedConfigChange] = "true"
			},
			wantErr: false,
		},
		{
			name: "pd is scaling",
			objectModifier: func(cluster *cloudv1.Cluster, currentTC, newTC *v1alpha1.TidbCluster) {
				cluster.Annotations[ClusterUpdateAnnotationKeyInstanceType] = "tidb,pd,tikv"
			},
			wantErr: false,
		}, {
			name: "pd is config changing",
			objectModifier: func(cluster *cloudv1.Cluster, currentTC, newTC *v1alpha1.TidbCluster) {
				cluster.Annotations[cloudv1.AnnoClusterCurrentConfigChangeName] = cc1.Name
			},
			wantErr: false,
		},
		{
			name: "pd is not found in currentTC",
			objectModifier: func(cluster *cloudv1.Cluster, currentTC, newTC *v1alpha1.TidbCluster) {
				currentTC.Spec.PD = nil
			},
			wantErr: false,
		}, {
			name: "pd is not found in newTC",
			objectModifier: func(cluster *cloudv1.Cluster, currentTC, newTC *v1alpha1.TidbCluster) {
				newTC.Spec.PD = nil
			},
			wantErr: false,
		},
		{
			name: "pd's config change is unexpected",
			objectModifier: func(cluster *cloudv1.Cluster, currentTC, newTC *v1alpha1.TidbCluster) {

			},
			wantErr: false, // no error, just leave log and metrics
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			g := NewGomegaWithT(t)
			scheme := runtime.NewScheme()
			g.Expect(cloudv1.AddToScheme(scheme)).To(Succeed())
			g.Expect(v1alpha1.AddToScheme(scheme)).To(Succeed())

			cluster := cluster.DeepCopy()
			currentTC := currentTC.DeepCopy()
			newTC := newTC.DeepCopy()
			tt.objectModifier(cluster, currentTC, newTC)
			cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(cc1).Build()
			ctx := context.Background()

			m := &configManager{
				Client: cli,
			}
			if err := m.ensureNoUnexpectedConfigChange(ctx, cluster, currentTC, newTC); (err != nil) != tt.wantErr {
				t.Errorf("ensureNoUnexpectedConfigChange() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

func TestIsPDClusterConfigChanged(t *testing.T) {
	tests := []struct {
		name        string
		current     map[string]interface{}
		desired     map[string]interface{}
		wantChanged bool
		wantErr     bool
	}{
		{
			name: "no change",
			current: map[string]interface{}{
				"key1": "value1",
				"key2": float64(123),
			},
			desired: map[string]interface{}{
				"key1": "value1",
				"key2": float64(123),
			},
			wantChanged: false,
			wantErr:     false,
		},
		{
			name: "value changed",
			current: map[string]interface{}{
				"key1": "value1",
				"key2": float64(123),
			},
			desired: map[string]interface{}{
				"key1": "value2",
				"key2": float64(123),
			},
			wantChanged: true,
			wantErr:     false,
		},
		{
			name: "expect unchanged when the current conf items is more than desired",
			current: map[string]interface{}{
				"key1": "value1",
				"key2": float64(123),
				"key3": "value3",
			},
			desired: map[string]interface{}{
				"key1": "value1",
				"key2": float64(123),
			},
			wantChanged: false,
			wantErr:     false,
		},
		{
			name: "new key added",
			current: map[string]interface{}{
				"key1": "value1",
			},
			desired: map[string]interface{}{
				"key1": "value1",
				"key2": 123,
			},
			wantChanged: true,
			wantErr:     false,
		},
		{
			name: "schedulersV2 is ignored",
			current: map[string]interface{}{
				"schedule": map[string]interface{}{
					"schedulers-v2": []interface{}{
						map[string]interface{}{
							"name": "balance-leader",
						},
					},
				},
			},
			desired: map[string]interface{}{
				"schedule": map[string]interface{}{
					"schedulers": []interface{}{
						map[string]interface{}{
							"name": "balance-leader",
						},
					},
				},
			},
			wantChanged: false,
			wantErr:     false,
		},
		{
			name: "nil or empty key-value is ignored in schedule.schedulers",
			current: UnmarshalConf(`
{
  "schedule": {
    "schedulers": [
      {
        "args": null,
        "args-payload": "",
        "disable": false,
        "type": "evict-slow-store"
      },
      {
        "args": null,
        "args-payload": "",
        "disable": false,
        "type": "balance-leader"
      },
      {
        "args": null,
        "args-payload": "",
        "disable": false,
        "type": "balance-region"
      },
      {
        "args": null,
        "args-payload": "",
        "disable": false,
        "type": "hot-region"
      }
    ]
  }
}`),
			desired: UnmarshalConf(`
{
  "schedule": {
    "schedulers": [
      {
        "type": "evict-slow-store"
      },
      {
        "type": "balance-leader"
      },
      {
        "type": "balance-region"
      },
      {
        "type": "hot-region"
      }
    ]
  }
}`),
			wantChanged: true,
			wantErr:     false,
		},
		{
			name: "float vs int type should be ok",
			current: map[string]interface{}{
				"replication": map[string]interface{}{
					"max-replicas": float64(3),
				},
			},
			desired: map[string]interface{}{
				"replication": map[string]interface{}{
					"max-replicas": 3,
				},
			},
			wantChanged: false,
			wantErr:     false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			g := NewGomegaWithT(t)

			changed, err := isPDClusterConfigChanged(testlog, tt.current, tt.desired)
			if tt.wantErr {
				g.Expect(err).To(HaveOccurred())
				return
			}
			g.Expect(err).NotTo(HaveOccurred())
			g.Expect(changed).To(Equal(tt.wantChanged))
		})
	}
}

func TestUpdatePDClusterConfHashIfChanged(t *testing.T) {
	tests := []struct {
		name    string
		cluster *cloudv1.Cluster
		desired map[string]interface{}
		wantErr bool
		newHash string
	}{
		{
			name: "no existing hash, should return new hash",
			cluster: &cloudv1.Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Annotations: map[string]string{},
				},
			},
			desired: map[string]interface{}{
				"replication": map[string]interface{}{
					"max-replicas": 3,
				},
			},
			wantErr: false,
			newHash: "1255915797889184516",
		},
		{
			name: "existing hash matches desired, return empty string",
			cluster: &cloudv1.Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Annotations: map[string]string{
						annoKeyPDClusterConfHash: "8321362626102621241",
					},
				},
			},
			desired: map[string]interface{}{
				"replication": map[string]interface{}{
					"max-replicas": 3,
				},
				"schedule": map[string]interface{}{
					"schedulers": []interface{}{
						map[string]interface{}{
							"type": "balance-leader",
						},
						map[string]interface{}{
							"type": "balance-region",
						},
					},
				},
			},
			wantErr: false,
			newHash: "",
		},
		{
			name: "existing hash differs from desired, should return new hash",
			cluster: &cloudv1.Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Annotations: map[string]string{
						annoKeyPDClusterConfHash: "old_hash",
					},
				},
			},
			desired: map[string]interface{}{
				"replication": map[string]interface{}{
					"max-replicas": 3,
				},
			},
			wantErr: false,
			newHash: "1255915797889184516",
		},
		{
			name: "empty desired config, should still generate hash",
			cluster: &cloudv1.Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Annotations: map[string]string{},
				},
			},
			desired: nil,
			wantErr: false,
			newHash: "12161962213042174405",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			g := NewGomegaWithT(t)
			logger := testlog

			newHash, err := isPDClusterConfHashChanged(logger, tt.cluster, tt.desired)

			if tt.wantErr {
				g.Expect(err).To(HaveOccurred())
				return
			}

			g.Expect(err).NotTo(HaveOccurred())
			g.Expect(newHash).To(Equal(tt.newHash))
		})
	}
}

func UnmarshalConf(s string) map[string]interface{} {
	var m map[string]interface{}
	err := json.Unmarshal([]byte(s), &m)
	if err != nil {
		panic(err)
	}
	return m
}

func Test_hackForGracefulWaitBeforeShutdownForTiDB(t *testing.T) {
	tests := []struct {
		name                string
		cluster             *cloudv1.Cluster
		tiproxyReplicasInTC int32
		target              map[string]interface{}
		current             map[string]interface{}
		expectedTarget      map[string]interface{}
	}{
		{
			name:                "tiproxy disabled, empty maps",
			cluster:             &cloudv1.Cluster{},
			tiproxyReplicasInTC: 0,
			target:              map[string]interface{}{},
			current:             map[string]interface{}{},
			expectedTarget:      map[string]interface{}{},
		},
		{
			name:                "tiproxy disabled, target has graceful-wait-before-shutdown",
			cluster:             &cloudv1.Cluster{},
			tiproxyReplicasInTC: 0,
			target:              map[string]interface{}{consts.TiDBGracefulShutdownWaitSecondsKey: 10},
			current:             map[string]interface{}{},
			expectedTarget:      map[string]interface{}{consts.TiDBGracefulShutdownWaitSecondsKey: 10},
		},
		{
			// once the graceful-wait-before-shutdown is set, it will not be removed
			name:                "tiproxy disabled, current has graceful-wait-before-shutdown and target doesn't, remain it unchanged",
			cluster:             &cloudv1.Cluster{},
			tiproxyReplicasInTC: 0,
			target:              map[string]interface{}{},
			current:             map[string]interface{}{consts.TiDBGracefulShutdownWaitSecondsKey: 10},
			expectedTarget:      map[string]interface{}{consts.TiDBGracefulShutdownWaitSecondsKey: 10},
		},
		{
			// once the graceful-wait-before-shutdown is set, it will not be removed
			name:                "tiproxy disabled, target already has only other values",
			cluster:             &cloudv1.Cluster{},
			tiproxyReplicasInTC: 0,
			target:              map[string]interface{}{"other-key": "value"},
			current:             map[string]interface{}{consts.TiDBGracefulShutdownWaitSecondsKey: 10},
			expectedTarget:      map[string]interface{}{"other-key": "value", consts.TiDBGracefulShutdownWaitSecondsKey: 10},
		},
		{
			// has set graceful-wait-before-shutdown in current, and try to change it via ConfigRevision
			name:                "tiproxy disabled, both current and target have graceful-wait-before-shutdown, use the value in target",
			cluster:             &cloudv1.Cluster{},
			tiproxyReplicasInTC: 0,
			target:              map[string]interface{}{consts.TiDBGracefulShutdownWaitSecondsKey: 20},
			current:             map[string]interface{}{consts.TiDBGracefulShutdownWaitSecondsKey: 10},
			expectedTarget:      map[string]interface{}{consts.TiDBGracefulShutdownWaitSecondsKey: 20},
		},
		{
			name:                "tiproxy disabled, target already has both other values and the graceful-wait-before-shutdown",
			cluster:             &cloudv1.Cluster{},
			tiproxyReplicasInTC: 0,
			target:              map[string]interface{}{"other-key": "value", consts.TiDBGracefulShutdownWaitSecondsKey: 20},
			current:             map[string]interface{}{consts.TiDBGracefulShutdownWaitSecondsKey: 10},
			expectedTarget:      map[string]interface{}{"other-key": "value", consts.TiDBGracefulShutdownWaitSecondsKey: 20},
		},
		{
			name: "tiproxy enabled, graceful-wait-before-shutdown not set, automatically set it to 30 seconds",
			cluster: &cloudv1.Cluster{
				Spec: cloudv1.ClusterSpec{
					Cluster: cloudv1.ClusterTopology{},
				},
			},
			tiproxyReplicasInTC: 2,
			target:              map[string]interface{}{},
			current:             map[string]interface{}{},
			expectedTarget:      map[string]interface{}{consts.TiDBGracefulShutdownWaitSecondsKey: consts.DefaultTidbGracefulShutdownWaitSecondsV2},
		},
		{
			name: "tiproxy enabled, graceful-wait-before-shutdown not set, cluster annotation set it to 15 seconds",
			cluster: &cloudv1.Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Annotations: map[string]string{
						cloudv1.AnnoKeyFeatureNamePrefix + consts.ClusterFeatureLegacyTiProxySettings: "true",
					},
				},
				Spec: cloudv1.ClusterSpec{
					Cluster: cloudv1.ClusterTopology{},
				},
			},
			tiproxyReplicasInTC: 2,
			target:              map[string]interface{}{},
			current:             map[string]interface{}{},
			expectedTarget:      map[string]interface{}{consts.TiDBGracefulShutdownWaitSecondsKey: consts.DefaultTidbGracefulShutdownWaitSeconds},
		},
		{
			name: "tiproxy enabled, graceful-wait-before-shutdown is set in target, use it",
			cluster: &cloudv1.Cluster{
				Spec: cloudv1.ClusterSpec{
					Cluster: cloudv1.ClusterTopology{},
				},
			},
			tiproxyReplicasInTC: 2,
			target:              map[string]interface{}{consts.TiDBGracefulShutdownWaitSecondsKey: 30},
			current:             map[string]interface{}{},
			expectedTarget:      map[string]interface{}{consts.TiDBGracefulShutdownWaitSecondsKey: 30},
		},
		{
			name: "tiproxy enabled, graceful-wait-before-shutdown is not set in target, use the current value",
			cluster: &cloudv1.Cluster{
				Spec: cloudv1.ClusterSpec{
					Cluster: cloudv1.ClusterTopology{},
				},
			},
			tiproxyReplicasInTC: 2,
			target:              map[string]interface{}{},
			current:             map[string]interface{}{consts.TiDBGracefulShutdownWaitSecondsKey: 30},
			expectedTarget:      map[string]interface{}{consts.TiDBGracefulShutdownWaitSecondsKey: 30},
		},
		{
			name: "tiproxy enabled, graceful-wait-before-shutdown is changed in target, use the target value",
			cluster: &cloudv1.Cluster{
				Spec: cloudv1.ClusterSpec{
					Cluster: cloudv1.ClusterTopology{},
				},
			},
			tiproxyReplicasInTC: 2,
			target:              map[string]interface{}{consts.TiDBGracefulShutdownWaitSecondsKey: 20},
			current:             map[string]interface{}{consts.TiDBGracefulShutdownWaitSecondsKey: 10},
			expectedTarget:      map[string]interface{}{consts.TiDBGracefulShutdownWaitSecondsKey: 20},
		},
		{
			name: "tc.spec.tiproxy is nil, empty map, use 0",
			cluster: &cloudv1.Cluster{
				Spec: cloudv1.ClusterSpec{
					Cluster: cloudv1.ClusterTopology{},
				},
			},
			tiproxyReplicasInTC: -1, // set tc.spec.tiproxy to nil
			target:              map[string]interface{}{},
			current:             map[string]interface{}{},
			expectedTarget:      map[string]interface{}{},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			g := NewGomegaWithT(t)
			tc := &v1alpha1.TidbCluster{
				Spec: v1alpha1.TidbClusterSpec{
					TiProxy: &v1alpha1.TiProxySpec{
						Replicas: tt.tiproxyReplicasInTC,
					},
				},
			}
			if tt.tiproxyReplicasInTC == -1 {
				tc.Spec.TiProxy = nil
			}

			hackForGracefulWaitBeforeShutdownForTiDB(tt.cluster, tc, tt.target, tt.current)

			g.Expect(tt.target).To(Equal(tt.expectedTarget))
		})
	}
}

func TestIsPDPutConfigSchedulersNotFoundErr(t *testing.T) {
	t.Parallel()
	tests := []struct {
		name string
		err  error
		want bool
	}{
		{name: "nil", err: nil, want: false},
		{name: "unrelated", err: fmt.Errorf("HTTP POST request to PD API failed with status 500: internal"), want: false},
		{name: "pd 400 body", err: fmt.Errorf(`HTTP POST request to PD API https://pd:2379/pd/api/v1/config failed with status 400: "config item schedulers not found"`), want: true},
		{name: "case mix", err: fmt.Errorf("SCHEDULERS NOT FOUND"), want: true},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			g := NewGomegaWithT(t)
			g.Expect(isPDPutConfigSchedulersNotFoundErr(tt.err)).To(Equal(tt.want))
		})
	}
}

func TestClonePDClusterConfigMPAndDeleteScheduleSchedulers(t *testing.T) {
	g := NewGomegaWithT(t)
	orig := map[string]interface{}{
		"schedule": map[string]interface{}{
			"schedulers": []interface{}{map[string]interface{}{"type": "balance-leader"}},
			"other":      1,
		},
		"replication": map[string]interface{}{"max-replicas": float64(3)},
	}
	clone, err := clonePDClusterConfigMP(orig)
	g.Expect(err).ToNot(HaveOccurred())
	deleteScheduleSchedulers(clone)

	g.Expect(orig["schedule"].(map[string]interface{})).To(HaveKey("schedulers"))
	g.Expect(clone["schedule"].(map[string]interface{})).ToNot(HaveKey("schedulers"))
	g.Expect(clone["schedule"].(map[string]interface{})["other"]).To(Equal(float64(1)))
	g.Expect(clone["replication"]).ToNot(BeNil())
}
