package sdk

import (
	"context"
	"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"
)

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, testlog, preConfigKeys, false)
		tcCli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(tc.tidbCluster).Build()
		fakePDCli := NewMockPDClient(tc.mockPDConfig)
		g.Expect(m.UpdateConfig(tc.cluster, RevFinderBuilder(infraCli), tc.tidbCluster, tcCli, fakePDCli, ConfigUpdateAllComponents, true)).To(Succeed())
		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()
		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,
	})
	g.Expect(mergeBRConfig(br, conf)).To(Succeed())
	g.Expect(br.LogLevel).To(Equal("debug"))
	g.Expect(*br.Concurrency).To(Equal(uint32(32)))

}
