package server

import (
	"context"
	"testing"

	"github.com/AlekSi/pointer"
	"github.com/stretchr/testify/assert"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	apiv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/cluster-server/v1"
	apicommon "github.com/tidbcloud/infra-provider/api/tidbcloud/common"
	"github.com/tidbcloud/infra-provider/pkg/scheme"
)

func Test_overlayConfigAndVersion(t *testing.T) {
	const (
		newVersion = "v7.6.0"
	)
	defaultCR := &cloudv1.ConfigRevision{
		Spec: cloudv1.ConfigRevisionSpec{
			TiKV: &cloudv1.Configuration{
				BaseImage: pointer.To("pingcap/tikv"),
				Tag:       pointer.To("v7.5.0"),
				Config: cloudv1.NewTomlConfig(map[string]interface{}{
					"tikv": "b",
				}),
			},
			TiFlash: &cloudv1.Configuration{
				BaseImage: pointer.To("pingcap/tiflash"),
				Tag:       pointer.To("v7.5.0"),
				Config: cloudv1.NewTomlConfig(map[string]interface{}{
					"tiflash": "b",
				}),
			},
			Version: "v7.5.0",
		},
	}
	type args struct {
		crMutator      func(*cloudv1.ConfigRevision)
		configurations []*apiv1.ComponentConfiguration
		version        *apiv1.ClusterVersion
	}
	tests := []struct {
		name            string
		args            args
		wantErr         bool
		wantCRGenerator func(*cloudv1.ConfigRevision)
	}{
		{
			name: "overlay for tikv",
			args: args{
				crMutator: nil,
				configurations: []*apiv1.ComponentConfiguration{
					{
						ConfigurationFileContents: "tikv = \"c\"",
						ComponentType:             apicommon.ComponentType_TIKV,
					},
				},
				version: &apiv1.ClusterVersion{
					ClusterVersion: newVersion,
					ComponentVersions: []*apiv1.ClusterVersion_ComponentVersion{
						{
							ComponentType: apicommon.ComponentType_TIKV,
							ImageRepo:     "pingcap/tikv",
							ImageTag:      newVersion,
						},
					},
				},
			},
			wantErr: false,
			wantCRGenerator: func(cr *cloudv1.ConfigRevision) {
				cr.Spec.TiKV.Config.MP["tikv"] = "c"
				cr.Spec.TiKV.Tag = pointer.To(newVersion)
				cr.Spec.Version = newVersion
			},
		}, {
			name: "overlay for tikv without version",
			args: args{
				crMutator: nil,
				configurations: []*apiv1.ComponentConfiguration{
					{
						ConfigurationFileContents: "tikv = \"c\"",
						ComponentType:             apicommon.ComponentType_TIKV,
					},
				},
			},
			wantErr: false,
			wantCRGenerator: func(cr *cloudv1.ConfigRevision) {
				cr.Spec.TiKV.Config.MP["tikv"] = "c"
			},
		},
		{
			name: "overlay for tiflash",
			args: args{
				crMutator: func(cr *cloudv1.ConfigRevision) {
					cr.Spec.TiFlashLearner = &cloudv1.Configuration{
						BaseImage: pointer.To("pingcap/tiflash"),
						Tag:       pointer.To("v7.5.0"),
						Config: cloudv1.NewTomlConfig(map[string]interface{}{
							"tiflashLearner": "b",
						}),
					}
				},
				configurations: []*apiv1.ComponentConfiguration{
					{
						ConfigurationFileContents: "tiflash = \"c\"",
						ComponentType:             apicommon.ComponentType_TIFLASH,
					},
					{
						ConfigurationFileContents: "tiflashLearner = \"d\"",
						ComponentType:             apicommon.ComponentType_TIFLASH_LEARNER,
					},
				},
				version: &apiv1.ClusterVersion{
					ClusterVersion: newVersion,
					ComponentVersions: []*apiv1.ClusterVersion_ComponentVersion{
						{
							ComponentType: apicommon.ComponentType_TIFLASH,
							ImageRepo:     "pingcap/tiflash",
							ImageTag:      newVersion,
						},
					},
				},
			},
			wantErr: false,
			wantCRGenerator: func(cr *cloudv1.ConfigRevision) {
				cr.Spec.TiFlash.Config.MP["tiflash"] = "c"
				cr.Spec.TiFlash.Tag = pointer.To(newVersion)
				cr.Spec.TiFlashLearner.Config.MP["tiflashLearner"] = "d"
				cr.Spec.TiFlashLearner.BaseImage = pointer.To("pingcap/tiflash")
				cr.Spec.TiFlashLearner.Tag = pointer.To(newVersion)
				cr.Spec.Version = newVersion
			},
		}, {
			name: "overlay for tiflash but ConfigRevision dont have tiflashLearner",
			args: args{
				crMutator: nil,
				configurations: []*apiv1.ComponentConfiguration{
					{
						ConfigurationFileContents: "tiflash = \"c\"",
						ComponentType:             apicommon.ComponentType_TIFLASH,
					},
				},
				version: &apiv1.ClusterVersion{
					ClusterVersion: newVersion,
					ComponentVersions: []*apiv1.ClusterVersion_ComponentVersion{
						{
							ComponentType: apicommon.ComponentType_TIFLASH,
							ImageRepo:     "pingcap/tiflash",
							ImageTag:      newVersion,
						},
					},
				},
			},
			wantErr: false,
			wantCRGenerator: func(cr *cloudv1.ConfigRevision) {
				cr.Spec.TiFlash.Config.MP["tiflash"] = "c"
				cr.Spec.TiFlash.Tag = pointer.To(newVersion)
				cr.Spec.TiFlashLearner = &cloudv1.Configuration{}
				cr.Spec.TiFlashLearner.BaseImage = pointer.To("pingcap/tiflash")
				cr.Spec.TiFlashLearner.Tag = pointer.To(newVersion)
				cr.Spec.Version = newVersion
			},
		}, {
			name: "overlay for tiflash without version",
			args: args{
				crMutator: nil,
				configurations: []*apiv1.ComponentConfiguration{
					{
						ConfigurationFileContents: "tiflash = \"c\"",
						ComponentType:             apicommon.ComponentType_TIFLASH,
					},
				},
			},
			wantErr: false,
			wantCRGenerator: func(cr *cloudv1.ConfigRevision) {
				cr.Spec.TiFlash.Config.MP["tiflash"] = "c"
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cr := defaultCR.DeepCopy()
			if tt.args.crMutator != nil {
				tt.args.crMutator(cr)
			}
			crBackup := cr.DeepCopy()

			ctx := context.Background()
			if err := overlayConfigAndVersion(ctx, cr, tt.args.configurations, tt.args.version); (err != nil) != tt.wantErr {
				t.Errorf("overlayConfigAndVersion() error = %v, wantErr %v", err, tt.wantErr)
			}
			tt.wantCRGenerator(crBackup) // generate want CR
			assert.Equalf(t, crBackup, cr, "compare ConfigRevision")
		})
	}
}

func Test_updateConfigWithoutConfigChangeCR(t *testing.T) {
	type testCase struct {
		configurations []*apiv1.ComponentConfiguration
		version        *apiv1.ClusterVersion
		setup          func(cli client.Client)
		assertFunc     func(cli client.Client)
		wantErr        bool
		wantNewCRName  string
	}

	const (
		clusterName      = "test-cluster"
		namespace        = "default"
		configRevisionV1 = "cr-v1"
		configRevisionV2 = "cr--tunknown-ctest-cluster-19849b33"
	)
	baseCluster := &cloudv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      clusterName,
			Namespace: namespace,
		},
		Spec: cloudv1.ClusterSpec{
			ConfigRevisionRef: &cloudv1.Reference{Name: configRevisionV1},
		},
	}
	baseCR := &cloudv1.ConfigRevision{
		ObjectMeta: metav1.ObjectMeta{
			Name: configRevisionV1,
		},
		Spec: cloudv1.ConfigRevisionSpec{
			TiKV: &cloudv1.Configuration{
				BaseImage: pointer.To("pingcap/tikv"),
				Tag:       pointer.To("v7.5.0"),
				Config:    cloudv1.NewTomlConfig(map[string]interface{}{"tikv": "b"}),
			},
			Version: "v7.5.0",
		},
	}
	ctx := context.Background()

	tests := map[string]testCase{
		"no changes, returns current CR name": {
			configurations: []*apiv1.ComponentConfiguration{
				{
					ConfigurationFileContents: "tikv = \"b\"",
					ComponentType:             apicommon.ComponentType_TIKV,
				},
			},
			version:       nil,
			wantErr:       false,
			wantNewCRName: configRevisionV1,
		},
		"config changes, creates new CR": {
			configurations: []*apiv1.ComponentConfiguration{
				{
					ConfigurationFileContents: "tikv = \"c\"",
					ComponentType:             apicommon.ComponentType_TIKV,
				},
			},
			version: &apiv1.ClusterVersion{
				ClusterVersion: "v7.6.0",
				ComponentVersions: []*apiv1.ClusterVersion_ComponentVersion{
					{
						ComponentType: apicommon.ComponentType_TIKV,
						ImageRepo:     "pingcap/tikv",
						ImageTag:      "v7.6.0",
					},
				},
			},
			assertFunc: func(cli client.Client) {
				cr := &cloudv1.ConfigRevision{}
				err := cli.Get(ctx, client.ObjectKey{Name: configRevisionV2}, cr)
				assert.NoError(t, err)
				assert.NotNil(t, cr)
				assert.Equal(t, "v7.6.0", *cr.Spec.TiKV.Tag)
				assert.Equal(t, "c", cr.Spec.TiKV.Config.MP["tikv"])
			},
			wantNewCRName: configRevisionV2,
		},
		"overlay error": {
			configurations: []*apiv1.ComponentConfiguration{
				{
					ConfigurationFileContents: "invalid toml = ",
					ComponentType:             apicommon.ComponentType_TIKV,
				},
			},
			version:       nil,
			wantErr:       true,
			wantNewCRName: "",
		},
	}

	for name, tc := range tests {
		t.Run(name, func(t *testing.T) {
			var cli client.Client
			if tc.setup != nil {
				cli = newFakeClient(nil)
				tc.setup(cli)
			} else {
				cli = newFakeClient([]client.Object{baseCR.DeepCopy(), baseCluster.DeepCopy()})
			}

			ctx := context.Background()
			cluster := &cloudv1.Cluster{}
			err := cli.Get(ctx, client.ObjectKey{Name: clusterName, Namespace: namespace}, cluster)
			assert.NoError(t, err)
			newCRName, err := updateConfigWithoutConfigChangeCR(ctx, cli, cluster, tc.configurations, tc.version)
			if tc.wantErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
				if tc.wantNewCRName != "" {
					assert.Equal(t, tc.wantNewCRName, newCRName)
				}

				if tc.assertFunc != nil {
					tc.assertFunc(cli)
				}
			}
		})
	}
}

func newFakeClient(initObjs []client.Object) client.Client {
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(initObjs...).Build()
	return cli
}
