// Copyright 2022 PingCAP, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// See the License for the specific language governing permissions and
// limitations under the License.

package sdk

import (
	"fmt"
	"os"
	"reflect"
	"testing"

	"github.com/stretchr/testify/require"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/pointer"
	clog "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"

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

func init() {
	clog.SetLogger(zap.New(
		zap.WriteTo(os.Stderr),
		zap.UseDevMode(true),
		zap.ConsoleEncoder(),
	))
}

func fakeLightningTagForTest(version string) string {
	return fmt.Sprintf("%s-lightning-tag", version)
}

func generateClusterConfigListForTest(t *testing.T) *v1.ClusterConfigList {
	cfgTOMLStr := "\n[checkpoint]\ndriver = \"file\"\ndsn = \"/etc/endpoint/cp.pb\"\nenable = true\n\n[cron]\nlog-progress = \"5m\"\nswitch-mode = \"5m\"\n\n[lightning]\ncheck-requirements = true\nfile = \"\"\nindex-concurrency = 4\nlevel = \"info\"\npprof-port = 8289\ntable-concurrency = 6\n\n[mydumper]\nstrict-format = false\n\n[mydumper.csv]\nbackslash-escape = true\ndelimiter = \"\\\"\"\nheader = true\nnot-null = false\nnull = \"\\\\N\"\nseparator = \",\"\n\n[post-restore]\nanalyze = \"optional\"\nchecksum = true\n\n[security]\nca-path = \"/var/lib/lightning-tls/ca.crt\"\ncert-path = \"/var/lib/lightning-tls/tls.crt\"\nkey-path = \"/var/lib/lightning-tls/tls.key\"\n\n[tidb]\nbuild-stats-concurrency = 4\ndistsql-scan-concurrency = 15\nhost = \"db-tidb\"\nindex-serial-scan-concurrency = 2\nlog-level = \"error\"\npd-addr = \"db-pd:2379\"\nport = 4000\nstatus-port = 10080\ntls = \"false\"\n\n[tikv-importer]\nbackend = \"local\"\nsorted-kv-dir = \"/etc/endpoint\"\n"
	lnCfgTpl := v1.NewTomlConfig(make(map[string]interface{}))
	err := lnCfgTpl.UnmarshalTOML([]byte(cfgTOMLStr))
	require.NoError(t, err)
	return &v1.ClusterConfigList{
		Items: []v1.ClusterConfig{
			{
				ObjectMeta: metav1.ObjectMeta{
					Name: "base--v6.0.96",
				},
				Spec: v1.ClusterConfigSpec{
					Version: "v6.0.96",
					Selector: &metav1.LabelSelector{
						MatchLabels: map[string]string{
							v1.VersionLabel: "v6.0.96",
						},
					},
				},
			},
			{
				ObjectMeta: metav1.ObjectMeta{
					Name: "base--v6.0.97",
				},
				Spec: v1.ClusterConfigSpec{
					Version: "v6.0.97",
					Lightning: &v1.Configuration{
						BaseImage: pointer.String("IMAGE_URL"),
						Tag:       pointer.String(fakeLightningTagForTest("v6.0.97")),
					},
					Selector: &metav1.LabelSelector{
						MatchLabels: map[string]string{
							v1.VersionLabel: "v6.0.97",
						},
					},
				},
			},
			{
				ObjectMeta: metav1.ObjectMeta{
					Name: "base--v6.0.98",
				},
				Spec: v1.ClusterConfigSpec{
					Version: "v6.0.98",
					Lightning: &v1.Configuration{
						Tag:    pointer.String(fakeLightningTagForTest("v6.0.98")),
						Config: lnCfgTpl,
					},
					Selector: &metav1.LabelSelector{
						MatchLabels: map[string]string{
							v1.VersionLabel: "v6.0.98",
						},
					},
				},
			},
			{
				ObjectMeta: metav1.ObjectMeta{
					Name: "base--v6.0.99",
				},
				Spec: v1.ClusterConfigSpec{
					Version: "v6.0.99",
					Lightning: &v1.Configuration{
						BaseImage: pointer.String("IMAGE_URL"),
						Config:    lnCfgTpl,
					},
					Selector: &metav1.LabelSelector{
						MatchLabels: map[string]string{
							v1.VersionLabel: "v6.0.99",
						},
					},
				},
			},
			{
				ObjectMeta: metav1.ObjectMeta{
					Name: "base--v6.1.99",
				},
				Spec: v1.ClusterConfigSpec{
					Version: "v6.1.99",
					Lightning: &v1.Configuration{
						BaseImage: pointer.String("IMAGE_URL"),
						Tag:       pointer.String(fakeLightningTagForTest("v6.1.99")),
						Config:    lnCfgTpl,
					},
					Selector: &metav1.LabelSelector{
						MatchLabels: map[string]string{
							v1.VersionLabel: "v6.1.99",
						},
					},
				},
			},
			{
				ObjectMeta: metav1.ObjectMeta{
					Name: "base--v6.1.99-dup",
				},
				Spec: v1.ClusterConfigSpec{
					Version: "v6.1.99",
					Lightning: &v1.Configuration{
						BaseImage: pointer.String("IMAGE_URL"),
						Tag:       pointer.String(fakeLightningTagForTest("v6.1.99")),
						Config:    lnCfgTpl,
					},
					Selector: &metav1.LabelSelector{
						MatchLabels: map[string]string{
							v1.VersionLabel: "v6.1.99",
						},
					},
				},
			},
			{
				ObjectMeta: metav1.ObjectMeta{
					Name: "base--v6.4.0",
				},
				Spec: v1.ClusterConfigSpec{
					Version: "v6.4.0",
					Lightning: &v1.Configuration{
						BaseImage: pointer.String("IMAGE_URL"),
						Tag:       pointer.String(fakeLightningTagForTest("v6.4.0")),
						Config:    lnCfgTpl,
					},
					Selector: &metav1.LabelSelector{
						MatchLabels: map[string]string{
							v1.VersionLabel: "v6.4.0",
						},
					},
				},
			},
			{
				ObjectMeta: metav1.ObjectMeta{
					Name: "base",
				},
				Spec: v1.ClusterConfigSpec{
					Version: "v6.5.0",
					Lightning: &v1.Configuration{
						BaseImage: pointer.String("IMAGE_URL"),
						Tag:       pointer.String(fakeLightningTagForTest("v6.5.0")),
						Config:    lnCfgTpl,
					},
					Selector: &metav1.LabelSelector{
						MatchLabels: map[string]string{
							v1.DefaultVersionLabel: "true",
						},
					},
				},
			},
			{
				ObjectMeta: metav1.ObjectMeta{
					Name: "base--v6.6.0",
				},
				Spec: v1.ClusterConfigSpec{
					Version: "v6.6.0",
					Lightning: &v1.Configuration{
						BaseImage: pointer.String("IMAGE_URL"),
						Tag:       pointer.String(fakeLightningTagForTest("v6.6.0")),
						Config:    lnCfgTpl,
					},
					Selector: &metav1.LabelSelector{
						MatchLabels: map[string]string{
							v1.VersionLabel: "v6.6.0",
						},
					},
				},
			},
			{
				ObjectMeta: metav1.ObjectMeta{
					Name: "base--v6.6.0--awsc52xlarge",
				},
				Spec: v1.ClusterConfigSpec{
					Version: "v6.6.0",
					Lightning: &v1.Configuration{
						BaseImage: pointer.String("IMAGE_URL"),
						Tag:       pointer.String(fakeLightningTagForTest("v6.6.0")),
						Config:    lnCfgTpl,
					},
					Selector: &metav1.LabelSelector{
						MatchLabels: map[string]string{
							v1.VersionLabel:      "v6.6.0",
							v1.InstanceTypeLabel: "aws.c5.2xlarge",
						},
					},
				},
			},
		},
	}
}

func TestGitopsTierLevelOverlay(t *testing.T) {
	g := &Gitops{}

	cfg := g.ProviderLevelOverlay(consts.ProviderTypeAWS)
	require.NotNil(t, cfg)
	require.Equal(t, v1.PriorityProvider, cfg.Spec.Priority)
	require.Nil(t, cfg.Spec.TiKV)
	require.Nil(t, cfg.Spec.Lightning)

	cfg = g.ProviderLevelOverlay(consts.ProviderTypeAWSFreeTier)
	require.NotNil(t, cfg)
	require.Equal(t, v1.PriorityProvider, cfg.Spec.Priority)
	require.NotNil(t, cfg.Spec.TiKV)
	require.Equal(t, int64(1), (cfg.Spec.TiKV.Config.MP["import"].(map[string]interface{}))["num-threads"])
	require.NotNil(t, cfg.Spec.Lightning)
	require.Equal(t, int64(2), (cfg.Spec.Lightning.Config.MP["lightning"].(map[string]interface{}))["region-concurrency"])
	require.Equal(t, int64(1), (cfg.Spec.Lightning.Config.MP["tidb"].(map[string]interface{}))["distsql-scan-concurrency"])
	require.Nil(t, (cfg.Spec.Lightning.Config.MP["lightning"].(map[string]interface{}))["not-exist"])
}

func TestGetLightningConfig(t *testing.T) {
	g := &Gitops{}
	ccl := generateClusterConfigListForTest(t)
	type testCase struct {
		Version          string
		ExpectedImageTag string
		ExpectErrMsg     string
	}
	for _, tc := range []testCase{
		{
			Version:      "v6.0.96",
			ExpectErrMsg: "retrieved Lightning config for version ('v6.0.96') is empty",
		},
		{
			Version:      "v6.0.97",
			ExpectErrMsg: "cannot fetch Lightning config template content for version ('v6.0.97')",
		},
		{
			Version:      "v6.0.98",
			ExpectErrMsg: "cannot fetch Lightning base image URL for version ('v6.0.98')",
		},
		{
			Version:      "v6.0.99",
			ExpectErrMsg: "cannot fetch Lightning image tag for version ('v6.0.99')",
		},
		{
			Version:      "v6.1.99",
			ExpectErrMsg: "get base cluster config for version 'v6.1.99' failed: should be only one base clusterconfig, but now: 2",
		},
		{
			Version:      "v6.3.0",
			ExpectErrMsg: "The returned base object's version ('v6.5.0') is different from the requested version ('v6.3.0')",
		},
		{
			Version:          "v6.4.0",
			ExpectedImageTag: fakeLightningTagForTest("v6.4.0"),
		},
		{
			Version:          "v6.5.0",
			ExpectedImageTag: fakeLightningTagForTest("v6.5.0"),
		},
		{
			Version:          "v6.6.0",
			ExpectedImageTag: fakeLightningTagForTest("v6.6.0"),
		},
	} {
		lnConfig, err := g.getLightningConfig(ccl, tc.Version)
		if len(tc.ExpectErrMsg) > 0 {
			require.Errorf(t, err, "input version: %s", tc.Version)
			require.Equalf(t, tc.ExpectErrMsg, err.Error(), "input version: %s", tc.Version)
		} else {
			require.NoErrorf(t, err, "input version: %s", tc.Version)
			var lnImageTag string
			if lnConfig.Tag != nil {
				lnImageTag = *lnConfig.Tag
			}
			require.Equalf(t, tc.ExpectedImageTag, lnImageTag, "input version: %s", tc.Version)
		}
	}
}

func TestGetClusterVersion(t *testing.T) {
	testCases := []struct {
		CaseName        string
		Cluster         *v1.Cluster
		ExpectedVersion string
	}{
		{
			CaseName:        "nil cluster",
			Cluster:         nil,
			ExpectedVersion: "",
		},
		{
			CaseName: "normal",
			Cluster: &v1.Cluster{
				Status: v1.ClusterStatus{
					DB: &v1.DBStatus{
						Version: "v6.6.0",
					},
				},
			},
			ExpectedVersion: "v6.6.0",
		},
		{
			CaseName:        "missing db status",
			Cluster:         new(v1.Cluster),
			ExpectedVersion: "",
		},
		{
			CaseName: "empty version",
			Cluster: &v1.Cluster{
				Status: v1.ClusterStatus{
					DB: &v1.DBStatus{
						Version: "",
					},
				},
			},
			ExpectedVersion: "",
		},
	}
	for _, tc := range testCases {
		t.Run(tc.CaseName, func(t *testing.T) {
			version := getClusterVersion(tc.Cluster)
			require.Equal(t, tc.ExpectedVersion, version)
		})
	}
}

func TestGetTargetVersion(t *testing.T) {
	testCases := []struct {
		CaseName        string
		Cluster         *v1.Cluster
		CustomOverlay   *v1.ClusterConfig
		ExpectedVersion string
	}{
		{
			CaseName: "decated tier and has overlay",
			Cluster: &v1.Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Labels: map[string]string{
						v1.ProviderTypeLabel: consts.ProviderTypeEKS,
					},
				},
				Status: v1.ClusterStatus{
					DB: &v1.DBStatus{
						Version: "v7.1.0",
					},
				},
			},
			CustomOverlay: &v1.ClusterConfig{
				Spec: v1.ClusterConfigSpec{
					Version: "v7.2.0",
				},
			},
			ExpectedVersion: "v7.2.0",
		},
		{
			CaseName: "decated tier and no overlay",
			Cluster: &v1.Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Labels: map[string]string{
						v1.ProviderTypeLabel: consts.ProviderTypeEKS,
					},
				},
				Status: v1.ClusterStatus{
					DB: &v1.DBStatus{
						Version: "v7.1.0",
					},
				},
			},
			ExpectedVersion: "v7.1.0",
		},
		{
			CaseName: "serverless tier and has overlay",
			Cluster: &v1.Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Labels: map[string]string{
						v1.ProviderTypeLabel: consts.ProviderTypeAWSFreeTier,
					},
				},
				Status: v1.ClusterStatus{
					DB: &v1.DBStatus{
						Version: "v6.6.0",
					},
				},
			},
			CustomOverlay: &v1.ClusterConfig{
				Spec: v1.ClusterConfigSpec{
					Version: "v7.2.0",
				},
			},
			ExpectedVersion: "v6.6.0",
		},
		{
			CaseName: "serverless tier and no overlay",
			Cluster: &v1.Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Labels: map[string]string{
						v1.ProviderTypeLabel: consts.ProviderTypeAWSFreeTier,
					},
				},
				Status: v1.ClusterStatus{
					DB: &v1.DBStatus{
						Version: "v6.6.0",
					},
				},
			},
			ExpectedVersion: "v6.6.0",
		},
	}
	for _, tc := range testCases {
		t.Run(tc.CaseName, func(t *testing.T) {
			version := getTargetVersion(tc.Cluster, tc.CustomOverlay)
			require.Equal(t, tc.ExpectedVersion, version)
		})
	}
}

func Test_mergeConfiguration(t *testing.T) {
	type args struct {
		c1 *v1.Configuration
		c2 *v1.Configuration
	}
	tests := []struct {
		name string
		args args
		want *v1.Configuration
	}{
		{ // it's an ugly case, when c2 is nil, the function directly return c1, and may cause some fields in c1 is uninitialized.
			name: "both nil",
			args: args{},
			want: nil,
		},
		{
			name: "c1 is non-nil and c2 is nil",
			args: args{
				c1: &v1.Configuration{Config: v1.NewTomlConfig(map[string]interface{}{"key1": "value1"})},
			},
			want: &v1.Configuration{Config: v1.NewTomlConfig(map[string]interface{}{"key1": "value1"})},
		},
		{
			name: "c1 is nil and c2 is non-nil",
			args: args{
				c2: &v1.Configuration{Config: v1.NewTomlConfig(map[string]interface{}{"key1": "value1"})},
			},
			want: &v1.Configuration{Config: v1.NewTomlConfig(map[string]interface{}{"key1": "value1"})},
		},
		{ // it's an ugly case, when c2 is nil, the function directly return c1, and may cause some fields in c1 is uninitialized.
			name: "c1.Config is nil and c2 is nil",
			args: args{
				c1: &v1.Configuration{Config: nil},
			},
			want: &v1.Configuration{Config: nil},
		},
		{
			name: "both non-nil",
			args: args{
				c1: &v1.Configuration{Config: v1.NewTomlConfig(map[string]interface{}{"key1": "value1", "key2": "value222"})},
				c2: &v1.Configuration{Config: v1.NewTomlConfig(map[string]interface{}{"key1": "value2", "key3": "value333"})},
			},
			want: &v1.Configuration{Config: v1.NewTomlConfig(map[string]interface{}{"key1": "value2", "key2": "value222", "key3": "value333"})},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := mergeConfiguration(tt.args.c1, tt.args.c2); !reflect.DeepEqual(got, tt.want) {
				t.Errorf("mergeConfiguration() = %v, want %v", got, tt.want)
			}
		})
	}
}
