package v1

import (
	"testing"

	. "github.com/onsi/gomega"
	"k8s.io/apimachinery/pkg/api/resource"
	"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.BoolPtr(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.BoolPtr(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,
		},
	}

	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 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: 1,
		},
		"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: 1,
		},
		"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,
		},
		"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,
		},
		"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,
		},
		"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))
			}
		})
	}
}
