package v1

import (
	"context"
	"fmt"
	"testing"

	. "github.com/onsi/gomega"
	"github.com/stretchr/testify/assert"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/pointer"
)

func TestSynced(t *testing.T) {
	var (
		instanceType        = "t3.media"
		anotherInstanceType = "t3.small"

		storageClass        = "gp3-3000-128"
		anotherStorageClass = "gp3-4000-400"
	)

	type testcase struct {
		modify func(c *Cluster)
		expect bool
	}

	cases := map[string]testcase{
		"Normal": {
			modify: func(c *Cluster) {},
			expect: true,
		},
		"Scaling": {
			modify: func(c *Cluster) {
				c.Status.Phase = ClusterPhaseScaling
			},
			expect: false,
		},
		"NoDBStatus": {
			modify: func(c *Cluster) {
				c.Status.DB = nil
			},
			expect: false,
		},
		"NoTiDBStatus": {
			modify: func(c *Cluster) {
				c.Status.DB.TiDB = nil
			},
			expect: false,
		},
		"TiDBUnhealthy": {
			modify: func(c *Cluster) {
				c.Status.DB.TiDB.Status = ComponentUnhealthy
			},
			expect: false,
		},
		"TiDBMemberDiff": {
			modify: func(c *Cluster) {
				c.Status.DB.TiDB.Members = make([]TiDBMember, 0)
			},
			expect: false,
		},
		"TiDBInstanceDiff": {
			modify: func(c *Cluster) {
				c.Status.DB.TiDB.Members = []TiDBMember{{InstanceType: instanceType}}
			},
			expect: true,
		},
		"TiDBInstanceDiffAnother": {
			modify: func(c *Cluster) {
				c.Status.DB.TiDB.Members = []TiDBMember{{InstanceType: anotherInstanceType}}
			},
			expect: false,
		},
		"TiDBInstanceScalePhase": {
			modify: func(c *Cluster) {
				c.Status.DB.TiDB.InstanceTypeScalePhase = InstanceTypeScalePhaseCreatingNodes
			},
			expect: false,
		},
		"NoTiKVStatus": {
			modify: func(c *Cluster) {
				c.Status.DB.TiKV = nil
			},
			expect: false,
		},
		"TiKVUnhealthy": {
			modify: func(c *Cluster) {
				c.Status.DB.TiKV.Status = ComponentUnhealthy
			},
			expect: false,
		},
		"TiKVMemberDiff": {
			modify: func(c *Cluster) {
				c.Status.DB.TiKV.Members = make([]StoreMember, 0)
			},
			expect: false,
		},
		"TiKVVolumeDiff": {
			modify: func(c *Cluster) {
				c.Spec.Cluster.TiKV.StorageSize = resource.MustParse("2Gi")
			},
			expect: false,
		},
		"TiKVVolumeLocalStorage": {
			modify: func(c *Cluster) {
				c.Spec.Cluster.TiKV.UseLocalStorage = pointer.Bool(true)
				c.Spec.Cluster.TiKV.StorageSize = resource.MustParse("2Gi")
			},
			expect: true,
		},
		"TiKVInstanceDiff": {
			modify: func(c *Cluster) {
				c.Status.DB.TiKV.Members = []StoreMember{{InstanceType: instanceType}}
			},
			expect: true,
		},
		"TiKVInstanceDiffAnother": {
			modify: func(c *Cluster) {
				c.Status.DB.TiKV.Members = []StoreMember{{InstanceType: anotherInstanceType}}
			},
			expect: false,
		},
		"TiKVInstanceScalePhase": {
			modify: func(c *Cluster) {
				c.Status.DB.TiKV.InstanceTypeScalePhase = InstanceTypeScalePhaseCordoningNodes
			},
			expect: false,
		},
		"TiKVStorageClassDiff": {
			modify: func(c *Cluster) {
				c.Status.DB.TiKV.Volume.CurrentStorageClass = storageClass
			},
			expect: true,
		},
		"TiKVStorageClassDiffAnother": {
			modify: func(c *Cluster) {
				c.Status.DB.TiKV.Volume.CurrentStorageClass = anotherStorageClass
			},
			expect: false,
		},
		"TiKVStorageClassEmtpy": {
			modify: func(c *Cluster) {
				c.Spec.Cluster.TiKV.StorageClassName = ""
			},
			expect: true,
		},
		"NoPDStatus": {
			modify: func(c *Cluster) {
				c.Status.DB.PD = nil
			},
			expect: false,
		},
		"PDUnhealthy": {
			modify: func(c *Cluster) {
				c.Status.DB.PD.Status = ComponentUnhealthy
			},
			expect: false,
		},
		"PDMemberDiff": {
			modify: func(c *Cluster) {
				c.Status.DB.PD.Members = make([]PDMember, 0)
			},
			expect: false,
		},
		"PDVolumeDiff": {
			modify: func(c *Cluster) {
				c.Spec.Cluster.PD.StorageSize = resource.MustParse("2Gi")
			},
			expect: false,
		},
		"PDInstanceDiff": {
			modify: func(c *Cluster) {
				c.Status.DB.PD.Members = []PDMember{{InstanceType: instanceType}}
			},
			expect: true,
		},
		"PDInstanceDiffAnother": {
			modify: func(c *Cluster) {
				c.Status.DB.PD.Members = []PDMember{{InstanceType: anotherInstanceType}}
			},
			expect: false,
		},
		"PDInstanceScalePhase": {
			modify: func(c *Cluster) {
				c.Status.DB.PD.InstanceTypeScalePhase = InstanceTypeScalePhaseRestartingPods
			},
			expect: false,
		},
		"PDStorageClassDiff": {
			modify: func(c *Cluster) {
				c.Status.DB.PD.Volume.CurrentStorageClass = storageClass
			},
			expect: true,
		},
		"PDStorageClassDiffAnother": {
			modify: func(c *Cluster) {
				c.Status.DB.PD.Volume.CurrentStorageClass = anotherStorageClass
			},
			expect: false,
		},
		"NoTiFlashStatus": {
			modify: func(c *Cluster) {
				c.Status.DB.TiFlash = nil
			},
			expect: false,
		},
		"TiFlashUnhealthy": {
			modify: func(c *Cluster) {
				c.Status.DB.TiFlash.Status = ComponentUnhealthy
			},
			expect: false,
		},
		"TiFlashMemberDiff": {
			modify: func(c *Cluster) {
				c.Status.DB.TiFlash.Members = make([]StoreMember, 0)
			},
			expect: false,
		},
		"TiFlashVolumeDiff": {
			modify: func(c *Cluster) {
				c.Spec.Cluster.TiFlash.StorageSize = resource.MustParse("2Gi")
			},
			expect: false,
		},
		"TiFlashVolumeLocalStorage": {
			modify: func(c *Cluster) {
				c.Spec.Cluster.TiFlash.UseLocalStorage = pointer.Bool(true)
				c.Spec.Cluster.TiFlash.StorageSize = resource.MustParse("2Gi")
			},
			expect: true,
		},
		"TiFlashInstanceDiff": {
			modify: func(c *Cluster) {
				c.Status.DB.TiFlash.Members = []StoreMember{{InstanceType: instanceType}}
			},
			expect: true,
		},
		"TiFlashInstanceDiffAnother": {
			modify: func(c *Cluster) {
				c.Status.DB.TiFlash.Members = []StoreMember{{InstanceType: anotherInstanceType}}
			},
			expect: false,
		},
		"TiFlashInstanceScalePhase": {
			modify: func(c *Cluster) {
				c.Status.DB.TiFlash.InstanceTypeScalePhase = InstanceTypeScalePhaseDeletingNodes
			},
			expect: false,
		},
		"TiFlashStorageClassDiff": {
			modify: func(c *Cluster) {
				c.Status.DB.TiFlash.Volume.CurrentStorageClass = storageClass
			},
			expect: true,
		},
		"TiFlashStorageClassDiffAnother": {
			modify: func(c *Cluster) {
				c.Status.DB.TiFlash.Volume.CurrentStorageClass = anotherStorageClass
			},
			expect: false,
		},
		"EKSCMEKNormal": {
			modify: func(c *Cluster) {
				SetEKSCMEKAnnotations(&c.ObjectMeta, "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012")
				mapper, err := RetrieveMapper(c)
				assert.NoError(t, err)
				c.Status.DB.PD.Volume.CurrentStorageClass = mapper.Name(storageClass)
				c.Status.DB.TiKV.Volume.CurrentStorageClass = mapper.Name(storageClass)
				c.Status.DB.TiFlash.Volume.CurrentStorageClass = mapper.Name(storageClass)
			},
			expect: true,
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			g := NewGomegaWithT(t)
			c := &Cluster{
				Spec: ClusterSpec{
					Cluster: ClusterTopology{
						TiDB: TiDB{
							Replicas:     1,
							InstanceType: instanceType,
						},
						TiKV: TiKV{
							Replicas:         1,
							StorageSize:      resource.MustParse("1Gi"),
							StorageClassName: storageClass,
							InstanceType:     instanceType,
						},
						PD: PD{
							Replicas:         1,
							StorageSize:      resource.MustParse("1Gi"),
							StorageClassName: storageClass,
							InstanceType:     instanceType,
						},
						TiFlash: TiFlash{
							Replicas:         1,
							StorageSize:      resource.MustParse("1Gi"),
							StorageClassName: storageClass,
							InstanceType:     instanceType,
						},
					},
				},
				Status: ClusterStatus{
					Phase: ClusterPhaseRunning,
					DB: &DBStatus{
						PD: &PDStatus{
							Status:  ComponentNormal,
							Members: make([]PDMember, 1),
							Volume: &VolumeStatus{
								CurrentCapacity: resource.MustParse("1Gi"),
							},
						},
						TiDB: &TiDBStatus{
							Status:  ComponentNormal,
							Members: make([]TiDBMember, 1),
						},
						TiKV: &TiKVStatus{
							Status:  ComponentNormal,
							Members: make([]StoreMember, 1),
							Volume: &VolumeStatus{
								CurrentCapacity: resource.MustParse("1Gi"),
							},
						},
						TiFlash: &TiFlashStatus{
							Status:  ComponentNormal,
							Members: make([]StoreMember, 1),
							Volume: &VolumeStatus{
								CurrentCapacity: resource.MustParse("1Gi"),
							},
						},
					},
				},
			}
			tc.modify(c)
			g.Expect(c.Synced()).Should(Equal(tc.expect))
		})
	}

	cases = map[string]testcase{
		"Normal": {
			modify: func(c *Cluster) {},
			expect: true,
		},
		"TiDBExist": {
			modify: func(c *Cluster) {
				c.Status.DB.TiDB.Members = make([]TiDBMember, 1)
			},
			expect: false,
		},
		"TiKVExist": {
			modify: func(c *Cluster) {
				c.Status.DB.TiKV.Members = make([]StoreMember, 1)
			},
			expect: false,
		},
		"PDExist": {
			modify: func(c *Cluster) {
				c.Status.DB.PD.Members = make([]PDMember, 1)
			},
			expect: false,
		},
		"TiFlashExist": {
			modify: func(c *Cluster) {
				c.Status.DB.TiFlash.Members = make([]StoreMember, 1)
			},
			expect: false,
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			g := NewGomegaWithT(t)
			c := &Cluster{
				Spec: ClusterSpec{
					Cluster: ClusterTopology{
						TiDB: TiDB{
							Replicas: 0,
						},
						TiKV: TiKV{
							Replicas: 0,
						},
						PD: PD{
							Replicas: 0,
						},
						TiFlash: TiFlash{
							Replicas: 0,
						},
					},
				},
				Status: ClusterStatus{
					Phase: ClusterPhaseRunning,
					DB: &DBStatus{
						PD: &PDStatus{
							Members: make([]PDMember, 0),
						},
						TiDB: &TiDBStatus{
							Members: make([]TiDBMember, 0),
						},
						TiKV: &TiKVStatus{
							Members: make([]StoreMember, 0),
						},
						TiFlash: &TiFlashStatus{
							Members: make([]StoreMember, 0),
						},
					},
				},
			}
			tc.modify(c)
			g.Expect(c.Synced()).Should(Equal(tc.expect))
		})
	}
}

func TestSyncedAllowsGKEComputeClassMemberInstanceTypes(t *testing.T) {
	cases := []struct {
		name   string
		modify func(*Cluster)
	}{
		{
			name: "tidb",
			modify: func(c *Cluster) {
				c.Labels = map[string]string{ProviderTypeLabel: ProviderTypeGKE}
				updateTestGKEComputeClassBinding(t, c, AnnoKeyGKEComputeClassForTiDBBindings, "n2-standard-8", "tidb-base", GKEComputeClassNodePoolBinding{InstanceType: "n2-standard-64", NodePoolName: "tidb-extra"})
				c.Spec.Cluster.TiDB.InstanceType = "n2-standard-8"
				c.Status.DB.TiDB.Members = []TiDBMember{{InstanceType: "n2-standard-64"}}
			},
		},
		{
			name: "tikv",
			modify: func(c *Cluster) {
				c.Labels = map[string]string{ProviderTypeLabel: ProviderTypeGKE}
				updateTestGKEComputeClassBinding(t, c, AnnoKeyGKEComputeClassForTiKVBindings, "n2-standard-8", "tikv-base", GKEComputeClassNodePoolBinding{InstanceType: "n2-standard-64", NodePoolName: "tikv-extra"})
				c.Spec.Cluster.TiKV.InstanceType = "n2-standard-8"
				c.Status.DB.TiKV.Members = []StoreMember{{InstanceType: "n2-standard-64"}}
			},
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			c := &Cluster{
				Spec: ClusterSpec{
					Cluster: ClusterTopology{
						TiDB: TiDB{Replicas: 1, InstanceType: "n2-standard-8"},
						TiKV: TiKV{
							Replicas:         1,
							StorageSize:      resource.MustParse("1Gi"),
							StorageClassName: "gp3-3000-128",
							InstanceType:     "n2-standard-8",
						},
						PD: PD{
							Replicas:         1,
							StorageSize:      resource.MustParse("1Gi"),
							StorageClassName: "gp3-3000-128",
							InstanceType:     "n2-standard-8",
						},
						TiFlash: TiFlash{
							Replicas:         1,
							StorageSize:      resource.MustParse("1Gi"),
							StorageClassName: "gp3-3000-128",
							InstanceType:     "n2-standard-8",
						},
					},
				},
				Status: ClusterStatus{
					Phase: ClusterPhaseRunning,
					DB: &DBStatus{
						PD: &PDStatus{
							Status:  ComponentNormal,
							Members: []PDMember{{InstanceType: "n2-standard-8"}},
							Volume:  &VolumeStatus{CurrentCapacity: resource.MustParse("1Gi")},
						},
						TiDB: &TiDBStatus{
							Status:  ComponentNormal,
							Members: []TiDBMember{{InstanceType: "n2-standard-8"}},
						},
						TiKV: &TiKVStatus{
							Status:  ComponentNormal,
							Members: []StoreMember{{InstanceType: "n2-standard-8"}},
							Volume:  &VolumeStatus{CurrentCapacity: resource.MustParse("1Gi")},
						},
						TiFlash: &TiFlashStatus{
							Status:  ComponentNormal,
							Members: []StoreMember{{InstanceType: "n2-standard-8"}},
							Volume:  &VolumeStatus{CurrentCapacity: resource.MustParse("1Gi")},
						},
					},
				},
			}
			tc.modify(c)
			assert.True(t, c.Synced())
		})
	}
}

func TestCluster_InstanceTypeChangedComponent(t *testing.T) {
	cluster := &Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Annotations: map[string]string{
				DefaultLoadBalancerIDAnnotationKey: "default",
			},
		},
	}

	defaultGroupKey := cluster.GetInstanceTypeChangedComponentAnnoKey("db")
	extraGroupKey := cluster.GetInstanceTypeChangedComponentAnnoKey("group1")

	type testCase struct {
		name     string
		annoKey  string
		annoVal  string
		group    string
		setup    func()
		expected []ComponentType
	}

	tests := []testCase{
		{
			name:     "empty annotation returns empty slice",
			annoKey:  defaultGroupKey,
			annoVal:  "",
			group:    "db",
			expected: []ComponentType{},
		},
		{
			name:     "single component in annotation",
			annoKey:  defaultGroupKey,
			annoVal:  "tidb",
			group:    "db",
			expected: []ComponentType{ComponentTiDB},
		},
		{
			name:     "multiple components in annotation",
			annoKey:  defaultGroupKey,
			annoVal:  "pd,tidb,tikv",
			group:    "db",
			expected: []ComponentType{ComponentPD, ComponentTiDB, ComponentTiKV},
		},
		{
			name:     "extra group annotation",
			annoKey:  extraGroupKey,
			annoVal:  "tidb,tiproxy",
			group:    "group1",
			expected: []ComponentType{ComponentTiDB, ComponentTiProxy},
		},
		{
			name:     "annotation with spaces and empty entries", // TODO: trim spaces and ignore empty entries
			annoKey:  defaultGroupKey,
			annoVal:  " tidb , , pd ",
			group:    "db",
			expected: []ComponentType{" tidb ", " ", " pd "},
		},
		{
			name:     "annotation with duplicate components", // TODO: deduplicate
			annoKey:  defaultGroupKey,
			annoVal:  "tidb,tidb,pd",
			group:    "db",
			expected: []ComponentType{ComponentTiDB, ComponentTiDB, ComponentPD},
		},
		{
			name:    "annotation key does not exist",
			annoKey: defaultGroupKey,
			annoVal: "",
			group:   "db",
			setup: func() {
				delete(cluster.Annotations, defaultGroupKey)
			},
			expected: []ComponentType{},
		},
		{
			name:  "nil annotations map",
			group: "db",
			setup: func() {
				cluster.Annotations = nil
			},
			expected: []ComponentType{},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			// Reset annotations for each test
			if cluster.Annotations == nil {
				cluster.Annotations = map[string]string{}
			} else {
				for k := range cluster.Annotations {
					delete(cluster.Annotations, k)
				}
			}
			cluster.Annotations[DefaultLoadBalancerIDAnnotationKey] = "default"

			if tc.annoKey != "" && tc.setup == nil {
				cluster.Annotations[tc.annoKey] = tc.annoVal
			}
			if tc.setup != nil {
				tc.setup()
			}
			got := cluster.InstanceTypeChangedComponent(tc.group)
			assert.ElementsMatch(t, tc.expected, got)
		})
	}
}

func TestCluster_GetInstanceTypeChangedComponentAnnoKey(t *testing.T) {
	cluster := &Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Annotations: map[string]string{},
		},
	}

	tests := []struct {
		name     string
		tcName   string
		expected string
	}{
		{
			name:     "default group",
			tcName:   "db",
			expected: AnnoClusterUpdateAnnotationKeyInstanceType,
		},
		{
			name:     "extra group group1",
			tcName:   "group1",
			expected: AnnoClusterUpdateAnnotationKeyInstanceType + "-group1",
		},
		{
			name:     "extra group with dash",
			tcName:   "group-extra",
			expected: AnnoClusterUpdateAnnotationKeyInstanceType + "-group-extra",
		},
		{
			name:     "empty tcName",
			tcName:   "",
			expected: AnnoClusterUpdateAnnotationKeyInstanceType + "-",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := cluster.GetInstanceTypeChangedComponentAnnoKey(tt.tcName)
			assert.Equal(t, tt.expected, got)
		})
	}
}

func TestCluster_GetMinReadySeconds(t *testing.T) {
	type args struct {
		groupId     string
		compType    ComponentType
		annotations map[string]string
	}
	tests := []struct {
		name     string
		args     args
		expected string
	}{
		{
			name: "override annotation not set, should return default",
			args: args{
				groupId:     "default-group",
				compType:    ComponentTiDB,
				annotations: map[string]string{},
			},
			expected: "180",
		},
		{
			name: "override annotation set, should return value",
			args: args{
				groupId:  "default-group",
				compType: ComponentTiDB,
				annotations: map[string]string{
					AnnoKeyOverrideTiDBMinReadySeconds: "300",
				},
			},
			expected: "300",
		},
		{
			name: "group-specific annotation set, should return group value",
			args: args{
				groupId:  "group1",
				compType: ComponentTiDB,
				annotations: map[string]string{
					AnnoKeyOverrideTiDBMinReadySeconds:             "200",
					AnnoKeyOverrideTiDBMinReadySeconds + "-group1": "250",
				},
			},
			expected: "250",
		},
		{
			name: "another group-specific annotation set, should ignore",
			args: args{
				groupId:  "group1",
				compType: ComponentTiDB,
				annotations: map[string]string{
					AnnoKeyOverrideTiDBMinReadySeconds:             "200",
					AnnoKeyOverrideTiDBMinReadySeconds + "-group2": "250",
				},
			},
			expected: "200",
		},
		{
			name: "group-specific annotation empty, fallback to default",
			args: args{
				groupId:  "group1",
				compType: ComponentTiDB,
				annotations: map[string]string{
					AnnoKeyOverrideTiDBMinReadySeconds + "-group1": "",
					AnnoKeyOverrideTiDBMinReadySeconds:             "200",
				},
			},
			expected: "200",
		},
		{
			name: "invalid annotation value, fallback to default",
			args: args{
				groupId:  "default-group",
				compType: ComponentTiDB,
				annotations: map[string]string{
					AnnoKeyOverrideTiDBMinReadySeconds: "invalid",
				},
			},
			expected: "180",
		},
		{
			name: "group-specific invalid, fallback to default",
			args: args{
				groupId:  "group1",
				compType: ComponentTiDB,
				annotations: map[string]string{
					AnnoKeyOverrideTiDBMinReadySeconds + "-group1": "invalid",
					AnnoKeyOverrideTiDBMinReadySeconds:             "210",
				},
			},
			expected: "180",
		},
		{
			name: "TiProxy default annotation not set, should return default",
			args: args{
				groupId:     "default-group",
				compType:    ComponentTiProxy,
				annotations: map[string]string{},
			},
			expected: "180",
		},
		{
			name: "TiProxy default annotation set, should return value",
			args: args{
				groupId:  "default-group",
				compType: ComponentTiProxy,
				annotations: map[string]string{
					AnnoKeyOverrideTiProxyMinReadySeconds: "400",
				},
			},
			expected: "400",
		},
		{
			name: "TiProxy group-specific annotation set, should return group value",
			args: args{
				groupId:  "group1",
				compType: ComponentTiProxy,
				annotations: map[string]string{
					AnnoKeyOverrideTiProxyMinReadySeconds:             "300",
					AnnoKeyOverrideTiProxyMinReadySeconds + "-group1": "350",
				},
			},
			expected: "350",
		},
		{
			name: "TiProxy group-specific annotation empty, fallback to default",
			args: args{
				groupId:  "group1",
				compType: ComponentTiProxy,
				annotations: map[string]string{
					AnnoKeyOverrideTiProxyMinReadySeconds + "-group1": "",
					AnnoKeyOverrideTiProxyMinReadySeconds:             "300",
				},
			},
			expected: "300",
		},
		{
			name: "TiProxy invalid annotation value, fallback to default",
			args: args{
				groupId:  "default-group",
				compType: ComponentTiProxy,
				annotations: map[string]string{
					AnnoKeyOverrideTiProxyMinReadySeconds: "invalid",
				},
			},
			expected: "180",
		},
		{
			name: "TiProxy group-specific invalid, fallback to default",
			args: args{
				groupId:  "group1",
				compType: ComponentTiProxy,
				annotations: map[string]string{
					AnnoKeyOverrideTiProxyMinReadySeconds + "-group1": "invalid",
					AnnoKeyOverrideTiProxyMinReadySeconds:             "310",
				},
			},
			expected: "180",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cluster := &Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Annotations: tt.args.annotations,
				},
			}
			got := cluster.GetMinReadySeconds(context.Background(), tt.args.groupId, tt.args.compType)
			assert.Equal(t, tt.expected, got, "GetMinReadySeconds should return expected value")
		})
	}
}

func TestCluster_getOverriddenValueFromAnno(t *testing.T) {
	type args struct {
		groupId     string
		baseKey     string
		defValue    string
		annotations map[string]string
	}
	tests := []struct {
		name     string
		args     args
		expected string
	}{
		{
			name: "no annotation, returns default",
			args: args{
				groupId:     "default",
				baseKey:     "test-key",
				defValue:    "default-value",
				annotations: map[string]string{},
			},
			expected: "default-value",
		},
		{
			name: "baseKey annotation present, returns value",
			args: args{
				groupId:     "default",
				baseKey:     "test-key",
				defValue:    "default-value",
				annotations: map[string]string{"test-key": "value1"},
			},
			expected: "value1",
		}, {
			name: "baseKey and group-specific annotation present, returns group value",
			args: args{
				groupId:     "group1",
				baseKey:     "test-key",
				defValue:    "default-value",
				annotations: map[string]string{"test-key-group1": "group-value", "test-key": "value1"},
			},
			expected: "group-value",
		}, {
			name: "group-specific annotation present, baseKey empty",
			args: args{
				groupId:     "group1",
				baseKey:     "test-key",
				defValue:    "default-value",
				annotations: map[string]string{"test-key-group1": "group-value", "test-key": ""},
			},
			expected: "group-value",
		},
		{
			name: "group-specific annotation empty, fallback to baseKey",
			args: args{
				groupId:     "group1",
				baseKey:     "test-key",
				defValue:    "default-value",
				annotations: map[string]string{"test-key-group1": "", "test-key": "value1"},
			},
			expected: "value1",
		}, {
			name: "group-specific annotation empty, baseKey empty, fallback to default",
			args: args{
				groupId:     "group1",
				baseKey:     "test-key",
				defValue:    "default-value",
				annotations: map[string]string{"test-key-group1": "", "test-key": ""},
			},
			expected: "default-value",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cluster := &Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Annotations: tt.args.annotations,
				},
			}
			got := cluster.getOverriddenValueFromAnno(context.Background(), tt.args.groupId, tt.args.baseKey, tt.args.defValue)
			assert.Equal(t, tt.expected, got)
		})
	}
}

func TestCluster_getOverriddenIntValueFromAnno(t *testing.T) {
	type args struct {
		groupId     string
		baseKey     string
		defValue    int64
		annotations map[string]string
	}
	tests := []struct {
		name     string
		args     args
		expected int64
	}{
		{
			name: "valid int value in baseKey",
			args: args{
				groupId:     "default",
				baseKey:     "int-key",
				defValue:    42,
				annotations: map[string]string{"int-key": "123"},
			},
			expected: 123,
		},
		{
			name: "invalid int value in baseKey, fallback to default",
			args: args{
				groupId:     "default",
				baseKey:     "int-key",
				defValue:    42,
				annotations: map[string]string{"int-key": "not-an-int"},
			},
			expected: 42,
		},
		{
			name: "group-specific valid int value",
			args: args{
				groupId:     "group1",
				baseKey:     "int-key",
				defValue:    42,
				annotations: map[string]string{"int-key-group1": "99", "int-key": "123"},
			},
			expected: 99,
		},
		{
			name: "group-specific invalid int value, fallback to default",
			args: args{
				groupId:     "group1",
				baseKey:     "int-key",
				defValue:    42,
				annotations: map[string]string{"int-key-group1": "bad", "int-key": "123"},
			},
			expected: 42,
		},
		{
			name: "group-specific empty, baseKey valid int",
			args: args{
				groupId:     "group1",
				baseKey:     "int-key",
				defValue:    42,
				annotations: map[string]string{"int-key-group1": "", "int-key": "123"},
			},
			expected: 123,
		},
		{
			name: "group-specific empty, baseKey invalid int, fallback to default",
			args: args{
				groupId:     "group1",
				baseKey:     "int-key",
				defValue:    42,
				annotations: map[string]string{"int-key-group1": "", "int-key": "bad"},
			},
			expected: 42,
		},
		{
			name: "no annotation, fallback to default",
			args: args{
				groupId:     "group1",
				baseKey:     "int-key",
				defValue:    42,
				annotations: map[string]string{},
			},
			expected: 42,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cluster := &Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Annotations: tt.args.annotations,
				},
			}
			got := cluster.getOverriddenIntValueFromAnno(context.Background(), tt.args.groupId, tt.args.baseKey, tt.args.defValue)
			assert.Equal(t, tt.expected, got)
		})
	}
}

func TestGetUpToDateReplicas(t *testing.T) {
	var (
		instanceType        = "t3.media"
		anotherInstanceType = "t3.large"

		storageSize        = resource.MustParse("10Gi")
		anotherStorageSize = resource.MustParse("20Gi")

		storageClass        = "gp3-3000-128"
		anotherStorageClass = "gp3-4000-400"
	)

	type testCase struct {
		comp   ComponentType
		modify func(c *Cluster)
		expect int
	}

	cases := map[string]testCase{
		"NoDBStatus": {
			comp: ComponentTiDB,
			modify: func(c *Cluster) {
				c.Status.DB = nil
			},
			expect: 0,
		},
		"NoTiDBStatus": {
			comp: ComponentTiDB,
			modify: func(c *Cluster) {
				c.Status.DB.TiDB = nil
			},
			expect: 0,
		},
		"NoTiKVStatus": {
			comp: ComponentTiKV,
			modify: func(c *Cluster) {
				c.Status.DB.TiKV = nil
			},
			expect: 0,
		},
		"NoPDStatus": {
			comp: ComponentPD,
			modify: func(c *Cluster) {
				c.Status.DB.PD = nil
			},
			expect: 0,
		},
		"NoTiFlashStatus": {
			comp: ComponentTiFlash,
			modify: func(c *Cluster) {
				c.Status.DB.TiFlash = nil
			},
			expect: 0,
		},
		"TiDBUpdatedReplicasAll": {
			comp: ComponentTiDB,
			modify: func(c *Cluster) {
				c.Status.DB.TiDB.Members = []TiDBMember{{Status: MemberStatusNormal}, {Status: MemberStatusNormal}}
				c.Status.DB.TiDB.UpdatedReplicas = 2
			},
			expect: 2,
		},
		"TiDBUpdatedReplicasPartial": {
			comp: ComponentTiDB,
			modify: func(c *Cluster) {
				c.Status.DB.TiDB.Members = []TiDBMember{{Status: MemberStatusNormal}, {Status: MemberStatusNormal}}
				c.Status.DB.TiDB.UpdatedReplicas = 1
			},
			expect: 1,
		},
		"TiDBUpdatedReplicasNone": {
			comp: ComponentTiDB,
			modify: func(c *Cluster) {
				c.Status.DB.TiDB.Members = []TiDBMember{{Status: MemberStatusNormal}, {Status: MemberStatusNormal}}
				c.Status.DB.TiDB.UpdatedReplicas = 0
			},
			expect: 0,
		},
		"TiDBInstanceTypeNotStarted": {
			comp: ComponentTiDB,
			modify: func(c *Cluster) {
				c.Status.DB.TiDB.Members = []TiDBMember{{Status: MemberStatusNormal, InstanceType: anotherInstanceType}, {Status: MemberStatusNormal, InstanceType: anotherInstanceType}}
				c.Status.DB.TiDB.UpdatedReplicas = 2
			},
			expect: 0,
		},
		"TiDBInstanceTypeNotFinished": {
			comp: ComponentTiDB,
			modify: func(c *Cluster) {
				c.Status.DB.TiDB.Members = []TiDBMember{{Status: MemberStatusNormal, InstanceType: instanceType}, {Status: MemberStatusNormal, InstanceType: anotherInstanceType}}
				c.Status.DB.TiDB.UpdatedReplicas = 2
			},
			expect: 1,
		},
		"TiDBStillScaling": {
			comp: ComponentTiDB,
			modify: func(c *Cluster) {
				c.Status.DB.TiDB.Members = []TiDBMember{{Status: MemberStatusNormal, InstanceType: instanceType}, {Status: MemberStatusNormal, InstanceType: instanceType}}
				c.Status.DB.TiDB.UpdatedReplicas = 2
				c.Status.DB.TiDB.Status = ComponentScaling
			},
			expect: 2,
		},
		"TiDBStillUnhealthy": {
			comp: ComponentTiDB,
			modify: func(c *Cluster) {
				c.Status.DB.TiDB.Members = []TiDBMember{{Status: MemberStatusNormal, InstanceType: instanceType}, {Status: MemberStatusNormal, InstanceType: instanceType}}
				c.Status.DB.TiDB.UpdatedReplicas = 2
				c.Status.DB.TiDB.Status = ComponentUnhealthy
			},
			expect: 2,
		},
		"TiDBHeterogeneousAllowed": {
			comp: ComponentTiDB,
			modify: func(c *Cluster) {
				c.Labels = map[string]string{ProviderTypeLabel: ProviderTypeGKE}
				updateTestGKEComputeClassBinding(t, c, AnnoKeyGKEComputeClassForTiDBBindings, instanceType, "tidb-base", GKEComputeClassNodePoolBinding{InstanceType: anotherInstanceType, NodePoolName: "tidb-extra"})
				c.Status.DB.TiDB.Members = []TiDBMember{
					{Status: MemberStatusNormal, InstanceType: anotherInstanceType},
					{Status: MemberStatusNormal, InstanceType: anotherInstanceType},
				}
				c.Status.DB.TiDB.UpdatedReplicas = 2
			},
			expect: 2,
		},
		"TiDBMoreUpdatedThanInstance": {
			comp: ComponentTiDB,
			modify: func(c *Cluster) {
				c.Spec.Cluster.TiDB.Replicas = 3
				c.Status.DB.TiDB.Members = []TiDBMember{
					{Status: MemberStatusNormal, InstanceType: instanceType},
					{Status: MemberStatusNormal, InstanceType: anotherInstanceType},
					{Status: MemberStatusNormal, InstanceType: anotherInstanceType}}
				c.Status.DB.TiDB.UpdatedReplicas = 2
			},
			expect: 1,
		},
		"TiDBScaleOutPending": {
			comp: ComponentTiDB,
			modify: func(c *Cluster) {
				c.Spec.Cluster.TiDB.Replicas = 3
				c.Status.DB.TiDB.Members = []TiDBMember{
					{Status: MemberStatusNormal, InstanceType: instanceType},
					{Status: MemberStatusCreating, InstanceType: instanceType},
					{Status: MemberStatusCreating, InstanceType: instanceType}}
				c.Status.DB.TiDB.UpdatedReplicas = 3 // updatedReplicas has been set even the pod is Pending
				c.Status.DB.TiDB.Status = ComponentScaling
			},
			expect: 1,
		},
		"TiDBScalingIn": {
			comp: ComponentTiDB,
			modify: func(c *Cluster) {
				c.Spec.Cluster.TiDB.Replicas = 1 // scale in: 2 -> 1
				c.Status.DB.TiDB.Members = []TiDBMember{
					{Status: MemberStatusNormal, InstanceType: instanceType},
					{Status: MemberStatusUnavailable, InstanceType: instanceType}}
				c.Status.DB.TiDB.UpdatedReplicas = 1
				c.Status.DB.TiDB.Status = ComponentUnhealthy
			},
			expect: 1,
		},
		"TiKVUpdatedReplicasAll": {
			comp: ComponentTiKV,
			modify: func(c *Cluster) {
				c.Status.DB.TiKV.Members = []StoreMember{{Status: MemberStatusNormal}, {Status: MemberStatusNormal}}
				c.Status.DB.TiKV.UpdatedReplicas = 2
			},
			expect: 2,
		},
		"TiKVUpdatedReplicasPartial": {
			comp: ComponentTiKV,
			modify: func(c *Cluster) {
				c.Status.DB.TiKV.Members = []StoreMember{{Status: MemberStatusNormal}, {Status: MemberStatusNormal}}
				c.Status.DB.TiKV.UpdatedReplicas = 1
			},
			expect: 1,
		},
		"TiKVUpdatedReplicasNone": {
			comp: ComponentTiKV,
			modify: func(c *Cluster) {
				c.Status.DB.TiKV.Members = []StoreMember{{Status: MemberStatusNormal}, {Status: MemberStatusNormal}}
				c.Status.DB.TiKV.UpdatedReplicas = 0
			},
			expect: 0,
		},
		"TiKVInstanceTypeNotStarted": {
			comp: ComponentTiKV,
			modify: func(c *Cluster) {
				c.Status.DB.TiKV.Members = []StoreMember{{Status: MemberStatusNormal, InstanceType: anotherInstanceType}, {Status: MemberStatusNormal, InstanceType: anotherInstanceType}}
				c.Status.DB.TiKV.UpdatedReplicas = 2
			},
			expect: 0,
		},
		"TiKVInstanceTypeNotFinished": {
			comp: ComponentTiKV,
			modify: func(c *Cluster) {
				c.Status.DB.TiKV.Members = []StoreMember{{Status: MemberStatusNormal, InstanceType: instanceType}, {Status: MemberStatusNormal, InstanceType: anotherInstanceType}}
				c.Status.DB.TiKV.UpdatedReplicas = 2
			},
			expect: 1,
		},
		"TiKVStillScaling": {
			comp: ComponentTiKV,
			modify: func(c *Cluster) {
				c.Status.DB.TiKV.Members = []StoreMember{{Status: MemberStatusNormal, InstanceType: instanceType}, {Status: MemberStatusNormal, InstanceType: instanceType}}
				c.Status.DB.TiKV.UpdatedReplicas = 2
				c.Status.DB.TiKV.Status = ComponentScaling
			},
			expect: 1,
		},
		"TiKVStillUnhealthy": {
			comp: ComponentTiKV,
			modify: func(c *Cluster) {
				c.Status.DB.TiKV.Members = []StoreMember{{Status: MemberStatusNormal, InstanceType: instanceType}, {Status: MemberStatusNormal, InstanceType: instanceType}}
				c.Status.DB.TiKV.UpdatedReplicas = 2
				c.Status.DB.TiKV.Status = ComponentUnhealthy
			},
			expect: 1,
		},
		"TiKVHeterogeneousAllowed": {
			comp: ComponentTiKV,
			modify: func(c *Cluster) {
				c.Labels = map[string]string{ProviderTypeLabel: ProviderTypeGKE}
				updateTestGKEComputeClassBinding(t, c, AnnoKeyGKEComputeClassForTiKVBindings, instanceType, "tikv-base", GKEComputeClassNodePoolBinding{InstanceType: anotherInstanceType, NodePoolName: "tikv-extra"})
				c.Status.DB.TiKV.Members = []StoreMember{
					{Status: MemberStatusNormal, InstanceType: anotherInstanceType},
					{Status: MemberStatusNormal, InstanceType: anotherInstanceType},
				}
				c.Status.DB.TiKV.UpdatedReplicas = 2
			},
			expect: 2,
		},
		"TiKVMoreUpdatedThanInstance": {
			comp: ComponentTiKV,
			modify: func(c *Cluster) {
				c.Spec.Cluster.TiKV.Replicas = 3
				c.Status.DB.TiKV.Members = []StoreMember{
					{Status: MemberStatusNormal, InstanceType: instanceType},
					{Status: MemberStatusNormal, InstanceType: anotherInstanceType},
					{Status: MemberStatusNormal, InstanceType: anotherInstanceType}}
				c.Status.DB.TiKV.UpdatedReplicas = 2
			},
			expect: 1,
		},
		"TiKVScaleOutPendingWithStorageUp": {
			comp: ComponentTiKV,
			modify: func(c *Cluster) {
				c.Spec.Cluster.TiKV.Replicas = 3
				c.Status.DB.TiKV.Members = []StoreMember{
					{Status: MemberStatusNormal, InstanceType: instanceType},
					{Status: MemberStatusNormal, InstanceType: instanceType},
					{Status: MemberStatusNormal, InstanceType: instanceType}}
				c.Status.DB.TiKV.Volume = &VolumeStatus{ModifiedCount: 0, ModifiedCapacity: storageSize, ModifiedStorageClass: storageClass} // storage scale not started
				c.Status.DB.TiKV.UpdatedReplicas = 3                                                                                         // updatedReplicas has been set even the pod is Pending
				c.Status.DB.TiKV.Status = ComponentScaling
			},
			expect: 0,
		},
		"TiKVScaleOutPendingWithoutStorageUp": {
			comp: ComponentTiKV,
			modify: func(c *Cluster) {
				c.Spec.Cluster.TiKV.Replicas = 3
				c.Status.DB.TiKV.Members = []StoreMember{
					{Status: MemberStatusNormal, InstanceType: instanceType},
					{Status: MemberStatusCreating, InstanceType: instanceType}, // still creating
					{Status: MemberStatusCreating, InstanceType: instanceType}}
				c.Status.DB.TiKV.Volume = &VolumeStatus{ModifiedCount: 3, ModifiedCapacity: storageSize, ModifiedStorageClass: storageClass}
				c.Status.DB.TiKV.UpdatedReplicas = 3 // updatedReplicas has been set even the pod is Pending
				c.Status.DB.TiKV.Status = ComponentScaling
			},
			expect: 1,
		},
		"TiKVScalingIn": {
			comp: ComponentTiKV,
			modify: func(c *Cluster) {
				c.Spec.Cluster.TiKV.Replicas = 1
				c.Status.DB.TiKV.Members = []StoreMember{
					{Status: MemberStatusNormal, InstanceType: instanceType},
					{Status: MemberStatusUnavailable, InstanceType: instanceType}}
				c.Status.DB.TiKV.UpdatedReplicas = 1
				c.Status.DB.TiKV.Status = ComponentUnhealthy
			},
			expect: 1,
		},
		"TiKVScalingLocalStorage": {
			comp: ComponentTiKV,
			modify: func(c *Cluster) {
				c.Spec.Cluster.TiKV.Replicas = 2
				c.Spec.Cluster.TiKV.UseLocalStorage = pointer.Bool(true)
				c.Status.DB.TiKV.Members = []StoreMember{
					{Status: MemberStatusNormal, InstanceType: instanceType},
					{Status: MemberStatusNormal, InstanceType: instanceType}}
				c.Status.DB.TiKV.Volume = &VolumeStatus{ModifiedCount: 0, ModifiedStorageClass: "local-storage"}
				c.Status.DB.TiKV.UpdatedReplicas = 2
				c.Status.DB.TiKV.Status = ComponentNormal
			},
			expect: 2,
		},
		"PDUpdatedReplicasAll": {
			comp: ComponentPD,
			modify: func(c *Cluster) {
				c.Status.DB.PD.Members = []PDMember{{Status: MemberStatusNormal}, {Status: MemberStatusNormal}}
				c.Status.DB.PD.UpdatedReplicas = 2
			},
			expect: 2,
		},
		"PDUpdatedReplicasPartial": {
			comp: ComponentPD,
			modify: func(c *Cluster) {
				c.Status.DB.PD.Members = []PDMember{{Status: MemberStatusNormal}, {Status: MemberStatusNormal}}
				c.Status.DB.PD.UpdatedReplicas = 1
			},
			expect: 1,
		},
		"PDUpdatedReplicasNone": {
			comp: ComponentPD,
			modify: func(c *Cluster) {
				c.Status.DB.PD.Members = []PDMember{{Status: MemberStatusNormal}, {Status: MemberStatusNormal}}
				c.Status.DB.PD.UpdatedReplicas = 0
			},
			expect: 0,
		},
		"PDInstanceTypeNotStarted": {
			comp: ComponentPD,
			modify: func(c *Cluster) {
				c.Status.DB.PD.Members = []PDMember{{Status: MemberStatusNormal, InstanceType: anotherInstanceType}, {Status: MemberStatusNormal, InstanceType: anotherInstanceType}}
				c.Status.DB.PD.UpdatedReplicas = 2
			},
			expect: 0,
		},
		"PDInstanceTypeNotFinished": {
			comp: ComponentPD,
			modify: func(c *Cluster) {
				c.Status.DB.PD.Members = []PDMember{{Status: MemberStatusNormal, InstanceType: instanceType}, {Status: MemberStatusNormal, InstanceType: anotherInstanceType}}
				c.Status.DB.PD.UpdatedReplicas = 2
			},
			expect: 1,
		},
		"PDStillScaling": {
			comp: ComponentPD,
			modify: func(c *Cluster) {
				c.Status.DB.PD.Members = []PDMember{{Status: MemberStatusNormal, InstanceType: instanceType}, {Status: MemberStatusNormal, InstanceType: instanceType}}
				c.Status.DB.PD.UpdatedReplicas = 2
				c.Status.DB.PD.Status = ComponentScaling
			},
			expect: 1,
		},
		"PDStillUnhealthy": {
			comp: ComponentPD,
			modify: func(c *Cluster) {
				c.Status.DB.PD.Members = []PDMember{{Status: MemberStatusNormal, InstanceType: instanceType}, {Status: MemberStatusNormal, InstanceType: instanceType}}
				c.Status.DB.PD.UpdatedReplicas = 2
				c.Status.DB.PD.Status = ComponentUnhealthy
			},
			expect: 1,
		},
		"PDMoreUpdatedThanInstance": {
			comp: ComponentPD,
			modify: func(c *Cluster) {
				c.Spec.Cluster.PD.Replicas = 3
				c.Status.DB.PD.Members = []PDMember{
					{Status: MemberStatusNormal, InstanceType: instanceType},
					{Status: MemberStatusNormal, InstanceType: anotherInstanceType},
					{Status: MemberStatusNormal, InstanceType: anotherInstanceType}}
				c.Status.DB.PD.UpdatedReplicas = 2
			},
			expect: 1,
		},
		"PDVolumeModifyPendingWithStorageUp": {
			comp: ComponentPD,
			modify: func(c *Cluster) {
				c.Spec.Cluster.PD.Replicas = 3
				c.Status.DB.PD.Members = []PDMember{
					{Status: MemberStatusNormal, InstanceType: instanceType},
					{Status: MemberStatusNormal, InstanceType: instanceType},
					{Status: MemberStatusNormal, InstanceType: instanceType}}
				c.Status.DB.PD.Volume = &VolumeStatus{ModifiedCount: 0, ModifiedCapacity: storageSize, ModifiedStorageClass: storageClass}
				c.Status.DB.PD.UpdatedReplicas = 3
			},
			expect: 0,
		},
		"PDVolumeModifyPendingWithoutStorageUp": {
			comp: ComponentPD,
			modify: func(c *Cluster) {
				c.Spec.Cluster.PD.Replicas = 3
				c.Status.DB.PD.Members = []PDMember{
					{Status: MemberStatusNormal, InstanceType: instanceType},
					{Status: MemberStatusCreating, InstanceType: instanceType},
					{Status: MemberStatusCreating, InstanceType: instanceType}}
				c.Status.DB.PD.Volume = &VolumeStatus{ModifiedCount: 3, ModifiedCapacity: storageSize, ModifiedStorageClass: storageClass}
				c.Status.DB.PD.UpdatedReplicas = 3
			},
			expect: 1,
		},
		"PDScalingIn": {
			comp: ComponentPD,
			modify: func(c *Cluster) {
				c.Spec.Cluster.PD.Replicas = 1
				c.Status.DB.PD.Members = []PDMember{
					{Status: MemberStatusNormal, InstanceType: instanceType},
					{Status: MemberStatusUnavailable, InstanceType: instanceType}}
				c.Status.DB.PD.UpdatedReplicas = 1
				c.Status.DB.PD.Status = ComponentUnhealthy
			},
			expect: 1,
		},
		"TiFlashUpdatedReplicasAll": {
			comp: ComponentTiFlash,
			modify: func(c *Cluster) {
				c.Status.DB.TiFlash.Members = []StoreMember{{Status: MemberStatusNormal}, {Status: MemberStatusNormal}}
				c.Status.DB.TiFlash.UpdatedReplicas = 2
			},
			expect: 2,
		},
		"TiFlashUpdatedReplicasPartial": {
			comp: ComponentTiFlash,
			modify: func(c *Cluster) {
				c.Status.DB.TiFlash.Members = []StoreMember{{Status: MemberStatusNormal}, {Status: MemberStatusNormal}}
				c.Status.DB.TiFlash.UpdatedReplicas = 1
			},
			expect: 1,
		},
		"TiFlashUpdatedReplicasNone": {
			comp: ComponentTiFlash,
			modify: func(c *Cluster) {
				c.Status.DB.TiFlash.Members = []StoreMember{{Status: MemberStatusNormal}, {Status: MemberStatusNormal}}
				c.Status.DB.TiFlash.UpdatedReplicas = 0
			},
			expect: 0,
		},
		"TiFlashInstanceTypeNotStarted": {
			comp: ComponentTiFlash,
			modify: func(c *Cluster) {
				c.Status.DB.TiFlash.Members = []StoreMember{{Status: MemberStatusNormal, InstanceType: anotherInstanceType}, {Status: MemberStatusNormal, InstanceType: anotherInstanceType}}
				c.Status.DB.TiFlash.UpdatedReplicas = 2
			},
			expect: 0,
		},
		"TiFlashInstanceTypeNotFinished": {
			comp: ComponentTiFlash,
			modify: func(c *Cluster) {
				c.Status.DB.TiFlash.Members = []StoreMember{{Status: MemberStatusNormal, InstanceType: instanceType}, {Status: MemberStatusNormal, InstanceType: anotherInstanceType}}
				c.Status.DB.TiFlash.UpdatedReplicas = 2
			},
			expect: 1,
		},
		"TiFlashStillScaling": {
			comp: ComponentTiFlash,
			modify: func(c *Cluster) {
				c.Status.DB.TiFlash.Members = []StoreMember{{Status: MemberStatusNormal, InstanceType: instanceType}, {Status: MemberStatusNormal, InstanceType: instanceType}}
				c.Status.DB.TiFlash.UpdatedReplicas = 2
				c.Status.DB.TiFlash.Status = ComponentScaling
			},
			expect: 1,
		},
		"TiFlashStillUnhealthy": {
			comp: ComponentTiFlash,
			modify: func(c *Cluster) {
				c.Status.DB.TiFlash.Members = []StoreMember{{Status: MemberStatusNormal, InstanceType: instanceType}, {Status: MemberStatusNormal, InstanceType: instanceType}}
				c.Status.DB.TiFlash.UpdatedReplicas = 2
				c.Status.DB.TiFlash.Status = ComponentUnhealthy
			},
			expect: 1,
		},
		"TiFlashMoreUpdatedThanInstance": {
			comp: ComponentTiFlash,
			modify: func(c *Cluster) {
				c.Spec.Cluster.TiFlash.Replicas = 3
				c.Status.DB.TiFlash.Members = []StoreMember{
					{Status: MemberStatusNormal, InstanceType: instanceType},
					{Status: MemberStatusNormal, InstanceType: anotherInstanceType},
					{Status: MemberStatusNormal, InstanceType: anotherInstanceType}}
				c.Status.DB.TiFlash.UpdatedReplicas = 2
			},
			expect: 1,
		},
		"TiFlashScaleOutPendingWithStorageUp": {
			comp: ComponentTiFlash,
			modify: func(c *Cluster) {
				c.Spec.Cluster.TiFlash.Replicas = 3
				c.Status.DB.TiFlash.Members = []StoreMember{
					{Status: MemberStatusNormal, InstanceType: instanceType},
					{Status: MemberStatusNormal, InstanceType: instanceType},
					{Status: MemberStatusNormal, InstanceType: instanceType}}
				c.Status.DB.TiFlash.Volume = &VolumeStatus{ModifiedCount: 0, ModifiedCapacity: storageSize, ModifiedStorageClass: storageClass}
				c.Status.DB.TiFlash.UpdatedReplicas = 3
				c.Status.DB.TiFlash.Status = ComponentScaling
			},
			expect: 0,
		},
		"TiFlashScaleOutPendingWithoutStorageUp": {
			comp: ComponentTiFlash,
			modify: func(c *Cluster) {
				c.Spec.Cluster.TiFlash.Replicas = 3
				c.Status.DB.TiFlash.Members = []StoreMember{
					{Status: MemberStatusNormal, InstanceType: instanceType},
					{Status: MemberStatusCreating, InstanceType: instanceType},
					{Status: MemberStatusCreating, InstanceType: instanceType}}
				c.Status.DB.TiFlash.Volume = &VolumeStatus{ModifiedCount: 3, ModifiedCapacity: storageSize, ModifiedStorageClass: storageClass}
				c.Status.DB.TiFlash.UpdatedReplicas = 3
				c.Status.DB.TiFlash.Status = ComponentScaling
			},
			expect: 1,
		},
		"TiFlashScalingIn": {
			comp: ComponentTiFlash,
			modify: func(c *Cluster) {
				c.Spec.Cluster.TiFlash.Replicas = 1
				c.Status.DB.TiFlash.Members = []StoreMember{
					{Status: MemberStatusNormal, InstanceType: instanceType},
					{Status: MemberStatusUnavailable, InstanceType: instanceType}}
				c.Status.DB.TiFlash.UpdatedReplicas = 1
				c.Status.DB.TiFlash.Status = ComponentUnhealthy
			},
			expect: 1,
		},
		"TiFlashScalingLocalStorage": {
			comp: ComponentTiFlash,
			modify: func(c *Cluster) {
				c.Spec.Cluster.TiFlash.Replicas = 2
				c.Spec.Cluster.TiFlash.UseLocalStorage = pointer.Bool(true)
				c.Status.DB.TiFlash.Members = []StoreMember{
					{Status: MemberStatusNormal, InstanceType: instanceType},
					{Status: MemberStatusNormal, InstanceType: instanceType}}
				c.Status.DB.TiFlash.Volume = &VolumeStatus{ModifiedCount: 0, ModifiedStorageClass: "local-storage"}
				c.Status.DB.TiFlash.UpdatedReplicas = 2
				c.Status.DB.TiFlash.Status = ComponentNormal
			},
			expect: 2,
		},
		"TiKVAllVolumeModified": {
			comp: ComponentTiKV,
			modify: func(c *Cluster) {
				c.Status.DB.TiKV.Members = []StoreMember{{Status: MemberStatusNormal, InstanceType: instanceType}, {Status: MemberStatusNormal, InstanceType: instanceType}}
				c.Status.DB.TiKV.Volume = &VolumeStatus{ModifiedCount: 2, ModifiedCapacity: storageSize, ModifiedStorageClass: storageClass}
				c.Status.DB.TiKV.UpdatedReplicas = 2
			},
			expect: 2,
		},
		"TiKVHalfVolumeModified": {
			comp: ComponentTiKV,
			modify: func(c *Cluster) {
				c.Status.DB.TiKV.Members = []StoreMember{{Status: MemberStatusNormal, InstanceType: instanceType}, {Status: MemberStatusNormal, InstanceType: instanceType}}
				c.Status.DB.TiKV.Volume = &VolumeStatus{ModifiedCount: 1, ModifiedCapacity: storageSize, ModifiedStorageClass: storageClass}
				c.Status.DB.TiKV.UpdatedReplicas = 2
			},
			expect: 1,
		},
		"TiKVVolumeModifyPending": {
			comp: ComponentTiKV,
			modify: func(c *Cluster) {
				c.Status.DB.TiKV.Members = []StoreMember{{Status: MemberStatusNormal, InstanceType: instanceType}, {Status: MemberStatusNormal, InstanceType: instanceType}}
				c.Status.DB.TiKV.Volume = &VolumeStatus{ModifiedCount: 2, ModifiedCapacity: anotherStorageSize, ModifiedStorageClass: anotherStorageClass}
				c.Status.DB.TiKV.UpdatedReplicas = 2
			},
			expect: 0,
		},
		"TiFlashAllVolumeModified": {
			comp: ComponentTiFlash,
			modify: func(c *Cluster) {
				c.Status.DB.TiFlash.Members = []StoreMember{{Status: MemberStatusNormal, InstanceType: instanceType}, {Status: MemberStatusNormal, InstanceType: instanceType}}
				c.Status.DB.TiFlash.Volume = &VolumeStatus{ModifiedCount: 2, ModifiedCapacity: storageSize, ModifiedStorageClass: storageClass}
				c.Status.DB.TiFlash.UpdatedReplicas = 2
			},
			expect: 2,
		},
		"TiFlashHalfVolumeModified": {
			comp: ComponentTiFlash,
			modify: func(c *Cluster) {
				c.Status.DB.TiFlash.Members = []StoreMember{{Status: MemberStatusNormal, InstanceType: instanceType}, {Status: MemberStatusNormal, InstanceType: instanceType}}
				c.Status.DB.TiFlash.Volume = &VolumeStatus{ModifiedCount: 1, ModifiedCapacity: storageSize, ModifiedStorageClass: storageClass}
				c.Status.DB.TiFlash.UpdatedReplicas = 2
			},
			expect: 1,
		},
		"TiFlashVolumeModifyPending": {
			comp: ComponentTiFlash,
			modify: func(c *Cluster) {
				c.Status.DB.TiFlash.Members = []StoreMember{{Status: MemberStatusNormal, InstanceType: instanceType}, {Status: MemberStatusNormal, InstanceType: instanceType}}
				c.Status.DB.TiFlash.Volume = &VolumeStatus{ModifiedCount: 2, ModifiedCapacity: anotherStorageSize, ModifiedStorageClass: anotherStorageClass}
				c.Status.DB.TiFlash.UpdatedReplicas = 2
			},
			expect: 0,
		},
		"PDAllVolumeModified": {
			comp: ComponentPD,
			modify: func(c *Cluster) {
				c.Status.DB.PD.Members = []PDMember{{Status: MemberStatusNormal, InstanceType: instanceType}, {Status: MemberStatusNormal, InstanceType: instanceType}}
				c.Status.DB.PD.Volume = &VolumeStatus{ModifiedCount: 2, ModifiedCapacity: storageSize, ModifiedStorageClass: storageClass}
				c.Status.DB.PD.UpdatedReplicas = 2
			},
			expect: 2,
		},
		"PDHalfVolumeModified": {
			comp: ComponentPD,
			modify: func(c *Cluster) {
				c.Status.DB.PD.Members = []PDMember{{Status: MemberStatusNormal, InstanceType: instanceType}, {Status: MemberStatusNormal, InstanceType: instanceType}}
				c.Status.DB.PD.Volume = &VolumeStatus{ModifiedCount: 1, ModifiedCapacity: storageSize, ModifiedStorageClass: storageClass}
				c.Status.DB.PD.UpdatedReplicas = 2
			},
			expect: 1,
		},
		"PDVolumeModifyPending": {
			comp: ComponentPD,
			modify: func(c *Cluster) {
				c.Status.DB.PD.Members = []PDMember{{Status: MemberStatusNormal, InstanceType: instanceType}, {Status: MemberStatusNormal, InstanceType: instanceType}}
				c.Status.DB.PD.Volume = &VolumeStatus{ModifiedCount: 2, ModifiedCapacity: anotherStorageSize, ModifiedStorageClass: anotherStorageClass}
				c.Status.DB.PD.UpdatedReplicas = 2
			},
			expect: 0,
		},
		"NoModifiedCount": {
			comp: ComponentTiKV,
			modify: func(c *Cluster) {
				c.Status.DB.TiKV.Members = []StoreMember{{Status: MemberStatusNormal, InstanceType: instanceType}, {Status: MemberStatusNormal, InstanceType: instanceType}}
				c.Status.DB.TiKV.Volume = &VolumeStatus{ResizedCount: 2, ModifiedCapacity: storageSize, ModifiedStorageClass: storageClass}
				c.Status.DB.TiKV.UpdatedReplicas = 2
			},
			expect: 2,
		},
		"NoModifiedStorageSize": {
			comp: ComponentTiKV,
			modify: func(c *Cluster) {
				c.Status.DB.TiKV.Members = []StoreMember{{Status: MemberStatusNormal, InstanceType: instanceType}, {Status: MemberStatusNormal, InstanceType: instanceType}}
				c.Status.DB.TiKV.Volume = &VolumeStatus{ModifiedCount: 2, ResizedCapacity: storageSize, ModifiedStorageClass: storageClass}
				c.Status.DB.TiKV.UpdatedReplicas = 2
			},
			expect: 2,
		},
		"NoModifiedStorageClass": {
			comp: ComponentTiKV,
			modify: func(c *Cluster) {
				c.Status.DB.TiKV.Members = []StoreMember{{Status: MemberStatusNormal, InstanceType: instanceType}, {Status: MemberStatusNormal, InstanceType: instanceType}}
				c.Status.DB.TiKV.Volume = &VolumeStatus{ModifiedCount: 2, ModifiedCapacity: storageSize}
				c.Status.DB.TiKV.UpdatedReplicas = 2
			},
			expect: 2,
		},
		"NoInstanceType": {
			comp: ComponentTiKV,
			modify: func(c *Cluster) {
				c.Status.DB.TiKV.Members = []StoreMember{{Status: MemberStatusNormal}, {Status: MemberStatusNormal}}
				c.Status.DB.TiKV.Volume = &VolumeStatus{ModifiedCount: 2, ModifiedCapacity: storageSize, ModifiedStorageClass: storageClass}
				c.Status.DB.TiKV.UpdatedReplicas = 2
			},
			expect: 2,
		},
		"MoreVolumeThanSts": {
			comp: ComponentTiKV,
			modify: func(c *Cluster) {
				c.Status.DB.TiKV.Members = []StoreMember{{Status: MemberStatusNormal, InstanceType: instanceType}}
				c.Status.DB.TiKV.Volume = &VolumeStatus{ModifiedCount: 2, ModifiedCapacity: storageSize, ModifiedStorageClass: storageClass}
				c.Status.DB.TiKV.UpdatedReplicas = 1
			},
			expect: 1,
		},
		"MoreStsThanCompute": {
			comp: ComponentTiKV,
			modify: func(c *Cluster) {
				// member status changed to unavailable should not change progress back (-1)
				c.Spec.Cluster.TiKV.Replicas = 3
				c.Status.DB.TiKV.Members = []StoreMember{
					{Status: MemberStatusNormal, InstanceType: instanceType},
					{Status: MemberStatusUnavailable, InstanceType: instanceType},
					{Status: MemberStatusUnavailable, InstanceType: instanceType}}
				c.Status.DB.TiKV.Volume = &VolumeStatus{ModifiedCount: 3, ModifiedCapacity: storageSize, ModifiedStorageClass: storageClass}
				c.Status.DB.TiKV.UpdatedReplicas = 2
			},
			expect: 2,
		},
		"EmtpyStorageClass": {
			comp: ComponentTiKV,
			modify: func(c *Cluster) {
				c.Spec.Cluster.TiKV.StorageClassName = ""
				c.Status.DB.TiKV.Members = []StoreMember{{Status: MemberStatusNormal, InstanceType: instanceType}, {Status: MemberStatusNormal, InstanceType: instanceType}}
				c.Status.DB.TiKV.Volume = &VolumeStatus{ModifiedCount: 2, ModifiedCapacity: storageSize}
				c.Status.DB.TiKV.UpdatedReplicas = 2
			},
			expect: 2,
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			g := NewGomegaWithT(t)
			c := &Cluster{
				Spec: ClusterSpec{
					Cluster: ClusterTopology{
						TiDB: TiDB{
							Replicas:     2,
							InstanceType: instanceType,
						},
						TiKV: TiKV{
							Replicas:         2,
							InstanceType:     instanceType,
							StorageSize:      storageSize,
							StorageClassName: storageClass,
						},
						PD: PD{
							Replicas:         2,
							InstanceType:     instanceType,
							StorageSize:      storageSize,
							StorageClassName: storageClass,
						},
						TiFlash: TiFlash{
							Replicas:         2,
							InstanceType:     instanceType,
							StorageSize:      storageSize,
							StorageClassName: storageClass,
						},
					},
				},
				Status: ClusterStatus{
					Phase: ClusterPhaseRunning,
					DB: &DBStatus{
						PD: &PDStatus{
							Status:  ComponentNormal,
							Members: make([]PDMember, 1),
						},
						TiDB: &TiDBStatus{
							Status:  ComponentNormal,
							Members: make([]TiDBMember, 1),
						},
						TiKV: &TiKVStatus{
							Status:  ComponentNormal,
							Members: make([]StoreMember, 1),
						},
						TiFlash: &TiFlashStatus{
							Status:  ComponentNormal,
							Members: make([]StoreMember, 1),
						},
					},
				},
			}

			tc.modify(c)
			g.Expect(c.GetUpToDateReplicas(tc.comp)).Should(Equal(tc.expect))
		})
	}
}

func TestMergeSuspendAction(t *testing.T) {
	type testcase struct {
		base   *SuspendAction
		sub    *SuspendAction
		expect *SuspendAction
	}

	cases := map[string]testcase{
		"Base SuspendAction nil PD SuspendAction nil": {
			base:   nil,
			sub:    nil,
			expect: nil,
		},
		"Base SuspendAction nil PD SuspendAction empty": {
			base:   nil,
			sub:    &SuspendAction{},
			expect: &SuspendAction{},
		},
		"Base SuspendAction nil PD SuspendAction true": {
			base: nil,
			sub: &SuspendAction{
				SuspendCompute: true,
			},
			expect: &SuspendAction{SuspendCompute: true},
		},
		"Base SuspendAction nil PD SuspendAction false": {
			base: nil,
			sub: &SuspendAction{
				SuspendCompute: false,
			},
			expect: &SuspendAction{SuspendCompute: false},
		},
		"Base SuspendAction empty PD SuspendAction nil": {
			base:   &SuspendAction{},
			sub:    nil,
			expect: &SuspendAction{},
		},
		"Base SuspendAction empty PD SuspendAction empty": {
			base:   &SuspendAction{},
			sub:    &SuspendAction{},
			expect: &SuspendAction{},
		},
		"Base SuspendAction empty PD SuspendAction true": {
			base: &SuspendAction{},
			sub: &SuspendAction{
				SuspendCompute: true,
			},
			expect: &SuspendAction{
				SuspendCompute: true,
			},
		},
		"Base SuspendAction empty PD SuspendAction false": {
			base: &SuspendAction{},
			sub: &SuspendAction{
				SuspendCompute: false,
			},
			expect: &SuspendAction{
				SuspendCompute: false,
			},
		},
		"Base SuspendAction true PD SuspendAction nil": {
			base: &SuspendAction{
				SuspendCompute: true,
			},
			sub: nil,
			expect: &SuspendAction{
				SuspendCompute: true,
			},
		},
		"Base SuspendAction true PD SuspendAction empty": {
			base: &SuspendAction{
				SuspendCompute: true,
			},
			sub:    &SuspendAction{},
			expect: &SuspendAction{},
		},
		"Base SuspendAction true PD SuspendAction true": {
			base: &SuspendAction{
				SuspendCompute: true,
			},
			sub: &SuspendAction{
				SuspendCompute: true,
			},
			expect: &SuspendAction{
				SuspendCompute: true,
			},
		},
		"Base SuspendAction true PD SuspendAction false": {
			base: &SuspendAction{
				SuspendCompute: true,
			},
			sub: &SuspendAction{
				SuspendCompute: false,
			},
			expect: &SuspendAction{
				SuspendCompute: false,
			},
		},
		"Base SuspendAction false PD SuspendAction nil": {
			base: &SuspendAction{
				SuspendCompute: false,
			},
			sub: nil,
			expect: &SuspendAction{
				SuspendCompute: false,
			},
		},
		"Base SuspendAction false PD SuspendAction empty": {
			base: &SuspendAction{
				SuspendCompute: false,
			},
			sub:    &SuspendAction{},
			expect: &SuspendAction{},
		},
		"Base SuspendAction false PD SuspendAction true": {
			base: &SuspendAction{
				SuspendCompute: false,
			},
			sub: &SuspendAction{
				SuspendCompute: true,
			},
			expect: &SuspendAction{
				SuspendCompute: true,
			},
		},
		"Base SuspendAction false PD SuspendAction false": {
			base: &SuspendAction{
				SuspendCompute: false,
			},
			sub: &SuspendAction{
				SuspendCompute: false,
			},
			expect: &SuspendAction{
				SuspendCompute: false,
			},
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			g := NewGomegaWithT(t)
			c := &Cluster{
				Spec: ClusterSpec{
					Cluster: ClusterTopology{
						TiDB: TiDB{
							Replicas: 1,
						},
						TiKV: TiKV{
							Replicas:    1,
							StorageSize: resource.MustParse("1Gi"),
						},
						PD: PD{
							Replicas:    1,
							StorageSize: resource.MustParse("1Gi"),
						},
						TiFlash: TiFlash{
							Replicas:    1,
							StorageSize: resource.MustParse("1Gi"),
						},
					},
				},
			}
			c.Spec.Cluster.SuspendAction = tc.base
			c.Spec.Cluster.PD.SuspendAction = tc.sub
			result := c.Spec.Cluster.PD.GetSuspendActions(tc.base)
			if tc.expect != nil {
				g.Expect((*result).SuspendCompute).Should(Equal((*tc.expect).SuspendCompute))
			} else {
				g.Expect(result).Should(Equal(tc.expect))
			}
		})
	}
}

func TestCluster_GetAllComponentsV2(t *testing.T) {
	type expectedComponent struct {
		compType   ComponentType
		groupId    string
		scalePhase InstanceTypeScalePhase
	}

	tests := []struct {
		name     string
		cluster  *Cluster
		expected []expectedComponent
	}{
		{
			name: "only default components with replicas",
			cluster: &Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Annotations: map[string]string{DefaultLoadBalancerIDAnnotationKey: "default"},
				},
				Spec: ClusterSpec{
					Cluster: ClusterTopology{
						PD:      PD{Replicas: 3},
						TiKV:    TiKV{Replicas: 3},
						TiDB:    TiDB{Replicas: 2},
						TiFlash: TiFlash{Replicas: 1},
						TiProxy: TiProxy{Replicas: 1},
					},
				},
			},
			expected: []expectedComponent{
				{compType: ComponentPD, groupId: "default", scalePhase: "default-pd"},
				{compType: ComponentTiKV, groupId: "default", scalePhase: "default-tikv"},
				{compType: ComponentTiDB, groupId: "default", scalePhase: "default-tidb"},
				{compType: ComponentTiFlash, groupId: "default", scalePhase: "default-tiflash"},
				{compType: ComponentTiProxy, groupId: "default", scalePhase: "default-tiproxy"},
			},
		},
		{
			name: "default components with some zero replicas",
			cluster: &Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Annotations: map[string]string{DefaultLoadBalancerIDAnnotationKey: "default"},
				},
				Spec: ClusterSpec{
					Cluster: ClusterTopology{
						PD:      PD{Replicas: 3},
						TiKV:    TiKV{Replicas: 3},
						TiDB:    TiDB{Replicas: 2},
						TiFlash: TiFlash{Replicas: 0},
						TiProxy: TiProxy{Replicas: 0},
					},
				},
			},
			expected: []expectedComponent{
				{compType: ComponentPD, groupId: "default", scalePhase: "default-pd"},
				{compType: ComponentTiKV, groupId: "default", scalePhase: "default-tikv"},
				{compType: ComponentTiDB, groupId: "default", scalePhase: "default-tidb"},
			},
		},
		{
			name: "with valid extra groups",
			cluster: &Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Annotations: map[string]string{DefaultLoadBalancerIDAnnotationKey: "default"},
				},
				Spec: ClusterSpec{
					Cluster: ClusterTopology{
						PD:   PD{Replicas: 3},
						TiKV: TiKV{Replicas: 3},
						TiDB: TiDB{Replicas: 2},
					},
					ExtraGroups: []ExtraGroupSpec{
						{
							Name:    "group1",
							TiDB:    TiDB{Replicas: 1},
							TiProxy: TiProxy{Replicas: 1},
						},
					},
				},
			},
			expected: []expectedComponent{
				{compType: ComponentPD, groupId: "default", scalePhase: "default-pd"},
				{compType: ComponentTiKV, groupId: "default", scalePhase: "default-tikv"},
				{compType: ComponentTiDB, groupId: "default", scalePhase: "default-tidb"},
				{compType: ComponentTiDB, groupId: "group1", scalePhase: "group1-tidb"},
				{compType: ComponentTiProxy, groupId: "group1", scalePhase: "group1-tiproxy"},
			},
		},
		{
			name: "with a deleted extra group",
			cluster: &Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Annotations: map[string]string{DefaultLoadBalancerIDAnnotationKey: "default"},
				},
				Spec: ClusterSpec{
					Cluster: ClusterTopology{
						PD:   PD{Replicas: 3},
						TiKV: TiKV{Replicas: 3},
						TiDB: TiDB{Replicas: 2},
					},
					ExtraGroups: []ExtraGroupSpec{
						{
							Name:    "deleted-group",
							TiDB:    TiDB{Replicas: 0}, // This makes it deleted
							TiProxy: TiProxy{Replicas: 1},
						},
					},
				},
			},
			expected: []expectedComponent{
				{compType: ComponentPD, groupId: "default", scalePhase: "default-pd"},
				{compType: ComponentTiKV, groupId: "default", scalePhase: "default-tikv"},
				{compType: ComponentTiDB, groupId: "default", scalePhase: "default-tidb"},
			},
		},
		{
			name: "with an extra group with partial zero replicas",
			cluster: &Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Annotations: map[string]string{DefaultLoadBalancerIDAnnotationKey: "default"},
				},
				Spec: ClusterSpec{
					Cluster: ClusterTopology{
						PD:   PD{Replicas: 3},
						TiKV: TiKV{Replicas: 3},
						TiDB: TiDB{Replicas: 2},
					},
					ExtraGroups: []ExtraGroupSpec{
						{
							Name:    "group1",
							TiDB:    TiDB{Replicas: 1},
							TiProxy: TiProxy{Replicas: 0},
						},
					},
				},
			},
			expected: []expectedComponent{
				{compType: ComponentPD, groupId: "default", scalePhase: "default-pd"},
				{compType: ComponentTiKV, groupId: "default", scalePhase: "default-tikv"},
				{compType: ComponentTiDB, groupId: "default", scalePhase: "default-tidb"},
				{compType: ComponentTiDB, groupId: "group1", scalePhase: "group1-tidb"},
			},
		},
		{
			name: "cluster with no extra groups and only base components",
			cluster: &Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Annotations: map[string]string{DefaultLoadBalancerIDAnnotationKey: "default"},
				},
				Spec: ClusterSpec{
					Cluster: ClusterTopology{
						PD:      PD{Replicas: 1},
						TiKV:    TiKV{Replicas: 1},
						TiDB:    TiDB{Replicas: 1},
						TiFlash: TiFlash{Replicas: 0},
						TiProxy: TiProxy{Replicas: 0},
					},
				},
			},
			expected: []expectedComponent{
				{compType: ComponentPD, groupId: "default", scalePhase: "default-pd"},
				{compType: ComponentTiKV, groupId: "default", scalePhase: "default-tikv"},
				{compType: ComponentTiDB, groupId: "default", scalePhase: "default-tidb"},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			components := tt.cluster.GetAllComponentsV2()
			assert.Len(t, components, len(tt.expected), "number of components should match")

			actuals := make([]expectedComponent, len(components))
			for i, c := range components {
				fakeScalePhase := fmt.Sprintf("%s-%s", c.GetGroupId(), c.GetComponentType())

				c.SetInstanceTypeScalePhase(InstanceTypeScalePhase(fakeScalePhase))
				actuals[i] = expectedComponent{
					compType:   c.GetComponentType(),
					groupId:    c.GetGroupId(),
					scalePhase: c.GetInstanceTypeScalePhase(),
				}
			}

			assert.ElementsMatch(t, tt.expected, actuals, "components should match expected")
		})
	}
}

func TestClusterComponentStatusWrapped_TransInstanceTypeScalePhase(t *testing.T) {
	type args struct {
		initialPhase InstanceTypeScalePhase
		newPhase     InstanceTypeScalePhase
	}
	tests := []struct {
		name     string
		args     args
		expected InstanceTypeScalePhase
		compType ComponentType
	}{
		{
			name: "PDStatus transitions phase",
			args: args{
				initialPhase: InstanceTypeScalePhaseCreatingNodes,
				newPhase:     InstanceTypeScalePhaseCordoningNodes,
			},
			expected: InstanceTypeScalePhaseCordoningNodes,
			compType: ComponentPD,
		},
		{
			name: "TiDBStatus transitions phase",
			args: args{
				initialPhase: InstanceTypeScalePhaseNone,
				newPhase:     InstanceTypeScalePhaseCreatingNodes,
			},
			expected: InstanceTypeScalePhaseCreatingNodes,
			compType: ComponentTiDB,
		},
		{
			name: "TiKVStatus transitions phase",
			args: args{
				initialPhase: InstanceTypeScalePhaseCordoningNodes,
				newPhase:     InstanceTypeScalePhaseRestartingPods,
			},
			expected: InstanceTypeScalePhaseRestartingPods,
			compType: ComponentTiKV,
		},
		{
			name: "TiFlashStatus transitions phase",
			args: args{
				initialPhase: InstanceTypeScalePhaseRestartingPods,
				newPhase:     InstanceTypeScalePhaseDeletingNodes,
			},
			expected: InstanceTypeScalePhaseDeletingNodes,
			compType: ComponentTiFlash,
		},
		{
			name: "TiProxyStatus transitions phase",
			args: args{
				initialPhase: InstanceTypeScalePhaseRestartingPods,
				newPhase:     InstanceTypeScalePhaseDeletingNodes,
			},
			expected: InstanceTypeScalePhaseDeletingNodes,
			compType: ComponentTiProxy,
		},
		{
			name: "invalid trans shouldn't take effect",
			args: args{
				initialPhase: InstanceTypeScalePhaseCreatingNodes,
				newPhase:     InstanceTypeScalePhase("new"),
			},
			expected: InstanceTypeScalePhaseCreatingNodes,
			compType: ComponentTiDB,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var status ClusterComponentStatus
			switch tt.compType {
			case "pd":
				status = &PDStatus{InstanceTypeScalePhase: tt.args.initialPhase}
			case "tidb":
				status = &TiDBStatus{InstanceTypeScalePhase: tt.args.initialPhase}
			case "tikv":
				status = &TiKVStatus{InstanceTypeScalePhase: tt.args.initialPhase}
			case "tiflash":
				status = &TiFlashStatus{InstanceTypeScalePhase: tt.args.initialPhase}
			case "tiproxy":
				status = &TiProxyStatus{InstanceTypeScalePhase: tt.args.initialPhase}
			}
			wrapped := &clusterComponentStatusWrapped{
				groupId:                "default",
				cluster:                &Cluster{},
				compType:               ComponentType(tt.compType),
				ClusterComponentStatus: status,
			}
			wrapped.TransInstanceTypeScalePhase(context.Background(), tt.args.newPhase)
			got := wrapped.GetInstanceTypeScalePhase()
			assert.Equal(t, tt.expected, got, "InstanceTypeScalePhase should be updated correctly")
		})
	}
}
func TestCluster_GetProject(t *testing.T) {
	t.Run("should use ProjectTagKey rather than ProjectLabel", func(t *testing.T) {
		c := &Cluster{
			ObjectMeta: metav1.ObjectMeta{
				Labels: map[string]string{
					ProjectLabel:  "ProjectLabel",
					ProjectTagKey: "ProjectTagLabel",
				},
			},
		}
		assert.Equalf(t, "ProjectTagLabel", c.GetProjectID(), "GetProject()")
	})
}

func TestCluster_GetStandbyNodeCnt(t *testing.T) {
	type args struct {
		comp            ComponentType
		zone            string
		clusterModifier func(cluster *Cluster)
	}
	tests := []struct {
		name          string
		args          args
		expectedCount uint
	}{
		{
			name: "annotations is nil should return 0",
			args: args{
				comp:            ComponentTiDB,
				zone:            "us-west-2a",
				clusterModifier: nil,
			},
			expectedCount: 0,
		},
		{
			name: "annotation is empty should return 0",
			args: args{
				comp: ComponentTiDB,
				zone: "us-west-2a",
				clusterModifier: func(cluster *Cluster) {
					cluster.UpdateAnnotation(AnnoStandbyNode, `{}`)
				},
			},
			expectedCount: 0,
		},
		{
			name: "annotation is invalid should return 0",
			args: args{
				comp: ComponentTiDB,
				zone: "us-west-2a",
				clusterModifier: func(cluster *Cluster) {
					cluster.UpdateAnnotation(AnnoStandbyNode, `abc`)
				},
			},
			expectedCount: 0,
		},
		{
			name: "has value for tidb but zone info is null should return 0",
			args: args{
				comp: ComponentTiDB,
				zone: "us-west-2a",
				clusterModifier: func(cluster *Cluster) {
					cluster.UpdateAnnotation(AnnoStandbyNode, `{"tidb":null}`)
				},
			},
			expectedCount: 0,
		},
		{
			name: "has value for tidb but mismatch zone should return 0",
			args: args{
				comp: ComponentTiDB,
				zone: "us-west-2a",
				clusterModifier: func(cluster *Cluster) {
					cluster.UpdateAnnotation(AnnoStandbyNode, `{"tidb":{"us-west-2b": 1}}`)
				},
			},
			expectedCount: 0,
		},
		{
			name: "has value for tidb and match zone should return 1",
			args: args{
				comp: ComponentTiDB,
				zone: "us-west-2a",
				clusterModifier: func(cluster *Cluster) {
					cluster.UpdateAnnotation(AnnoStandbyNode, `{"tidb":{"us-west-2a": 1}}`)
				},
			},
			expectedCount: 1,
		},
	}
	cluster := &Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "cluster1",
			Namespace: "ns1",
		},
		Spec: ClusterSpec{
			Cluster: ClusterTopology{
				TiDB: TiDB{
					Replicas: 1,
				},
			},
		},
	}
	for _, tt := range tests {
		cluster := cluster.DeepCopy()
		if tt.args.clusterModifier != nil {
			tt.args.clusterModifier(cluster)
		}
		t.Run(tt.name, func(t *testing.T) {
			cnt := cluster.GetStandbyNodeCnt(tt.args.comp, tt.args.zone)
			assert.Equalf(t, tt.expectedCount, cnt, "compare standby node count")
		})
	}
}

func TestCluster_SetStandbyNodeCnt(t *testing.T) {
	type args struct {
		comp            ComponentType
		zone            string
		cnt             uint
		clusterModifier func(cluster *Cluster)
	}
	tests := []struct {
		name               string
		args               args
		wantErr            bool
		expectedAnnotation string
	}{
		{
			name: "anno doesn't exist",
			args: args{
				comp:            ComponentTiDB,
				zone:            "us-west-2a",
				cnt:             1,
				clusterModifier: nil,
			},
			wantErr:            false,
			expectedAnnotation: `{"tidb":{"us-west-2a":1}}`,
		},
		{
			name: "increase standby node cnt for tidb in us-west-2a",
			args: args{
				comp: ComponentTiDB,
				zone: "us-west-2a",
				cnt:  2,
				clusterModifier: func(cluster *Cluster) {
					cluster.UpdateAnnotation(AnnoStandbyNode, `{"tidb":{"us-west-2a":1}}`)
				},
			},
			wantErr:            false,
			expectedAnnotation: `{"tidb":{"us-west-2a":2}}`,
		},
		{
			name: "create standby node for tikv",
			args: args{
				comp: ComponentTiKV,
				zone: "us-west-2b",
				cnt:  1,
				clusterModifier: func(cluster *Cluster) {
					cluster.UpdateAnnotation(AnnoStandbyNode, `{"tidb":{"us-west-2a":1}}`)
				},
			},
			wantErr:            false,
			expectedAnnotation: `{"tidb":{"us-west-2a":1},"tikv":{"us-west-2b":1}}`,
		},
		{
			name: "invalid anno value should return error",
			args: args{
				comp: ComponentTiKV,
				zone: "us-west-2b",
				cnt:  1,
				clusterModifier: func(cluster *Cluster) {
					cluster.UpdateAnnotation(AnnoStandbyNode, `abc`)
				},
			},
			wantErr:            true,
			expectedAnnotation: "abc", // no change
		},
	}
	cluster := &Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "cluster1",
			Namespace: "ns1",
		},
		Spec: ClusterSpec{
			Cluster: ClusterTopology{
				TiDB: TiDB{
					Replicas: 1,
				},
			},
		},
	}
	for _, tt := range tests {
		cluster := cluster.DeepCopy()
		if tt.args.clusterModifier != nil {
			tt.args.clusterModifier(cluster)
		}
		t.Run(tt.name, func(t *testing.T) {
			err := cluster.SetStandbyNodeCnt(tt.args.comp, tt.args.zone, tt.args.cnt)
			if tt.wantErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
			}
			assert.Equalf(t, tt.expectedAnnotation, cluster.Annotations[AnnoStandbyNode], "compare standby node annotation")
		})
	}
}

func TestCluster_GetSQLEndpoints(t *testing.T) {
	type testcase struct {
		name            string
		groupId         string
		expectedDnsName string
		expectedFound   bool
	}

	cluster := &Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Annotations: map[string]string{
				DefaultLoadBalancerIDAnnotationKey: "default-group",
			},
		},
		Spec: ClusterSpec{
			SqlEndpoints: SqlEndpoints{
				DNS: DNS{Identity: "default-group"},
			},
			ExtraGroups: []ExtraGroupSpec{
				{
					Name: "group1",
					SqlEndpoints: SqlEndpoints{
						DNS: DNS{Identity: "group1"},
					},
				},
			},
		},
	}
	tests := []testcase{
		{
			name:            "find by default group",
			groupId:         "default-group",
			expectedDnsName: "default-group",
			expectedFound:   true,
		},
		{
			name:            "extra group found",
			groupId:         "group1",
			expectedDnsName: "group1",
			expectedFound:   true,
		},
		{
			name:            "extra group not found",
			groupId:         "non-existent-group",
			expectedDnsName: "",
			expectedFound:   false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, found := cluster.GetSQLEndpoints(tt.groupId)
			assert.Equal(t, tt.expectedFound, found)
			if tt.expectedFound {
				assert.Equal(t, tt.expectedDnsName, result.DNS.Identity)
			} else {
				assert.Nil(t, result)
			}
		})
	}
}

func TestCluster_GetComponentInGroup(t *testing.T) {
	cluster := &Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Annotations: map[string]string{
				DefaultLoadBalancerIDAnnotationKey: "default-group",
			},
		},
		Spec: ClusterSpec{
			Cluster: ClusterTopology{
				TiDB: TiDB{
					Replicas: 1,
				},
				TiProxy: TiProxy{
					Replicas: 2,
				},
			},
			ExtraGroups: []ExtraGroupSpec{
				{
					Name: "group1",
					TiDB: TiDB{
						Replicas: 3,
					},
					TiProxy: TiProxy{
						Replicas: 4,
					},
				},
			},
		},
	}

	tests := []struct {
		name             string
		groupId          string
		componentType    ComponentType
		expectedReplicas int
		wantErr          bool
		errMsg           string
	}{
		{
			name:             "get component from default group",
			groupId:          "default-group",
			componentType:    ComponentTiDB,
			expectedReplicas: 1,
			wantErr:          false,
		},
		{
			name:             "get component from extra group",
			groupId:          "group1",
			componentType:    ComponentTiDB,
			expectedReplicas: 3,
			wantErr:          false,
		},
		{
			name:          "get component from non-existent group",
			groupId:       "non-existent-group",
			componentType: ComponentTiDB,
			wantErr:       true,
			errMsg:        "TiDB group 'non-existent-group' not found",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			comp, err := cluster.GetComponentSpecInGroup(tt.groupId, tt.componentType)
			if tt.wantErr {
				assert.Error(t, err)
				assert.Contains(t, err.Error(), tt.errMsg)
				assert.Nil(t, comp)
			} else {
				assert.NoError(t, err)
				assert.NotNil(t, comp)
				assert.Equal(t, tt.expectedReplicas, comp.GetReplicas())
			}
		})
	}
}

func TestCluster_GetComponentStatusInGroup(t *testing.T) {
	cluster := &Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Annotations: map[string]string{
				DefaultLoadBalancerIDAnnotationKey: "default-group",
			},
		},
		Status: ClusterStatus{
			DB: &DBStatus{
				PD: &PDStatus{
					Status:                 ComponentNormal,
					Members:                []PDMember{{Status: MemberStatusNormal}},
					InstanceTypeScalePhase: "pd", // hack for test
				},
				TiDB: &TiDBStatus{
					Status:                 ComponentNormal,
					Members:                []TiDBMember{{Status: MemberStatusNormal}},
					InstanceTypeScalePhase: "tidb", // hack for test
				},
				TiKV: &TiKVStatus{
					Status:                 ComponentNormal,
					Members:                []StoreMember{{Status: MemberStatusNormal}},
					InstanceTypeScalePhase: "tikv", // hack for test
				},
				TiFlash: &TiFlashStatus{
					Status:                 ComponentNormal,
					Members:                []StoreMember{{Status: MemberStatusNormal}},
					InstanceTypeScalePhase: "tiflash", // hack for test
				},
				TiProxy: &TiProxyStatus{
					Status:                 ComponentNormal,
					Members:                []TiProxyMember{{Status: MemberStatusNormal}},
					InstanceTypeScalePhase: "tiproxy", // hack for test
				},
			},
			ExtraEndpoints: []ExtraGroupStatus{
				{
					Name: "group1",
					TiDB: &TiDBStatus{
						Status:                 ComponentNormal,
						Members:                []TiDBMember{{Status: MemberStatusNormal}, {Status: MemberStatusNormal}},
						InstanceTypeScalePhase: InstanceTypeScalePhase("group1-" + "tidb"), // hack for test
					},
					TiProxy: &TiProxyStatus{
						Status:                 ComponentNormal,
						Members:                []TiProxyMember{{Status: MemberStatusNormal}},
						InstanceTypeScalePhase: InstanceTypeScalePhase("group1-" + "tiproxy"), // hack for test
					},
				},
			},
		},
		Spec: ClusterSpec{
			Cluster: ClusterTopology{
				TiDB:    TiDB{Replicas: 1},
				TiKV:    TiKV{Replicas: 1},
				PD:      PD{Replicas: 1},
				TiFlash: TiFlash{Replicas: 1},
				TiProxy: TiProxy{Replicas: 1},
			},
			ExtraGroups: []ExtraGroupSpec{
				{
					Name:    "group1",
					TiDB:    TiDB{Replicas: 2},
					TiProxy: TiProxy{Replicas: 1},
				},
			},
		},
	}

	tests := []struct {
		name                       string
		groupId                    string
		componentType              ComponentType
		wantErr                    bool
		wantPanicWith              string
		wantNil                    bool
		wantInstanceTypeScalePhase InstanceTypeScalePhase
	}{
		{
			name:                       "default group PD status",
			groupId:                    "default-group",
			componentType:              ComponentPD,
			wantErr:                    false,
			wantNil:                    false,
			wantInstanceTypeScalePhase: "pd",
		},
		{
			name:                       "default group TiDB status",
			groupId:                    "default-group",
			componentType:              ComponentTiDB,
			wantErr:                    false,
			wantNil:                    false,
			wantInstanceTypeScalePhase: "tidb",
		},
		{
			name:                       "extra group TiDB status",
			groupId:                    "group1",
			componentType:              ComponentTiDB,
			wantErr:                    false,
			wantNil:                    false,
			wantInstanceTypeScalePhase: "group1-tidb",
		},
		{
			name:                       "extra group TiProxy status",
			groupId:                    "group1",
			componentType:              ComponentTiProxy,
			wantErr:                    false,
			wantNil:                    false,
			wantInstanceTypeScalePhase: "group1-tiproxy",
		},
		{
			name:          "extra group not found",
			groupId:       "non-existent-group",
			componentType: ComponentTiDB,
			wantErr:       true,
			wantNil:       true,
		},
		{
			name:                       "ungrouped component status ignores extra group",
			groupId:                    "group1",
			componentType:              ComponentPD,
			wantErr:                    false,
			wantNil:                    false,
			wantInstanceTypeScalePhase: "pd",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var (
				status ClusterComponentStatus
				err    error
			)

			if tt.wantPanicWith != "" {
				assert.PanicsWithValue(t, tt.wantPanicWith, func() {
					status, err = cluster.GetComponentStatusInGroup(tt.groupId, tt.componentType)
				})
			} else {
				status, err = cluster.GetComponentStatusInGroup(tt.groupId, tt.componentType)
			}

			if tt.wantErr {
				assert.Error(t, err)
				assert.Nil(t, status)
				return
			}

			assert.NoError(t, err)
			if tt.wantNil {
				assert.Nil(t, status)
			} else {
				assert.NotNil(t, status)
				assert.Equalf(t, tt.wantInstanceTypeScalePhase, status.GetInstanceTypeScalePhase(), "instanceTypeScalePhase")
			}
		})
	}
}

func TestCluster_GetOrCreateComponentStatusInGroup(t *testing.T) {
	cluster := &Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Annotations: map[string]string{
				DefaultLoadBalancerIDAnnotationKey: "default-group",
			},
		},
		Status: ClusterStatus{
			DB: &DBStatus{
				PD: &PDStatus{
					Status:                 ComponentNormal,
					Members:                []PDMember{{Status: MemberStatusNormal}},
					InstanceTypeScalePhase: "pd",
				},
				TiDB: &TiDBStatus{
					Status:                 ComponentNormal,
					Members:                []TiDBMember{{Status: MemberStatusNormal}},
					InstanceTypeScalePhase: "tidb",
				},
				TiKV: &TiKVStatus{
					Status:                 ComponentNormal,
					Members:                []StoreMember{{Status: MemberStatusNormal}},
					InstanceTypeScalePhase: "tikv",
				},
				TiFlash: &TiFlashStatus{
					Status:                 ComponentNormal,
					Members:                []StoreMember{{Status: MemberStatusNormal}},
					InstanceTypeScalePhase: "tiflash",
				},
			},
			ExtraEndpoints: []ExtraGroupStatus{
				{
					Name: "group1",
					TiDB: &TiDBStatus{
						Status:                 ComponentNormal,
						Members:                []TiDBMember{{Status: MemberStatusNormal}},
						InstanceTypeScalePhase: InstanceTypeScalePhase("group1-tidb"),
					},
				},
			},
		},
		Spec: ClusterSpec{
			Cluster: ClusterTopology{
				TiDB:    TiDB{Replicas: 1},
				TiKV:    TiKV{Replicas: 1},
				PD:      PD{Replicas: 1},
				TiFlash: TiFlash{Replicas: 1},
				TiProxy: TiProxy{Replicas: 1},
			},
			ExtraGroups: []ExtraGroupSpec{
				{
					Name:    "group1",
					TiDB:    TiDB{Replicas: 1},
					TiProxy: TiProxy{Replicas: 1},
				},
				{
					Name: "group2",
					TiDB: TiDB{Replicas: 1},
				},
			},
		},
	}

	tests := []struct {
		name            string
		groupId         string
		componentType   ComponentType
		wantErrContains string
		wantNil         bool
		wantPhase       InstanceTypeScalePhase
	}{
		{
			name:            "default group PD status",
			groupId:         "default-group",
			componentType:   ComponentPD,
			wantErrContains: "",
			wantNil:         false,
			wantPhase:       "pd",
		},
		{
			name:            "default group TiDB status",
			groupId:         "default-group",
			componentType:   ComponentTiDB,
			wantErrContains: "",
			wantNil:         false,
			wantPhase:       "tidb",
		},
		{
			name:            "non-existence component status Tiproxy in the default group status",
			groupId:         "default-group",
			componentType:   ComponentTiProxy,
			wantErrContains: "",
			wantNil:         false,
			wantPhase:       "",
		},
		{
			name:            "extra group TiDB status",
			groupId:         "group1",
			componentType:   ComponentTiDB,
			wantErrContains: "",
			wantNil:         false,
			wantPhase:       "group1-tidb",
		},
		{
			name:            "non-existence component status TiProxy in extra group 'group1'",
			groupId:         "group1",
			componentType:   ComponentTiProxy,
			wantErrContains: "",
			wantNil:         false,
			wantPhase:       "",
		},
		{
			name:            "non-existence component spec TiProxy in extra group 'group2'",
			groupId:         "group2",
			componentType:   ComponentTiProxy,
			wantErrContains: "",
			wantNil:         false,
			wantPhase:       "",
		},
		{
			name:            "extra group not found",
			groupId:         "non-existent-group",
			componentType:   ComponentTiDB,
			wantErrContains: "group 'non-existent-group' not found",
			wantNil:         true,
		},
		{
			name:            "nil DB status",
			groupId:         "default-group",
			componentType:   ComponentPD,
			wantErrContains: "",
			wantNil:         false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// For nil DB status test
			if tt.name == "nil DB status" {
				cluster.Status.DB = nil
			}
			statusWrapped, err := cluster.GetOrCreateComponentStatusInGroup(tt.groupId, tt.componentType)
			if tt.wantErrContains != "" {
				assert.ErrorContainsf(t, err, tt.wantErrContains, "check err msg")
				assert.Nil(t, statusWrapped)
				return
			}
			assert.NoError(t, err)

			if tt.wantNil {
				assert.Nil(t, statusWrapped)
			} else {
				assert.NotNil(t, statusWrapped)
				assert.Equal(t, tt.wantPhase, statusWrapped.GetInstanceTypeScalePhase())
				assert.Equal(t, tt.groupId, statusWrapped.(*clusterComponentStatusWrapped).groupId)
				assert.Equal(t, cluster, statusWrapped.(*clusterComponentStatusWrapped).cluster)
			}
		})
	}
}

func TestAWSSubnetGroups_SubnetsFilterByGroupNameOrZones(t *testing.T) {
	// Test data setup based on real cluster configuration
	subnetGroups := AWSSubnetGroups{
		{
			Name: AWSSubnetGroupNodes,
			Subnets: []AWSSubnet{
				{SubnetID: "subnet-057d32bd9fcf8985e", Zone: "us-west-2a"},
				{SubnetID: "subnet-01f92e11761285497", Zone: "us-west-2b"},
				{SubnetID: "subnet-093cad6419d167be9", Zone: "us-west-2c"},
			},
		},
		{
			Name: AWSSubnetGroupPublicNLB,
			Subnets: []AWSSubnet{
				{SubnetID: "subnet-011d5dece1f1ba105", Zone: "us-west-2a"},
				{SubnetID: "subnet-0d4a431495400fe90", Zone: "us-west-2b"},
				{SubnetID: "subnet-0934cc1e712e52c0e", Zone: "us-west-2c"},
			},
		},
		{
			Name: AWSSubnetGroupPrivateNLB,
			Subnets: []AWSSubnet{
				{SubnetID: "subnet-05c6103ee80b6de21", Zone: "us-west-2a"},
				{SubnetID: "subnet-003dd94fc849f664a", Zone: "us-west-2b"},
				{SubnetID: "subnet-0f8bbdfcc975b3127", Zone: "us-west-2c"},
			},
		},
		{
			Name: AWSSubnetGroupSecondaryCIDR,
			Subnets: []AWSSubnet{
				{SubnetID: "subnet-0570e418697dec5c3", Zone: "us-west-2a"},
				{SubnetID: "subnet-06c6ec9237221dad7", Zone: "us-west-2a"}, // duplicate zone
				{SubnetID: "subnet-0459eabdab160a356", Zone: "us-west-2b"},
				{SubnetID: "subnet-0ee54c208a96ed748", Zone: "us-west-2b"}, // duplicate zone
				{SubnetID: "subnet-0ab50b4537282ae37", Zone: "us-west-2c"},
				{SubnetID: "subnet-0af403b1cbd83b2a8", Zone: "us-west-2c"}, // duplicate zone
			},
		},
	}

	tests := []struct {
		name          string
		groupNames    []AWSSubnetGroupName
		zoneFilters   []string
		expectZones   []string
		expectSubnets []string
	}{
		{
			name:          "no zoneFilter - get all subnets of any zones (nodes group)",
			groupNames:    []AWSSubnetGroupName{AWSSubnetGroupNodes},
			zoneFilters:   []string{},
			expectZones:   []string{"us-west-2a", "us-west-2b", "us-west-2c"},
			expectSubnets: []string{"subnet-01f92e11761285497", "subnet-057d32bd9fcf8985e", "subnet-093cad6419d167be9"},
		},
		{
			name:          "no zoneFilter - get all subnets from secondary-cidr (multiple subnets per zone)",
			groupNames:    []AWSSubnetGroupName{AWSSubnetGroupSecondaryCIDR},
			zoneFilters:   []string{},
			expectZones:   []string{"us-west-2a", "us-west-2b", "us-west-2c"},
			expectSubnets: []string{"subnet-0459eabdab160a356", "subnet-0570e418697dec5c3", "subnet-06c6ec9237221dad7", "subnet-0ab50b4537282ae37", "subnet-0af403b1cbd83b2a8", "subnet-0ee54c208a96ed748"},
		},
		{
			name:          "normal functionality - single zone filter with nodes",
			groupNames:    []AWSSubnetGroupName{AWSSubnetGroupNodes},
			zoneFilters:   []string{"us-west-2a"},
			expectZones:   []string{"us-west-2a"},
			expectSubnets: []string{"subnet-057d32bd9fcf8985e"},
		},
		{
			name:          "normal functionality - single zone filter with secondary-cidr (multiple subnets)",
			groupNames:    []AWSSubnetGroupName{AWSSubnetGroupSecondaryCIDR},
			zoneFilters:   []string{"us-west-2a"},
			expectZones:   []string{"us-west-2a"},
			expectSubnets: []string{"subnet-0570e418697dec5c3", "subnet-06c6ec9237221dad7"},
		},
		{
			name:          "normal functionality - multiple zone filters",
			groupNames:    []AWSSubnetGroupName{AWSSubnetGroupNodes},
			zoneFilters:   []string{"us-west-2a", "us-west-2c"},
			expectZones:   []string{"us-west-2a", "us-west-2c"},
			expectSubnets: []string{"subnet-057d32bd9fcf8985e", "subnet-093cad6419d167be9"},
		},
		{
			name:          "multiple group names with zone filter",
			groupNames:    []AWSSubnetGroupName{AWSSubnetGroupNodes, AWSSubnetGroupSecondaryCIDR},
			zoneFilters:   []string{"us-west-2a", "us-west-2b"},
			expectZones:   []string{"us-west-2a", "us-west-2b"},
			expectSubnets: []string{"subnet-01f92e11761285497", "subnet-0459eabdab160a356", "subnet-0570e418697dec5c3", "subnet-057d32bd9fcf8985e", "subnet-06c6ec9237221dad7", "subnet-0ee54c208a96ed748"},
		},
		{
			name:          "zone returned is unique - secondary-cidr has multiple subnets per zone",
			groupNames:    []AWSSubnetGroupName{AWSSubnetGroupSecondaryCIDR},
			zoneFilters:   []string{"us-west-2b"},
			expectZones:   []string{"us-west-2b"}, // should be unique even though there are 2 subnets in us-west-2b
			expectSubnets: []string{"subnet-0459eabdab160a356", "subnet-0ee54c208a96ed748"},
		},
		{
			name:          "zone and subnetId is sorted - complex case with multiple groups",
			groupNames:    []AWSSubnetGroupName{AWSSubnetGroupNodes, AWSSubnetGroupSecondaryCIDR, AWSSubnetGroupPublicNLB},
			zoneFilters:   []string{"us-west-2c", "us-west-2a"},
			expectZones:   []string{"us-west-2a", "us-west-2c"},                                                                                                                                                                                                     // sorted alphabetically
			expectSubnets: []string{"subnet-011d5dece1f1ba105", "subnet-0570e418697dec5c3", "subnet-057d32bd9fcf8985e", "subnet-06c6ec9237221dad7", "subnet-0934cc1e712e52c0e", "subnet-093cad6419d167be9", "subnet-0ab50b4537282ae37", "subnet-0af403b1cbd83b2a8"}, // sorted alphabetically
		},
		{
			name:          "real world scenario - nodes and secondary-cidr for all zones",
			groupNames:    []AWSSubnetGroupName{AWSSubnetGroupNodes, AWSSubnetGroupSecondaryCIDR},
			zoneFilters:   []string{},
			expectZones:   []string{"us-west-2a", "us-west-2b", "us-west-2c"},
			expectSubnets: []string{"subnet-01f92e11761285497", "subnet-0459eabdab160a356", "subnet-0570e418697dec5c3", "subnet-057d32bd9fcf8985e", "subnet-06c6ec9237221dad7", "subnet-093cad6419d167be9", "subnet-0ab50b4537282ae37", "subnet-0af403b1cbd83b2a8", "subnet-0ee54c208a96ed748"},
		},
		{
			name:          "all NLB groups - public and private",
			groupNames:    []AWSSubnetGroupName{AWSSubnetGroupPublicNLB, AWSSubnetGroupPrivateNLB},
			zoneFilters:   []string{"us-west-2a", "us-west-2c"},
			expectZones:   []string{"us-west-2a", "us-west-2c"},
			expectSubnets: []string{"subnet-011d5dece1f1ba105", "subnet-05c6103ee80b6de21", "subnet-0934cc1e712e52c0e", "subnet-0f8bbdfcc975b3127"},
		},
		{
			name:          "non-existent zone filter",
			groupNames:    []AWSSubnetGroupName{AWSSubnetGroupNodes},
			zoneFilters:   []string{"us-west-2z"},
			expectZones:   nil,
			expectSubnets: nil,
		},
		{
			name:          "non-existent group name",
			groupNames:    []AWSSubnetGroupName{"non-existent-group"},
			zoneFilters:   []string{"us-west-2a"},
			expectZones:   nil,
			expectSubnets: nil,
		},
		{
			name:          "empty group names",
			groupNames:    []AWSSubnetGroupName{},
			zoneFilters:   []string{"us-west-2a"},
			expectZones:   nil,
			expectSubnets: nil,
		},
		{
			name:          "all groups without zone filter - comprehensive test",
			groupNames:    []AWSSubnetGroupName{AWSSubnetGroupNodes, AWSSubnetGroupSecondaryCIDR, AWSSubnetGroupPublicNLB, AWSSubnetGroupPrivateNLB},
			zoneFilters:   []string{},
			expectZones:   []string{"us-west-2a", "us-west-2b", "us-west-2c"},
			expectSubnets: []string{"subnet-003dd94fc849f664a", "subnet-011d5dece1f1ba105", "subnet-01f92e11761285497", "subnet-0459eabdab160a356", "subnet-0570e418697dec5c3", "subnet-057d32bd9fcf8985e", "subnet-05c6103ee80b6de21", "subnet-06c6ec9237221dad7", "subnet-0934cc1e712e52c0e", "subnet-093cad6419d167be9", "subnet-0ab50b4537282ae37", "subnet-0af403b1cbd83b2a8", "subnet-0d4a431495400fe90", "subnet-0ee54c208a96ed748", "subnet-0f8bbdfcc975b3127"},
		},
		{
			name:          "edge case - mixed zones with secondary-cidr duplicates",
			groupNames:    []AWSSubnetGroupName{AWSSubnetGroupSecondaryCIDR},
			zoneFilters:   []string{"us-west-2a", "us-west-2c"},
			expectZones:   []string{"us-west-2a", "us-west-2c"}, // unique zones despite multiple subnets per zone
			expectSubnets: []string{"subnet-0570e418697dec5c3", "subnet-06c6ec9237221dad7", "subnet-0ab50b4537282ae37", "subnet-0af403b1cbd83b2a8"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			zones, subnets := subnetGroups.SubnetsFilterByGroupNameOrZones(tt.groupNames, tt.zoneFilters)

			// Verify zones are unique and sorted
			assert.Equal(t, tt.expectZones, zones, "zones should match expected")

			// Verify subnets are sorted
			assert.Equal(t, tt.expectSubnets, subnets, "subnets should match expected")

			// Additional checks for uniqueness and sorting
			if len(zones) > 1 {
				for i := 1; i < len(zones); i++ {
					assert.True(t, zones[i-1] < zones[i], "zones should be sorted: %v", zones)
				}
			}

			if len(subnets) > 1 {
				for i := 1; i < len(subnets); i++ {
					assert.True(t, subnets[i-1] < subnets[i], "subnets should be sorted: %v", subnets)
				}
			}

			// Verify zones are unique
			zoneSet := make(map[string]bool)
			for _, zone := range zones {
				assert.False(t, zoneSet[zone], "zone %s should be unique", zone)
				zoneSet[zone] = true
			}
		})
	}
}

func TestCluster_MutateAnnotations(t *testing.T) {
	tests := []struct {
		name         string
		initialAnno  map[string]string
		mutator      func(map[string]string)
		expectedAnno map[string]string
		expectedNil  bool
	}{
		{
			name:        "nil annotations should create new map",
			initialAnno: nil,
			mutator: func(anno map[string]string) {
				anno["key1"] = "value1"
			},
			expectedAnno: map[string]string{
				"key1": "value1",
			},
			expectedNil: false,
		},
		{
			name:        "empty annotations should work",
			initialAnno: map[string]string{},
			mutator: func(anno map[string]string) {
				anno["key1"] = "value1"
			},
			expectedAnno: map[string]string{
				"key1": "value1",
			},
			expectedNil: false,
		},
		{
			name: "existing annotations should be preserved and updated",
			initialAnno: map[string]string{
				"key1": "value1",
				"key2": "value2",
			},
			mutator: func(anno map[string]string) {
				anno["key1"] = "updated-value1"
				anno["key3"] = "value3"
			},
			expectedAnno: map[string]string{
				"key1": "updated-value1",
				"key2": "value2",
				"key3": "value3",
			},
			expectedNil: false,
		},
		{
			name: "delete annotation key",
			initialAnno: map[string]string{
				"key1": "value1",
				"key2": "value2",
			},
			mutator: func(anno map[string]string) {
				delete(anno, "key1")
			},
			expectedAnno: map[string]string{
				"key2": "value2",
			},
			expectedNil: false,
		},
		{
			name: "multiple operations in mutator",
			initialAnno: map[string]string{
				"key1": "value1",
			},
			mutator: func(anno map[string]string) {
				anno["key2"] = "value2"
				anno["key3"] = "value3"
				delete(anno, "key1")
				anno["key4"] = "value4"
			},
			expectedAnno: map[string]string{
				"key2": "value2",
				"key3": "value3",
				"key4": "value4",
			},
			expectedNil: false,
		},
		{
			name: "mutator does nothing",
			initialAnno: map[string]string{
				"key1": "value1",
			},
			mutator: func(anno map[string]string) {
				// do nothing
			},
			expectedAnno: map[string]string{
				"key1": "value1",
			},
			expectedNil: false,
		},
		{
			name: "mutator clears all annotations",
			initialAnno: map[string]string{
				"key1": "value1",
				"key2": "value2",
			},
			mutator: func(anno map[string]string) {
				for k := range anno {
					delete(anno, k)
				}
			},
			expectedAnno: map[string]string{},
			expectedNil:  false,
		},
		{
			name:        "nil annotations with empty mutator",
			initialAnno: nil,
			mutator: func(anno map[string]string) {
				// do nothing
			},
			expectedAnno: map[string]string{},
			expectedNil:  false,
		},
		{
			name:         "nil mutator with nil annotations should not panic",
			initialAnno:  nil,
			mutator:      nil,
			expectedAnno: nil,
			expectedNil:  true,
		},
		{
			name: "nil mutator with existing annotations should preserve them",
			initialAnno: map[string]string{
				"key1": "value1",
				"key2": "value2",
			},
			mutator: nil,
			expectedAnno: map[string]string{
				"key1": "value1",
				"key2": "value2",
			},
			expectedNil: false,
		},
		{
			name:         "nil mutator with empty annotations should preserve empty map",
			initialAnno:  map[string]string{},
			mutator:      nil,
			expectedAnno: map[string]string{},
			expectedNil:  false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cluster := &Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Annotations: tt.initialAnno,
				},
			}

			cluster.MutateAnnotations(tt.mutator)

			resultAnno := cluster.GetAnnotations()
			if tt.expectedNil {
				assert.Nil(t, resultAnno)
			} else {
				assert.NotNil(t, resultAnno)
				assert.Equal(t, tt.expectedAnno, resultAnno, "annotations should match expected")
			}
		})
	}
}
