package resourcepool

import (
	"context"
	"math"
	"testing"

	"github.com/stretchr/testify/require"
	clusterngv1beta "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"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"
	mockregionalserver "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/adapter/regionalserver/mock_regionalserver"
	mockremoteencryptor "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/adapter/remoteencryptor/mock_remoteencryptor"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/entity"
	mockpassword "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/password/mock_password"
	mockregion "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/region/mock_region"
	mockresourcepool "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/resourcepool/mock_resourcepool"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/pkg/externalmocks"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	mockidgenerator "github.com/tidbcloud/tidb-management-service/utilities/idgenerator/mock_idgenerator"
)

func TestCreateResourcePoolBootstrapValidateOnlyHasNoMutation(t *testing.T) {
	ctrl := gomock.NewController(t)
	regionRepo := mockregion.NewMockRepo(ctrl)
	providerIdentityRepo := mockregion.NewMockProviderIdentityRepo(ctrl)
	resourcePoolRepo := mockresourcepool.NewMockRepo(ctrl)
	regionalCli := mockregionalserver.NewMockClient(ctrl)
	service := &serviceImpl{
		regionRepo:                 regionRepo,
		regionProviderIdentityRepo: providerIdentityRepo,
		resourcePoolRepo:           resourcePoolRepo,
		regionalCli:                regionalCli,
		billingCli:                 &allowBillingClient{},
	}

	request := bootstrapTestCreateRequest()
	request.ValidateOnly = true
	request.CreatedBy = "creator@example.com"
	request.ResourcePool.Id = 999
	request.ResourcePool.ProviderIdentityId = 998
	request.ResourcePool.DataplaneId = "should-not-be-returned"
	request.ResourcePool.Name = "  orders  "
	request.ResourcePool.ProviderId = globalcommonv1.CloudProvider_AWS
	request.ResourcePool.RegionId = globalcommonv1.Region_AWS_US_EAST_1
	request.ResourcePool.Cidr = "10.0.0.0/16"
	request.ResourcePool.ServicePlan = globalcommonv1.ServicePlan_BYOC
	request.ResourcePool.ProviderIdentity = "cloud-identity"
	request.ResourcePool.VcpusLimit = 100

	cloudRegion := &entity.Region{
		Region: &globalcommonv1.Region{
			Id:              globalcommonv1.Region_AWS_US_EAST_1,
			CloudProviderId: globalcommonv1.CloudProvider_AWS,
			State:           globalcommonv1.Region_ACTIVE,
			ServicePlan:     globalcommonv1.ServicePlan_BYOC,
			ServiceAddress:  "https://regional.example.com",
			OpenedTenantIds: []uint64{42},
		},
		RegionID: 71,
	}
	regionRepo.EXPECT().SelectRegion(gomock.Any(), gomock.Any()).Return(cloudRegion, nil)
	providerIdentityRepo.EXPECT().GetProviderIdentityByIdentity(gomock.Any(), uint64(71), "cloud-identity").Return(
		&entity.RegionProviderIdentity{RegionProviderIdentity: &globalngv1.RegionProviderIdentity{Id: 81, Identity: "cloud-identity"}},
		nil,
	)
	regionalCli.EXPECT().ListNodeSpecs(gomock.Any(), cloudRegion, &clusterngv1beta.ListNodeSpecsRequest{
		ComponentType: clusterngv1beta.ComponentType_TIKV,
		TenantId:      "42",
	}).Return(bootstrapTestDiscoveredSpecs(
		8,
		32,
		3,
		9,
		clusterngv1beta.NodeCountRuleType_NODE_COUNT_RULE_TYPE_MULTIPLE_OF,
		3,
		clusterngv1beta.Component_Storage_DATA,
		clusterngv1beta.Component_Storage_RAFTLOG,
	), nil)
	regionalCli.EXPECT().ListNodeSpecs(gomock.Any(), cloudRegion, &clusterngv1beta.ListNodeSpecsRequest{
		ComponentType: clusterngv1beta.ComponentType_TIFLASH_COMPUTE,
		TenantId:      "42",
	}).Return(bootstrapTestDiscoveredSpecs(
		16,
		64,
		1,
		1,
		clusterngv1beta.NodeCountRuleType_NODE_COUNT_RULE_TYPE_NONE,
		0,
		clusterngv1beta.Component_Storage_DATA,
	), nil)
	regionalCli.EXPECT().GetSharedPoolProvisionConfiguration(
		gomock.Any(),
		cloudRegion,
		&clusterngv1beta.GetSharedPoolProvisionConfigurationRequest{},
	).Return(&clusterngv1beta.SharedPoolProvisionConfiguration{O11YClusterCidr: "10.250.0.0/20"}, nil)
	resourcePoolRepo.EXPECT().ListLiveBYOCOrganizationResourcePoolsInRegion(
		gomock.Any(),
		uint64(42),
		globalcommonv1.CloudProvider_AWS,
		globalcommonv1.Region_AWS_US_EAST_1,
	).Return(nil, nil)
	regionalCli.EXPECT().EvaluateSharedPoolNetworkCapacity(gomock.Any(), cloudRegion, gomock.Any()).Return(
		&clusterngv1beta.EvaluateSharedPoolNetworkCapacityResponse{Supported: true, SupportedNodeCount: 128}, nil)
	resourcePoolRepo.EXPECT().HasLiveNormalizedName(gomock.Any(), uint64(42), "orders", uint64(0)).Return(false, nil)

	response, err := service.CreateResourcePool(t.Context(), request)

	require.NoError(t, err)
	require.Zero(t, response.GetResourcePool().GetId())
	require.Zero(t, response.GetResourcePool().GetProviderIdentityId())
	require.Empty(t, response.GetResourcePool().GetDataplaneId())
	require.Equal(t, "creator@example.com", response.GetResourcePool().GetCreatedBy())
	require.Equal(t, "orders", response.GetResourcePool().GetName())
	require.Equal(t, int64(8), response.GetCapacity().GetTikv().GetNodeSpec().GetCpu())
	require.Len(t, response.GetCapacity().GetTikv().GetStorages(), 1)
	require.Equal(t, "data", response.GetCapacity().GetTikv().GetStorages()[0].GetName())
	require.Equal(t, "10.250.0.0/20", response.GetO11YClusterCidr())
	require.Equal(t, uint32(500), request.GetDemand().GetTikvData().GetDiskSizeGi())
	require.Equal(t, defaultTikvRaftlogDiskSizeGi, request.GetDemand().GetTikvRaftlog().GetDiskSizeGi())
	require.Equal(t, uint32(600), request.GetDemand().GetTiflashCn().GetDiskSizeGi())
}

func TestCreateResourcePoolValidateOnlyWithoutBootstrapHasNoMutation(t *testing.T) {
	ctrl := gomock.NewController(t)
	regionRepo := mockregion.NewMockRepo(ctrl)
	providerIdentityRepo := mockregion.NewMockProviderIdentityRepo(ctrl)
	resourcePoolRepo := mockresourcepool.NewMockRepo(ctrl)
	regionalCli := mockregionalserver.NewMockClient(ctrl)
	service := &serviceImpl{
		regionRepo:                 regionRepo,
		regionProviderIdentityRepo: providerIdentityRepo,
		resourcePoolRepo:           resourcePoolRepo,
		regionalCli:                regionalCli,
		billingCli:                 &allowBillingClient{},
	}

	request := bootstrapTestCreateRequest()
	request.BootstrapCapacity = nil
	request.ValidateOnly = true
	request.CreatedBy = " creator@example.com "
	request.ResourcePool.Id = 999
	request.ResourcePool.Name = "  orders  "
	request.ResourcePool.ProviderId = globalcommonv1.CloudProvider_AWS
	request.ResourcePool.RegionId = globalcommonv1.Region_AWS_US_EAST_1
	request.ResourcePool.Cidr = "10.0.0.0/16"
	request.ResourcePool.ServicePlan = globalcommonv1.ServicePlan_BYOC
	request.ResourcePool.ProviderIdentity = "cloud-identity"
	request.ResourcePool.VcpusLimit = 100

	cloudRegion := &entity.Region{
		Region: &globalcommonv1.Region{
			Id:              globalcommonv1.Region_AWS_US_EAST_1,
			CloudProviderId: globalcommonv1.CloudProvider_AWS,
			State:           globalcommonv1.Region_ACTIVE,
			ServicePlan:     globalcommonv1.ServicePlan_BYOC,
		},
		RegionID: 71,
	}
	regionRepo.EXPECT().SelectRegion(gomock.Any(), gomock.Any()).Return(cloudRegion, nil)
	providerIdentityRepo.EXPECT().GetProviderIdentityByIdentity(gomock.Any(), uint64(71), "cloud-identity").Return(
		&entity.RegionProviderIdentity{RegionProviderIdentity: &globalngv1.RegionProviderIdentity{Id: 81, Identity: "cloud-identity"}},
		nil,
	)
	resourcePoolRepo.EXPECT().ListLiveBYOCOrganizationResourcePoolsInRegion(
		gomock.Any(),
		uint64(42),
		globalcommonv1.CloudProvider_AWS,
		globalcommonv1.Region_AWS_US_EAST_1,
	).Return(nil, nil)
	regionalCli.EXPECT().GetSharedPoolProvisionConfiguration(gomock.Any(), cloudRegion, gomock.Any()).Return(
		&clusterngv1beta.SharedPoolProvisionConfiguration{O11YClusterCidr: "10.250.0.0/20"}, nil)
	regionalCli.EXPECT().EvaluateSharedPoolNetworkCapacity(gomock.Any(), cloudRegion, gomock.Any()).Return(
		&clusterngv1beta.EvaluateSharedPoolNetworkCapacityResponse{Supported: true, SupportedNodeCount: 128}, nil)
	resourcePoolRepo.EXPECT().HasLiveNormalizedName(gomock.Any(), uint64(42), "orders", uint64(0)).Return(false, nil)

	response, err := service.CreateResourcePool(t.Context(), request)

	require.NoError(t, err)
	require.Zero(t, response.GetResourcePool().GetId())
	require.Zero(t, response.GetResourcePool().GetProviderIdentityId())
	require.Equal(t, "creator@example.com", response.GetResourcePool().GetCreatedBy())
	require.Equal(t, "orders", response.GetResourcePool().GetName())
	require.Nil(t, response.GetCapacity())
	require.Equal(t, "10.250.0.0/20", response.GetO11YClusterCidr())
}

func TestCreateSharedResourcePoolPreservesOmittedHighAvailability(t *testing.T) {
	ctrl := gomock.NewController(t)
	regionRepo := mockregion.NewMockRepo(ctrl)
	providerIdentityRepo := mockregion.NewMockProviderIdentityRepo(ctrl)
	service := &serviceImpl{
		regionRepo:                 regionRepo,
		regionProviderIdentityRepo: providerIdentityRepo,
	}

	request := &globalngv1.CreateResourcePoolRequest{
		ValidateOnly: true,
		ResourcePool: &globalngv1.ResourcePool{
			Name:             "essential-shared",
			ProviderId:       globalcommonv1.CloudProvider_AWS,
			RegionId:         globalcommonv1.Region_AWS_US_EAST_1,
			ServicePlan:      globalcommonv1.ServicePlan_Essential_V2,
			OwnerType:        globalngv1.ResourcePool_SHARED,
			ProviderIdentity: "cloud-identity",
		},
	}
	cloudRegion := &entity.Region{
		Region: &globalcommonv1.Region{
			Id:              globalcommonv1.Region_AWS_US_EAST_1,
			CloudProviderId: globalcommonv1.CloudProvider_AWS,
			ServicePlan:     globalcommonv1.ServicePlan_Essential_V2,
		},
		RegionID: 71,
	}
	regionRepo.EXPECT().SelectRegion(gomock.Any(), gomock.Any()).Return(cloudRegion, nil)
	providerIdentityRepo.EXPECT().GetProviderIdentityByIdentity(gomock.Any(), uint64(71), "cloud-identity").Return(
		&entity.RegionProviderIdentity{RegionProviderIdentity: &globalngv1.RegionProviderIdentity{
			Id:       81,
			Identity: "cloud-identity",
		}},
		nil,
	)

	response, err := service.CreateResourcePool(t.Context(), request)

	require.NoError(t, err)
	require.Equal(t, globalngv1.ResourcePool_HIGH_AVAILABILITY_TYPE_UNSPECIFIED, response.GetResourcePool().GetHighAvailabilityType())
}

func TestCreateResourcePoolRejectsBootstrapCapacityOverVcpusLimitBeforeRegionalPreflight(t *testing.T) {
	ctrl := gomock.NewController(t)
	regionRepo := mockregion.NewMockRepo(ctrl)
	providerIdentityRepo := mockregion.NewMockProviderIdentityRepo(ctrl)
	regionalCli := mockregionalserver.NewMockClient(ctrl)
	service := &serviceImpl{
		regionRepo:                 regionRepo,
		regionProviderIdentityRepo: providerIdentityRepo,
		regionalCli:                regionalCli,
		billingCli:                 &allowBillingClient{},
	}

	request := bootstrapTestCreateRequest()
	request.ResourcePool.ProviderId = globalcommonv1.CloudProvider_AWS
	request.ResourcePool.RegionId = globalcommonv1.Region_AWS_US_EAST_1
	request.ResourcePool.ServicePlan = globalcommonv1.ServicePlan_BYOC
	request.ResourcePool.ProviderIdentity = "cloud-identity"
	request.ResourcePool.VcpusLimit = 100
	request.BootstrapCapacity = bootstrapCapacityForVcpusLimitTest(8, 5, 10, 2)

	cloudRegion := &entity.Region{
		Region: &globalcommonv1.Region{
			Id:              globalcommonv1.Region_AWS_US_EAST_1,
			CloudProviderId: globalcommonv1.CloudProvider_AWS,
			ServicePlan:     globalcommonv1.ServicePlan_BYOC,
		},
		RegionID: 71,
	}
	regionRepo.EXPECT().SelectRegion(gomock.Any(), gomock.Any()).Return(cloudRegion, nil)
	providerIdentityRepo.EXPECT().GetProviderIdentityByIdentity(gomock.Any(), uint64(71), "cloud-identity").Return(
		&entity.RegionProviderIdentity{RegionProviderIdentity: &globalngv1.RegionProviderIdentity{
			Id:       81,
			Identity: "cloud-identity",
		}},
		nil,
	)

	response, err := service.CreateResourcePool(t.Context(), request)

	require.Nil(t, response)
	require.Error(t, err)
	require.Equal(t, bizerr.ErrCodeParams, err.ErrCode())
	require.Equal(t, bootstrapCapacityVcpusLimitError, err.ErrMsg())
}

func TestCreatePremiumResourcePoolValidateOnlyUsesOrganizationNamespaceValidation(t *testing.T) {
	ctrl := gomock.NewController(t)
	regionRepo := mockregion.NewMockRepo(ctrl)
	providerIdentityRepo := mockregion.NewMockProviderIdentityRepo(ctrl)
	resourcePoolRepo := mockresourcepool.NewMockRepo(ctrl)
	regionalCli := mockregionalserver.NewMockClient(ctrl)
	service := &serviceImpl{
		regionRepo:                 regionRepo,
		regionProviderIdentityRepo: providerIdentityRepo,
		resourcePoolRepo:           resourcePoolRepo,
		regionalCli:                regionalCli,
	}

	request := bootstrapTestCreateRequest()
	request.BootstrapCapacity = nil
	request.ValidateOnly = true
	request.ResourcePool.Name = "default-premium-pool"
	request.ResourcePool.ProviderId = globalcommonv1.CloudProvider_ALICLOUD
	request.ResourcePool.RegionId = globalcommonv1.Region_ALICLOUD_AP_SOUTHEAST_1
	request.ResourcePool.Cidr = "10.0.0.0/16"
	request.ResourcePool.ServicePlan = globalcommonv1.ServicePlan_Premium
	request.ResourcePool.ProviderIdentity = "cloud-identity"
	request.ResourcePool.VcpusLimit = 100

	cloudRegion := &entity.Region{
		Region: &globalcommonv1.Region{
			Id:              globalcommonv1.Region_ALICLOUD_AP_SOUTHEAST_1,
			CloudProviderId: globalcommonv1.CloudProvider_ALICLOUD,
			State:           globalcommonv1.Region_ACTIVE,
			ServicePlan:     globalcommonv1.ServicePlan_Premium,
		},
		RegionID: 71,
	}
	regionRepo.EXPECT().SelectRegion(gomock.Any(), gomock.Any()).Return(cloudRegion, nil)
	providerIdentityRepo.EXPECT().GetProviderIdentityByIdentity(gomock.Any(), uint64(71), "cloud-identity").Return(
		&entity.RegionProviderIdentity{RegionProviderIdentity: &globalngv1.RegionProviderIdentity{
			Id:       81,
			Identity: "cloud-identity",
		}},
		nil,
	)
	resourcePoolRepo.EXPECT().HasLiveNormalizedName(
		gomock.Any(), uint64(42), "default-premium-pool", uint64(0),
	).Return(false, nil)
	response, err := service.CreateResourcePool(t.Context(), request)

	require.NoError(t, err)
	require.Equal(t, globalcommonv1.ServicePlan_Premium, response.GetResourcePool().GetServicePlan())
	require.Equal(t, globalcommonv1.CloudProvider_ALICLOUD, response.GetResourcePool().GetProviderId())
}

func TestCreateResourcePoolPersistsCreatingLifecycle(t *testing.T) {
	ctrl := gomock.NewController(t)
	regionRepo := mockregion.NewMockRepo(ctrl)
	providerIdentityRepo := mockregion.NewMockProviderIdentityRepo(ctrl)
	resourcePoolRepo := mockresourcepool.NewMockRepo(ctrl)
	regionalCli := mockregionalserver.NewMockClient(ctrl)
	passwordRepo := mockpassword.NewMockRepo(ctrl)
	encryptor := mockremoteencryptor.NewMockClient(ctrl)
	transaction := externalmocks.NewMockTransactioner(ctrl)
	idGenerator := mockidgenerator.NewMockIDGenerator(ctrl)
	service := &serviceImpl{
		regionRepo:                 regionRepo,
		regionProviderIdentityRepo: providerIdentityRepo,
		resourcePoolRepo:           resourcePoolRepo,
		regionalCli:                regionalCli,
		billingCli:                 &allowBillingClient{},
		passwordRepo:               passwordRepo,
		encryptor:                  encryptor,
		transaction:                transaction,
		idGeneratorAdapter:         idGenerator,
	}

	request := bootstrapTestCreateRequest()
	request.BootstrapCapacity = nil
	request.ResourcePool.Name = "orders"
	request.ResourcePool.ProviderId = globalcommonv1.CloudProvider_AWS
	request.ResourcePool.RegionId = globalcommonv1.Region_AWS_US_EAST_1
	request.ResourcePool.Cidr = "10.0.0.0/16"
	request.ResourcePool.ServicePlan = globalcommonv1.ServicePlan_BYOC
	request.ResourcePool.ProviderIdentity = "cloud-identity"
	request.ResourcePool.VcpusLimit = 100

	cloudRegion := &entity.Region{
		Region: &globalcommonv1.Region{
			Id:              globalcommonv1.Region_AWS_US_EAST_1,
			CloudProviderId: globalcommonv1.CloudProvider_AWS,
			State:           globalcommonv1.Region_ACTIVE,
			ServicePlan:     globalcommonv1.ServicePlan_BYOC,
		},
		RegionID: 71,
	}
	regionRepo.EXPECT().SelectRegion(gomock.Any(), gomock.Any()).Return(cloudRegion, nil)
	providerIdentityRepo.EXPECT().GetProviderIdentityByIdentity(gomock.Any(), uint64(71), "cloud-identity").Return(
		&entity.RegionProviderIdentity{RegionProviderIdentity: &globalngv1.RegionProviderIdentity{Id: 81, Identity: "cloud-identity"}},
		nil,
	)
	resourcePoolRepo.EXPECT().ListLiveBYOCOrganizationResourcePoolsInRegion(
		gomock.Any(),
		uint64(42),
		globalcommonv1.CloudProvider_AWS,
		globalcommonv1.Region_AWS_US_EAST_1,
	).Return(nil, nil).Times(2)
	regionalCli.EXPECT().GetSharedPoolProvisionConfiguration(gomock.Any(), cloudRegion, gomock.Any()).Return(
		&clusterngv1beta.SharedPoolProvisionConfiguration{O11YClusterCidr: "10.250.0.0/20"}, nil)
	regionalCli.EXPECT().EvaluateSharedPoolNetworkCapacity(gomock.Any(), cloudRegion, gomock.Any()).Return(
		&clusterngv1beta.EvaluateSharedPoolNetworkCapacityResponse{Supported: true, SupportedNodeCount: 128}, nil)
	idGenerator.EXPECT().MustNew().Return(uint64(101))
	transaction.EXPECT().Transaction(gomock.Any(), gomock.Any()).DoAndReturn(
		func(ctx context.Context, fn func(context.Context) error) error {
			return fn(ctx)
		},
	)
	resourcePoolRepo.EXPECT().LockOrganizationNamespace(gomock.Any(), uint64(42)).Return(nil)
	resourcePoolRepo.EXPECT().HasLiveNormalizedName(gomock.Any(), uint64(42), "orders", uint64(0)).Return(false, nil)
	encryptor.EXPECT().EncryptBase64(gomock.Any(), gomock.Any()).Return("ciphertext", nil).Times(4)
	passwordRepo.EXPECT().SetEncryptedPassword(
		gomock.Any(),
		uint64(101),
		entity.SystemTidbId,
		gomock.Any(),
		"ciphertext",
	).Return(nil).Times(4)
	resourcePoolRepo.EXPECT().Create(gomock.Any(), gomock.Any()).DoAndReturn(
		func(_ context.Context, resourcePool *entity.ResourcePool) bizerr.BizError {
			require.Equal(t, globalngv1.ResourcePool_CREATING, resourcePool.GetLifecycleState())
			require.Empty(t, resourcePool.GetStateMessageCode())
			return bizerr.New(bizerr.ErrCodeUnknown, "stop after persistence assertion")
		},
	)

	response, err := service.CreateResourcePool(t.Context(), request)

	require.Nil(t, response)
	require.Error(t, err)
	require.ErrorContains(t, err, "create resource pool failed")
}

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

	validSpecs := &clusterngv1beta.ListNodeSpecsResponse{
		NodeSpecOptions: []*clusterngv1beta.NodeSpecOptions{
			bootstrapTestNodeSpecOptions(
				8,
				32,
				clusterngv1beta.Component_Storage_DATA,
				clusterngv1beta.Component_Storage_RAFTLOG,
			),
		},
		NodeCountOptions: &clusterngv1beta.NodeCountOptions{
			MinNodeCount: 3,
			MaxNodeCount: 9,
			Rule: &clusterngv1beta.NodeCountRule{
				Type:  clusterngv1beta.NodeCountRuleType_NODE_COUNT_RULE_TYPE_MULTIPLE_OF,
				Value: 3,
			},
		},
	}
	duplicateStorageOption := bootstrapTestNodeSpecOptions(
		8,
		32,
		clusterngv1beta.Component_Storage_DATA,
		clusterngv1beta.Component_Storage_RAFTLOG,
	)
	duplicateStorageOption.StorageOptions = append(
		duplicateStorageOption.StorageOptions,
		duplicateStorageOption.StorageOptions[0],
	)

	tests := []struct {
		name      string
		component *globalngv1.ComponentCapacity
		specs     *clusterngv1beta.ListNodeSpecsResponse
		wantCode  bizerr.ErrCode
	}{
		{
			name: "valid and normalizes data storage name",
			component: bootstrapTestComponent(8, 32, 3,
				&globalngv1.Storage{Name: "DATA", SizeGi: 500},
			),
			specs: validSpecs,
		},
		{
			name: "soft storage bounds are not create validation",
			component: bootstrapTestComponent(8, 32, 3,
				&globalngv1.Storage{Name: "data", SizeGi: 1},
			),
			specs: validSpecs,
		},
		{
			name: "missing node count presence",
			component: &globalngv1.ComponentCapacity{
				NodeSpec: &globalngv1.NodeSpecSelection{Cpu: 8, MemoryGi: 32},
				Storages: []*globalngv1.Storage{{Name: "data", SizeGi: 500}},
			},
			specs:    validSpecs,
			wantCode: bizerr.ErrCodeParams,
		},
		{
			name: "node spec tuple is unavailable",
			component: bootstrapTestComponent(
				16,
				64,
				3,
				&globalngv1.Storage{Name: "data", SizeGi: 500},
			),
			specs:    validSpecs,
			wantCode: bizerr.ErrCode(codes.FailedPrecondition),
		},
		{
			name: "node spec tuple is ambiguous",
			component: bootstrapTestComponent(
				8,
				32,
				3,
				&globalngv1.Storage{Name: "data", SizeGi: 500},
			),
			specs: &clusterngv1beta.ListNodeSpecsResponse{
				NodeSpecOptions: []*clusterngv1beta.NodeSpecOptions{
					bootstrapTestNodeSpecOptions(
						8,
						32,
						clusterngv1beta.Component_Storage_DATA,
						clusterngv1beta.Component_Storage_RAFTLOG,
					),
					bootstrapTestNodeSpecOptions(
						8,
						32,
						clusterngv1beta.Component_Storage_DATA,
						clusterngv1beta.Component_Storage_RAFTLOG,
					),
				},
				NodeCountOptions: validSpecs.GetNodeCountOptions(),
			},
			wantCode: bizerr.ErrCode(codes.FailedPrecondition),
		},
		{
			name: "node count violates multiple",
			component: bootstrapTestComponent(
				8,
				32,
				4,
				&globalngv1.Storage{Name: "data", SizeGi: 500},
			),
			specs:    validSpecs,
			wantCode: bizerr.ErrCode(codes.FailedPrecondition),
		},
		{
			name: "regional node count options are missing",
			component: bootstrapTestComponent(
				8,
				32,
				3,
				&globalngv1.Storage{Name: "data", SizeGi: 500},
			),
			specs: &clusterngv1beta.ListNodeSpecsResponse{
				NodeSpecOptions: validSpecs.GetNodeSpecOptions(),
			},
			wantCode: bizerr.ErrCode(codes.FailedPrecondition),
		},
		{
			name: "regional node spec is missing required cloud storage",
			component: bootstrapTestComponent(
				8,
				32,
				3,
				&globalngv1.Storage{Name: "data", SizeGi: 500},
			),
			specs: &clusterngv1beta.ListNodeSpecsResponse{
				NodeSpecOptions: []*clusterngv1beta.NodeSpecOptions{
					bootstrapTestNodeSpecOptions(8, 32, clusterngv1beta.Component_Storage_DATA),
				},
				NodeCountOptions: validSpecs.GetNodeCountOptions(),
			},
			wantCode: bizerr.ErrCode(codes.FailedPrecondition),
		},
		{
			name: "regional node spec has duplicate cloud storage",
			component: bootstrapTestComponent(
				8,
				32,
				3,
				&globalngv1.Storage{Name: "data", SizeGi: 500},
			),
			specs: &clusterngv1beta.ListNodeSpecsResponse{
				NodeSpecOptions: []*clusterngv1beta.NodeSpecOptions{
					duplicateStorageOption,
				},
				NodeCountOptions: validSpecs.GetNodeCountOptions(),
			},
			wantCode: bizerr.ErrCode(codes.Unavailable),
		},
		{
			name:      "missing required storage",
			component: bootstrapTestComponent(8, 32, 3),
			specs:     validSpecs,
			wantCode:  bizerr.ErrCode(codes.FailedPrecondition),
		},
		{
			name: "duplicate storage",
			component: bootstrapTestComponent(8, 32, 3,
				&globalngv1.Storage{Name: "data", SizeGi: 500},
				&globalngv1.Storage{Name: "DATA", SizeGi: 600},
			),
			specs:    validSpecs,
			wantCode: bizerr.ErrCode(codes.FailedPrecondition),
		},
		{
			name: "service-managed raftlog storage is rejected",
			component: bootstrapTestComponent(8, 32, 3,
				&globalngv1.Storage{Name: "data", SizeGi: 500},
				&globalngv1.Storage{Name: "raftlog", SizeGi: 100},
			),
			specs:    validSpecs,
			wantCode: bizerr.ErrCode(codes.FailedPrecondition),
		},
		{
			name: "storage size overflows regional uint32",
			component: bootstrapTestComponent(8, 32, 3,
				&globalngv1.Storage{Name: "data", SizeGi: int64(math.MaxUint32) + 1},
			),
			specs:    validSpecs,
			wantCode: bizerr.ErrCodeParams,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()

			err := validateBootstrapComponent(
				test.component,
				test.specs,
				[]string{bootstrapStorageData},
				[]string{bootstrapStorageData, bootstrapStorageRaftlog},
			)
			if test.wantCode != bizerr.ErrCodeOK {
				require.Error(t, err)
				require.Equal(t, test.wantCode, err.ErrCode())
				return
			}
			require.NoError(t, err)
			require.Equal(t, "data", test.component.GetStorages()[0].GetName())
			require.Len(t, test.component.GetStorages(), 1)
		})
	}
}

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

	tests := []struct {
		name       string
		bootstrap  *globalngv1.ResourcePoolBootstrapCapacity
		vcpusLimit uint64
		wantErr    bool
	}{
		{
			name:       "exactly fifty percent is accepted",
			bootstrap:  bootstrapCapacityForVcpusLimitTest(8, 5, 10, 1),
			vcpusLimit: 100,
		},
		{
			name:       "combined TiKV and TiFlash capacity exceeds fifty percent",
			bootstrap:  bootstrapCapacityForVcpusLimitTest(8, 5, 10, 2),
			vcpusLimit: 100,
			wantErr:    true,
		},
		{
			name:       "odd limit rejects capacity above the fractional half",
			bootstrap:  bootstrapCapacityForVcpusLimitTest(8, 1, 0, 0),
			vcpusLimit: 15,
			wantErr:    true,
		},
		{
			name:       "unlimited pool is not constrained",
			bootstrap:  bootstrapCapacityForVcpusLimitTest(64, 10, 64, 10),
			vcpusLimit: 0,
		},
		{
			name:       "capacity multiplication overflow is rejected",
			bootstrap:  bootstrapCapacityForVcpusLimitTest(math.MaxInt64, 2, 0, 0),
			vcpusLimit: math.MaxUint64,
			wantErr:    true,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()

			err := validateBootstrapCapacityVcpusLimit(test.bootstrap, test.vcpusLimit)
			if test.wantErr {
				require.Error(t, err)
				require.Equal(t, bizerr.ErrCodeParams, err.ErrCode())
				require.Equal(t, bootstrapCapacityVcpusLimitError, err.ErrMsg())
				return
			}
			require.NoError(t, err)
		})
	}
}

func bootstrapCapacityForVcpusLimitTest(
	tikvCPU int64,
	tikvNodeCount int64,
	tiflashCPU int64,
	tiflashNodeCount int64,
) *globalngv1.ResourcePoolBootstrapCapacity {
	bootstrap := &globalngv1.ResourcePoolBootstrapCapacity{
		Tikv: &globalngv1.ComponentCapacity{
			NodeSpec:  &globalngv1.NodeSpecSelection{Cpu: tikvCPU},
			NodeCount: proto.Int64(tikvNodeCount),
		},
	}
	if tiflashCPU > 0 {
		bootstrap.Tiflash = &globalngv1.ComponentCapacity{
			NodeSpec:  &globalngv1.NodeSpecSelection{Cpu: tiflashCPU},
			NodeCount: proto.Int64(tiflashNodeCount),
		}
	}
	return bootstrap
}

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

	tests := []struct {
		name      string
		value     string
		wantError bool
	}{
		{name: "canonical", value: "10.0.0.0/16"},
		{name: "host bits set", value: "10.0.1.1/16", wantError: true},
		{name: "ipv6", value: "2001:db8::/64", wantError: true},
		{name: "malformed", value: "invalid", wantError: true},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()
			_, err := parseCanonicalIPv4Prefix(test.value, "CIDR")
			if test.wantError {
				require.Error(t, err)
				return
			}
			require.NoError(t, err)
		})
	}
}

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

	tests := []struct {
		name             string
		resourcePoolCIDR string
		o11yCIDR         string
		wantCode         bizerr.ErrCode
	}{
		{name: "non-overlapping", resourcePoolCIDR: "10.0.0.0/16", o11yCIDR: "10.250.0.0/20"},
		{name: "resource pool request is malformed", resourcePoolCIDR: "10.0.1.1/16", o11yCIDR: "10.250.0.0/20", wantCode: bizerr.ErrCodeParams},
		{name: "O11Y configuration is missing", resourcePoolCIDR: "10.0.0.0/16", wantCode: bizerr.ErrCode(codes.FailedPrecondition)},
		{
			name:             "O11Y configuration is malformed",
			resourcePoolCIDR: "10.0.0.0/16",
			o11yCIDR:         "10.250.1.1/20",
			wantCode:         bizerr.ErrCode(codes.FailedPrecondition),
		},
		{
			name:             "O11Y configuration overlaps",
			resourcePoolCIDR: "10.0.0.0/16",
			o11yCIDR:         "10.0.16.0/20",
			wantCode:         bizerr.ErrCode(codes.FailedPrecondition),
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()

			canonicalCIDR, err := validateBootstrapNetworkCIDRs(test.resourcePoolCIDR, test.o11yCIDR)
			if test.wantCode != bizerr.ErrCodeOK {
				require.Error(t, err)
				require.Equal(t, test.wantCode, err.ErrCode())
				return
			}
			require.NoError(t, err)
			require.Equal(t, test.resourcePoolCIDR, canonicalCIDR)
		})
	}
}

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

	request := bootstrapTestCreateRequest()
	sharedPoolRequest, err := buildCreateSharedPoolRequest(request, 100, "cloud-identity", nil)
	require.NoError(t, err)

	require.Equal(t, clusterngv1beta.CapacityMode_RCU_DRIVEN, sharedPoolRequest.GetCapacityMode())
	require.Nil(t, sharedPoolRequest.GetRequestUnit())
	require.True(t, sharedPoolRequest.GetEnforceO11YNetworkNonOverlap())
	require.Equal(t, uint32(8), sharedPoolRequest.GetBootstrapCapacity().GetTikv().GetNodeSpec().GetCpu())
	require.Equal(t, uint32(3), sharedPoolRequest.GetBootstrapCapacity().GetTikv().GetNodeCount())
	require.Equal(t, uint32(16), sharedPoolRequest.GetBootstrapCapacity().GetTiflashCompute().GetNodeSpec().GetCpu())
	require.Equal(t, uint32(1), sharedPoolRequest.GetBootstrapCapacity().GetTiflashCompute().GetNodeCount())
}

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

	request := bootstrapTestCreateRequest()
	request.BootstrapCapacity = nil
	sharedPoolRequest, err := buildCreateSharedPoolRequest(request, 100, "cloud-identity", nil)
	require.NoError(t, err)

	require.NotNil(t, sharedPoolRequest.GetRequestUnit())
	require.False(t, sharedPoolRequest.GetEnforceO11YNetworkNonOverlap())
	require.Nil(t, sharedPoolRequest.GetBootstrapCapacity())
}

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

	request := bootstrapTestCreateRequest()
	request.ResourcePool.CloudResourceTags = map[string]string{
		"cost-center": "database",
		"owner":       "platform",
	}

	sharedPoolRequest, err := buildCreateSharedPoolRequest(request, 100, "cloud-identity", nil)
	require.NoError(t, err)
	require.Equal(t, request.GetResourcePool().GetCloudResourceTags(), sharedPoolRequest.GetSharedPool().GetCloudResourceTags())

	sharedPoolRequest.SharedPool.CloudResourceTags["owner"] = "mutated"
	require.Equal(t, "platform", request.GetResourcePool().GetCloudResourceTags()["owner"])
}

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

	request := bootstrapTestCreateRequest()
	request.Demand.TikvRaftlog.DiskSizeGi = defaultTikvRaftlogDiskSizeGi
	applyBootstrapStorageSizes(request.GetDemand(), request.GetBootstrapCapacity())

	require.Equal(t, uint32(500), request.GetDemand().GetTikvData().GetDiskSizeGi())
	require.Equal(t, defaultTikvRaftlogDiskSizeGi, request.GetDemand().GetTikvRaftlog().GetDiskSizeGi())
	require.Equal(t, uint32(600), request.GetDemand().GetTiflashCn().GetDiskSizeGi())
	require.Equal(t, uint32(0), request.GetDemand().GetTiflashWn().GetDiskSizeGi())
}

func bootstrapTestCreateRequest() *globalngv1.CreateResourcePoolRequest {
	return &globalngv1.CreateResourcePoolRequest{
		ResourcePool: &globalngv1.ResourcePool{OwnerType: globalngv1.ResourcePool_ORGANIZATION, OwnerId: 42},
		Demand: &globalngv1.CreateResourcePoolRequest_Demand{
			InitialRcu:  20000,
			TikvData:    &globalngv1.CreateResourcePoolRequest_Storage{},
			TikvRaftlog: &globalngv1.CreateResourcePoolRequest_Storage{},
			TiflashCn:   &globalngv1.CreateResourcePoolRequest_Storage{},
			TiflashWn:   &globalngv1.CreateResourcePoolRequest_Storage{},
		},
		BootstrapCapacity: &globalngv1.ResourcePoolBootstrapCapacity{
			Tikv: bootstrapTestComponent(8, 32, 3,
				&globalngv1.Storage{Name: "data", SizeGi: 500},
			),
			Tiflash: bootstrapTestComponent(16, 64, 1,
				&globalngv1.Storage{Name: "data", SizeGi: 600},
			),
		},
	}
}

func bootstrapTestComponent(
	cpu int64,
	memoryGi int64,
	nodeCount int64,
	storages ...*globalngv1.Storage,
) *globalngv1.ComponentCapacity {
	return &globalngv1.ComponentCapacity{
		NodeSpec:  &globalngv1.NodeSpecSelection{Cpu: cpu, MemoryGi: memoryGi},
		NodeCount: proto.Int64(nodeCount),
		Storages:  storages,
	}
}

func bootstrapTestDiscoveredSpecs(
	cpu uint32,
	memoryGi uint32,
	minCount uint32,
	maxCount uint32,
	ruleType clusterngv1beta.NodeCountRuleType,
	ruleValue uint32,
	storageNames ...clusterngv1beta.Component_Storage_Name,
) *clusterngv1beta.ListNodeSpecsResponse {
	return &clusterngv1beta.ListNodeSpecsResponse{
		NodeSpecOptions: []*clusterngv1beta.NodeSpecOptions{
			bootstrapTestNodeSpecOptions(cpu, memoryGi, storageNames...),
		},
		NodeCountOptions: &clusterngv1beta.NodeCountOptions{
			DefaultNodeCount: minCount,
			MinNodeCount:     minCount,
			MaxNodeCount:     maxCount,
			Rule: &clusterngv1beta.NodeCountRule{
				Type:  ruleType,
				Value: ruleValue,
			},
		},
	}
}

func bootstrapTestNodeSpecOptions(
	cpu uint32,
	memoryGi uint32,
	storageNames ...clusterngv1beta.Component_Storage_Name,
) *clusterngv1beta.NodeSpecOptions {
	storageOptions := make([]*clusterngv1beta.StorageSizeOptions, 0, len(storageNames))
	for _, name := range storageNames {
		storageOptions = append(storageOptions, &clusterngv1beta.StorageSizeOptions{
			Name:          name,
			Medium:        clusterngv1beta.StorageMedium_STORAGE_MEDIUM_CLOUD,
			DefaultSizeGi: 500,
			MinSizeGi:     100,
			MaxSizeGi:     4000,
		})
	}
	return &clusterngv1beta.NodeSpecOptions{
		NodeSpec:       &clusterngv1beta.NodeSpec{Cpu: cpu, MemoryGi: memoryGi},
		StorageOptions: storageOptions,
	}
}
