package configchange

import (
	"context"
	"reflect"
	"strings"
	"testing"

	"go.uber.org/mock/gomock"
	"sigs.k8s.io/controller-runtime/pkg/log"

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

func Test_hotReloadApplier_filterComponentTypes(t *testing.T) {
	tests := []struct {
		name              string
		changedComponents []cloudv1.ComponentType
		want              []cloudv1.ComponentType
	}{
		{
			name:              "empty",
			changedComponents: nil,
			want:              []cloudv1.ComponentType{},
		},
		{
			name:              "not in hotReloadComponents",
			changedComponents: []cloudv1.ComponentType{cloudv1.ComponentTiDB},
			want:              []cloudv1.ComponentType{},
		},
		{
			name:              "order is not same as hotReloadComponents, should be sorted",
			changedComponents: []cloudv1.ComponentType{cloudv1.ComponentTiKV, cloudv1.ComponentPD, cloudv1.ComponentTiFlash},
			want:              []cloudv1.ComponentType{cloudv1.ComponentPD, cloudv1.ComponentTiFlash, cloudv1.ComponentTiKV},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			r := &hotReloadApplier{}
			cc := &cloudv1.ConfigChange{}
			cc.Status.ChangedComponents = tt.changedComponents
			if got := r.filterComponentTypes(cc); !reflect.DeepEqual(got, tt.want) {
				t.Errorf("filterComponentTypes() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_buildConfigChangeSQL(t *testing.T) {
	type args struct {
		component cloudv1.ComponentType
		key       string
		value     interface{}
	}
	tests := []struct {
		name string
		args args
		want string
	}{
		{
			name: "boolean",
			args: args{
				component: cloudv1.ComponentTiKV,
				key:       "gc.enable-auto-tune",
				value:     true,
			},
			want: "set config tikv `gc.enable-auto-tune`=true",
		},
		{
			name: "string",
			args: args{
				component: cloudv1.ComponentTiKV,
				key:       "log.level",
				value:     "warn",
			},
			want: "set config tikv `log.level`='warn'",
		},
		{
			name: "integer",
			args: args{
				component: cloudv1.ComponentTiKV,
				key:       "log.count",
				value:     100,
			},
			want: "set config tikv `log.count`=100",
		},
		{
			name: "float",
			args: args{
				component: cloudv1.ComponentTiKV,
				key:       "ratio",
				value:     0.2,
			},
			want: "set config tikv `ratio`=0.2",
		},
		{
			name: "modify tiflashLearner via `set confit tiflash`",
			args: args{
				component: cloudv1.ComponentTiFlashLearner,
				key:       "enable",
				value:     false,
			},
			want: "set config tiflash `raftstore-proxy.enable`=false",
		},
		{
			name: "modify config of tiflashLearner should add prefix 'raftstore-proxy.' when the key starts with 'raftstore.'`",
			args: args{
				component: cloudv1.ComponentTiFlashLearner,
				key:       "raftstore.region-split-check-diff",
				value:     "8MiB",
			},
			want: "set config tiflash `raftstore-proxy.raftstore.region-split-check-diff`='8MiB'",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := buildConfigChangeSQL(tt.args.component, tt.args.key, tt.args.value); got != tt.want {
				t.Errorf("buildConfigChangeSQL() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_buildGetConfigSQL(t *testing.T) {
	type args struct {
		component cloudv1.ComponentType
		key       string
	}
	tests := []struct {
		name string
		args args
		want string
	}{
		{
			name: "tikv",
			args: args{
				component: cloudv1.ComponentTiKV,
				key:       "gc.enable-auto-tune",
			},
			want: "show config where type='tikv' and name='gc.enable-auto-tune'",
		},
		{
			name: "tiflash",
			args: args{
				component: cloudv1.ComponentTiFlash,
				key:       "gc.enable-auto-tune",
			},
			want: "show config where type='tiflash' and name='gc.enable-auto-tune'",
		},
		{
			name: "get config for tiflashLearner",
			args: args{
				component: cloudv1.ComponentTiFlashLearner,
				key:       "enable",
			},
			want: "show config where type='tiflash' and name='raftstore-proxy.enable'",
		},
		{
			name: "get config of tiflashLearner should add prefix 'raftstore-proxy.' when the key starts with 'raftstore.'`",
			args: args{
				component: cloudv1.ComponentTiFlashLearner,
				key:       "raftstore.region-split-check-diff",
			},
			want: "show config where type='tiflash' and name='raftstore-proxy.raftstore.region-split-check-diff'",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := buildGetConfigSQL(tt.args.component, tt.args.key); got != tt.want {
				t.Errorf("buildGetConfigSQL() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_compareConfig(t *testing.T) {
	type args struct {
		replica       int
		key           string
		expectedValue interface{}
		resultStr     string
	}
	tests := []struct {
		name              string
		args              args
		wantInconsistent  []string
		errContainsSubStr string
	}{
		{
			name: "replica > len(result.Instance)",
			args: args{
				replica:       2,
				key:           "quota.enable-auto-tune",
				expectedValue: true,
				resultStr:     `{"Instance":["aTE="],"Value":["dHJ1ZQ=="]}`,
			},
			wantInconsistent:  nil,
			errContainsSubStr: "not equal to replicas",
		},
		{
			name: "inconsistent",
			args: args{
				replica:       2,
				key:           "quota.enable-auto-tune",
				expectedValue: true,
				resultStr:     `{"Instance":["aTE=","aTI="],"Value":["dHJ1ZQ==","ZmFsc2U="]}`,
			},
			wantInconsistent:  []string{"i2,actual=false"},
			errContainsSubStr: "",
		},
		{
			name: "compare boolean",
			args: args{
				replica:       1,
				key:           "quota.enable-auto-tune",
				expectedValue: true,
				resultStr:     `{"Instance":["aTE="],"Value":["dHJ1ZQ=="]}`,
			},
			wantInconsistent:  nil,
			errContainsSubStr: "",
		},
		{
			name: "compare string",
			args: args{
				replica:       1,
				key:           "log.level",
				expectedValue: "debug",
				resultStr:     `{"Instance":["aTE="],"Value":["ZGVidWc="]}`,
			},
			wantInconsistent:  nil,
			errContainsSubStr: "",
		},
		{
			name: "compare integer",
			args: args{
				replica:       1,
				key:           "coprocessor.batch-split-limit",
				expectedValue: 11,
				resultStr:     `{"Instance":["aTE="],"Value":["MTE="]}`,
			},
			wantInconsistent:  nil,
			errContainsSubStr: "",
		},
		{
			name: "compare float",
			args: args{
				replica:       1,
				key:           "gc.ratio-threshold",
				expectedValue: 1.2,
				resultStr:     `{"Instance":["aTE="],"Value":["MS4y"]}`,
			},
			wantInconsistent:  nil,
			errContainsSubStr: "",
		},
	}
	for _, tt := range tests {
		ctx := context.Background()
		rx := &ReconcileContext{
			Logger: log.FromContext(ctx),
			Cluster: &cloudv1.Cluster{
				Spec: cloudv1.ClusterSpec{
					Cluster: cloudv1.ClusterTopology{
						TiKV: cloudv1.TiKV{
							Replicas: 1,
						},
					},
				},
			},
		}
		t.Run(tt.name, func(t *testing.T) {
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()
			sqlExecutor := NewMockSQLExecutor(ctrl)
			rx.Cluster.Spec.Cluster.TiKV.Replicas = tt.args.replica

			sqlExecutor.EXPECT().Query(gomock.Any(), gomock.Any()).Return([]byte(tt.args.resultStr), nil)

			gotInconsistent, err := compareConfig(ctx, rx, sqlExecutor, cloudv1.ComponentTiKV, tt.args.key, tt.args.expectedValue)
			if tt.errContainsSubStr != "" && (err == nil || !strings.Contains(err.Error(), tt.errContainsSubStr)) {
				t.Errorf("compareConfig() error = '%s', wantSubstring '%v'", err, tt.errContainsSubStr)
				return
			}
			if !reflect.DeepEqual(gotInconsistent, tt.wantInconsistent) {
				t.Errorf("compareConfig() gotInconsistent = %v, want %v", gotInconsistent, tt.wantInconsistent)
			}
		})
	}
}

func Test_compareConfigurationValue(t *testing.T) {
	type args struct {
		actual   string
		expected string
	}
	tests := []struct {
		name string
		args args
		want bool
	}{
		{
			name: "equal",
			args: args{
				actual:   "1MB",
				expected: "1MB",
			},
			want: true,
		},
		{
			name: "misused units",
			args: args{
				actual:   "1MiB",
				expected: "1MB",
			},
			want: true,
		},
		{
			name: "misused units, but is reversed actual and expected",
			args: args{
				actual:   "1MB",
				expected: "1MiB",
			},
			want: false,
		},
		{
			name: "misused units, but not defined in MisusedUnitsOfConfigurationValue",
			args: args{
				actual:   "1Mib",
				expected: "1Mb",
			},
			want: false,
		},
		{
			name: "not equal",
			args: args{
				actual:   "1MB",
				expected: "2MB",
			},
			want: false,
		},
		{
			name: "undefined in MisusedUnitsOfConfigurationValue and is not equal",
			args: args{
				actual:   "true",
				expected: "false",
			},
			want: false,
		},
		{
			name: "undefined in MisusedUnitsOfConfigurationValue and is equal",
			args: args{
				actual:   "true",
				expected: "true",
			},
			want: true,
		},
		{
			name: "converted units",
			args: args{
				actual:   "30GiB",
				expected: "31457280KiB",
			},
			want: true,
		}, {
			name: "converted units and misused units",
			args: args{
				actual:   "30GiB",
				expected: "31457280KB",
			},
			want: true,
		}, {
			name: "converted units and misused units, but mismatch value",
			args: args{
				actual:   "30GiB",
				expected: "31457281KB", // 1KB more than 30GiB
			},
			want: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := compareConfigurationValue(context.Background(), tt.args.actual, tt.args.expected); got != tt.want {
				t.Errorf("compareConfigurationValue() = %v, want %v", got, tt.want)
			}
		})
	}
}
