package region

import (
	"context"
	"encoding/json"
	"strconv"
	"testing"

	"github.com/onsi/gomega"
	clusterngv1beta "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	ffentity "github.com/tidbcloud/fastflow/pkg/entity"
	"go.uber.org/mock/gomock"
	"google.golang.org/grpc/codes"
	"google.golang.org/protobuf/proto"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	globalngv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/adapter/regionalserver/mock_regionalserver"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/region/mock_region"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/resourcepool/mock_resourcepool"
	workflowcommon "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/service/workflow/common"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/idgenerator/mock_idgenerator"
	utilworkflow "github.com/tidbcloud/tidb-management-service/utilities/workflow"
	mockworkflow "github.com/tidbcloud/tidb-management-service/utilities/workflow/mock_workflow"
)

type passthroughTransactioner struct{}

func (passthroughTransactioner) Transaction(ctx context.Context, cb func(context.Context) error) error {
	return cb(ctx)
}

func TestScaleByocSharedComponents_ReturnsOperationID(t *testing.T) {
	g := gomega.NewWithT(t)
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	resourcePoolRepo := mock_resourcepool.NewMockRepo(ctrl)
	regionRepo := mock_region.NewMockRepo(ctrl)
	regionalCli := mock_regionalserver.NewMockClient(ctrl)
	idGenerator := mock_idgenerator.NewMockIDGenerator(ctrl)
	workflow := mockworkflow.NewMockWorkflow(ctrl)

	svc := NewService(regionRepo, nil, nil, resourcePoolRepo, nil, passthroughTransactioner{}, regionalCli, idGenerator, nil, workflow)
	resourcePool := &entity.ResourcePool{
		ResourcePool: &globalngv1.ResourcePool{
			Id:             21,
			OwnerId:        1001,
			OwnerType:      globalngv1.ResourcePool_ORGANIZATION,
			ServicePlan:    globalcommonv1.ServicePlan_BYOC,
			VcpusLimit:     80,
			LifecycleState: globalngv1.ResourcePool_ACTIVE,
		},
		RegionId: 9,
	}
	region := &entity.Region{Region: &globalcommonv1.Region{Id: globalcommonv1.Region_AWS_US_WEST_2}}

	resourcePoolRepo.EXPECT().Load(gomock.Any(), uint64(21), false).Return(resourcePool, nil)
	regionRepo.EXPECT().Get(gomock.Any(), uint64(9)).Return(region, nil)
	regionalCli.EXPECT().GetSharedPool(gomock.Any(), region, &clusterngv1beta.GetSharedPoolRequest{
		SharedPoolId: "21",
	}).Return(&clusterngv1beta.SharedPool{
		ComponentGroups: []*clusterngv1beta.Component{
			{
				Id:            "cg-tikv",
				ComponentType: clusterngv1beta.ComponentType_TIKV,
				NodeQuota: &clusterngv1beta.NodeQuota{
					NodeCount: 3,
					NodeSpec: &clusterngv1beta.NodeSpec{
						Cpu:      4,
						MemoryGi: 16,
					},
				},
			},
		},
	}, nil)
	regionalCli.EXPECT().GetSharedPoolScaleConstraints(gomock.Any(), region, &clusterngv1beta.GetSharedPoolScaleConstraintsRequest{
		SharedPoolId: "21",
	}).Return(&clusterngv1beta.GetSharedPoolScaleConstraintsResponse{
		Components: []*clusterngv1beta.SharedPoolScaleConstraintComponent{
			{
				ComponentType: clusterngv1beta.ComponentType_TIKV,
				NodeCountConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeCountConstraints{
					Current:        3,
					Min:            1,
					Max:            10,
					AllowCrossZero: false,
				},
				NodeSpecConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeSpecConstraints{
					Candidates: []*clusterngv1beta.SharedPoolScaleConstraintNodeSpecCandidate{
						{
							Name:      "4c16g",
							Cpu:       4,
							MemoryGi:  16,
							Available: true,
							IsCurrent: true,
						},
					},
				},
			},
		},
	}, nil)
	regionalCli.EXPECT().GetSharedPoolVcpusUsage(gomock.Any(), region, &clusterngv1beta.GetSharedPoolVcpusUsageRequest{
		SharedPoolId: "21",
	}).Return(&clusterngv1beta.GetSharedPoolVcpusUsageResponse{VcpusUsed: 12}, nil)
	workflow.EXPECT().ListRunningDagInstances(
		gomock.Any(),
		[]string{workflowcommon.ScaleByocSharedComponents},
		"21",
	).Return(nil, nil).Times(2)
	loadForUpdate := resourcePoolRepo.EXPECT().LoadForUpdate(gomock.Any(), uint64(21), false).Return(resourcePool, nil)
	markModifying := resourcePoolRepo.EXPECT().UpdateLifecycle(
		gomock.Any(), uint64(21), globalngv1.ResourcePool_MODIFYING, "",
	).Return(nil).After(loadForUpdate)
	idGenerator.EXPECT().MustNew().Return(uint64(12345))
	workflow.EXPECT().RunDagWithBizId(
		gomock.Any(),
		workflowcommon.ScaleByocSharedComponents,
		"1001",
		"0",
		"21",
		gomock.Any(),
	).After(markModifying).DoAndReturn(func(_ any, _ string, _ string, _ string, _ string, vars map[string]string) error {
		g.Expect(vars[workflowcommon.KeyOperationId]).To(gomega.Equal("12345"))
		var payload workflowcommon.ByocSharedComponentScaleWorkflowPayload
		g.Expect(json.Unmarshal([]byte(vars[workflowcommon.ScaleTaskKeyRequestBody]), &payload)).To(gomega.Succeed())
		g.Expect(payload.Components).To(gomega.HaveLen(1))
		g.Expect(payload.Components[0].ComponentID).To(gomega.Equal("cg-tikv"))
		g.Expect(payload.Components[0].Current.GetNodeCount()).To(gomega.Equal(uint32(3)))
		g.Expect(payload.Components[0].Current.GetCpu()).To(gomega.Equal(uint32(4)))
		g.Expect(payload.Components[0].Current.GetMemoryGi()).To(gomega.Equal(uint32(16)))
		g.Expect(payload.Components[0].Target.GetNodeCount()).To(gomega.Equal(uint32(4)))
		return nil
	})

	resp, err := svc.ScaleByocSharedComponents(t.Context(), &globalngv1.ScaleByocSharedComponentsRequest{
		ResourcePoolId: 21,
		OrganizationId: 1001,
		Tikv: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(4),
		},
	})
	g.Expect(err).ToNot(gomega.HaveOccurred())
	g.Expect(resp.GetOperationId()).To(gomega.Equal("12345"))
}

func TestScaleByocSharedComponents_RejectsPauseResumeLifecycleChangedDuringSubmit(t *testing.T) {
	tests := []struct {
		name           string
		lifecycleState globalngv1.ResourcePool_LifecycleState
	}{
		{
			name:           "pausing",
			lifecycleState: globalngv1.ResourcePool_PAUSING,
		},
		{
			name:           "paused",
			lifecycleState: globalngv1.ResourcePool_PAUSED,
		},
		{
			name:           "resuming",
			lifecycleState: globalngv1.ResourcePool_RESUMING,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			g := gomega.NewWithT(t)
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()

			resourcePoolRepo := mock_resourcepool.NewMockRepo(ctrl)
			regionRepo := mock_region.NewMockRepo(ctrl)
			regionalCli := mock_regionalserver.NewMockClient(ctrl)
			workflow := mockworkflow.NewMockWorkflow(ctrl)
			resourcePool := &entity.ResourcePool{
				ResourcePool: &globalngv1.ResourcePool{
					Id:             50,
					OwnerId:        1001,
					OwnerType:      globalngv1.ResourcePool_ORGANIZATION,
					ServicePlan:    globalcommonv1.ServicePlan_BYOC,
					VcpusLimit:     80,
					LifecycleState: globalngv1.ResourcePool_ACTIVE,
				},
				RegionId: 25,
			}
			region := &entity.Region{Region: &globalcommonv1.Region{Id: globalcommonv1.Region_AWS_US_WEST_2}}
			svc := NewService(
				regionRepo,
				nil,
				nil,
				resourcePoolRepo,
				nil,
				passthroughTransactioner{},
				regionalCli,
				nil,
				nil,
				workflow,
			)

			expectTikvScaleState(resourcePoolRepo, regionRepo, regionalCli, workflow, resourcePool, region, false)
			resourcePoolRepo.EXPECT().LoadForUpdate(gomock.Any(), uint64(50), false).Return(&entity.ResourcePool{
				ResourcePool: &globalngv1.ResourcePool{
					Id:             50,
					LifecycleState: tt.lifecycleState,
				},
			}, nil)
			workflow.EXPECT().ListRunningDagInstances(
				gomock.Any(),
				[]string{workflowcommon.ScaleByocSharedComponents},
				"50",
			).Return(nil, nil)

			resp, err := svc.ScaleByocSharedComponents(t.Context(), &globalngv1.ScaleByocSharedComponentsRequest{
				ResourcePoolId: 50,
				OrganizationId: 1001,
				Tikv: &globalngv1.ByocSharedComponentScaleTarget{
					NodeCount: proto.Uint32(4),
				},
			})

			g.Expect(resp).To(gomega.BeNil())
			g.Expect(err).To(gomega.HaveOccurred())
			g.Expect(err.ErrCode()).To(gomega.Equal(bizerr.ErrCode(codes.FailedPrecondition)))
			g.Expect(err.Error()).To(gomega.ContainSubstring("operation is not allowed while resource pool lifecycle state is"))
		})
	}
}

func TestGetByocSharedComponentScaleConstraints_BlocksWhenPoolModifying(t *testing.T) {
	g := gomega.NewWithT(t)
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	resourcePoolRepo := mock_resourcepool.NewMockRepo(ctrl)
	regionRepo := mock_region.NewMockRepo(ctrl)
	regionalCli := mock_regionalserver.NewMockClient(ctrl)
	workflow := mockworkflow.NewMockWorkflow(ctrl)

	svc := NewService(regionRepo, nil, nil, resourcePoolRepo, nil, nil, regionalCli, nil, nil, workflow)
	resourcePool := &entity.ResourcePool{
		ResourcePool: &globalngv1.ResourcePool{
			Id:             40,
			OwnerId:        1001,
			OwnerType:      globalngv1.ResourcePool_ORGANIZATION,
			ServicePlan:    globalcommonv1.ServicePlan_BYOC,
			VcpusLimit:     80,
			LifecycleState: globalngv1.ResourcePool_ACTIVE,
		},
		RegionId: 20,
	}
	region := &entity.Region{Region: &globalcommonv1.Region{Id: globalcommonv1.Region_AWS_US_WEST_2}}

	resourcePoolRepo.EXPECT().Load(gomock.Any(), uint64(40), false).Return(resourcePool, nil)
	regionRepo.EXPECT().Get(gomock.Any(), uint64(20)).Return(region, nil)
	regionalCli.EXPECT().GetSharedPool(gomock.Any(), region, &clusterngv1beta.GetSharedPoolRequest{
		SharedPoolId: "40",
	}).Return(&clusterngv1beta.SharedPool{Phase: clusterngv1beta.SharedPool_AVAILABLE}, nil)
	workflow.EXPECT().ListRunningDagInstances(
		gomock.Any(),
		[]string{workflowcommon.ScaleByocSharedComponents},
		"40",
	).Return(runningScaleWorkflow("40"), nil)
	regionalCli.EXPECT().GetSharedPoolScaleConstraints(gomock.Any(), region, &clusterngv1beta.GetSharedPoolScaleConstraintsRequest{
		SharedPoolId: "40",
	}).Return(&clusterngv1beta.GetSharedPoolScaleConstraintsResponse{}, nil)
	regionalCli.EXPECT().GetSharedPoolVcpusUsage(gomock.Any(), region, &clusterngv1beta.GetSharedPoolVcpusUsageRequest{
		SharedPoolId: "40",
	}).Return(&clusterngv1beta.GetSharedPoolVcpusUsageResponse{}, nil)

	resp, err := svc.GetByocSharedComponentScaleConstraints(t.Context(), &globalngv1.GetByocSharedComponentScaleConstraintsRequest{
		ResourcePoolId: 40,
		OrganizationId: 1001,
	})
	g.Expect(err).ToNot(gomega.HaveOccurred())
	g.Expect(resp.GetPreview().GetAllowed()).To(gomega.BeFalse())
	g.Expect(resp.GetPreview().GetBlockingReasons()).To(gomega.ContainElement(byocVcpuPoolModifyingBlockingReason))
}

func TestGetByocSharedComponentScaleConstraints_BlocksPauseResumeLifecycle(t *testing.T) {
	tests := []struct {
		name           string
		lifecycleState globalngv1.ResourcePool_LifecycleState
	}{
		{name: "pausing", lifecycleState: globalngv1.ResourcePool_PAUSING},
		{name: "paused", lifecycleState: globalngv1.ResourcePool_PAUSED},
		{name: "resuming", lifecycleState: globalngv1.ResourcePool_RESUMING},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			g := gomega.NewWithT(t)
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()

			resourcePoolRepo := mock_resourcepool.NewMockRepo(ctrl)
			regionRepo := mock_region.NewMockRepo(ctrl)
			regionalCli := mock_regionalserver.NewMockClient(ctrl)
			workflow := mockworkflow.NewMockWorkflow(ctrl)
			resourcePool := &entity.ResourcePool{
				ResourcePool: &globalngv1.ResourcePool{
					Id:             45,
					OwnerId:        1001,
					OwnerType:      globalngv1.ResourcePool_ORGANIZATION,
					ServicePlan:    globalcommonv1.ServicePlan_BYOC,
					VcpusLimit:     80,
					LifecycleState: tt.lifecycleState,
				},
				RegionId: 25,
			}
			region := &entity.Region{Region: &globalcommonv1.Region{Id: globalcommonv1.Region_AWS_US_WEST_2}}
			svc := NewService(regionRepo, nil, nil, resourcePoolRepo, nil, nil, regionalCli, nil, nil, workflow)

			expectTikvScaleState(resourcePoolRepo, regionRepo, regionalCli, workflow, resourcePool, region, false)

			resp, err := svc.GetByocSharedComponentScaleConstraints(
				t.Context(),
				&globalngv1.GetByocSharedComponentScaleConstraintsRequest{
					ResourcePoolId: 45,
					OrganizationId: 1001,
				},
			)

			g.Expect(err).ToNot(gomega.HaveOccurred())
			g.Expect(resp.GetPreview().GetAllowed()).To(gomega.BeFalse())
			g.Expect(resp.GetPreview().GetBlockingReasons()).To(gomega.ContainElement(
				scaleBlockedByLifecycleReason(tt.lifecycleState),
			))
		})
	}
}

func TestScaleByocSharedComponents_ValidateOnlyBlocksWhenPoolModifying(t *testing.T) {
	g := gomega.NewWithT(t)
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	resourcePoolRepo := mock_resourcepool.NewMockRepo(ctrl)
	regionRepo := mock_region.NewMockRepo(ctrl)
	regionalCli := mock_regionalserver.NewMockClient(ctrl)
	workflow := mockworkflow.NewMockWorkflow(ctrl)

	svc := NewService(regionRepo, nil, nil, resourcePoolRepo, nil, nil, regionalCli, nil, nil, workflow)
	resourcePool := &entity.ResourcePool{
		ResourcePool: &globalngv1.ResourcePool{
			Id:             41,
			OwnerId:        1001,
			OwnerType:      globalngv1.ResourcePool_ORGANIZATION,
			ServicePlan:    globalcommonv1.ServicePlan_BYOC,
			VcpusLimit:     80,
			LifecycleState: globalngv1.ResourcePool_ACTIVE,
		},
		RegionId: 21,
	}
	region := &entity.Region{Region: &globalcommonv1.Region{Id: globalcommonv1.Region_AWS_US_WEST_2}}

	expectTikvScaleState(resourcePoolRepo, regionRepo, regionalCli, workflow, resourcePool, region, true)

	resp, err := svc.ScaleByocSharedComponents(t.Context(), &globalngv1.ScaleByocSharedComponentsRequest{
		ResourcePoolId: 41,
		OrganizationId: 1001,
		Tikv: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(4),
		},
		ValidateOnly: true,
	})
	g.Expect(err).ToNot(gomega.HaveOccurred())
	g.Expect(resp.GetPreview().GetAllowed()).To(gomega.BeFalse())
	g.Expect(resp.GetPreview().GetBlockingReasons()).To(gomega.ContainElement(byocVcpuPoolModifyingBlockingReason))
}

func TestScaleByocSharedComponents_ValidateOnlyBlocksPauseResumeLifecycle(t *testing.T) {
	tests := []struct {
		name           string
		lifecycleState globalngv1.ResourcePool_LifecycleState
	}{
		{name: "pausing", lifecycleState: globalngv1.ResourcePool_PAUSING},
		{name: "paused", lifecycleState: globalngv1.ResourcePool_PAUSED},
		{name: "resuming", lifecycleState: globalngv1.ResourcePool_RESUMING},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			g := gomega.NewWithT(t)
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()

			resourcePoolRepo := mock_resourcepool.NewMockRepo(ctrl)
			regionRepo := mock_region.NewMockRepo(ctrl)
			regionalCli := mock_regionalserver.NewMockClient(ctrl)
			workflow := mockworkflow.NewMockWorkflow(ctrl)
			resourcePool := &entity.ResourcePool{
				ResourcePool: &globalngv1.ResourcePool{
					Id:             46,
					OwnerId:        1001,
					OwnerType:      globalngv1.ResourcePool_ORGANIZATION,
					ServicePlan:    globalcommonv1.ServicePlan_BYOC,
					VcpusLimit:     80,
					LifecycleState: tt.lifecycleState,
				},
				RegionId: 26,
			}
			region := &entity.Region{Region: &globalcommonv1.Region{Id: globalcommonv1.Region_AWS_US_WEST_2}}
			svc := NewService(regionRepo, nil, nil, resourcePoolRepo, nil, nil, regionalCli, nil, nil, workflow)

			expectTikvScaleState(resourcePoolRepo, regionRepo, regionalCli, workflow, resourcePool, region, false)

			resp, err := svc.ScaleByocSharedComponents(t.Context(), &globalngv1.ScaleByocSharedComponentsRequest{
				ResourcePoolId: 46,
				OrganizationId: 1001,
				Tikv: &globalngv1.ByocSharedComponentScaleTarget{
					NodeCount: proto.Uint32(4),
				},
				ValidateOnly: true,
			})

			g.Expect(err).ToNot(gomega.HaveOccurred())
			g.Expect(resp.GetPreview().GetAllowed()).To(gomega.BeFalse())
			g.Expect(resp.GetPreview().GetBlockingReasons()).To(gomega.ContainElement(
				scaleBlockedByLifecycleReason(tt.lifecycleState),
			))
		})
	}
}

func TestScaleByocSharedComponents_ValidateOnlyNoopStillReturnsPreviewWhenPoolModifying(t *testing.T) {
	g := gomega.NewWithT(t)
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	resourcePoolRepo := mock_resourcepool.NewMockRepo(ctrl)
	regionRepo := mock_region.NewMockRepo(ctrl)
	regionalCli := mock_regionalserver.NewMockClient(ctrl)
	workflow := mockworkflow.NewMockWorkflow(ctrl)

	svc := NewService(regionRepo, nil, nil, resourcePoolRepo, nil, nil, regionalCli, nil, nil, workflow)
	resourcePool := &entity.ResourcePool{
		ResourcePool: &globalngv1.ResourcePool{
			Id:             44,
			OwnerId:        1001,
			OwnerType:      globalngv1.ResourcePool_ORGANIZATION,
			ServicePlan:    globalcommonv1.ServicePlan_BYOC,
			VcpusLimit:     80,
			LifecycleState: globalngv1.ResourcePool_ACTIVE,
		},
		RegionId: 24,
	}
	region := &entity.Region{Region: &globalcommonv1.Region{Id: globalcommonv1.Region_AWS_US_WEST_2}}

	expectTikvScaleState(resourcePoolRepo, regionRepo, regionalCli, workflow, resourcePool, region, true)

	resp, err := svc.ScaleByocSharedComponents(t.Context(), &globalngv1.ScaleByocSharedComponentsRequest{
		ResourcePoolId: 44,
		OrganizationId: 1001,
		Tikv: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(3),
		},
		ValidateOnly: true,
	})
	g.Expect(err).ToNot(gomega.HaveOccurred())
	g.Expect(resp.GetPreview().GetAllowed()).To(gomega.BeFalse())
	g.Expect(resp.GetPreview().GetBlockingReasons()).To(gomega.ContainElement(byocVcpuPoolModifyingBlockingReason))
}

func TestScaleByocSharedComponents_RejectsWriteWhenPoolModifying(t *testing.T) {
	g := gomega.NewWithT(t)
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	resourcePoolRepo := mock_resourcepool.NewMockRepo(ctrl)
	regionRepo := mock_region.NewMockRepo(ctrl)
	regionalCli := mock_regionalserver.NewMockClient(ctrl)
	workflow := mockworkflow.NewMockWorkflow(ctrl)

	svc := NewService(regionRepo, nil, nil, resourcePoolRepo, nil, passthroughTransactioner{}, regionalCli, nil, nil, workflow)
	resourcePool := &entity.ResourcePool{
		ResourcePool: &globalngv1.ResourcePool{
			Id:             42,
			OwnerId:        1001,
			OwnerType:      globalngv1.ResourcePool_ORGANIZATION,
			ServicePlan:    globalcommonv1.ServicePlan_BYOC,
			VcpusLimit:     80,
			LifecycleState: globalngv1.ResourcePool_ACTIVE,
		},
		RegionId: 22,
	}
	region := &entity.Region{Region: &globalcommonv1.Region{Id: globalcommonv1.Region_AWS_US_WEST_2}}

	expectTikvScaleState(resourcePoolRepo, regionRepo, regionalCli, workflow, resourcePool, region, true)

	resp, err := svc.ScaleByocSharedComponents(t.Context(), &globalngv1.ScaleByocSharedComponentsRequest{
		ResourcePoolId: 42,
		OrganizationId: 1001,
		Tikv: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(4),
		},
	})
	g.Expect(resp).To(gomega.BeNil())
	g.Expect(err).To(gomega.HaveOccurred())
	g.Expect(err.Error()).To(gomega.ContainSubstring("scale operation already in progress for resource pool 42"))
}

func TestScaleByocSharedComponents_RejectsWorkflowCreatedDuringSubmit(t *testing.T) {
	g := gomega.NewWithT(t)
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	resourcePoolRepo := mock_resourcepool.NewMockRepo(ctrl)
	regionRepo := mock_region.NewMockRepo(ctrl)
	regionalCli := mock_regionalserver.NewMockClient(ctrl)
	workflow := mockworkflow.NewMockWorkflow(ctrl)

	svc := NewService(regionRepo, nil, nil, resourcePoolRepo, nil, passthroughTransactioner{}, regionalCli, nil, nil, workflow)
	resourcePool := &entity.ResourcePool{
		ResourcePool: &globalngv1.ResourcePool{
			Id:             43,
			OwnerId:        1001,
			OwnerType:      globalngv1.ResourcePool_ORGANIZATION,
			ServicePlan:    globalcommonv1.ServicePlan_BYOC,
			VcpusLimit:     80,
			LifecycleState: globalngv1.ResourcePool_ACTIVE,
		},
		RegionId: 23,
	}
	region := &entity.Region{Region: &globalcommonv1.Region{Id: globalcommonv1.Region_AWS_US_WEST_2}}

	expectTikvScaleState(resourcePoolRepo, regionRepo, regionalCli, workflow, resourcePool, region, false)
	resourcePoolRepo.EXPECT().LoadForUpdate(gomock.Any(), uint64(43), false).Return(resourcePool, nil)
	workflow.EXPECT().ListRunningDagInstances(
		gomock.Any(),
		[]string{workflowcommon.ScaleByocSharedComponents},
		"43",
	).Return(runningScaleWorkflow("43"), nil)

	resp, err := svc.ScaleByocSharedComponents(t.Context(), &globalngv1.ScaleByocSharedComponentsRequest{
		ResourcePoolId: 43,
		OrganizationId: 1001,
		Tikv: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(4),
		},
	})
	g.Expect(resp).To(gomega.BeNil())
	g.Expect(err).To(gomega.HaveOccurred())
	g.Expect(err.Error()).To(gomega.ContainSubstring("scale operation already in progress for resource pool 43"))
}

func runningScaleWorkflow(resourcePoolID string) []*ffentity.DagInstance {
	return []*ffentity.DagInstance{
		{
			Tags: []ffentity.DagInstanceTag{
				{Key: utilworkflow.BizIdKey, Value: resourcePoolID},
			},
		},
	}
}

func expectTikvScaleState(
	resourcePoolRepo *mock_resourcepool.MockRepo,
	regionRepo *mock_region.MockRepo,
	regionalCli *mock_regionalserver.MockClient,
	workflow *mockworkflow.MockWorkflow,
	resourcePool *entity.ResourcePool,
	region *entity.Region,
	modifying bool,
) {
	resourcePoolID := strconv.FormatUint(resourcePool.GetId(), 10)
	resourcePoolRepo.EXPECT().Load(gomock.Any(), resourcePool.GetId(), false).Return(resourcePool, nil)
	regionRepo.EXPECT().Get(gomock.Any(), resourcePool.RegionId).Return(region, nil)
	regionalCli.EXPECT().GetSharedPool(gomock.Any(), region, &clusterngv1beta.GetSharedPoolRequest{
		SharedPoolId: resourcePoolID,
	}).Return(&clusterngv1beta.SharedPool{
		Phase: clusterngv1beta.SharedPool_AVAILABLE,
		ComponentGroups: []*clusterngv1beta.Component{
			{
				Id:            "cg-tikv",
				ComponentType: clusterngv1beta.ComponentType_TIKV,
				NodeQuota: &clusterngv1beta.NodeQuota{
					NodeCount: 3,
					NodeSpec: &clusterngv1beta.NodeSpec{
						Cpu:      4,
						MemoryGi: 16,
					},
				},
			},
		},
	}, nil)
	runningWorkflows := []*ffentity.DagInstance(nil)
	if modifying {
		runningWorkflows = runningScaleWorkflow(resourcePoolID)
	}
	workflow.EXPECT().ListRunningDagInstances(
		gomock.Any(),
		[]string{workflowcommon.ScaleByocSharedComponents},
		resourcePoolID,
	).Return(runningWorkflows, nil)
	regionalCli.EXPECT().GetSharedPoolScaleConstraints(gomock.Any(), region, &clusterngv1beta.GetSharedPoolScaleConstraintsRequest{
		SharedPoolId: resourcePoolID,
	}).Return(&clusterngv1beta.GetSharedPoolScaleConstraintsResponse{
		Components: []*clusterngv1beta.SharedPoolScaleConstraintComponent{
			{
				ComponentType: clusterngv1beta.ComponentType_TIKV,
				NodeCountConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeCountConstraints{
					Current:        3,
					Min:            1,
					Max:            10,
					AllowCrossZero: false,
				},
				NodeSpecConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeSpecConstraints{
					Candidates: []*clusterngv1beta.SharedPoolScaleConstraintNodeSpecCandidate{
						{
							Name:      "4c16g",
							Cpu:       4,
							MemoryGi:  16,
							Available: true,
							IsCurrent: true,
						},
					},
				},
			},
		},
	}, nil)
	regionalCli.EXPECT().GetSharedPoolVcpusUsage(gomock.Any(), region, &clusterngv1beta.GetSharedPoolVcpusUsageRequest{
		SharedPoolId: resourcePoolID,
	}).Return(&clusterngv1beta.GetSharedPoolVcpusUsageResponse{VcpusUsed: 12}, nil)
}

func TestScaleByocSharedComponents_AllowsTiflashScaleFromZeroWhenComponentExists(t *testing.T) {
	g := gomega.NewWithT(t)
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	resourcePoolRepo := mock_resourcepool.NewMockRepo(ctrl)
	regionRepo := mock_region.NewMockRepo(ctrl)
	regionalCli := mock_regionalserver.NewMockClient(ctrl)

	svc := NewService(regionRepo, nil, nil, resourcePoolRepo, nil, nil, regionalCli, nil, nil, noRunningWorkflow{})
	resourcePool := &entity.ResourcePool{
		ResourcePool: &globalngv1.ResourcePool{
			Id:             24,
			OwnerId:        1001,
			OwnerType:      globalngv1.ResourcePool_ORGANIZATION,
			ServicePlan:    globalcommonv1.ServicePlan_BYOC,
			VcpusLimit:     80,
			LifecycleState: globalngv1.ResourcePool_ACTIVE,
		},
		RegionId: 12,
	}
	region := &entity.Region{Region: &globalcommonv1.Region{Id: globalcommonv1.Region_AWS_US_WEST_2}}

	resourcePoolRepo.EXPECT().Load(gomock.Any(), uint64(24), false).Return(resourcePool, nil)
	regionRepo.EXPECT().Get(gomock.Any(), uint64(12)).Return(region, nil)
	regionalCli.EXPECT().GetSharedPool(gomock.Any(), region, &clusterngv1beta.GetSharedPoolRequest{
		SharedPoolId: "24",
	}).Return(&clusterngv1beta.SharedPool{
		ComponentGroups: []*clusterngv1beta.Component{
			{
				Id:            "cg-tiflash-write",
				ComponentType: clusterngv1beta.ComponentType_TIFLASH_WRITE,
				NodeQuota: &clusterngv1beta.NodeQuota{
					NodeCount: 0,
					NodeSpec: &clusterngv1beta.NodeSpec{
						Cpu:      8,
						MemoryGi: 32,
					},
				},
			},
		},
	}, nil)
	regionalCli.EXPECT().GetSharedPoolScaleConstraints(gomock.Any(), region, &clusterngv1beta.GetSharedPoolScaleConstraintsRequest{
		SharedPoolId: "24",
	}).Return(&clusterngv1beta.GetSharedPoolScaleConstraintsResponse{
		Components: []*clusterngv1beta.SharedPoolScaleConstraintComponent{
			{
				ComponentType: clusterngv1beta.ComponentType_TIFLASH_WRITE,
				NodeCountConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeCountConstraints{
					Current:        0,
					Min:            0,
					Max:            10,
					AllowCrossZero: true,
				},
				NodeSpecConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeSpecConstraints{
					Candidates: []*clusterngv1beta.SharedPoolScaleConstraintNodeSpecCandidate{
						{
							Name:      "8c32g",
							Cpu:       8,
							MemoryGi:  32,
							Available: true,
							IsCurrent: true,
						},
					},
				},
			},
		},
	}, nil)
	regionalCli.EXPECT().GetSharedPoolVcpusUsage(gomock.Any(), region, &clusterngv1beta.GetSharedPoolVcpusUsageRequest{
		SharedPoolId: "24",
	}).Return(&clusterngv1beta.GetSharedPoolVcpusUsageResponse{VcpusUsed: 0}, nil)

	resp, err := svc.ScaleByocSharedComponents(t.Context(), &globalngv1.ScaleByocSharedComponentsRequest{
		ResourcePoolId: 24,
		OrganizationId: 1001,
		TiflashWrite: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(2),
		},
		ValidateOnly: true,
	})
	g.Expect(err).ToNot(gomega.HaveOccurred())
	g.Expect(resp.GetPreview().GetAllowed()).To(gomega.BeTrue())
	g.Expect(resp.GetComponents()[1].GetAllowed()).To(gomega.BeTrue())
	g.Expect(resp.GetComponents()[1].GetComponentId()).To(gomega.Equal("cg-tiflash-write"))
}

func TestScaleByocSharedComponents_AllowsTiflashScaleToZeroInPreview(t *testing.T) {
	g := gomega.NewWithT(t)
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	resourcePoolRepo := mock_resourcepool.NewMockRepo(ctrl)
	regionRepo := mock_region.NewMockRepo(ctrl)
	regionalCli := mock_regionalserver.NewMockClient(ctrl)

	svc := NewService(regionRepo, nil, nil, resourcePoolRepo, nil, nil, regionalCli, nil, nil, noRunningWorkflow{})
	resourcePool := &entity.ResourcePool{
		ResourcePool: &globalngv1.ResourcePool{
			Id:             30,
			OwnerId:        1001,
			OwnerType:      globalngv1.ResourcePool_ORGANIZATION,
			ServicePlan:    globalcommonv1.ServicePlan_BYOC,
			VcpusLimit:     80,
			LifecycleState: globalngv1.ResourcePool_ACTIVE,
		},
		RegionId: 18,
	}
	region := &entity.Region{Region: &globalcommonv1.Region{Id: globalcommonv1.Region_AWS_US_WEST_2}}

	resourcePoolRepo.EXPECT().Load(gomock.Any(), uint64(30), false).Return(resourcePool, nil)
	regionRepo.EXPECT().Get(gomock.Any(), uint64(18)).Return(region, nil)
	regionalCli.EXPECT().GetSharedPool(gomock.Any(), region, &clusterngv1beta.GetSharedPoolRequest{
		SharedPoolId: "30",
	}).Return(&clusterngv1beta.SharedPool{
		ComponentGroups: []*clusterngv1beta.Component{
			{
				Id:            "cg-tiflash-compute",
				ComponentType: clusterngv1beta.ComponentType_TIFLASH_COMPUTE,
				NodeQuota: &clusterngv1beta.NodeQuota{
					NodeCount: 3,
					NodeSpec: &clusterngv1beta.NodeSpec{
						Cpu:      8,
						MemoryGi: 32,
					},
				},
			},
		},
	}, nil)
	regionalCli.EXPECT().GetSharedPoolScaleConstraints(gomock.Any(), region, &clusterngv1beta.GetSharedPoolScaleConstraintsRequest{
		SharedPoolId: "30",
	}).Return(&clusterngv1beta.GetSharedPoolScaleConstraintsResponse{
		Components: []*clusterngv1beta.SharedPoolScaleConstraintComponent{
			{
				ComponentType: clusterngv1beta.ComponentType_TIFLASH_COMPUTE,
				NodeCountConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeCountConstraints{
					Current:        3,
					Min:            0,
					Max:            10,
					AllowCrossZero: true,
				},
				NodeSpecConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeSpecConstraints{
					Candidates: []*clusterngv1beta.SharedPoolScaleConstraintNodeSpecCandidate{
						{
							Name:      "8c32g",
							Cpu:       8,
							MemoryGi:  32,
							Available: true,
							IsCurrent: true,
						},
					},
				},
			},
		},
	}, nil)
	regionalCli.EXPECT().GetSharedPoolVcpusUsage(gomock.Any(), region, &clusterngv1beta.GetSharedPoolVcpusUsageRequest{
		SharedPoolId: "30",
	}).Return(&clusterngv1beta.GetSharedPoolVcpusUsageResponse{VcpusUsed: 24}, nil)

	resp, err := svc.ScaleByocSharedComponents(t.Context(), &globalngv1.ScaleByocSharedComponentsRequest{
		ResourcePoolId: 30,
		OrganizationId: 1001,
		TiflashCompute: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(0),
		},
		ValidateOnly: true,
	})
	g.Expect(err).ToNot(gomega.HaveOccurred())
	g.Expect(resp.GetPreview().GetCurrentVcpusUsed()).To(gomega.Equal(uint64(24)))
	g.Expect(resp.GetPreview().GetScalingVcpusUsed()).To(gomega.Equal(uint64(0)))
	g.Expect(resp.GetPreview().GetAllowed()).To(gomega.BeTrue())
	g.Expect(resp.GetPreview().GetBlockingReasons()).ToNot(gomega.ContainElement(workflowcommon.ByocSharedComponentScaleInBlockingReason))
	g.Expect(resp.GetComponents()[2].GetAllowed()).To(gomega.BeTrue())
	g.Expect(resp.GetComponents()[2].GetComponentId()).To(gomega.Equal("cg-tiflash-compute"))
}

func TestScaleByocSharedComponents_AllowsTiflashWriteScaleToZeroInPreview(t *testing.T) {
	g := gomega.NewWithT(t)
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	resourcePoolRepo := mock_resourcepool.NewMockRepo(ctrl)
	regionRepo := mock_region.NewMockRepo(ctrl)
	regionalCli := mock_regionalserver.NewMockClient(ctrl)

	svc := NewService(regionRepo, nil, nil, resourcePoolRepo, nil, nil, regionalCli, nil, nil, noRunningWorkflow{})
	resourcePool := &entity.ResourcePool{
		ResourcePool: &globalngv1.ResourcePool{
			Id:             34,
			OwnerId:        1001,
			OwnerType:      globalngv1.ResourcePool_ORGANIZATION,
			ServicePlan:    globalcommonv1.ServicePlan_BYOC,
			VcpusLimit:     80,
			LifecycleState: globalngv1.ResourcePool_ACTIVE,
		},
		RegionId: 19,
	}
	region := &entity.Region{Region: &globalcommonv1.Region{Id: globalcommonv1.Region_AWS_US_WEST_2}}

	resourcePoolRepo.EXPECT().Load(gomock.Any(), uint64(34), false).Return(resourcePool, nil)
	regionRepo.EXPECT().Get(gomock.Any(), uint64(19)).Return(region, nil)
	regionalCli.EXPECT().GetSharedPool(gomock.Any(), region, &clusterngv1beta.GetSharedPoolRequest{
		SharedPoolId: "34",
	}).Return(&clusterngv1beta.SharedPool{
		ComponentGroups: []*clusterngv1beta.Component{
			{
				Id:            "cg-tiflash-write",
				ComponentType: clusterngv1beta.ComponentType_TIFLASH_WRITE,
				NodeQuota: &clusterngv1beta.NodeQuota{
					NodeCount: 2,
					NodeSpec: &clusterngv1beta.NodeSpec{
						Cpu:      8,
						MemoryGi: 32,
					},
				},
			},
		},
	}, nil)
	regionalCli.EXPECT().GetSharedPoolScaleConstraints(gomock.Any(), region, &clusterngv1beta.GetSharedPoolScaleConstraintsRequest{
		SharedPoolId: "34",
	}).Return(&clusterngv1beta.GetSharedPoolScaleConstraintsResponse{
		Components: []*clusterngv1beta.SharedPoolScaleConstraintComponent{
			{
				ComponentType: clusterngv1beta.ComponentType_TIFLASH_WRITE,
				NodeCountConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeCountConstraints{
					Current:        2,
					Min:            0,
					Max:            10,
					AllowCrossZero: true,
				},
				NodeSpecConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeSpecConstraints{
					Candidates: []*clusterngv1beta.SharedPoolScaleConstraintNodeSpecCandidate{
						{
							Name:      "8c32g",
							Cpu:       8,
							MemoryGi:  32,
							Available: true,
							IsCurrent: true,
						},
					},
				},
			},
		},
	}, nil)
	regionalCli.EXPECT().GetSharedPoolVcpusUsage(gomock.Any(), region, &clusterngv1beta.GetSharedPoolVcpusUsageRequest{
		SharedPoolId: "34",
	}).Return(&clusterngv1beta.GetSharedPoolVcpusUsageResponse{VcpusUsed: 16}, nil)

	resp, err := svc.ScaleByocSharedComponents(t.Context(), &globalngv1.ScaleByocSharedComponentsRequest{
		ResourcePoolId: 34,
		OrganizationId: 1001,
		TiflashWrite: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(0),
		},
		ValidateOnly: true,
	})
	g.Expect(err).ToNot(gomega.HaveOccurred())
	g.Expect(resp.GetPreview().GetScalingVcpusUsed()).To(gomega.Equal(uint64(0)))
	g.Expect(resp.GetPreview().GetAllowed()).To(gomega.BeTrue())
	g.Expect(resp.GetPreview().GetBlockingReasons()).ToNot(gomega.ContainElement(workflowcommon.ByocSharedComponentScaleInBlockingReason))
	g.Expect(resp.GetComponents()[1].GetAllowed()).To(gomega.BeTrue())
	g.Expect(resp.GetComponents()[1].GetComponentId()).To(gomega.Equal("cg-tiflash-write"))
}

func TestGetByocSharedComponentScaleConstraints_ReportsTikvScaleFromZeroBlock(t *testing.T) {
	g := gomega.NewWithT(t)
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	resourcePoolRepo := mock_resourcepool.NewMockRepo(ctrl)
	regionRepo := mock_region.NewMockRepo(ctrl)
	regionalCli := mock_regionalserver.NewMockClient(ctrl)

	svc := NewService(regionRepo, nil, nil, resourcePoolRepo, nil, nil, regionalCli, nil, nil, noRunningWorkflow{})
	resourcePool := &entity.ResourcePool{
		ResourcePool: &globalngv1.ResourcePool{
			Id:             22,
			OwnerId:        1001,
			OwnerType:      globalngv1.ResourcePool_ORGANIZATION,
			ServicePlan:    globalcommonv1.ServicePlan_BYOC,
			VcpusLimit:     80,
			LifecycleState: globalngv1.ResourcePool_ACTIVE,
		},
		RegionId: 10,
	}
	region := &entity.Region{Region: &globalcommonv1.Region{Id: globalcommonv1.Region_AWS_US_WEST_2}}

	resourcePoolRepo.EXPECT().Load(gomock.Any(), uint64(22), false).Return(resourcePool, nil)
	regionRepo.EXPECT().Get(gomock.Any(), uint64(10)).Return(region, nil)
	regionalCli.EXPECT().GetSharedPool(gomock.Any(), region, &clusterngv1beta.GetSharedPoolRequest{
		SharedPoolId: "22",
	}).Return(&clusterngv1beta.SharedPool{}, nil)
	regionalCli.EXPECT().GetSharedPoolScaleConstraints(gomock.Any(), region, &clusterngv1beta.GetSharedPoolScaleConstraintsRequest{
		SharedPoolId: "22",
	}).Return(&clusterngv1beta.GetSharedPoolScaleConstraintsResponse{}, nil)
	regionalCli.EXPECT().GetSharedPoolVcpusUsage(gomock.Any(), region, &clusterngv1beta.GetSharedPoolVcpusUsageRequest{
		SharedPoolId: "22",
	}).Return(&clusterngv1beta.GetSharedPoolVcpusUsageResponse{}, nil)

	resp, err := svc.GetByocSharedComponentScaleConstraints(t.Context(), &globalngv1.GetByocSharedComponentScaleConstraintsRequest{
		ResourcePoolId: 22,
		OrganizationId: 1001,
	})
	g.Expect(err).ToNot(gomega.HaveOccurred())
	g.Expect(resp.GetPreview().GetAllowed()).To(gomega.BeFalse())
	g.Expect(resp.GetPreview().GetBlockingReasons()).To(gomega.ContainElement("tikv scale-from-zero is not supported"))
	g.Expect(resp.GetPreview().GetBlockingReasons()).To(gomega.ContainElement("tiflash_write component is not provisioned in the shared pool"))
	g.Expect(resp.GetPreview().GetBlockingReasons()).To(gomega.ContainElement("tiflash_compute component is not provisioned in the shared pool"))
	g.Expect(resp.GetComponents()).To(gomega.HaveLen(3))
	g.Expect(resp.GetComponents()[0].GetComponentType()).To(gomega.Equal(globalngv1.ByocSharedComponentType_BYOC_SHARED_COMPONENT_TYPE_TIKV))
	g.Expect(resp.GetComponents()[0].GetBlockingReasons()).To(gomega.ContainElement(
		gomega.WithTransform(func(in *globalngv1.ByocSharedComponentScaleConstraintReason) string {
			if in == nil {
				return ""
			}
			return in.GetMessage()
		}, gomega.Equal("tikv scale-from-zero is not supported")),
	))
	g.Expect(resp.GetComponents()[1].GetComponentType()).To(gomega.Equal(globalngv1.ByocSharedComponentType_BYOC_SHARED_COMPONENT_TYPE_TIFLASH_WRITE))
	g.Expect(resp.GetComponents()[1].GetExists()).To(gomega.BeFalse())
	g.Expect(resp.GetComponents()[1].GetBlockingReasons()).To(gomega.ContainElement(
		gomega.WithTransform(func(in *globalngv1.ByocSharedComponentScaleConstraintReason) string {
			if in == nil {
				return ""
			}
			return in.GetMessage()
		}, gomega.Equal("tiflash_write component is not provisioned in the shared pool")),
	))
	g.Expect(resp.GetComponents()[2].GetComponentType()).To(
		gomega.Equal(globalngv1.ByocSharedComponentType_BYOC_SHARED_COMPONENT_TYPE_TIFLASH_COMPUTE),
	)
	g.Expect(resp.GetComponents()[2].GetExists()).To(gomega.BeFalse())
	g.Expect(resp.GetComponents()[2].GetBlockingReasons()).To(gomega.ContainElement(
		gomega.WithTransform(func(in *globalngv1.ByocSharedComponentScaleConstraintReason) string {
			if in == nil {
				return ""
			}
			return in.GetMessage()
		}, gomega.Equal("tiflash_compute component is not provisioned in the shared pool")),
	))
}

func TestScaleByocSharedComponents_RejectsNoopRequest(t *testing.T) {
	g := gomega.NewWithT(t)
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	resourcePoolRepo := mock_resourcepool.NewMockRepo(ctrl)
	regionRepo := mock_region.NewMockRepo(ctrl)
	regionalCli := mock_regionalserver.NewMockClient(ctrl)

	svc := NewService(regionRepo, nil, nil, resourcePoolRepo, nil, nil, regionalCli, nil, nil, noRunningWorkflow{})
	resourcePool := &entity.ResourcePool{
		ResourcePool: &globalngv1.ResourcePool{
			Id:             23,
			OwnerId:        1001,
			OwnerType:      globalngv1.ResourcePool_ORGANIZATION,
			ServicePlan:    globalcommonv1.ServicePlan_BYOC,
			VcpusLimit:     80,
			LifecycleState: globalngv1.ResourcePool_ACTIVE,
		},
		RegionId: 11,
	}
	region := &entity.Region{Region: &globalcommonv1.Region{Id: globalcommonv1.Region_AWS_US_WEST_2}}

	resourcePoolRepo.EXPECT().Load(gomock.Any(), uint64(23), false).Return(resourcePool, nil)
	regionRepo.EXPECT().Get(gomock.Any(), uint64(11)).Return(region, nil)
	regionalCli.EXPECT().GetSharedPool(gomock.Any(), region, &clusterngv1beta.GetSharedPoolRequest{
		SharedPoolId: "23",
	}).Return(&clusterngv1beta.SharedPool{
		ComponentGroups: []*clusterngv1beta.Component{
			{
				Id:            "cg-tikv",
				ComponentType: clusterngv1beta.ComponentType_TIKV,
				NodeQuota: &clusterngv1beta.NodeQuota{
					NodeCount: 3,
					NodeSpec: &clusterngv1beta.NodeSpec{
						Cpu:      4,
						MemoryGi: 16,
					},
				},
			},
		},
	}, nil)
	regionalCli.EXPECT().GetSharedPoolScaleConstraints(gomock.Any(), region, &clusterngv1beta.GetSharedPoolScaleConstraintsRequest{
		SharedPoolId: "23",
	}).Return(&clusterngv1beta.GetSharedPoolScaleConstraintsResponse{
		Components: []*clusterngv1beta.SharedPoolScaleConstraintComponent{
			{
				ComponentType: clusterngv1beta.ComponentType_TIKV,
				NodeCountConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeCountConstraints{
					Current:        3,
					Min:            1,
					Max:            10,
					AllowCrossZero: false,
				},
				NodeSpecConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeSpecConstraints{
					Candidates: []*clusterngv1beta.SharedPoolScaleConstraintNodeSpecCandidate{
						{
							Name:      "4c16g",
							Cpu:       4,
							MemoryGi:  16,
							Available: true,
							IsCurrent: true,
						},
					},
				},
			},
		},
	}, nil)
	regionalCli.EXPECT().GetSharedPoolVcpusUsage(gomock.Any(), region, &clusterngv1beta.GetSharedPoolVcpusUsageRequest{
		SharedPoolId: "23",
	}).Return(&clusterngv1beta.GetSharedPoolVcpusUsageResponse{VcpusUsed: 12}, nil)

	resp, err := svc.ScaleByocSharedComponents(t.Context(), &globalngv1.ScaleByocSharedComponentsRequest{
		ResourcePoolId: 23,
		OrganizationId: 1001,
		Tikv: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(3),
		},
		ValidateOnly: true,
	})
	g.Expect(resp).To(gomega.BeNil())
	g.Expect(err).To(gomega.HaveOccurred())
	g.Expect(err.Error()).To(gomega.ContainSubstring("no effective changes"))
}

func TestValidateScaleRequestTargets_RejectsInvalidStorageNames(t *testing.T) {
	g := gomega.NewWithT(t)

	err := validateScaleRequestTargets(map[globalngv1.ByocSharedComponentType]*globalngv1.ByocSharedComponentScaleTarget{
		globalngv1.ByocSharedComponentType_BYOC_SHARED_COMPONENT_TYPE_TIFLASH_WRITE: {
			Storages: []*globalngv1.ByocSharedComponentStorage{
				{StorageName: "DATA", SizeGi: proto.Uint32(200)},
				{StorageName: "data", SizeGi: proto.Uint32(300)},
			},
		},
	})
	g.Expect(err).To(gomega.HaveOccurred())
	g.Expect(err.Error()).To(gomega.ContainSubstring("duplicate storage_name data"))

	err = validateScaleRequestTargets(map[globalngv1.ByocSharedComponentType]*globalngv1.ByocSharedComponentScaleTarget{
		globalngv1.ByocSharedComponentType_BYOC_SHARED_COMPONENT_TYPE_TIFLASH_WRITE: {
			Storages: []*globalngv1.ByocSharedComponentStorage{
				{StorageName: " ", SizeGi: proto.Uint32(200)},
			},
		},
	})
	g.Expect(err).To(gomega.HaveOccurred())
	g.Expect(err.Error()).To(gomega.ContainSubstring("storage_name is required"))
}

func TestScaleByocSharedComponents_PreviewUsesWholePoolUsageDelta(t *testing.T) {
	g := gomega.NewWithT(t)
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	resourcePoolRepo := mock_resourcepool.NewMockRepo(ctrl)
	regionRepo := mock_region.NewMockRepo(ctrl)
	regionalCli := mock_regionalserver.NewMockClient(ctrl)

	svc := NewService(regionRepo, nil, nil, resourcePoolRepo, nil, nil, regionalCli, nil, nil, noRunningWorkflow{})
	resourcePool := &entity.ResourcePool{
		ResourcePool: &globalngv1.ResourcePool{
			Id:             25,
			OwnerId:        1001,
			OwnerType:      globalngv1.ResourcePool_ORGANIZATION,
			ServicePlan:    globalcommonv1.ServicePlan_BYOC,
			VcpusLimit:     20,
			LifecycleState: globalngv1.ResourcePool_ACTIVE,
		},
		RegionId: 13,
	}
	region := &entity.Region{Region: &globalcommonv1.Region{Id: globalcommonv1.Region_AWS_US_WEST_2}}

	resourcePoolRepo.EXPECT().Load(gomock.Any(), uint64(25), false).Return(resourcePool, nil)
	regionRepo.EXPECT().Get(gomock.Any(), uint64(13)).Return(region, nil)
	regionalCli.EXPECT().GetSharedPool(gomock.Any(), region, &clusterngv1beta.GetSharedPoolRequest{
		SharedPoolId: "25",
	}).Return(&clusterngv1beta.SharedPool{
		ComponentGroups: []*clusterngv1beta.Component{
			{
				Id:            "cg-tikv",
				ComponentType: clusterngv1beta.ComponentType_TIKV,
				NodeQuota: &clusterngv1beta.NodeQuota{
					NodeCount: 3,
					NodeSpec: &clusterngv1beta.NodeSpec{
						Cpu:      4,
						MemoryGi: 16,
					},
				},
			},
		},
	}, nil)
	regionalCli.EXPECT().GetSharedPoolScaleConstraints(gomock.Any(), region, &clusterngv1beta.GetSharedPoolScaleConstraintsRequest{
		SharedPoolId: "25",
	}).Return(&clusterngv1beta.GetSharedPoolScaleConstraintsResponse{
		Components: []*clusterngv1beta.SharedPoolScaleConstraintComponent{
			{
				ComponentType: clusterngv1beta.ComponentType_TIKV,
				NodeCountConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeCountConstraints{
					Current:        3,
					Min:            1,
					Max:            10,
					AllowCrossZero: false,
				},
				NodeSpecConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeSpecConstraints{
					Candidates: []*clusterngv1beta.SharedPoolScaleConstraintNodeSpecCandidate{
						{
							Name:      "4c16g",
							Cpu:       4,
							MemoryGi:  16,
							Available: true,
							IsCurrent: true,
						},
					},
				},
			},
		},
	}, nil)
	regionalCli.EXPECT().GetSharedPoolVcpusUsage(gomock.Any(), region, &clusterngv1beta.GetSharedPoolVcpusUsageRequest{
		SharedPoolId: "25",
	}).Return(&clusterngv1beta.GetSharedPoolVcpusUsageResponse{VcpusUsed: 19}, nil)

	resp, err := svc.ScaleByocSharedComponents(t.Context(), &globalngv1.ScaleByocSharedComponentsRequest{
		ResourcePoolId: 25,
		OrganizationId: 1001,
		Tikv: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(4),
		},
		ValidateOnly: true,
	})
	g.Expect(err).ToNot(gomega.HaveOccurred())
	g.Expect(resp.GetPreview().GetCurrentVcpusUsed()).To(gomega.Equal(uint64(19)))
	g.Expect(resp.GetPreview().GetScalingVcpusUsed()).To(gomega.Equal(uint64(23)))
	g.Expect(resp.GetPreview().GetAllowed()).To(gomega.BeFalse())
	g.Expect(resp.GetPreview().GetBlockingReasons()).To(gomega.ContainElement(workflowcommon.ByocSharedComponentScaleVcpusLimitBlockingReason))
}

func TestScaleByocSharedComponents_ZeroVcpusLimitMeansNoUpperLimit(t *testing.T) {
	g := gomega.NewWithT(t)
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	resourcePoolRepo := mock_resourcepool.NewMockRepo(ctrl)
	regionRepo := mock_region.NewMockRepo(ctrl)
	regionalCli := mock_regionalserver.NewMockClient(ctrl)

	svc := NewService(regionRepo, nil, nil, resourcePoolRepo, nil, nil, regionalCli, nil, nil, noRunningWorkflow{})
	resourcePool := &entity.ResourcePool{
		ResourcePool: &globalngv1.ResourcePool{
			Id:             26,
			OwnerId:        1001,
			OwnerType:      globalngv1.ResourcePool_ORGANIZATION,
			ServicePlan:    globalcommonv1.ServicePlan_BYOC,
			VcpusLimit:     0,
			LifecycleState: globalngv1.ResourcePool_ACTIVE,
		},
		RegionId: 14,
	}
	region := &entity.Region{Region: &globalcommonv1.Region{Id: globalcommonv1.Region_AWS_US_WEST_2}}

	resourcePoolRepo.EXPECT().Load(gomock.Any(), uint64(26), false).Return(resourcePool, nil)
	regionRepo.EXPECT().Get(gomock.Any(), uint64(14)).Return(region, nil)
	regionalCli.EXPECT().GetSharedPool(gomock.Any(), region, &clusterngv1beta.GetSharedPoolRequest{
		SharedPoolId: "26",
	}).Return(&clusterngv1beta.SharedPool{
		ComponentGroups: []*clusterngv1beta.Component{
			{
				Id:            "cg-tikv",
				ComponentType: clusterngv1beta.ComponentType_TIKV,
				NodeQuota: &clusterngv1beta.NodeQuota{
					NodeCount: 3,
					NodeSpec: &clusterngv1beta.NodeSpec{
						Cpu:      4,
						MemoryGi: 16,
					},
				},
			},
		},
	}, nil)
	regionalCli.EXPECT().GetSharedPoolScaleConstraints(gomock.Any(), region, &clusterngv1beta.GetSharedPoolScaleConstraintsRequest{
		SharedPoolId: "26",
	}).Return(&clusterngv1beta.GetSharedPoolScaleConstraintsResponse{
		Components: []*clusterngv1beta.SharedPoolScaleConstraintComponent{
			{
				ComponentType: clusterngv1beta.ComponentType_TIKV,
				NodeCountConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeCountConstraints{
					Current:        3,
					Min:            1,
					Max:            10,
					AllowCrossZero: false,
				},
				NodeSpecConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeSpecConstraints{
					Candidates: []*clusterngv1beta.SharedPoolScaleConstraintNodeSpecCandidate{
						{
							Name:      "4c16g",
							Cpu:       4,
							MemoryGi:  16,
							Available: true,
							IsCurrent: true,
						},
					},
				},
			},
		},
	}, nil)
	regionalCli.EXPECT().GetSharedPoolVcpusUsage(gomock.Any(), region, &clusterngv1beta.GetSharedPoolVcpusUsageRequest{
		SharedPoolId: "26",
	}).Return(&clusterngv1beta.GetSharedPoolVcpusUsageResponse{VcpusUsed: 66}, nil)

	resp, err := svc.ScaleByocSharedComponents(t.Context(), &globalngv1.ScaleByocSharedComponentsRequest{
		ResourcePoolId: 26,
		OrganizationId: 1001,
		Tikv: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(4),
		},
		ValidateOnly: true,
	})
	g.Expect(err).ToNot(gomega.HaveOccurred())
	g.Expect(resp.GetPreview().GetCurrentVcpusUsed()).To(gomega.Equal(uint64(66)))
	g.Expect(resp.GetPreview().GetScalingVcpusUsed()).To(gomega.Equal(uint64(70)))
	g.Expect(resp.GetPreview().GetMaxVcpusLimit()).To(gomega.Equal(uint64(0)))
	g.Expect(resp.GetPreview().GetAllowed()).To(gomega.BeTrue())
	g.Expect(resp.GetPreview().GetBlockingReasons()).ToNot(gomega.ContainElement(workflowcommon.ByocSharedComponentScaleVcpusLimitBlockingReason))
}

func TestScaleByocSharedComponents_MissingComponentStorageRequestReturnsBlockedResponse(t *testing.T) {
	g := gomega.NewWithT(t)
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	resourcePoolRepo := mock_resourcepool.NewMockRepo(ctrl)
	regionRepo := mock_region.NewMockRepo(ctrl)
	regionalCli := mock_regionalserver.NewMockClient(ctrl)

	svc := NewService(regionRepo, nil, nil, resourcePoolRepo, nil, nil, regionalCli, nil, nil, noRunningWorkflow{})
	resourcePool := &entity.ResourcePool{
		ResourcePool: &globalngv1.ResourcePool{
			Id:             29,
			OwnerId:        1001,
			OwnerType:      globalngv1.ResourcePool_ORGANIZATION,
			ServicePlan:    globalcommonv1.ServicePlan_BYOC,
			VcpusLimit:     80,
			LifecycleState: globalngv1.ResourcePool_ACTIVE,
		},
		RegionId: 17,
	}
	region := &entity.Region{Region: &globalcommonv1.Region{Id: globalcommonv1.Region_AWS_US_WEST_2}}

	resourcePoolRepo.EXPECT().Load(gomock.Any(), uint64(29), false).Return(resourcePool, nil)
	regionRepo.EXPECT().Get(gomock.Any(), uint64(17)).Return(region, nil)
	regionalCli.EXPECT().GetSharedPool(gomock.Any(), region, &clusterngv1beta.GetSharedPoolRequest{
		SharedPoolId: "29",
	}).Return(&clusterngv1beta.SharedPool{}, nil)
	regionalCli.EXPECT().GetSharedPoolScaleConstraints(gomock.Any(), region, &clusterngv1beta.GetSharedPoolScaleConstraintsRequest{
		SharedPoolId: "29",
	}).Return(&clusterngv1beta.GetSharedPoolScaleConstraintsResponse{
		Components: []*clusterngv1beta.SharedPoolScaleConstraintComponent{
			{
				ComponentType: clusterngv1beta.ComponentType_TIFLASH_WRITE,
				NodeCountConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeCountConstraints{
					Current:        0,
					Min:            0,
					Max:            10,
					AllowCrossZero: true,
				},
				NodeSpecConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeSpecConstraints{
					Candidates: []*clusterngv1beta.SharedPoolScaleConstraintNodeSpecCandidate{
						{
							Name:      "8c32g",
							Cpu:       8,
							MemoryGi:  32,
							Available: true,
							StorageConstraints: []*clusterngv1beta.SharedPoolScaleConstraintStorageConstraint{
								{
									Name:          clusterngv1beta.Component_Storage_DATA,
									Resizable:     true,
									CurrentSizeGi: 0,
									MaxSizeGi:     proto.Uint32(1000),
								},
							},
						},
					},
				},
			},
		},
	}, nil)
	regionalCli.EXPECT().GetSharedPoolVcpusUsage(gomock.Any(), region, &clusterngv1beta.GetSharedPoolVcpusUsageRequest{
		SharedPoolId: "29",
	}).Return(&clusterngv1beta.GetSharedPoolVcpusUsageResponse{VcpusUsed: 0}, nil)

	resp, err := svc.ScaleByocSharedComponents(t.Context(), &globalngv1.ScaleByocSharedComponentsRequest{
		ResourcePoolId: 29,
		OrganizationId: 1001,
		TiflashWrite: &globalngv1.ByocSharedComponentScaleTarget{
			Cpu:      proto.Uint32(8),
			MemoryGi: proto.Uint32(32),
			Storages: []*globalngv1.ByocSharedComponentStorage{
				{
					StorageName: "DATA",
					SizeGi:      proto.Uint32(200),
				},
			},
		},
		ValidateOnly: true,
	})
	g.Expect(err).ToNot(gomega.HaveOccurred())
	g.Expect(resp.GetPreview().GetAllowed()).To(gomega.BeFalse())
	g.Expect(resp.GetPreview().GetBlockingReasons()).To(gomega.ContainElement("tiflash_write component is not provisioned in the shared pool"))
	g.Expect(resp.GetComponents()[1].GetAllowed()).To(gomega.BeFalse())
	g.Expect(resp.GetComponents()[1].GetBlockingReasons()).To(gomega.ContainElement("tiflash_write component is not provisioned in the shared pool"))
}

func TestScaleByocSharedComponents_BlocksNodeCountScaleInInPreview(t *testing.T) {
	g := gomega.NewWithT(t)
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	resourcePoolRepo := mock_resourcepool.NewMockRepo(ctrl)
	regionRepo := mock_region.NewMockRepo(ctrl)
	regionalCli := mock_regionalserver.NewMockClient(ctrl)

	svc := NewService(regionRepo, nil, nil, resourcePoolRepo, nil, nil, regionalCli, nil, nil, noRunningWorkflow{})
	resourcePool := &entity.ResourcePool{
		ResourcePool: &globalngv1.ResourcePool{
			Id:             26,
			OwnerId:        1001,
			OwnerType:      globalngv1.ResourcePool_ORGANIZATION,
			ServicePlan:    globalcommonv1.ServicePlan_BYOC,
			VcpusLimit:     80,
			LifecycleState: globalngv1.ResourcePool_ACTIVE,
		},
		RegionId: 14,
	}
	region := &entity.Region{Region: &globalcommonv1.Region{Id: globalcommonv1.Region_AWS_US_WEST_2}}

	resourcePoolRepo.EXPECT().Load(gomock.Any(), uint64(26), false).Return(resourcePool, nil)
	regionRepo.EXPECT().Get(gomock.Any(), uint64(14)).Return(region, nil)
	regionalCli.EXPECT().GetSharedPool(gomock.Any(), region, &clusterngv1beta.GetSharedPoolRequest{
		SharedPoolId: "26",
	}).Return(&clusterngv1beta.SharedPool{
		ComponentGroups: []*clusterngv1beta.Component{
			{
				Id:            "cg-tiflash-write",
				ComponentType: clusterngv1beta.ComponentType_TIFLASH_WRITE,
				NodeQuota: &clusterngv1beta.NodeQuota{
					NodeCount: 3,
					NodeSpec: &clusterngv1beta.NodeSpec{
						Cpu:      8,
						MemoryGi: 32,
					},
				},
			},
		},
	}, nil)
	regionalCli.EXPECT().GetSharedPoolScaleConstraints(gomock.Any(), region, &clusterngv1beta.GetSharedPoolScaleConstraintsRequest{
		SharedPoolId: "26",
	}).Return(&clusterngv1beta.GetSharedPoolScaleConstraintsResponse{
		Components: []*clusterngv1beta.SharedPoolScaleConstraintComponent{
			{
				ComponentType: clusterngv1beta.ComponentType_TIFLASH_WRITE,
				NodeCountConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeCountConstraints{
					Current:        3,
					Min:            0,
					Max:            10,
					AllowCrossZero: true,
				},
				NodeSpecConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeSpecConstraints{
					Candidates: []*clusterngv1beta.SharedPoolScaleConstraintNodeSpecCandidate{
						{
							Name:      "8c32g",
							Cpu:       8,
							MemoryGi:  32,
							Available: true,
							IsCurrent: true,
						},
						{
							Name:      "4c16g",
							Cpu:       4,
							MemoryGi:  16,
							Available: true,
						},
					},
				},
			},
		},
	}, nil)
	regionalCli.EXPECT().GetSharedPoolVcpusUsage(gomock.Any(), region, &clusterngv1beta.GetSharedPoolVcpusUsageRequest{
		SharedPoolId: "26",
	}).Return(&clusterngv1beta.GetSharedPoolVcpusUsageResponse{VcpusUsed: 24}, nil)

	resp, err := svc.ScaleByocSharedComponents(t.Context(), &globalngv1.ScaleByocSharedComponentsRequest{
		ResourcePoolId: 26,
		OrganizationId: 1001,
		TiflashWrite: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(2),
		},
		ValidateOnly: true,
	})
	g.Expect(err).ToNot(gomega.HaveOccurred())
	g.Expect(resp.GetPreview().GetAllowed()).To(gomega.BeFalse())
	g.Expect(resp.GetPreview().GetBlockingReasons()).To(gomega.ContainElement("scale-in for node_count is not supported in phase 1"))
	g.Expect(resp.GetPreview().GetBlockingReasons()).To(gomega.ContainElement(workflowcommon.ByocSharedComponentScaleInBlockingReason))
	g.Expect(resp.GetComponents()[1].GetAllowed()).To(gomega.BeFalse())
	g.Expect(resp.GetComponents()[1].GetBlockingReasons()).To(gomega.ContainElement("scale-in for node_count is not supported in phase 1"))
}

func TestScaleByocSharedComponents_BlocksNodeSpecScaleInInPreview(t *testing.T) {
	g := gomega.NewWithT(t)
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	resourcePoolRepo := mock_resourcepool.NewMockRepo(ctrl)
	regionRepo := mock_region.NewMockRepo(ctrl)
	regionalCli := mock_regionalserver.NewMockClient(ctrl)

	svc := NewService(regionRepo, nil, nil, resourcePoolRepo, nil, nil, regionalCli, nil, nil, noRunningWorkflow{})
	resourcePool := &entity.ResourcePool{
		ResourcePool: &globalngv1.ResourcePool{
			Id:             27,
			OwnerId:        1001,
			OwnerType:      globalngv1.ResourcePool_ORGANIZATION,
			ServicePlan:    globalcommonv1.ServicePlan_BYOC,
			VcpusLimit:     80,
			LifecycleState: globalngv1.ResourcePool_ACTIVE,
		},
		RegionId: 15,
	}
	region := &entity.Region{Region: &globalcommonv1.Region{Id: globalcommonv1.Region_AWS_US_WEST_2}}

	resourcePoolRepo.EXPECT().Load(gomock.Any(), uint64(27), false).Return(resourcePool, nil)
	regionRepo.EXPECT().Get(gomock.Any(), uint64(15)).Return(region, nil)
	regionalCli.EXPECT().GetSharedPool(gomock.Any(), region, &clusterngv1beta.GetSharedPoolRequest{
		SharedPoolId: "27",
	}).Return(&clusterngv1beta.SharedPool{
		ComponentGroups: []*clusterngv1beta.Component{
			{
				Id:            "cg-tiflash-write",
				ComponentType: clusterngv1beta.ComponentType_TIFLASH_WRITE,
				NodeQuota: &clusterngv1beta.NodeQuota{
					NodeCount: 3,
					NodeSpec: &clusterngv1beta.NodeSpec{
						Cpu:      8,
						MemoryGi: 32,
					},
				},
			},
		},
	}, nil)
	regionalCli.EXPECT().GetSharedPoolScaleConstraints(gomock.Any(), region, &clusterngv1beta.GetSharedPoolScaleConstraintsRequest{
		SharedPoolId: "27",
	}).Return(&clusterngv1beta.GetSharedPoolScaleConstraintsResponse{
		Components: []*clusterngv1beta.SharedPoolScaleConstraintComponent{
			{
				ComponentType: clusterngv1beta.ComponentType_TIFLASH_WRITE,
				NodeCountConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeCountConstraints{
					Current:        3,
					Min:            0,
					Max:            10,
					AllowCrossZero: true,
				},
				NodeSpecConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeSpecConstraints{
					Candidates: []*clusterngv1beta.SharedPoolScaleConstraintNodeSpecCandidate{
						{
							Name:      "8c32g",
							Cpu:       8,
							MemoryGi:  32,
							Available: true,
							IsCurrent: true,
						},
						{
							Name:      "4c16g",
							Cpu:       4,
							MemoryGi:  16,
							Available: true,
						},
					},
				},
			},
		},
	}, nil)
	regionalCli.EXPECT().GetSharedPoolVcpusUsage(gomock.Any(), region, &clusterngv1beta.GetSharedPoolVcpusUsageRequest{
		SharedPoolId: "27",
	}).Return(&clusterngv1beta.GetSharedPoolVcpusUsageResponse{VcpusUsed: 24}, nil)

	resp, err := svc.ScaleByocSharedComponents(t.Context(), &globalngv1.ScaleByocSharedComponentsRequest{
		ResourcePoolId: 27,
		OrganizationId: 1001,
		TiflashWrite: &globalngv1.ByocSharedComponentScaleTarget{
			Cpu: proto.Uint32(4),
		},
		ValidateOnly: true,
	})
	g.Expect(err).ToNot(gomega.HaveOccurred())
	g.Expect(resp.GetPreview().GetAllowed()).To(gomega.BeFalse())
	g.Expect(resp.GetPreview().GetBlockingReasons()).To(gomega.ContainElement("scale-in for component vcpus is not supported in phase 1"))
	g.Expect(resp.GetComponents()[1].GetAllowed()).To(gomega.BeFalse())
	g.Expect(resp.GetComponents()[1].GetBlockingReasons()).To(gomega.ContainElement("scale-in for component vcpus is not supported in phase 1"))
}

func TestScaleByocSharedComponents_BlocksStorageScaleInInPreview(t *testing.T) {
	g := gomega.NewWithT(t)
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	resourcePoolRepo := mock_resourcepool.NewMockRepo(ctrl)
	regionRepo := mock_region.NewMockRepo(ctrl)
	regionalCli := mock_regionalserver.NewMockClient(ctrl)

	svc := NewService(regionRepo, nil, nil, resourcePoolRepo, nil, nil, regionalCli, nil, nil, noRunningWorkflow{})
	resourcePool := &entity.ResourcePool{
		ResourcePool: &globalngv1.ResourcePool{
			Id:             28,
			OwnerId:        1001,
			OwnerType:      globalngv1.ResourcePool_ORGANIZATION,
			ServicePlan:    globalcommonv1.ServicePlan_BYOC,
			VcpusLimit:     80,
			LifecycleState: globalngv1.ResourcePool_ACTIVE,
		},
		RegionId: 16,
	}
	region := &entity.Region{Region: &globalcommonv1.Region{Id: globalcommonv1.Region_AWS_US_WEST_2}}

	resourcePoolRepo.EXPECT().Load(gomock.Any(), uint64(28), false).Return(resourcePool, nil)
	regionRepo.EXPECT().Get(gomock.Any(), uint64(16)).Return(region, nil)
	regionalCli.EXPECT().GetSharedPool(gomock.Any(), region, &clusterngv1beta.GetSharedPoolRequest{
		SharedPoolId: "28",
	}).Return(&clusterngv1beta.SharedPool{
		ComponentGroups: []*clusterngv1beta.Component{
			{
				Id:            "cg-tiflash-write",
				ComponentType: clusterngv1beta.ComponentType_TIFLASH_WRITE,
				NodeQuota: &clusterngv1beta.NodeQuota{
					NodeCount: 3,
					NodeSpec: &clusterngv1beta.NodeSpec{
						Cpu:      8,
						MemoryGi: 32,
					},
				},
				Storages: []*clusterngv1beta.Component_Storage{
					{
						Name:          clusterngv1beta.Component_Storage_DATA,
						SizeGi:        500,
						Iops:          3000,
						ThroughputMbs: 125,
					},
				},
			},
		},
	}, nil)
	regionalCli.EXPECT().GetSharedPoolScaleConstraints(gomock.Any(), region, &clusterngv1beta.GetSharedPoolScaleConstraintsRequest{
		SharedPoolId: "28",
	}).Return(&clusterngv1beta.GetSharedPoolScaleConstraintsResponse{
		Components: []*clusterngv1beta.SharedPoolScaleConstraintComponent{
			{
				ComponentType: clusterngv1beta.ComponentType_TIFLASH_WRITE,
				NodeCountConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeCountConstraints{
					Current:        3,
					Min:            0,
					Max:            10,
					AllowCrossZero: true,
				},
				NodeSpecConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeSpecConstraints{
					Candidates: []*clusterngv1beta.SharedPoolScaleConstraintNodeSpecCandidate{
						{
							Name:      "8c32g",
							Cpu:       8,
							MemoryGi:  32,
							Available: true,
							IsCurrent: true,
							StorageConstraints: []*clusterngv1beta.SharedPoolScaleConstraintStorageConstraint{
								{
									Name:          clusterngv1beta.Component_Storage_DATA,
									Resizable:     true,
									CurrentSizeGi: 500,
									MaxSizeGi:     proto.Uint32(1000),
								},
							},
						},
					},
				},
			},
		},
	}, nil)
	regionalCli.EXPECT().GetSharedPoolVcpusUsage(gomock.Any(), region, &clusterngv1beta.GetSharedPoolVcpusUsageRequest{
		SharedPoolId: "28",
	}).Return(&clusterngv1beta.GetSharedPoolVcpusUsageResponse{VcpusUsed: 24}, nil)

	resp, err := svc.ScaleByocSharedComponents(t.Context(), &globalngv1.ScaleByocSharedComponentsRequest{
		ResourcePoolId: 28,
		OrganizationId: 1001,
		TiflashWrite: &globalngv1.ByocSharedComponentScaleTarget{
			Storages: []*globalngv1.ByocSharedComponentStorage{
				{
					StorageName: "DATA",
					SizeGi:      proto.Uint32(400),
				},
			},
		},
		ValidateOnly: true,
	})
	g.Expect(err).ToNot(gomega.HaveOccurred())
	g.Expect(resp.GetPreview().GetAllowed()).To(gomega.BeFalse())
	g.Expect(resp.GetPreview().GetBlockingReasons()).To(gomega.ContainElement("scale-in for storage DATA size is not supported in phase 1"))
	g.Expect(resp.GetComponents()[1].GetAllowed()).To(gomega.BeFalse())
	g.Expect(resp.GetComponents()[1].GetBlockingReasons()).To(gomega.ContainElement("scale-in for storage DATA size is not supported in phase 1"))
}

func TestValidatePlanComponent_BlocksTikvNodeSpecChanges(t *testing.T) {
	g := gomega.NewWithT(t)

	reasons := validatePlanComponent(&byocSharedPoolPlanComponent{
		componentType: globalngv1.ByocSharedComponentType_BYOC_SHARED_COMPONENT_TYPE_TIKV,
		current: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(3),
			Cpu:       proto.Uint32(4),
			MemoryGi:  proto.Uint32(16),
		},
		target: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(3),
			Cpu:       proto.Uint32(8),
			MemoryGi:  proto.Uint32(32),
		},
		requestTarget: &globalngv1.ByocSharedComponentScaleTarget{
			Cpu:      proto.Uint32(8),
			MemoryGi: proto.Uint32(32),
		},
		requested: true,
		constraintComponent: &clusterngv1beta.SharedPoolScaleConstraintComponent{
			ComponentType: clusterngv1beta.ComponentType_TIKV,
			NodeSpecConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeSpecConstraints{
				Candidates: []*clusterngv1beta.SharedPoolScaleConstraintNodeSpecCandidate{
					{
						Cpu:       8,
						MemoryGi:  32,
						Available: true,
					},
				},
			},
		},
	})

	g.Expect(reasons).To(gomega.ContainElement("tikv node spec changes are not supported"))
}

func TestValidatePlanComponent_BlocksComponentVcpuScaleIn(t *testing.T) {
	g := gomega.NewWithT(t)

	reasons := validatePlanComponent(&byocSharedPoolPlanComponent{
		componentType: globalngv1.ByocSharedComponentType_BYOC_SHARED_COMPONENT_TYPE_TIFLASH_WRITE,
		componentID:   "cg-tiflash-write",
		current: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(4),
			Cpu:       proto.Uint32(8),
			MemoryGi:  proto.Uint32(32),
		},
		target: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(2),
			Cpu:       proto.Uint32(8),
			MemoryGi:  proto.Uint32(32),
		},
		requestTarget: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(2),
		},
		requested: true,
		constraintComponent: &clusterngv1beta.SharedPoolScaleConstraintComponent{
			ComponentType: clusterngv1beta.ComponentType_TIFLASH_WRITE,
			NodeCountConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeCountConstraints{
				Current:        4,
				Min:            1,
				Max:            10,
				AllowCrossZero: true,
			},
			NodeSpecConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeSpecConstraints{
				Candidates: []*clusterngv1beta.SharedPoolScaleConstraintNodeSpecCandidate{
					{
						Cpu:       8,
						MemoryGi:  32,
						Available: true,
						IsCurrent: true,
					},
				},
			},
		},
		targetSpecCandidate: &clusterngv1beta.SharedPoolScaleConstraintNodeSpecCandidate{
			Cpu:       8,
			MemoryGi:  32,
			Available: true,
			IsCurrent: true,
		},
	})

	g.Expect(reasons).To(gomega.ContainElement("scale-in for component vcpus is not supported in phase 1"))
}

func TestValidatePlanComponent_BlocksTiflashScaleToZeroWithSpecChange(t *testing.T) {
	g := gomega.NewWithT(t)

	reasons := validatePlanComponent(&byocSharedPoolPlanComponent{
		componentType: globalngv1.ByocSharedComponentType_BYOC_SHARED_COMPONENT_TYPE_TIFLASH_WRITE,
		componentID:   "cg-tiflash-write",
		current: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(3),
			Cpu:       proto.Uint32(8),
			MemoryGi:  proto.Uint32(32),
		},
		target: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(0),
			Cpu:       proto.Uint32(16),
			MemoryGi:  proto.Uint32(32),
		},
		requestTarget: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(0),
			Cpu:       proto.Uint32(16),
		},
		requested: true,
		constraintComponent: &clusterngv1beta.SharedPoolScaleConstraintComponent{
			ComponentType: clusterngv1beta.ComponentType_TIFLASH_WRITE,
			NodeCountConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeCountConstraints{
				Current:        3,
				Min:            0,
				Max:            10,
				AllowCrossZero: true,
			},
			NodeSpecConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeSpecConstraints{
				Candidates: []*clusterngv1beta.SharedPoolScaleConstraintNodeSpecCandidate{
					{
						Cpu:       16,
						MemoryGi:  32,
						Available: true,
					},
				},
			},
		},
		targetSpecCandidate: &clusterngv1beta.SharedPoolScaleConstraintNodeSpecCandidate{
			Cpu:       8,
			MemoryGi:  32,
			Available: true,
			IsCurrent: true,
		},
	})

	g.Expect(reasons).To(gomega.ContainElement("tiflash scale-to-zero only supports node_count=0"))
	g.Expect(reasons).ToNot(gomega.ContainElement("scale-in for node_count is not supported in phase 1"))
	g.Expect(reasons).ToNot(gomega.ContainElement("scale-in for component vcpus is not supported in phase 1"))
}

func TestValidatePlanComponent_BlocksTikvScaleToZero(t *testing.T) {
	g := gomega.NewWithT(t)

	reasons := validatePlanComponent(&byocSharedPoolPlanComponent{
		componentType: globalngv1.ByocSharedComponentType_BYOC_SHARED_COMPONENT_TYPE_TIKV,
		current: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(3),
			Cpu:       proto.Uint32(4),
			MemoryGi:  proto.Uint32(16),
		},
		target: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(0),
			Cpu:       proto.Uint32(4),
			MemoryGi:  proto.Uint32(16),
		},
		requestTarget: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(0),
		},
		requested: true,
		constraintComponent: &clusterngv1beta.SharedPoolScaleConstraintComponent{
			ComponentType: clusterngv1beta.ComponentType_TIKV,
			NodeCountConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeCountConstraints{
				Current:        3,
				Min:            0,
				Max:            9,
				AllowCrossZero: true,
			},
			NodeSpecConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeSpecConstraints{
				Candidates: []*clusterngv1beta.SharedPoolScaleConstraintNodeSpecCandidate{
					{
						Cpu:       4,
						MemoryGi:  16,
						Available: true,
						IsCurrent: true,
					},
				},
			},
		},
	})

	g.Expect(reasons).To(gomega.ContainElement("scale-in for node_count is not supported in phase 1"))
	g.Expect(reasons).To(gomega.ContainElement("scale-in for component vcpus is not supported in phase 1"))
}

func TestValidatePlanComponent_AllowsTiflashScaleToZeroWithoutSpecChange(t *testing.T) {
	g := gomega.NewWithT(t)

	reasons := validatePlanComponent(&byocSharedPoolPlanComponent{
		componentType: globalngv1.ByocSharedComponentType_BYOC_SHARED_COMPONENT_TYPE_TIFLASH_WRITE,
		componentID:   "cg-tiflash-write",
		current: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(3),
			Cpu:       proto.Uint32(8),
			MemoryGi:  proto.Uint32(32),
		},
		target: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(0),
			Cpu:       proto.Uint32(8),
			MemoryGi:  proto.Uint32(32),
		},
		requestTarget: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(0),
		},
		requested: true,
		constraintComponent: &clusterngv1beta.SharedPoolScaleConstraintComponent{
			ComponentType: clusterngv1beta.ComponentType_TIFLASH_WRITE,
			NodeCountConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeCountConstraints{
				Current:        3,
				Min:            0,
				Max:            10,
				AllowCrossZero: true,
			},
			NodeSpecConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeSpecConstraints{
				Candidates: []*clusterngv1beta.SharedPoolScaleConstraintNodeSpecCandidate{
					{
						Cpu:       8,
						MemoryGi:  32,
						Available: true,
						IsCurrent: true,
					},
				},
			},
		},
		targetSpecCandidate: &clusterngv1beta.SharedPoolScaleConstraintNodeSpecCandidate{
			Cpu:       8,
			MemoryGi:  32,
			Available: true,
			IsCurrent: true,
		},
	})

	g.Expect(reasons).To(gomega.BeEmpty())
}

func TestValidatePlanComponent_AllowsTiflashScaleFromZeroWithSpecAndStorageChange(t *testing.T) {
	g := gomega.NewWithT(t)

	reasons := validatePlanComponent(&byocSharedPoolPlanComponent{
		componentType: globalngv1.ByocSharedComponentType_BYOC_SHARED_COMPONENT_TYPE_TIFLASH_COMPUTE,
		componentID:   "cg-tiflash-compute",
		current: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(0),
			Cpu:       proto.Uint32(8),
			MemoryGi:  proto.Uint32(32),
			Storages: []*globalngv1.ByocSharedComponentStorage{
				{StorageName: "DATA", SizeGi: proto.Uint32(500)},
			},
		},
		target: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(2),
			Cpu:       proto.Uint32(16),
			MemoryGi:  proto.Uint32(64),
			Storages: []*globalngv1.ByocSharedComponentStorage{
				{StorageName: "DATA", SizeGi: proto.Uint32(800)},
			},
		},
		requestTarget: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(2),
			Cpu:       proto.Uint32(16),
			MemoryGi:  proto.Uint32(64),
			Storages: []*globalngv1.ByocSharedComponentStorage{
				{StorageName: "DATA", SizeGi: proto.Uint32(800)},
			},
		},
		requested: true,
		constraintComponent: &clusterngv1beta.SharedPoolScaleConstraintComponent{
			ComponentType: clusterngv1beta.ComponentType_TIFLASH_COMPUTE,
			NodeCountConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeCountConstraints{
				Current:        0,
				Min:            0,
				Max:            10,
				AllowCrossZero: true,
			},
			NodeSpecConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeSpecConstraints{
				Candidates: []*clusterngv1beta.SharedPoolScaleConstraintNodeSpecCandidate{
					{
						Cpu:       16,
						MemoryGi:  64,
						Available: true,
						StorageConstraints: []*clusterngv1beta.SharedPoolScaleConstraintStorageConstraint{
							{
								Name:          clusterngv1beta.Component_Storage_DATA,
								Resizable:     true,
								CurrentSizeGi: 500,
								MaxSizeGi:     proto.Uint32(1000),
							},
						},
					},
				},
			},
		},
		targetSpecCandidate: &clusterngv1beta.SharedPoolScaleConstraintNodeSpecCandidate{
			Cpu:       16,
			MemoryGi:  64,
			Available: true,
			StorageConstraints: []*clusterngv1beta.SharedPoolScaleConstraintStorageConstraint{
				{
					Name:          clusterngv1beta.Component_Storage_DATA,
					Resizable:     true,
					CurrentSizeGi: 500,
					MaxSizeGi:     proto.Uint32(1000),
				},
			},
		},
	})

	g.Expect(reasons).To(gomega.BeEmpty())
}

func TestHasAnyEffectiveScaleChange_RejectsTiflashZeroToZero(t *testing.T) {
	g := gomega.NewWithT(t)

	components := []*byocSharedPoolPlanComponent{
		{
			componentType: globalngv1.ByocSharedComponentType_BYOC_SHARED_COMPONENT_TYPE_TIFLASH_WRITE,
			current: &globalngv1.ByocSharedComponentScaleTarget{
				NodeCount: proto.Uint32(0),
				Cpu:       proto.Uint32(8),
				MemoryGi:  proto.Uint32(32),
			},
			target: &globalngv1.ByocSharedComponentScaleTarget{
				NodeCount: proto.Uint32(0),
				Cpu:       proto.Uint32(8),
				MemoryGi:  proto.Uint32(32),
			},
			requested: true,
		},
	}

	g.Expect(hasAnyEffectiveScaleChange(components)).To(gomega.BeFalse())
}

func TestValidatePlanComponent_AllowsTiflashLowerCpuSpecWhenComponentVcpusDoNotDecrease(t *testing.T) {
	g := gomega.NewWithT(t)

	reasons := validatePlanComponent(&byocSharedPoolPlanComponent{
		componentType: globalngv1.ByocSharedComponentType_BYOC_SHARED_COMPONENT_TYPE_TIFLASH_COMPUTE,
		current: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(3),
			Cpu:       proto.Uint32(8),
			MemoryGi:  proto.Uint32(32),
		},
		target: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(6),
			Cpu:       proto.Uint32(4),
			MemoryGi:  proto.Uint32(32),
		},
		requestTarget: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(6),
			Cpu:       proto.Uint32(4),
			MemoryGi:  proto.Uint32(32),
		},
		requested: true,
		constraintComponent: &clusterngv1beta.SharedPoolScaleConstraintComponent{
			ComponentType: clusterngv1beta.ComponentType_TIFLASH_COMPUTE,
			NodeCountConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeCountConstraints{
				Current:        3,
				Min:            1,
				Max:            10,
				AllowCrossZero: true,
			},
			NodeSpecConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeSpecConstraints{
				Candidates: []*clusterngv1beta.SharedPoolScaleConstraintNodeSpecCandidate{
					{
						Cpu:       4,
						MemoryGi:  32,
						Available: true,
					},
				},
			},
		},
	})

	g.Expect(reasons).ToNot(gomega.ContainElement("scale-in for component vcpus is not supported in phase 1"))
}

func TestValidatePlanComponent_BlocksTikvNonDataStorageAndStoragePerformanceChanges(t *testing.T) {
	g := gomega.NewWithT(t)

	reasons := validatePlanComponent(&byocSharedPoolPlanComponent{
		componentType: globalngv1.ByocSharedComponentType_BYOC_SHARED_COMPONENT_TYPE_TIKV,
		componentID:   "cg-tikv",
		current: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(3),
			Cpu:       proto.Uint32(4),
			MemoryGi:  proto.Uint32(16),
			Storages: []*globalngv1.ByocSharedComponentStorage{
				{
					StorageName: "DATA",
					SizeGi:      proto.Uint32(500),
					Iops:        proto.Uint32(3000),
				},
				{
					StorageName: "RAFTLOG",
					SizeGi:      proto.Uint32(100),
				},
			},
		},
		target: &globalngv1.ByocSharedComponentScaleTarget{
			NodeCount: proto.Uint32(3),
			Cpu:       proto.Uint32(4),
			MemoryGi:  proto.Uint32(16),
			Storages: []*globalngv1.ByocSharedComponentStorage{
				{
					StorageName: "DATA",
					SizeGi:      proto.Uint32(600),
					Iops:        proto.Uint32(4000),
				},
				{
					StorageName: "RAFTLOG",
					SizeGi:      proto.Uint32(200),
				},
			},
		},
		targetSpecCandidate: &clusterngv1beta.SharedPoolScaleConstraintNodeSpecCandidate{
			Cpu:       4,
			MemoryGi:  16,
			Available: true,
			IsCurrent: true,
			StorageConstraints: []*clusterngv1beta.SharedPoolScaleConstraintStorageConstraint{
				{
					Name:          clusterngv1beta.Component_Storage_DATA,
					Resizable:     true,
					CurrentSizeGi: 500,
				},
				{
					Name:          clusterngv1beta.Component_Storage_RAFTLOG,
					Resizable:     true,
					CurrentSizeGi: 100,
				},
			},
		},
		requestTarget: &globalngv1.ByocSharedComponentScaleTarget{
			Storages: []*globalngv1.ByocSharedComponentStorage{
				{
					StorageName: "DATA",
					SizeGi:      proto.Uint32(600),
					Iops:        proto.Uint32(4000),
				},
				{
					StorageName: "RAFTLOG",
					SizeGi:      proto.Uint32(200),
				},
			},
		},
		requested: true,
		constraintComponent: &clusterngv1beta.SharedPoolScaleConstraintComponent{
			ComponentType: clusterngv1beta.ComponentType_TIKV,
			NodeSpecConstraints: &clusterngv1beta.SharedPoolScaleConstraintNodeSpecConstraints{
				Candidates: []*clusterngv1beta.SharedPoolScaleConstraintNodeSpecCandidate{
					{
						Cpu:       4,
						MemoryGi:  16,
						Available: true,
						IsCurrent: true,
						StorageConstraints: []*clusterngv1beta.SharedPoolScaleConstraintStorageConstraint{
							{
								Name:          clusterngv1beta.Component_Storage_DATA,
								Resizable:     true,
								CurrentSizeGi: 500,
							},
							{
								Name:          clusterngv1beta.Component_Storage_RAFTLOG,
								Resizable:     true,
								CurrentSizeGi: 100,
							},
						},
					},
				},
			},
		},
	})

	g.Expect(reasons).To(gomega.ContainElement("storage DATA iops changes are not supported"))
	g.Expect(reasons).To(gomega.ContainElement("tikv only supports resizing DATA storage"))
}

func TestNormalizedSubmissionFieldsDoNotCountAsCapacityChanges(t *testing.T) {
	g := gomega.NewWithT(t)
	current := &globalngv1.ByocSharedComponentScaleTarget{
		NodeCount: proto.Uint32(3),
		Cpu:       proto.Uint32(4),
		MemoryGi:  proto.Uint32(16),
		Storages: []*globalngv1.ByocSharedComponentStorage{
			{StorageName: "DATA", SizeGi: proto.Uint32(500), Iops: proto.Uint32(3000)},
			{StorageName: "RAFTLOG", SizeGi: proto.Uint32(100)},
		},
	}
	normalized := cloneScaleTarget(current)
	normalized.NodeCount = proto.Uint32(6)

	g.Expect(nodeSpecRequestChanges(current, normalized)).To(gomega.BeFalse())
	g.Expect(storageRequestChanges(current.GetStorages()[0], normalized.GetStorages()[0])).To(gomega.BeFalse())
	g.Expect(storageRequestChanges(current.GetStorages()[1], normalized.GetStorages()[1])).To(gomega.BeFalse())
}

func TestNormalizedComputeZeroTargetDoesNotChangeNonCountCapacity(t *testing.T) {
	g := gomega.NewWithT(t)
	current := &globalngv1.ByocSharedComponentScaleTarget{
		NodeCount: proto.Uint32(2),
		Cpu:       proto.Uint32(8),
		MemoryGi:  proto.Uint32(32),
		Storages: []*globalngv1.ByocSharedComponentStorage{
			{StorageName: "DATA", SizeGi: proto.Uint32(500)},
		},
	}
	normalized := cloneScaleTarget(current)
	normalized.NodeCount = proto.Uint32(0)

	g.Expect(scaleToZeroRequestChangesCapacity(current, normalized)).To(gomega.BeFalse())
	normalized.MemoryGi = proto.Uint32(64)
	g.Expect(scaleToZeroRequestChangesCapacity(current, normalized)).To(gomega.BeTrue())
}
