package region

import (
	"context"
	"errors"
	"math"
	"strconv"
	"sync/atomic"
	"testing"
	"time"

	. "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"

	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"
	clusterrepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/cluster"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/cluster/mock_cluster"
	regionrepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/region"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/region/mock_region"
	resourcepoolrepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/resourcepool"
	"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"
	utilworkflow "github.com/tidbcloud/tidb-management-service/utilities/workflow"
	mockworkflow "github.com/tidbcloud/tidb-management-service/utilities/workflow/mock_workflow"
)

type noRunningWorkflow struct {
	utilworkflow.Workflow
}

type emptyClusterRepo struct {
	clusterrepo.Repo
}

func (emptyClusterRepo) Query(context.Context, *clusterrepo.Filter) ([]*entity.Cluster, bizerr.BizError) {
	return []*entity.Cluster{}, nil
}

func (noRunningWorkflow) ListRunningDagInstances(
	context.Context,
	[]string,
	string,
) ([]*ffentity.DagInstance, error) {
	return []*ffentity.DagInstance{}, nil
}

func (noRunningWorkflow) ListRunningDagInstancesByBizIds(
	context.Context,
	[]string,
	[]string,
) ([]*ffentity.DagInstance, error) {
	return []*ffentity.DagInstance{}, nil
}

func (noRunningWorkflow) ListRunningDagInstancesByOrgId(
	context.Context,
	[]string,
	string,
) ([]*ffentity.DagInstance, error) {
	return []*ffentity.DagInstance{}, nil
}

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

	mockRegionRepo := mock_region.NewMockRepo(ctrl)
	mockResourcePoolRepo := mock_resourcepool.NewMockRepo(ctrl)
	mockStrategyRepo := mock_resourcepool.NewMockStrategyRepo(ctrl)
	mockRegionalCli := mock_regionalserver.NewMockClient(ctrl)

	svc := NewService(
		mockRegionRepo,
		emptyClusterRepo{},
		nil,
		mockResourcePoolRepo,
		mockStrategyRepo,
		nil,
		mockRegionalCli,
		nil,
		nil,
		noRunningWorkflow{},
	)

	mockResourcePoolRepo.EXPECT().Query(gomock.Any(), gomock.AssignableToTypeOf(&resourcepoolrepo.Filter{})).
		Return([]*entity.ResourcePool{
			{
				ResourcePool: &globalngv1.ResourcePool{
					Id:          22,
					Name:        " analytics ",
					ProviderId:  globalcommonv1.CloudProvider_AWS,
					RegionId:    globalcommonv1.Region_AWS_US_WEST_2,
					ServicePlan: globalcommonv1.ServicePlan_BYOC,
					DataplaneId: " dp-1 ",
					VcpusLimit:  20,
				},
				RegionId:        101,
				CloudProviderId: 11,
			},
			{
				ResourcePool: &globalngv1.ResourcePool{
					Id:          21,
					Name:        "default",
					ProviderId:  globalcommonv1.CloudProvider_AWS,
					RegionId:    globalcommonv1.Region_AWS_US_WEST_2,
					ServicePlan: globalcommonv1.ServicePlan_BYOC,
					DataplaneId: "dp-1",
					VcpusLimit:  10,
				},
				RegionId:        101,
				CloudProviderId: 11,
			},
			{
				ResourcePool: &globalngv1.ResourcePool{
					Id:          31,
					Name:        "  ",
					ProviderId:  globalcommonv1.CloudProvider_GCP,
					RegionId:    globalcommonv1.Region_GCP_US_EAST1,
					ServicePlan: globalcommonv1.ServicePlan_BYOC,
					DataplaneId: "   ",
					VcpusLimit:  5,
				},
				RegionId:        202,
				CloudProviderId: 22,
			},
		}, int64(3), nil)

	mockRegionRepo.EXPECT().ListRegions(gomock.Any(), gomock.AssignableToTypeOf(regionrepo.Filter{})).
		Return([]*entity.Region{
			{
				Region: &globalcommonv1.Region{
					Id:              globalcommonv1.Region_AWS_US_WEST_2,
					CloudProviderId: globalcommonv1.CloudProvider_AWS,
					DisplayName:     "Oregon",
				},
				RegionID: 101,
			},
			{
				Region: &globalcommonv1.Region{
					Id:              globalcommonv1.Region_GCP_US_EAST1,
					CloudProviderId: globalcommonv1.CloudProvider_GCP,
					DisplayName:     "Virginia",
				},
				RegionID: 202,
			},
		}, nil)

	mockStrategyRepo.EXPECT().Query(gomock.Any(), gomock.AssignableToTypeOf(&resourcepoolrepo.StrategyFilter{})).
		Return([]*entity.ResourcePoolStrategy{
			{
				ResourcePoolStrategy: &globalngv1.ResourcePoolStrategy{
					OrganizationId:        1001,
					DefaultResourcePoolId: 21,
					ProviderId:            globalcommonv1.CloudProvider_AWS,
					RegionId:              globalcommonv1.Region_AWS_US_WEST_2,
					ServicePlan:           globalcommonv1.ServicePlan_BYOC,
				},
				RegionId:        101,
				CloudProviderId: 11,
			},
		}, nil)
	mockRegionalCli.EXPECT().ListSharedPools(
		gomock.Any(),
		gomock.Any(),
		gomock.Any(),
	).DoAndReturn(func(_ any, _ any, req *clusterngv1beta.ListSharedPoolsRequest) (*clusterngv1beta.ListSharedPoolsResponse, bizerr.BizError) {
		sharedPools := make([]*clusterngv1beta.SharedPool, 0, len(req.GetIds()))
		for _, id := range req.GetIds() {
			nodeZones := []string{"us-west-2a", "us-west-2b", "us-west-2c"}
			if id == "31" {
				nodeZones = []string{"us-east1-b"}
			}
			sharedPools = append(sharedPools, &clusterngv1beta.SharedPool{
				Id:    id,
				Phase: clusterngv1beta.SharedPool_AVAILABLE,
				Provider: &clusterngv1beta.Provider{
					NodeZones: nodeZones,
				},
			})
		}
		return &clusterngv1beta.ListSharedPoolsResponse{
			SharedPools: sharedPools,
		}, nil
	}).Times(2)
	mockRegionalCli.EXPECT().GetSharedPoolVcpusUsage(
		gomock.Any(),
		gomock.Any(),
		gomock.Any(),
	).DoAndReturn(func(_ any, _ any, req *clusterngv1beta.GetSharedPoolVcpusUsageRequest) (*clusterngv1beta.GetSharedPoolVcpusUsageResponse, bizerr.BizError) {
		poolID, err := strconv.ParseUint(req.GetSharedPoolId(), 10, 64)
		if err != nil {
			return nil, bizerr.WrapError(err, "failed to parse shared pool id")
		}
		return &clusterngv1beta.GetSharedPoolVcpusUsageResponse{
			SharedPoolId: req.GetSharedPoolId(),
			VcpusUsed:    poolID,
		}, nil
	}).Times(3)

	resp, err := svc.ListByocVcpuPools(t.Context(), &globalngv1.ListByocVcpuPoolsRequest{
		OrganizationId: 1001,
	})
	g.Expect(err).Should(BeNil())
	g.Expect(resp.GetGroups()).Should(HaveLen(2))

	firstGroup := resp.GetGroups()[0]
	g.Expect(firstGroup.GetRegion().GetCloudProviderId()).To(Equal(globalcommonv1.CloudProvider_AWS))
	g.Expect(firstGroup.GetRegion().GetId()).To(Equal(globalcommonv1.Region_AWS_US_WEST_2))
	g.Expect(firstGroup.GetTotalVcpusLimit()).To(Equal(int64(30)))
	g.Expect(firstGroup.GetPools()).To(HaveLen(2))
	g.Expect(firstGroup.GetPools()[0].GetResourcePoolId()).To(Equal(uint64(21)))
	g.Expect(firstGroup.GetPools()[0].GetIsDefault()).To(BeTrue())
	g.Expect(firstGroup.GetPools()[0].GetVcpusUsed()).To(Equal(int64(21)))
	g.Expect(firstGroup.GetPools()[0].GetHighAvailabilityType()).To(Equal(globalngv1.ResourcePool_REGIONAL))
	g.Expect(firstGroup.GetPools()[0].GetZoneConfig().GetNodeZones()).To(Equal([]string{"us-west-2a", "us-west-2b", "us-west-2c"}))
	g.Expect(firstGroup.GetPools()[1].GetName()).To(Equal("analytics"))
	g.Expect(firstGroup.GetPools()[1].GetDataplaneId()).To(Equal("dp-1"))
	g.Expect(firstGroup.GetPools()[1].GetVcpusUsed()).To(Equal(int64(22)))

	secondGroup := resp.GetGroups()[1]
	g.Expect(secondGroup.GetRegion().GetCloudProviderId()).To(Equal(globalcommonv1.CloudProvider_GCP))
	g.Expect(secondGroup.GetPools()).To(HaveLen(1))
	g.Expect(secondGroup.GetPools()[0].GetName()).To(Equal(""))
	g.Expect(secondGroup.GetPools()[0].GetDataplaneId()).To(Equal(""))
	g.Expect(secondGroup.GetPools()[0].GetVcpusUsed()).To(Equal(int64(31)))
	g.Expect(secondGroup.GetPools()[0].GetHighAvailabilityType()).To(Equal(globalngv1.ResourcePool_ZONAL))
	g.Expect(secondGroup.GetPools()[0].GetZoneConfig().GetNodeZones()).To(Equal([]string{"us-east1-b"}))
	g.Expect(secondGroup.GetPools()[0].GetIsDefault()).To(BeFalse())
}

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

	mockRegionRepo := mock_region.NewMockRepo(ctrl)
	mockResourcePoolRepo := mock_resourcepool.NewMockRepo(ctrl)
	mockStrategyRepo := mock_resourcepool.NewMockStrategyRepo(ctrl)
	mockRegionalCli := mock_regionalserver.NewMockClient(ctrl)

	svc := NewService(
		mockRegionRepo,
		emptyClusterRepo{},
		nil,
		mockResourcePoolRepo,
		mockStrategyRepo,
		nil,
		mockRegionalCli,
		nil,
		nil,
		noRunningWorkflow{},
	)

	mockResourcePoolRepo.EXPECT().Query(gomock.Any(), gomock.AssignableToTypeOf(&resourcepoolrepo.Filter{})).
		Return([]*entity.ResourcePool{
			{
				ResourcePool: &globalngv1.ResourcePool{
					Id:          21,
					Name:        "available",
					ProviderId:  globalcommonv1.CloudProvider_AWS,
					RegionId:    globalcommonv1.Region_AWS_US_WEST_2,
					ServicePlan: globalcommonv1.ServicePlan_BYOC,
					DataplaneId: "dp-available",
					VcpusLimit:  10,
				},
				RegionId: 101,
			},
			{
				ResourcePool: &globalngv1.ResourcePool{
					Id:          22,
					Name:        "creating",
					ProviderId:  globalcommonv1.CloudProvider_AWS,
					RegionId:    globalcommonv1.Region_AWS_US_WEST_2,
					ServicePlan: globalcommonv1.ServicePlan_BYOC,
					DataplaneId: "dp-creating",
					VcpusLimit:  20,
				},
				RegionId: 101,
			},
			{
				ResourcePool: &globalngv1.ResourcePool{
					Id:          20,
					Name:        "unspecified",
					ProviderId:  globalcommonv1.CloudProvider_AWS,
					RegionId:    globalcommonv1.Region_AWS_US_WEST_2,
					ServicePlan: globalcommonv1.ServicePlan_BYOC,
					DataplaneId: "dp-unspecified",
					VcpusLimit:  5,
				},
				RegionId: 101,
			},
			{
				ResourcePool: &globalngv1.ResourcePool{
					Id:          23,
					Name:        "deleting",
					ProviderId:  globalcommonv1.CloudProvider_AWS,
					RegionId:    globalcommonv1.Region_AWS_US_WEST_2,
					ServicePlan: globalcommonv1.ServicePlan_BYOC,
					DataplaneId: "dp-deleting",
					VcpusLimit:  30,
				},
				RegionId: 101,
			},
			{
				ResourcePool: &globalngv1.ResourcePool{
					Id:          24,
					Name:        "modifying",
					ProviderId:  globalcommonv1.CloudProvider_AWS,
					RegionId:    globalcommonv1.Region_AWS_US_WEST_2,
					ServicePlan: globalcommonv1.ServicePlan_BYOC,
					DataplaneId: "dp-modifying",
					VcpusLimit:  40,
				},
				RegionId: 101,
			},
			{
				ResourcePool: &globalngv1.ResourcePool{
					Id:          25,
					Name:        "unavailable",
					ProviderId:  globalcommonv1.CloudProvider_AWS,
					RegionId:    globalcommonv1.Region_AWS_US_WEST_2,
					ServicePlan: globalcommonv1.ServicePlan_BYOC,
					DataplaneId: "dp-unavailable",
					VcpusLimit:  50,
				},
				RegionId: 101,
			},
			{
				ResourcePool: &globalngv1.ResourcePool{
					Id:          26,
					Name:        "paused",
					ProviderId:  globalcommonv1.CloudProvider_AWS,
					RegionId:    globalcommonv1.Region_AWS_US_WEST_2,
					ServicePlan: globalcommonv1.ServicePlan_BYOC,
					DataplaneId: "dp-paused",
					VcpusLimit:  60,
				},
				RegionId: 101,
			},
			{
				ResourcePool: &globalngv1.ResourcePool{
					Id:          27,
					Name:        "pausing",
					ProviderId:  globalcommonv1.CloudProvider_AWS,
					RegionId:    globalcommonv1.Region_AWS_US_WEST_2,
					ServicePlan: globalcommonv1.ServicePlan_BYOC,
					DataplaneId: "dp-pausing",
					VcpusLimit:  70,
				},
				RegionId: 101,
			},
			{
				ResourcePool: &globalngv1.ResourcePool{
					Id:          28,
					Name:        "resuming",
					ProviderId:  globalcommonv1.CloudProvider_AWS,
					RegionId:    globalcommonv1.Region_AWS_US_WEST_2,
					ServicePlan: globalcommonv1.ServicePlan_BYOC,
					DataplaneId: "dp-resuming",
					VcpusLimit:  80,
				},
				RegionId: 101,
			},
			{
				ResourcePool: &globalngv1.ResourcePool{
					Id:          29,
					Name:        "unavailable-tidb-creating",
					ProviderId:  globalcommonv1.CloudProvider_AWS,
					RegionId:    globalcommonv1.Region_AWS_US_WEST_2,
					ServicePlan: globalcommonv1.ServicePlan_BYOC,
					DataplaneId: "dp-unavailable-tidb-creating",
					VcpusLimit:  90,
				},
				RegionId: 101,
			},
			{
				ResourcePool: &globalngv1.ResourcePool{
					Id:          30,
					Name:        "available-tidb-deleting",
					ProviderId:  globalcommonv1.CloudProvider_AWS,
					RegionId:    globalcommonv1.Region_AWS_US_WEST_2,
					ServicePlan: globalcommonv1.ServicePlan_BYOC,
					DataplaneId: "dp-available-tidb-deleting",
					VcpusLimit:  100,
				},
				RegionId: 101,
			},
			{
				ResourcePool: &globalngv1.ResourcePool{
					Id:          31,
					Name:        "unavailable-tiproxy-creating",
					ProviderId:  globalcommonv1.CloudProvider_AWS,
					RegionId:    globalcommonv1.Region_AWS_US_WEST_2,
					ServicePlan: globalcommonv1.ServicePlan_BYOC,
					DataplaneId: "dp-unavailable-tiproxy-creating",
					VcpusLimit:  110,
				},
				RegionId: 101,
			},
		}, int64(12), nil)
	mockRegionRepo.EXPECT().ListRegions(gomock.Any(), gomock.AssignableToTypeOf(regionrepo.Filter{})).
		Return([]*entity.Region{
			{
				Region: &globalcommonv1.Region{
					Id:              globalcommonv1.Region_AWS_US_WEST_2,
					CloudProviderId: globalcommonv1.CloudProvider_AWS,
				},
				RegionID: 101,
			},
		}, nil)
	mockStrategyRepo.EXPECT().Query(gomock.Any(), gomock.AssignableToTypeOf(&resourcepoolrepo.StrategyFilter{})).
		Return([]*entity.ResourcePoolStrategy{
			{
				ResourcePoolStrategy: &globalngv1.ResourcePoolStrategy{
					OrganizationId:        1001,
					DefaultResourcePoolId: 21,
					ProviderId:            globalcommonv1.CloudProvider_AWS,
					RegionId:              globalcommonv1.Region_AWS_US_WEST_2,
					ServicePlan:           globalcommonv1.ServicePlan_BYOC,
				},
				RegionId: 101,
			},
		}, nil)
	mockRegionalCli.EXPECT().ListSharedPools(
		gomock.Any(),
		gomock.Any(),
		&clusterngv1beta.ListSharedPoolsRequest{Ids: []string{"21", "22", "20", "23", "24", "25", "26", "27", "28", "29", "30", "31"}},
	).Return(&clusterngv1beta.ListSharedPoolsResponse{
		SharedPools: []*clusterngv1beta.SharedPool{
			{Id: "21", Phase: clusterngv1beta.SharedPool_AVAILABLE},
			{Id: "22", Phase: clusterngv1beta.SharedPool_CREATING},
			{Id: "20", Phase: clusterngv1beta.SharedPool_PHASE_UNSPECIFIED},
			{Id: "23", Phase: clusterngv1beta.SharedPool_DELETING},
			{Id: "24", Phase: clusterngv1beta.SharedPool_MODIFYING},
			{Id: "25", Phase: clusterngv1beta.SharedPool_UNAVAILABLE},
			{Id: "26", Phase: clusterngv1beta.SharedPool_PAUSED},
			{Id: "27", Phase: clusterngv1beta.SharedPool_PAUSING},
			{Id: "28", Phase: clusterngv1beta.SharedPool_RESUMING},
			{
				Id:    "29",
				Phase: clusterngv1beta.SharedPool_UNAVAILABLE,
				ComponentGroups: []*clusterngv1beta.Component{
					{
						ComponentType: clusterngv1beta.ComponentType_TIDB,
						Phase:         clusterngv1beta.Component_CREATING,
					},
				},
			},
			{
				Id:    "30",
				Phase: clusterngv1beta.SharedPool_AVAILABLE,
				ComponentGroups: []*clusterngv1beta.Component{
					{
						ComponentType: clusterngv1beta.ComponentType_TIDB,
						Phase:         clusterngv1beta.Component_DELETING,
					},
				},
			},
			{
				Id:    "31",
				Phase: clusterngv1beta.SharedPool_UNAVAILABLE,
				ComponentGroups: []*clusterngv1beta.Component{
					{
						ComponentType: clusterngv1beta.ComponentType_TIPROXY,
						Phase:         clusterngv1beta.Component_CREATING,
					},
				},
			},
		},
	}, nil)
	mockRegionalCli.EXPECT().GetSharedPoolVcpusUsage(
		gomock.Any(),
		gomock.Any(),
		gomock.Any(),
	).DoAndReturn(func(_ any, _ any, req *clusterngv1beta.GetSharedPoolVcpusUsageRequest) (*clusterngv1beta.GetSharedPoolVcpusUsageResponse, bizerr.BizError) {
		switch req.GetSharedPoolId() {
		case "21":
			return &clusterngv1beta.GetSharedPoolVcpusUsageResponse{SharedPoolId: "21", VcpusUsed: 7}, nil
		case "24":
			return &clusterngv1beta.GetSharedPoolVcpusUsageResponse{SharedPoolId: "24", VcpusUsed: 9}, nil
		case "30":
			return &clusterngv1beta.GetSharedPoolVcpusUsageResponse{SharedPoolId: "30", VcpusUsed: 13}, nil
		default:
			t.Fatalf("unexpected usage request for shared pool %s", req.GetSharedPoolId())
			return nil, bizerr.New(bizerr.ErrCodeUnknown, "unexpected usage request for shared pool %s", req.GetSharedPoolId())
		}
	}).Times(3)

	resp, err := svc.ListByocVcpuPools(t.Context(), &globalngv1.ListByocVcpuPoolsRequest{
		OrganizationId: 1001,
	})
	g.Expect(err).Should(BeNil())
	g.Expect(resp.GetGroups()).Should(HaveLen(1))
	g.Expect(resp.GetGroups()[0].GetTotalVcpusLimit()).To(Equal(int64(150)))
	g.Expect(resp.GetGroups()[0].GetPools()).Should(HaveLen(3))

	poolByID := make(map[uint64]*globalngv1.ListByocVcpuPoolsResponse_Pool)
	for _, pool := range resp.GetGroups()[0].GetPools() {
		poolByID[pool.GetResourcePoolId()] = pool
	}
	g.Expect(poolByID).To(HaveKey(uint64(21)))
	g.Expect(poolByID[21].GetState()).To(Equal(globalngv1.ListByocVcpuPoolsResponse_AVAILABLE))
	g.Expect(poolByID[21].GetVcpusUsed()).To(Equal(int64(7)))
	g.Expect(poolByID).To(HaveKey(uint64(24)))
	g.Expect(poolByID[24].GetState()).To(Equal(globalngv1.ListByocVcpuPoolsResponse_MODIFYING))
	g.Expect(poolByID[24].GetVcpusUsed()).To(Equal(int64(9)))
	g.Expect(poolByID).To(HaveKey(uint64(30)))
	g.Expect(poolByID[30].GetState()).To(Equal(globalngv1.ListByocVcpuPoolsResponse_AVAILABLE))
	g.Expect(poolByID[30].GetTidbInstances()).To(BeEmpty())
	g.Expect(poolByID[30].GetVcpusUsed()).To(Equal(int64(13)))
	g.Expect(poolByID).NotTo(HaveKey(uint64(29)))
	g.Expect(poolByID).NotTo(HaveKey(uint64(31)))
}

func TestByocVcpuPoolSharedPoolPhaseMapping(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name        string
		sharedPool  *clusterngv1beta.SharedPool
		wantInclude bool
		wantState   globalngv1.ListByocVcpuPoolsResponse_PoolState
	}{
		{
			name: "available pool remains available",
			sharedPool: &clusterngv1beta.SharedPool{
				Phase: clusterngv1beta.SharedPool_AVAILABLE,
			},
			wantInclude: true,
			wantState:   globalngv1.ListByocVcpuPoolsResponse_AVAILABLE,
		},
		{
			name: "modifying pool remains modifying",
			sharedPool: &clusterngv1beta.SharedPool{
				Phase: clusterngv1beta.SharedPool_MODIFYING,
			},
			wantInclude: true,
			wantState:   globalngv1.ListByocVcpuPoolsResponse_MODIFYING,
		},
		{
			name: "available pool with creating tidb component keeps pool state available",
			sharedPool: &clusterngv1beta.SharedPool{
				Phase: clusterngv1beta.SharedPool_AVAILABLE,
				ComponentGroups: []*clusterngv1beta.Component{
					{
						ComponentType: clusterngv1beta.ComponentType_TIDB,
						Phase:         clusterngv1beta.Component_CREATING,
					},
				},
			},
			wantInclude: true,
			wantState:   globalngv1.ListByocVcpuPoolsResponse_AVAILABLE,
		},
		{
			name: "available pool counts tidb component phases",
			sharedPool: &clusterngv1beta.SharedPool{
				Phase: clusterngv1beta.SharedPool_AVAILABLE,
				ComponentGroups: []*clusterngv1beta.Component{
					{
						ComponentType: clusterngv1beta.ComponentType_TIDB,
						Phase:         clusterngv1beta.Component_AVAILABLE,
					},
					{
						ComponentType: clusterngv1beta.ComponentType_TIDB,
						Phase:         clusterngv1beta.Component_MODIFYING,
					},
					{
						ComponentType: clusterngv1beta.ComponentType_TIDB,
						Phase:         clusterngv1beta.Component_CREATING,
					},
					{
						ComponentType: clusterngv1beta.ComponentType_TIDB,
						Phase:         clusterngv1beta.Component_DELETING,
					},
					{
						ComponentType: clusterngv1beta.ComponentType_TIPROXY,
						Phase:         clusterngv1beta.Component_CREATING,
					},
					{
						ComponentType: clusterngv1beta.ComponentType_TIDB_WORKER,
						Phase:         clusterngv1beta.Component_DELETING,
					},
				},
			},
			wantInclude: true,
			wantState:   globalngv1.ListByocVcpuPoolsResponse_AVAILABLE,
		},
		{
			name: "unavailable pool remains filtered despite deleting tidb component",
			sharedPool: &clusterngv1beta.SharedPool{
				Phase: clusterngv1beta.SharedPool_UNAVAILABLE,
				ComponentGroups: []*clusterngv1beta.Component{
					{
						ComponentType: clusterngv1beta.ComponentType_TIDB,
						Phase:         clusterngv1beta.Component_DELETING,
					},
				},
			},
		},
		{
			name: "unavailable pool without tidb lifecycle component is filtered",
			sharedPool: &clusterngv1beta.SharedPool{
				Phase: clusterngv1beta.SharedPool_UNAVAILABLE,
			},
		},
		{
			name: "unavailable pool with creating tiproxy component is filtered",
			sharedPool: &clusterngv1beta.SharedPool{
				Phase: clusterngv1beta.SharedPool_UNAVAILABLE,
				ComponentGroups: []*clusterngv1beta.Component{
					{
						ComponentType: clusterngv1beta.ComponentType_TIPROXY,
						Phase:         clusterngv1beta.Component_CREATING,
					},
				},
			},
		},
		{
			name: "unavailable pool with creating tidb worker component is filtered",
			sharedPool: &clusterngv1beta.SharedPool{
				Phase: clusterngv1beta.SharedPool_UNAVAILABLE,
				ComponentGroups: []*clusterngv1beta.Component{
					{
						ComponentType: clusterngv1beta.ComponentType_TIDB_WORKER,
						Phase:         clusterngv1beta.Component_CREATING,
					},
				},
			},
		},
		{
			name: "creating pool is filtered even with creating tidb component",
			sharedPool: &clusterngv1beta.SharedPool{
				Phase: clusterngv1beta.SharedPool_CREATING,
				ComponentGroups: []*clusterngv1beta.Component{
					{
						ComponentType: clusterngv1beta.ComponentType_TIDB,
						Phase:         clusterngv1beta.Component_CREATING,
					},
				},
			},
		},
	}

	for i := range tests {
		tt := tests[i]
		t.Run(tt.name, func(t *testing.T) {
			g := NewWithT(t)

			g.Expect(shouldIncludeByocVcpuPool(tt.sharedPool)).To(Equal(tt.wantInclude))
			if tt.wantInclude {
				g.Expect(byocVcpuPoolState(tt.sharedPool)).To(Equal(tt.wantState))
			}
		})
	}
}

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

	clusterRepo := mock_cluster.NewMockRepo(ctrl)
	workflow := mockworkflow.NewMockWorkflow(ctrl)
	svc := &service{clusterRepo: clusterRepo, workflow: workflow}

	clusterRepo.EXPECT().Query(gomock.Any(), &clusterrepo.Filter{
		OrganizationIds: []uint64{1001},
		SharedPoolIds:   []uint64{21},
		ShowDeleted:     false,
	}).Return([]*entity.Cluster{
		{Cluster: &globalngv1.Cluster{ClusterId: 1, SharedPoolId: "21", State: globalngv1.Cluster_CREATING}},
		{Cluster: &globalngv1.Cluster{ClusterId: 2, SharedPoolId: "21", State: globalngv1.Cluster_AVAILABLE}},
	}, nil)
	workflow.EXPECT().ListRunningDagInstancesByOrgId(
		gomock.Any(),
		[]string{workflowcommon.DeleteCluster},
		"1001",
	).Return([]*ffentity.DagInstance{
		{Tags: []ffentity.DagInstanceTag{{Key: utilworkflow.BizIdKey, Value: "2"}}},
		{Tags: []ffentity.DagInstanceTag{{Key: utilworkflow.BizIdKey, Value: "3"}}},
		{Tags: []ffentity.DagInstanceTag{{Key: utilworkflow.BizIdKey, Value: "4"}}},
	}, nil)
	clusterRepo.EXPECT().Query(gomock.Any(), gomock.AssignableToTypeOf(&clusterrepo.Filter{})).
		DoAndReturn(func(_ context.Context, filter *clusterrepo.Filter) ([]*entity.Cluster, bizerr.BizError) {
			g.Expect(filter.OrganizationIds).To(Equal([]uint64{1001}))
			g.Expect(filter.ClusterIds).To(ConsistOf(uint64(3), uint64(4)))
			g.Expect(filter.ShowDeleted).To(BeTrue())
			return []*entity.Cluster{
				{Cluster: &globalngv1.Cluster{ClusterId: 3, SharedPoolId: "21", State: globalngv1.Cluster_DELETED}},
				{Cluster: &globalngv1.Cluster{ClusterId: 4, SharedPoolId: "99", State: globalngv1.Cluster_DELETED}},
			}, nil
		})

	instancesByPoolID, err := svc.listByocVcpuPoolTidbInstances(t.Context(), 1001, []uint64{21})
	g.Expect(err).To(BeNil())
	g.Expect(instancesByPoolID[21]).To(Equal([]*globalngv1.ListByocVcpuPoolsResponse_TidbInstance{
		{ClusterId: 1, BaseState: globalngv1.Cluster_CREATING},
		{ClusterId: 2, BaseState: globalngv1.Cluster_AVAILABLE, Deleting: true},
		{ClusterId: 3, BaseState: globalngv1.Cluster_DELETED, Deleting: true},
	}))
}

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

	clusterRepo := mock_cluster.NewMockRepo(ctrl)
	workflow := mockworkflow.NewMockWorkflow(ctrl)
	svc := &service{clusterRepo: clusterRepo, workflow: workflow}

	clusterRepo.EXPECT().Query(gomock.Any(), gomock.Any()).Return([]*entity.Cluster{}, nil)
	workflow.EXPECT().ListRunningDagInstancesByOrgId(gomock.Any(), gomock.Any(), "1001").
		Return([]*ffentity.DagInstance{{Tags: []ffentity.DagInstanceTag{{Key: utilworkflow.BizIdKey, Value: "invalid"}}}}, nil)

	_, err := svc.listByocVcpuPoolTidbInstances(t.Context(), 1001, []uint64{21})
	g.Expect(err).NotTo(BeNil())
	g.Expect(err.Error()).To(ContainSubstring("invalid biz-id"))
}

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

	mockRegionRepo := mock_region.NewMockRepo(ctrl)
	mockResourcePoolRepo := mock_resourcepool.NewMockRepo(ctrl)
	mockStrategyRepo := mock_resourcepool.NewMockStrategyRepo(ctrl)
	mockRegionalCli := mock_regionalserver.NewMockClient(ctrl)
	workflow := mockworkflow.NewMockWorkflow(ctrl)

	svc := NewService(mockRegionRepo, emptyClusterRepo{}, nil, mockResourcePoolRepo, mockStrategyRepo, nil, mockRegionalCli, nil, nil, workflow)

	expectSingleByocVcpuPoolListSetup(
		mockResourcePoolRepo,
		mockRegionRepo,
		mockStrategyRepo,
		mockRegionalCli,
		clusterngv1beta.SharedPool_AVAILABLE,
	)
	workflow.EXPECT().ListRunningDagInstancesByOrgId(
		gomock.Any(),
		[]string{workflowcommon.DeleteCluster},
		"1001",
	).Return([]*ffentity.DagInstance{}, nil)
	workflow.EXPECT().ListRunningDagInstancesByBizIds(
		gomock.Any(),
		[]string{workflowcommon.ScaleByocSharedComponents},
		[]string{"21"},
	).Return([]*ffentity.DagInstance{
		{
			Tags: []ffentity.DagInstanceTag{
				{Key: utilworkflow.BizIdKey, Value: "21"},
			},
		},
	}, nil)
	mockRegionalCli.EXPECT().GetSharedPoolVcpusUsage(
		gomock.Any(),
		gomock.Any(),
		&clusterngv1beta.GetSharedPoolVcpusUsageRequest{SharedPoolId: "21"},
	).Return(&clusterngv1beta.GetSharedPoolVcpusUsageResponse{SharedPoolId: "21", VcpusUsed: 7}, nil)

	resp, err := svc.ListByocVcpuPools(t.Context(), &globalngv1.ListByocVcpuPoolsRequest{
		OrganizationId: 1001,
	})
	g.Expect(err).Should(BeNil())
	g.Expect(resp.GetGroups()).To(HaveLen(1))
	g.Expect(resp.GetGroups()[0].GetPools()).To(HaveLen(1))
	g.Expect(resp.GetGroups()[0].GetPools()[0].GetState()).To(Equal(globalngv1.ListByocVcpuPoolsResponse_MODIFYING))
}

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

	mockRegionRepo := mock_region.NewMockRepo(ctrl)
	mockResourcePoolRepo := mock_resourcepool.NewMockRepo(ctrl)
	mockStrategyRepo := mock_resourcepool.NewMockStrategyRepo(ctrl)
	mockRegionalCli := mock_regionalserver.NewMockClient(ctrl)
	workflow := mockworkflow.NewMockWorkflow(ctrl)

	svc := NewService(mockRegionRepo, emptyClusterRepo{}, nil, mockResourcePoolRepo, mockStrategyRepo, nil, mockRegionalCli, nil, nil, workflow)

	expectSingleByocVcpuPoolListSetup(
		mockResourcePoolRepo,
		mockRegionRepo,
		mockStrategyRepo,
		mockRegionalCli,
		clusterngv1beta.SharedPool_AVAILABLE,
	)
	workflow.EXPECT().ListRunningDagInstancesByOrgId(
		gomock.Any(),
		[]string{workflowcommon.DeleteCluster},
		"1001",
	).Return([]*ffentity.DagInstance{}, nil)
	workflow.EXPECT().ListRunningDagInstancesByBizIds(
		gomock.Any(),
		[]string{workflowcommon.ScaleByocSharedComponents},
		[]string{"21"},
	).Return(nil, errors.New("workflow unavailable"))

	_, err := svc.ListByocVcpuPools(t.Context(), &globalngv1.ListByocVcpuPoolsRequest{
		OrganizationId: 1001,
	})
	g.Expect(err).ShouldNot(BeNil())
	g.Expect(err.Error()).To(ContainSubstring("failed to list running scale workflows"))
}

func TestListByocVcpuPools_MissingWorkflowReturnsError(t *testing.T) {
	g := NewWithT(t)
	svc := &service{clusterRepo: emptyClusterRepo{}}

	_, err := svc.listByocVcpuPoolTidbInstances(t.Context(), 1001, []uint64{21})
	g.Expect(err).ShouldNot(BeNil())
	g.Expect(err.Error()).To(ContainSubstring("workflow is not configured"))
}

func expectSingleByocVcpuPoolListSetup(
	mockResourcePoolRepo *mock_resourcepool.MockRepo,
	mockRegionRepo *mock_region.MockRepo,
	mockStrategyRepo *mock_resourcepool.MockStrategyRepo,
	mockRegionalCli *mock_regionalserver.MockClient,
	phase clusterngv1beta.SharedPool_Phase,
) {
	mockResourcePoolRepo.EXPECT().Query(gomock.Any(), gomock.AssignableToTypeOf(&resourcepoolrepo.Filter{})).
		Return([]*entity.ResourcePool{
			{
				ResourcePool: &globalngv1.ResourcePool{
					Id:          21,
					Name:        "default",
					ProviderId:  globalcommonv1.CloudProvider_AWS,
					RegionId:    globalcommonv1.Region_AWS_US_WEST_2,
					ServicePlan: globalcommonv1.ServicePlan_BYOC,
					VcpusLimit:  10,
				},
				RegionId: 101,
			},
		}, int64(1), nil)
	mockRegionRepo.EXPECT().ListRegions(gomock.Any(), gomock.AssignableToTypeOf(regionrepo.Filter{})).
		Return([]*entity.Region{
			{
				Region: &globalcommonv1.Region{
					Id:              globalcommonv1.Region_AWS_US_WEST_2,
					CloudProviderId: globalcommonv1.CloudProvider_AWS,
				},
				RegionID: 101,
			},
		}, nil)
	mockStrategyRepo.EXPECT().Query(gomock.Any(), gomock.AssignableToTypeOf(&resourcepoolrepo.StrategyFilter{})).
		Return(nil, nil)
	mockRegionalCli.EXPECT().ListSharedPools(
		gomock.Any(),
		gomock.Any(),
		&clusterngv1beta.ListSharedPoolsRequest{Ids: []string{"21"}},
	).Return(&clusterngv1beta.ListSharedPoolsResponse{
		SharedPools: []*clusterngv1beta.SharedPool{
			{Id: "21", Phase: phase},
		},
	}, nil)
}

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

	mockRegionRepo := mock_region.NewMockRepo(ctrl)
	mockResourcePoolRepo := mock_resourcepool.NewMockRepo(ctrl)
	mockStrategyRepo := mock_resourcepool.NewMockStrategyRepo(ctrl)
	mockRegionalCli := mock_regionalserver.NewMockClient(ctrl)

	svc := NewService(
		mockRegionRepo,
		emptyClusterRepo{},
		nil,
		mockResourcePoolRepo,
		mockStrategyRepo,
		nil,
		mockRegionalCli,
		nil,
		nil,
		noRunningWorkflow{},
	)

	mockResourcePoolRepo.EXPECT().Query(gomock.Any(), gomock.AssignableToTypeOf(&resourcepoolrepo.Filter{})).
		Return([]*entity.ResourcePool{
			{
				ResourcePool: &globalngv1.ResourcePool{
					Id:          1,
					ProviderId:  globalcommonv1.CloudProvider_AWS,
					RegionId:    globalcommonv1.Region_AWS_US_WEST_2,
					ServicePlan: globalcommonv1.ServicePlan_BYOC,
					VcpusLimit:  math.MaxInt64,
				},
				RegionId: 101,
			},
			{
				ResourcePool: &globalngv1.ResourcePool{
					Id:          2,
					ProviderId:  globalcommonv1.CloudProvider_AWS,
					RegionId:    globalcommonv1.Region_AWS_US_WEST_2,
					ServicePlan: globalcommonv1.ServicePlan_BYOC,
					VcpusLimit:  1,
				},
				RegionId: 101,
			},
		}, int64(2), nil)

	_, err := svc.ListByocVcpuPools(t.Context(), &globalngv1.ListByocVcpuPoolsRequest{
		OrganizationId: 1001,
	})
	g.Expect(err).ShouldNot(BeNil())
}

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

	mockRegionRepo := mock_region.NewMockRepo(ctrl)
	mockResourcePoolRepo := mock_resourcepool.NewMockRepo(ctrl)

	svc := NewService(mockRegionRepo, nil, nil, mockResourcePoolRepo, nil, nil, nil, nil, nil, nil)

	_, err := svc.ListByocVcpuPools(t.Context(), &globalngv1.ListByocVcpuPoolsRequest{
		OrganizationId: 1001,
	})
	g.Expect(err).ShouldNot(BeNil())
}

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

	mockRegionRepo := mock_region.NewMockRepo(ctrl)
	mockResourcePoolRepo := mock_resourcepool.NewMockRepo(ctrl)
	mockStrategyRepo := mock_resourcepool.NewMockStrategyRepo(ctrl)
	mockRegionalCli := mock_regionalserver.NewMockClient(ctrl)

	svc := NewService(
		mockRegionRepo,
		emptyClusterRepo{},
		nil,
		mockResourcePoolRepo,
		mockStrategyRepo,
		nil,
		mockRegionalCli,
		nil,
		nil,
		noRunningWorkflow{},
	)

	mockResourcePoolRepo.EXPECT().Query(gomock.Any(), gomock.AssignableToTypeOf(&resourcepoolrepo.Filter{})).
		Return([]*entity.ResourcePool{
			{
				ResourcePool: &globalngv1.ResourcePool{
					Id:          21,
					ProviderId:  globalcommonv1.CloudProvider_AWS,
					RegionId:    globalcommonv1.Region_AWS_US_WEST_2,
					ServicePlan: globalcommonv1.ServicePlan_BYOC,
					VcpusLimit:  10,
				},
				RegionId: 101,
			},
		}, int64(1), nil)
	mockRegionRepo.EXPECT().ListRegions(gomock.Any(), gomock.AssignableToTypeOf(regionrepo.Filter{})).
		Return([]*entity.Region{
			{
				Region: &globalcommonv1.Region{
					Id:              globalcommonv1.Region_AWS_US_WEST_2,
					CloudProviderId: globalcommonv1.CloudProvider_AWS,
				},
				RegionID: 101,
			},
		}, nil)
	mockStrategyRepo.EXPECT().Query(gomock.Any(), gomock.AssignableToTypeOf(&resourcepoolrepo.StrategyFilter{})).
		Return(nil, nil)
	mockRegionalCli.EXPECT().ListSharedPools(
		gomock.Any(),
		gomock.Any(),
		&clusterngv1beta.ListSharedPoolsRequest{Ids: []string{"21"}},
	).Return(&clusterngv1beta.ListSharedPoolsResponse{
		SharedPools: []*clusterngv1beta.SharedPool{
			{Id: "21", Phase: clusterngv1beta.SharedPool_AVAILABLE},
		},
	}, nil)
	mockRegionalCli.EXPECT().GetSharedPoolVcpusUsage(
		gomock.Any(),
		gomock.Any(),
		&clusterngv1beta.GetSharedPoolVcpusUsageRequest{SharedPoolId: "21"},
	).Return(nil, bizerr.New(bizerr.ErrCodeUnknown, "boom"))

	_, err := svc.ListByocVcpuPools(t.Context(), &globalngv1.ListByocVcpuPoolsRequest{
		OrganizationId: 1001,
	})
	g.Expect(err).ShouldNot(BeNil())
}

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

	mockRegionalCli := mock_regionalserver.NewMockClient(ctrl)
	svc, ok := NewService(nil, nil, nil, nil, nil, nil, mockRegionalCli, nil, nil, nil).(*service)
	g.Expect(ok).To(BeTrue())
	regionEntity := &entity.Region{Region: &globalcommonv1.Region{Id: globalcommonv1.Region_AWS_US_WEST_2}}

	group := &byocVcpuPoolGroup{
		pools: make([]*globalngv1.ListByocVcpuPoolsResponse_Pool, 0, byocVcpuPoolUsageConcurrencyLimit+2),
	}
	for i := uint64(1); i <= byocVcpuPoolUsageConcurrencyLimit+2; i++ {
		group.pools = append(group.pools, &globalngv1.ListByocVcpuPoolsResponse_Pool{
			ResourcePoolId: i,
		})
	}

	started := make(chan struct{}, len(group.pools))
	release := make(chan struct{})
	var inFlight atomic.Int32
	var maxInFlight atomic.Int32

	mockRegionalCli.EXPECT().GetSharedPoolVcpusUsage(gomock.Any(), regionEntity, gomock.Any()).
		Times(len(group.pools)).
		DoAndReturn(func(_ any, _ *entity.Region, req *clusterngv1beta.GetSharedPoolVcpusUsageRequest) (*clusterngv1beta.GetSharedPoolVcpusUsageResponse, bizerr.BizError) {
			current := inFlight.Add(1)
			for {
				maxCurrent := maxInFlight.Load()
				if current <= maxCurrent || maxInFlight.CompareAndSwap(maxCurrent, current) {
					break
				}
			}
			started <- struct{}{}
			<-release
			inFlight.Add(-1)
			return &clusterngv1beta.GetSharedPoolVcpusUsageResponse{
				SharedPoolId: req.GetSharedPoolId(),
				VcpusUsed:    1,
			}, nil
		})
	errCh := make(chan bizerr.BizError, 1)
	go func() {
		errCh <- svc.fillByocVcpuPoolRuntimeInfo(t.Context(), group, regionEntity)
	}()

	for i := range byocVcpuPoolUsageConcurrencyLimit {
		select {
		case <-started:
		case <-time.After(2 * time.Second):
			t.Fatalf("expected %d concurrent usage fetches, only observed %d", byocVcpuPoolUsageConcurrencyLimit, i)
		}
	}

	select {
	case <-started:
		t.Fatalf("observed more than %d concurrent usage fetches", byocVcpuPoolUsageConcurrencyLimit)
	default:
	}

	close(release)
	err := <-errCh
	g.Expect(err).Should(BeNil())
	g.Expect(maxInFlight.Load()).To(Equal(int32(byocVcpuPoolUsageConcurrencyLimit)))
	for _, pool := range group.pools {
		g.Expect(pool.GetVcpusUsed()).To(Equal(int64(1)))
	}
}
