package cluster

import (
	"context"
	"errors"
	"fmt"
	"testing"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/go-logr/logr"
	. "github.com/onsi/gomega"
	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	clusterng "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	ccproto "github.com/tidbcloud/config-center/proto/config_center"
	commonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	nextgenv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	mgmtv1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"go.uber.org/mock/gomock"
	"google.golang.org/grpc"
	"google.golang.org/protobuf/types/known/timestamppb"

	"github.com/tidbcloud/devops/api"
	clusterapi "github.com/tidbcloud/devops/api/cluster"
	tntent "github.com/tidbcloud/devops/entity/tenant"
	"github.com/tidbcloud/devops/pkg/infra/clients/accountcli"
	"github.com/tidbcloud/devops/pkg/infra/clients/coresvccli"
	"github.com/tidbcloud/devops/pkg/infra/clients/mgmthttpcli"
	"github.com/tidbcloud/devops/pkg/infra/errs"
	tenantsvc "github.com/tidbcloud/devops/pkg/service/tenant"
)

type fakeNextGenAPI struct {
	mgmthttpcli.NextGenRegionServer

	resp  *clusterng.GetCassandraProxyResponse
	err   error
	calls int
}

func (f *fakeNextGenAPI) GetCassandraProxy(context.Context, string) (*clusterng.GetCassandraProxyResponse, error) {
	f.calls++
	return f.resp, f.err
}

type fakeNextGenControlPlane struct {
	mgmthttpcli.NextGenAPI

	sharedPool         *mgmthttpcli.SharedPool
	sharedPoolErr      error
	tidbGroups         *mgmthttpcli.ListTiDBGroupsResp
	tidbGroupsErr      error
	getSharedPoolCalls int
	listTiDBGroupCalls int
}

func (f *fakeNextGenControlPlane) GetSharedPool(context.Context, string, string) (*mgmthttpcli.SharedPool, error) {
	f.getSharedPoolCalls++
	return f.sharedPool, f.sharedPoolErr
}

func (f *fakeNextGenControlPlane) ListTiDBGroups(context.Context, string) (*mgmthttpcli.ListTiDBGroupsResp, error) {
	f.listTiDBGroupCalls++
	return f.tidbGroups, f.tidbGroupsErr
}

type fakeResourcePoolServiceClient struct {
	mgmtv1.ResourcePoolServiceClient

	resourcePools         map[uint64]*nextgenv1.ResourcePoolProfile
	getResourcePoolReqs   []*nextgenv1.GetResourcePoolRequest
	listResourcePoolsReqs []*nextgenv1.ListResourcePoolsRequest
	listResourcePoolsResp *nextgenv1.ListResourcePoolsResponse
	listOrgResourcePools  *nextgenv1.ListOwnerResourcePoolsResponse
	err                   error
}

func (f *fakeResourcePoolServiceClient) GetResourcePool(_ context.Context, req *nextgenv1.GetResourcePoolRequest, _ ...grpc.CallOption) (*nextgenv1.ResourcePoolProfile, error) {
	f.getResourcePoolReqs = append(f.getResourcePoolReqs, req)
	if f.err != nil {
		return nil, f.err
	}
	return f.resourcePools[req.GetResourcePoolId()], nil
}

func (f *fakeResourcePoolServiceClient) ListResourcePools(_ context.Context, req *nextgenv1.ListResourcePoolsRequest, _ ...grpc.CallOption) (*nextgenv1.ListResourcePoolsResponse, error) {
	f.listResourcePoolsReqs = append(f.listResourcePoolsReqs, cloneListResourcePoolsRequest(req))
	if f.err != nil {
		return nil, f.err
	}
	if f.listResourcePoolsResp != nil {
		return f.listResourcePoolsResp, nil
	}
	return &nextgenv1.ListResourcePoolsResponse{}, nil
}

func (f *fakeResourcePoolServiceClient) ListOrgResourcePools(_ context.Context, _ *mgmtv1.ListOrgResourcePoolsRequest, _ ...grpc.CallOption) (*nextgenv1.ListOwnerResourcePoolsResponse, error) {
	if f.err != nil {
		return nil, f.err
	}
	if f.listOrgResourcePools != nil {
		return f.listOrgResourcePools, nil
	}
	return &nextgenv1.ListOwnerResourcePoolsResponse{}, nil
}

func cloneListResourcePoolsRequest(req *nextgenv1.ListResourcePoolsRequest) *nextgenv1.ListResourcePoolsRequest {
	return &nextgenv1.ListResourcePoolsRequest{
		ResourcePoolIds:  append([]uint64(nil), req.GetResourcePoolIds()...),
		ResourcePoolName: req.GetResourcePoolName(),
		ProviderIds:      append([]commonv1.CloudProvider_Name(nil), req.GetProviderIds()...),
		RegionIds:        append([]commonv1.Region_Name(nil), req.GetRegionIds()...),
		OwnerType:        req.GetOwnerType(),
		OwnerIds:         append([]uint64(nil), req.GetOwnerIds()...),
		ServicePlan:      req.GetServicePlan(),
		PageSize:         req.GetPageSize(),
		Page:             req.GetPage(),
		ShowDeleted:      req.GetShowDeleted(),
		View:             req.GetView(),
	}
}

type fakeMgmtHTTPClient struct {
	mgmthttpcli.MgmtHTTPClient

	nextGenAPI          mgmthttpcli.NextGenAPI
	nextGenRegionServer mgmthttpcli.NextGenRegionServer
	configCenterNgProxy mgmthttpcli.ConfigCenterNgProxy
}

func (f *fakeMgmtHTTPClient) NextGen() mgmthttpcli.NextGenAPI {
	return f.nextGenAPI
}

func (f *fakeMgmtHTTPClient) NextGenRegionServer(string, string) mgmthttpcli.NextGenRegionServer {
	return f.nextGenRegionServer
}

func (f *fakeMgmtHTTPClient) ConfigCenterNgProxy() mgmthttpcli.ConfigCenterNgProxy {
	return f.configCenterNgProxy
}

type fakeResourcePoolRegionServer struct {
	mgmthttpcli.NextGenRegionServer

	sharedPool              *mgmthttpcli.SharedPool
	placementPolicies       map[string]*clusterng.PlacementPolicy
	getPlacementPolicyReqs  []*clusterng.GetPlacementPolicyRequest
	listPlacementPolicyReqs []*clusterng.ListPlacementPoliciesRequest
	err                     error
	placementPoliciesErr    error
}

func (f *fakeResourcePoolRegionServer) GetSharedPool(context.Context, string) (*mgmthttpcli.SharedPool, error) {
	return f.sharedPool, f.err
}

func (f *fakeResourcePoolRegionServer) GetPlacementPolicy(_ context.Context, req *clusterng.GetPlacementPolicyRequest) (*clusterng.PlacementPolicy, error) {
	f.getPlacementPolicyReqs = append(f.getPlacementPolicyReqs, &clusterng.GetPlacementPolicyRequest{
		SharedPoolId:      req.GetSharedPoolId(),
		PlacementPolicyId: req.GetPlacementPolicyId(),
	})
	if f.placementPoliciesErr != nil {
		return nil, f.placementPoliciesErr
	}
	if policy, ok := f.placementPolicies[req.GetPlacementPolicyId()]; ok {
		return policy, nil
	}
	return &clusterng.PlacementPolicy{}, nil
}

func (f *fakeResourcePoolRegionServer) ListPlacementPolicies(_ context.Context, req *clusterng.ListPlacementPoliciesRequest) (*clusterng.ListPlacementPoliciesResponse, error) {
	f.listPlacementPolicyReqs = append(f.listPlacementPolicyReqs, &clusterng.ListPlacementPoliciesRequest{
		SharedPoolId: req.GetSharedPoolId(),
		TikvGroupId:  req.GetTikvGroupId(),
	})
	if f.placementPoliciesErr != nil {
		return nil, f.placementPoliciesErr
	}

	resp := &clusterng.ListPlacementPoliciesResponse{
		PlacementPolicies: make([]*clusterng.PlacementPolicy, 0, len(f.placementPolicies)),
	}
	for _, policy := range f.placementPolicies {
		if req.GetTikvGroupId() != "" && policy.GetTikvGroupId() != req.GetTikvGroupId() {
			continue
		}
		resp.PlacementPolicies = append(resp.PlacementPolicies, policy)
	}
	return resp, nil
}

type fakeConfigCenterNgProxy struct {
	mgmthttpcli.ConfigCenterNgProxy

	requests  []fakeConfigCenterSearchRequest
	responses map[string]*ccproto.SearchClusterComponentsResponse
	resp      *ccproto.SearchClusterComponentsResponse
	err       error
}

type fakeConfigCenterSearchRequest struct {
	logicalRegion string
	cloudProvider string
	req           *ccproto.SearchClusterComponentsRequest
}

func (f *fakeConfigCenterNgProxy) SearchClusterComponents(_ context.Context, logicalRegion, cloudProvider string, req *ccproto.SearchClusterComponentsRequest) (*ccproto.SearchClusterComponentsResponse, error) {
	f.requests = append(f.requests, fakeConfigCenterSearchRequest{
		logicalRegion: logicalRegion,
		cloudProvider: cloudProvider,
		req:           cloneSearchClusterComponentsRequest(req),
	})
	if f.err != nil {
		return nil, f.err
	}
	if resp, ok := f.responses[configCenterSearchKey(req.GetClusterType(), logicalRegion, cloudProvider)]; ok {
		return resp, nil
	}
	if f.resp == nil {
		return &ccproto.SearchClusterComponentsResponse{}, nil
	}
	return f.resp, nil
}

func cloneSearchClusterComponentsRequest(req *ccproto.SearchClusterComponentsRequest) *ccproto.SearchClusterComponentsRequest {
	cloned := &ccproto.SearchClusterComponentsRequest{
		ClusterType:      req.GetClusterType(),
		ComponentName:    req.GetComponentName(),
		ClusterIds:       append([]string(nil), req.GetClusterIds()...),
		ComponentVersion: req.GetComponentVersion(),
		ImageTag:         req.GetImageTag(),
		PageSize:         req.GetPageSize(),
		Page:             req.GetPage(),
		ShowDeleted:      req.GetShowDeleted(),
	}
	if req.LogicalRegion != nil {
		logicalRegion := req.GetLogicalRegion()
		cloned.LogicalRegion = &logicalRegion
	}
	return cloned
}

func configCenterSearchKey(clusterType ccproto.ClusterType, logicalRegion, cloudProvider string) string {
	return fmt.Sprintf("%d/%s/%s", clusterType, logicalRegion, cloudProvider)
}

func newResourcePoolTestController(resourcePools ...*nextgenv1.ResourcePoolProfile) *ClusterController {
	poolsByID := make(map[uint64]*nextgenv1.ResourcePoolProfile, len(resourcePools))
	for _, rp := range resourcePools {
		poolsByID[rp.GetResourcePool().GetId()] = rp
	}
	return &ClusterController{
		coreSvcCli: &coresvccli.CoreSvcCli{
			Mgmt: &coresvccli.MgmtService{
				ResourcePool: &fakeResourcePoolServiceClient{resourcePools: poolsByID},
			},
		},
		mgmtHTTP: &fakeMgmtHTTPClient{
			nextGenRegionServer: &fakeResourcePoolRegionServer{err: errors.New("not found")},
			configCenterNgProxy: &fakeConfigCenterNgProxy{},
		},
		logger: logr.Discard(),
	}
}

func newResourcePoolProfile(id, tenantID uint64, ownerType nextgenv1.ResourcePool_OwnerType) *nextgenv1.ResourcePoolProfile {
	return &nextgenv1.ResourcePoolProfile{
		ResourcePool: &nextgenv1.ResourcePool{
			Id:            id,
			Name:          fmt.Sprintf("rp-%d", id),
			ProviderId:    commonv1.CloudProvider_AWS,
			RegionId:      commonv1.Region_AWS_US_EAST_1,
			OwnerType:     ownerType,
			OwnerId:       tenantID,
			Cidr:          "10.0.0.0/16",
			ServicePlan:   commonv1.ServicePlan_Premium,
			InstanceLimit: 10,
			VcpusLimit:    20,
			CreateTime:    timestamppb.New(time.Unix(123, 0)),
		},
		Region: &commonv1.Region{
			Id:              commonv1.Region_AWS_US_EAST_1,
			CloudProviderId: commonv1.CloudProvider_AWS,
			LogicalName:     "aws-us-east-1",
		},
		IsDefault: true,
	}
}

func newOpsMaintenanceWindowCheckRouter(c *ClusterController) *gin.Engine {
	gin.SetMode(gin.TestMode)
	r := gin.New()
	r.GET("/ops-maintenance-windows/:cluster_id/check", c.IsInClusterOpsMaintenanceWindow)
	return r
}

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

	tests := []struct {
		name          string
		resp          *clusterng.GetCassandraProxyResponse
		err           error
		wantComponent bool
	}{
		{
			name: "enabled",
			resp: &clusterng.GetCassandraProxyResponse{
				HostId:             "host-1",
				InternalEndpoint:   "cassandra.svc:9042",
				PrivatelinkDnsName: "privatelink.cassandra.example.com",
				Phase:              clusterng.Component_AVAILABLE,
				PrivateLinkService: &clusterng.CassandraProxyPrivateLinkServiceInfo{
					Phase:               clusterng.PrivateLinkService_ACTIVE,
					ServiceName:         "com.amazonaws.vpce.us-east-1.vpce-svc-123",
					AvailabilityZoneIds: []string{"use1-az1"},
				},
				NodeQuota: &clusterng.NodeQuota{
					NodeCount: 1,
					NodeSpec:  &clusterng.NodeSpec{Cpu: 4, MemoryGi: 8, Arch: "amd64"},
				},
			},
			wantComponent: true,
		},
		{
			name: "not found omitted",
			err:  errs.APIErrorNew(api.ErrCodeNotFound, errors.New("not found")),
		},
		{
			name: "upstream error omitted",
			err:  errors.New("upstream failed"),
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			g := NewWithT(t)
			components := &clusterapi.GetClusterComponentsResp{
				Cassandra: make([]*clusterapi.ComponentInfo, 0),
			}

			appendCassandraProxyComponent(context.Background(), &fakeNextGenAPI{resp: tt.resp, err: tt.err}, "123", components, logr.Discard())

			if !tt.wantComponent {
				g.Expect(components.Cassandra).To(BeEmpty())
				return
			}

			g.Expect(components.Cassandra).To(HaveLen(1))
			component := components.Cassandra[0]
			g.Expect(component.ComponentType).To(Equal(clusterapi.ComponentTypeCassandra))
			g.Expect(component.Count).To(Equal(uint32(1)))
			g.Expect(component.CpuNum).To(Equal(uint32(4)))
			g.Expect(component.MemorySizeGi).To(Equal(uint32(8)))
			g.Expect(component.InstanceCpuArch).To(Equal("amd64"))
			g.Expect(component.Phase).To(Equal("AVAILABLE"))
			g.Expect(component.HostID).To(Equal("host-1"))
			g.Expect(component.InternalEndpoint).To(Equal("cassandra.svc:9042"))
			g.Expect(component.PrivatelinkDNSName).To(Equal("privatelink.cassandra.example.com"))
			g.Expect(component.PrivateLinkService.ServiceName).To(Equal("com.amazonaws.vpce.us-east-1.vpce-svc-123"))
			g.Expect(component.PrivateLinkService.AvailabilityZoneIDs).To(Equal([]string{"use1-az1"}))
		})
	}
}

func TestGetClusterComponentsCoreQueriesCassandraProxyOnlyWhenPresent(t *testing.T) {
	tests := []struct {
		name              string
		componentGroups   []*mgmthttpcli.Component
		cassandraResponse *clusterng.GetCassandraProxyResponse
		cassandraErr      error
		wantCalls         int
		wantComponents    int
	}{
		{
			name: "no cassandra component",
		},
		{
			name: "cassandra component belongs to another cluster",
			componentGroups: []*mgmthttpcli.Component{
				{Id: "456", ComponentType: "CASSANDRA_PROXY"},
			},
		},
		{
			name: "matching cassandra component",
			componentGroups: []*mgmthttpcli.Component{
				{Id: "123", ComponentType: "CASSANDRA_PROXY"},
			},
			cassandraResponse: &clusterng.GetCassandraProxyResponse{
				HostId: "host-1",
				Phase:  clusterng.Component_AVAILABLE,
			},
			wantCalls:      1,
			wantComponents: 1,
		},
		{
			name: "matching component deleted before query",
			componentGroups: []*mgmthttpcli.Component{
				{Id: "123", ComponentType: "CASSANDRA_PROXY"},
			},
			cassandraErr: errs.APIErrorNew(api.ErrCodeNotFound, errors.New("not found")),
			wantCalls:    1,
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			g := NewWithT(t)
			mockController := gomock.NewController(t)
			clusterClient := coresvccli.NewMockClusterServiceClient(mockController)
			cluster := newNextGenMgmtCluster(123, "aws-us-east-1")
			cluster.GetNextGenCluster().SharedPoolId = "789"
			clusterClient.EXPECT().ListClusters(gomock.Any(), gomock.Any()).Return(&mgmtv1.ListClustersResponse{
				Clusters: []*mgmtv1.Cluster{cluster},
			}, nil)
			clusterClient.EXPECT().CountClusters(gomock.Any(), gomock.Any()).Return(&mgmtv1.CountClustersResponse{Count: 1}, nil)

			resourcePoolClient := &fakeResourcePoolServiceClient{
				resourcePools: map[uint64]*nextgenv1.ResourcePoolProfile{
					789: newResourcePoolProfile(789, 456, nextgenv1.ResourcePool_SHARED),
				},
			}
			nextGenAPI := &fakeNextGenControlPlane{
				sharedPool: &mgmthttpcli.SharedPool{ComponentGroups: tt.componentGroups},
				tidbGroups: &mgmthttpcli.ListTiDBGroupsResp{},
			}
			cassandraRegionServer := &fakeNextGenAPI{resp: tt.cassandraResponse, err: tt.cassandraErr}
			controller := &ClusterController{
				coreSvcCli: &coresvccli.CoreSvcCli{Mgmt: &coresvccli.MgmtService{
					Cluster:      clusterClient,
					ResourcePool: resourcePoolClient,
				}},
				mgmtHTTP: &fakeMgmtHTTPClient{
					nextGenAPI:          nextGenAPI,
					nextGenRegionServer: cassandraRegionServer,
				},
				logger: logr.Discard(),
			}

			components, err := controller.GetClusterComponentsCore(context.Background(), 123)

			g.Expect(err).NotTo(HaveOccurred())
			g.Expect(components.Cassandra).To(HaveLen(tt.wantComponents))
			g.Expect(cassandraRegionServer.calls).To(Equal(tt.wantCalls))
			g.Expect(resourcePoolClient.getResourcePoolReqs).To(HaveLen(tt.wantCalls))
			g.Expect(nextGenAPI.getSharedPoolCalls).To(Equal(1))
			g.Expect(nextGenAPI.listTiDBGroupCalls).To(Equal(1))
		})
	}
}

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

	tests := []struct {
		name           string
		ownerType      nextgenv1.ResourcePool_OwnerType
		wantComponents []clusterapi.ComponentType
	}{
		{
			name:      "organization resource pool hides tidb and tiproxy",
			ownerType: nextgenv1.ResourcePool_ORGANIZATION,
			wantComponents: []clusterapi.ComponentType{
				clusterapi.ComponentType("TIKV"),
			},
		},
		{
			name:      "shared resource pool hides tidb and keeps tiproxy",
			ownerType: nextgenv1.ResourcePool_SHARED,
			wantComponents: []clusterapi.ComponentType{
				clusterapi.ComponentType("TIPROXY"),
				clusterapi.ComponentType("TIKV"),
			},
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			g := NewWithT(t)
			rp := newResourcePoolProfile(123, 456, tt.ownerType)
			c := newResourcePoolTestController(rp)
			c.mgmtHTTP = &fakeMgmtHTTPClient{
				nextGenRegionServer: &fakeResourcePoolRegionServer{
					sharedPool: resourcePoolDetailsSharedPool(),
				},
				configCenterNgProxy: &fakeConfigCenterNgProxy{},
			}

			got := c.buildResourcePoolClusterCore(context.Background(), rp)

			g.Expect(got.ResourcePoolDetails.SharedPool).NotTo(BeNil())
			gotComponents := make([]clusterapi.ComponentType, 0, len(got.ResourcePoolDetails.SharedPool.ComponentGroups))
			for _, group := range got.ResourcePoolDetails.SharedPool.ComponentGroups {
				gotComponents = append(gotComponents, group.ComponentType)
			}
			g.Expect(gotComponents).To(Equal(tt.wantComponents))
			g.Expect(got.Status).To(Equal("AVAILABLE"))
		})
	}
}

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

	g := NewWithT(t)
	rp := newResourcePoolProfile(123, 0, nextgenv1.ResourcePool_SHARED)
	rp.Instances = []*nextgenv1.ResourcePoolProfile_Instance{
		{ClusterId: 1001, Name: "logical-1"},
		{ClusterId: 1002, Name: "logical-2"},
	}
	regionServer := &fakeResourcePoolRegionServer{
		sharedPool: resourcePoolDetailsSharedPool(),
		placementPolicies: map[string]*clusterng.PlacementPolicy{
			"1001": {SharedPoolId: "123", PlacementPolicyId: "1001", ClusterId: "1001", TikvGroupId: "tikv-dedicated"},
		},
	}
	c := newResourcePoolTestController(rp)
	c.mgmtHTTP = &fakeMgmtHTTPClient{
		nextGenRegionServer: regionServer,
		configCenterNgProxy: &fakeConfigCenterNgProxy{},
	}

	resp, err := c.GetSharedResourcePoolCore(context.Background(), 123)

	g.Expect(err).NotTo(HaveOccurred())
	g.Expect(resp.ResourcePoolDetails.LogicalClusters).To(Equal([]clusterapi.LogicalCluster{
		{ID: "1001", Name: "logical-1", TiKVGroupID: "tikv-dedicated"},
		{ID: "1002", Name: "logical-2"},
	}))
	g.Expect(regionServer.listPlacementPolicyReqs).To(HaveLen(1))
	g.Expect(regionServer.listPlacementPolicyReqs[0].GetSharedPoolId()).To(Equal("123"))
	g.Expect(regionServer.getPlacementPolicyReqs).To(BeEmpty())
}

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

	g := NewWithT(t)
	mockCtrl := gomock.NewController(t)
	clusterClient := coresvccli.NewMockClusterServiceClient(mockCtrl)
	clusterClient.EXPECT().
		ListClusters(gomock.Any(), gomock.Any()).
		DoAndReturn(func(_ context.Context, req *mgmtv1.ListClustersRequest, _ ...grpc.CallOption) (*mgmtv1.ListClustersResponse, error) {
			g.Expect(req.GetFilter().GetClusterIds()).To(Equal([]uint64{123}))
			return &mgmtv1.ListClustersResponse{}, nil
		})

	rp := newResourcePoolProfile(123, 0, nextgenv1.ResourcePool_SHARED)
	rp.Instances = []*nextgenv1.ResourcePoolProfile_Instance{
		{ClusterId: 1001, Name: "logical-1"},
		{ClusterId: 1002, Name: "logical-2"},
	}
	regionServer := &fakeResourcePoolRegionServer{
		sharedPool: resourcePoolDetailsSharedPool(),
		placementPolicies: map[string]*clusterng.PlacementPolicy{
			"1001": {SharedPoolId: "123", PlacementPolicyId: "1001", ClusterId: "1001", TikvGroupId: "tikv-dedicated"},
			"1002": {SharedPoolId: "123", PlacementPolicyId: "1002", ClusterId: "1002", TikvGroupId: "tikv-secondary"},
		},
	}
	controller := &ClusterController{
		coreSvcCli: &coresvccli.CoreSvcCli{
			Mgmt: &coresvccli.MgmtService{
				Cluster:      clusterClient,
				ResourcePool: &fakeResourcePoolServiceClient{resourcePools: map[uint64]*nextgenv1.ResourcePoolProfile{123: rp}},
			},
		},
		mgmtHTTP: &fakeMgmtHTTPClient{
			nextGenRegionServer: regionServer,
			configCenterNgProxy: &fakeConfigCenterNgProxy{},
		},
		logger: logr.Discard(),
	}

	resp, err := controller.GetClusterCore(context.Background(), 123)

	g.Expect(err).NotTo(HaveOccurred())
	g.Expect(resp.ResourcePoolDetails.LogicalClusters).To(Equal([]clusterapi.LogicalCluster{
		{ID: "1001", Name: "logical-1", TiKVGroupID: "tikv-dedicated"},
		{ID: "1002", Name: "logical-2", TiKVGroupID: "tikv-secondary"},
	}))
	g.Expect(regionServer.listPlacementPolicyReqs).To(HaveLen(1))
	g.Expect(regionServer.listPlacementPolicyReqs[0].GetSharedPoolId()).To(Equal("123"))
	g.Expect(regionServer.getPlacementPolicyReqs).To(BeEmpty())
}

func TestGetClusterCoreFillsNextGenTiKVGroupID(t *testing.T) {
	g := NewWithT(t)
	mockCtrl := gomock.NewController(t)
	clusterClient := coresvccli.NewMockClusterServiceClient(mockCtrl)
	tenantSvc := tenantsvc.NewMockTenantServiceAPI(mockCtrl)
	regionServer := &fakeResourcePoolRegionServer{
		placementPolicies: map[string]*clusterng.PlacementPolicy{
			"1001": {SharedPoolId: "123", PlacementPolicyId: "1001", ClusterId: "1001", TikvGroupId: "tikv-dedicated"},
		},
	}
	clusterClient.EXPECT().
		ListClusters(gomock.Any(), gomock.Any()).
		DoAndReturn(func(_ context.Context, req *mgmtv1.ListClustersRequest, _ ...grpc.CallOption) (*mgmtv1.ListClustersResponse, error) {
			g.Expect(req.GetFilter().GetClusterIds()).To(Equal([]uint64{1001}))
			return &mgmtv1.ListClustersResponse{
				Clusters: []*mgmtv1.Cluster{newDetailedNextGenMgmtCluster(1001, 777, "123")},
			}, nil
		})
	tenantSvc.EXPECT().
		GetTenant(gomock.Any(), uint64(777)).
		Return(&tntent.Tenant{Id: 777, Name: "Acme", CountryRegion: tntent.TenantRegionNA}, nil)
	controller := &ClusterController{
		coreSvcCli: &coresvccli.CoreSvcCli{
			Mgmt: &coresvccli.MgmtService{
				Cluster: clusterClient,
			},
		},
		mgmtHTTP: &fakeMgmtHTTPClient{
			nextGenRegionServer: regionServer,
		},
		tenantSvc: tenantSvc,
		logger:    logr.Discard(),
	}

	resp, err := controller.GetClusterCore(context.Background(), 1001)

	g.Expect(err).NotTo(HaveOccurred())
	g.Expect(resp.NextGenDetails).NotTo(BeNil())
	g.Expect(resp.NextGenDetails.TiKVGroupID).To(Equal("tikv-dedicated"))
	g.Expect(regionServer.getPlacementPolicyReqs).To(HaveLen(1))
	g.Expect(regionServer.getPlacementPolicyReqs[0].GetSharedPoolId()).To(Equal("123"))
	g.Expect(regionServer.getPlacementPolicyReqs[0].GetPlacementPolicyId()).To(Equal("1001"))
}

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

	g := NewWithT(t)
	fakeResourcePools := &fakeResourcePoolServiceClient{
		listResourcePoolsResp: &nextgenv1.ListResourcePoolsResponse{
			ResourcePools: []*nextgenv1.ResourcePoolProfile{
				newResourcePoolProfile(101, 0, nextgenv1.ResourcePool_ORGANIZATION),
				newResourcePoolProfile(202, 0, nextgenv1.ResourcePool_SHARED),
			},
			TotalSize: 12,
		},
	}
	c := &ClusterController{
		coreSvcCli: &coresvccli.CoreSvcCli{
			Mgmt: &coresvccli.MgmtService{
				ResourcePool: fakeResourcePools,
			},
		},
		mgmtHTTP: &fakeMgmtHTTPClient{
			nextGenRegionServer: &fakeResourcePoolRegionServer{err: errors.New("not found")},
			configCenterNgProxy: &fakeConfigCenterNgProxy{},
		},
		logger: logr.Discard(),
	}

	resp, err := c.ListResourcePoolsCore(context.Background(), &clusterapi.ListResourcePoolsReq{
		ResourcePoolIDs: []string{"101", "202"},
		Page:            2,
		PageSize:        20,
	})

	g.Expect(err).NotTo(HaveOccurred())
	g.Expect(resp.Clusters).To(HaveLen(2))
	g.Expect(resp.Total).To(Equal(int64(12)))
	g.Expect(fakeResourcePools.listResourcePoolsReqs).To(HaveLen(1))
	req := fakeResourcePools.listResourcePoolsReqs[0]
	g.Expect(req.GetResourcePoolIds()).To(Equal([]uint64{101, 202}))
	g.Expect(req.GetOwnerType()).To(Equal(nextgenv1.ResourcePool_OWNER_TYPE_UNSPECIFIED))
	g.Expect(req.GetPage()).To(Equal(uint32(2)))
	g.Expect(req.GetPageSize()).To(Equal(uint32(20)))
	g.Expect(req.GetView()).To(Equal(nextgenv1.ListResourcePoolsRequest_BASIC))
}

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

	g := NewWithT(t)
	mockCtrl := gomock.NewController(t)
	fakeResourcePools := &fakeResourcePoolServiceClient{
		listResourcePoolsResp: &nextgenv1.ListResourcePoolsResponse{
			ResourcePools: []*nextgenv1.ResourcePoolProfile{
				newResourcePoolProfile(101, 777, nextgenv1.ResourcePool_ORGANIZATION),
				newResourcePoolProfile(202, 0, nextgenv1.ResourcePool_SHARED),
				newResourcePoolProfile(303, 888, nextgenv1.ResourcePool_ORGANIZATION),
			},
		},
	}
	accountCli := accountcli.NewMockClient(mockCtrl)
	accountCli.EXPECT().
		ListTenants(gomock.Any(), gomock.Any()).
		DoAndReturn(func(_ context.Context, in *account.OrgFilter) ([]*account.Org, error) {
			g.Expect(in.GetIds()).To(Equal([]uint64{777, 888}))
			return []*account.Org{
				{Id: 777, Name: "Acme"},
				{Id: 888, Name: "Beta"},
			}, nil
		})
	c := &ClusterController{
		coreSvcCli: &coresvccli.CoreSvcCli{
			Mgmt: &coresvccli.MgmtService{
				ResourcePool: fakeResourcePools,
			},
		},
		mgmtHTTP: &fakeMgmtHTTPClient{
			nextGenRegionServer: &fakeResourcePoolRegionServer{err: errors.New("not found")},
			configCenterNgProxy: &fakeConfigCenterNgProxy{},
		},
		accountCli: accountCli,
		logger:     logr.Discard(),
	}

	resp, err := c.ListResourcePoolsCore(context.Background(), &clusterapi.ListResourcePoolsReq{})

	g.Expect(err).NotTo(HaveOccurred())
	g.Expect(resp.Clusters).To(HaveLen(3))
	clustersByID := make(map[uint64]clusterapi.Cluster, len(resp.Clusters))
	for _, cluster := range resp.Clusters {
		clustersByID[cluster.ID] = cluster
	}
	g.Expect(clustersByID[101].TenantName).To(Equal("Acme"))
	g.Expect(clustersByID[202].TenantName).To(BeEmpty())
	g.Expect(clustersByID[303].TenantName).To(Equal("Beta"))
}

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

	g := NewWithT(t)
	mockCtrl := gomock.NewController(t)
	resourcePool := newResourcePoolProfile(101, 777, nextgenv1.ResourcePool_ORGANIZATION)
	fakeResourcePools := &fakeResourcePoolServiceClient{
		listOrgResourcePools: &nextgenv1.ListOwnerResourcePoolsResponse{
			RegionResourcePools: []*nextgenv1.ListOwnerResourcePoolsResponse_RegionResourcePools{
				{ResourcePools: []*nextgenv1.ResourcePoolProfile{resourcePool}},
			},
		},
	}
	accountCli := accountcli.NewMockClient(mockCtrl)
	accountCli.EXPECT().
		ListTenants(gomock.Any(), gomock.Any()).
		Return([]*account.Org{{Id: 777, Name: "Acme"}}, nil)
	c := &ClusterController{
		coreSvcCli: &coresvccli.CoreSvcCli{
			Mgmt: &coresvccli.MgmtService{ResourcePool: fakeResourcePools},
		},
		mgmtHTTP: &fakeMgmtHTTPClient{
			nextGenRegionServer: &fakeResourcePoolRegionServer{
				sharedPool: &mgmthttpcli.SharedPool{Phase: "AVAILABLE"},
			},
			configCenterNgProxy: &fakeConfigCenterNgProxy{},
		},
		accountCli: accountCli,
		logger:     logr.Discard(),
	}

	resp, err := c.listTenantResourcePoolsCore(context.Background(), coresvccli.ListClustersParam{
		TenantIDs: []uint64{777},
	})

	g.Expect(err).NotTo(HaveOccurred())
	g.Expect(resp.Clusters).To(HaveLen(1))
	g.Expect(resp.Clusters[0].Status).To(Equal("AVAILABLE"))
	g.Expect(resp.Clusters[0].TenantName).To(Equal("Acme"))
}

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

	g := NewWithT(t)
	fakeResourcePools := &fakeResourcePoolServiceClient{
		listResourcePoolsResp: &nextgenv1.ListResourcePoolsResponse{
			ResourcePools: []*nextgenv1.ResourcePoolProfile{
				newResourcePoolProfile(202, 0, nextgenv1.ResourcePool_ORGANIZATION),
				newResourcePoolProfile(201, 0, nextgenv1.ResourcePool_SHARED),
			},
		},
	}
	cfgCenter := &fakeConfigCenterNgProxy{
		resp: &ccproto.SearchClusterComponentsResponse{
			Total: 2,
			Components: []*ccproto.ClusterComponent{
				{
					ClusterType:       ccproto.ClusterType_SHARED_POOL,
					ClusterId:         "201",
					ComponentName:     ccproto.SHARED_POOL_COMPONENT_TIKV,
					EffectiveImageTag: "tikv:v201",
				},
				{
					ClusterType:       ccproto.ClusterType_SHARED_POOL,
					ClusterId:         "202",
					ComponentName:     ccproto.SHARED_POOL_COMPONENT_TIKV,
					EffectiveImageTag: "tikv:v202",
				},
			},
		},
	}
	c := &ClusterController{
		coreSvcCli: &coresvccli.CoreSvcCli{
			Mgmt: &coresvccli.MgmtService{
				ResourcePool: fakeResourcePools,
			},
		},
		mgmtHTTP: &fakeMgmtHTTPClient{
			nextGenRegionServer: &fakeResourcePoolRegionServer{err: errors.New("not found")},
			configCenterNgProxy: cfgCenter,
		},
		logger: logr.Discard(),
	}

	resp, err := c.ListResourcePoolsCore(context.Background(), &clusterapi.ListResourcePoolsReq{})

	g.Expect(err).NotTo(HaveOccurred())
	g.Expect(resp.Clusters).To(HaveLen(2))
	clustersByID := make(map[uint64]clusterapi.Cluster, len(resp.Clusters))
	for _, cluster := range resp.Clusters {
		clustersByID[cluster.ID] = cluster
	}
	g.Expect(clustersByID[201].ResourcePoolDetails.TiKVTag).To(Equal("tikv:v201"))
	g.Expect(clustersByID[202].ResourcePoolDetails.TiKVTag).To(Equal("tikv:v202"))
	g.Expect(cfgCenter.requests).To(HaveLen(1))
	req := cfgCenter.requests[0]
	g.Expect(req.req.GetClusterIds()).To(Equal([]string{"201", "202"}))
	g.Expect(req.req.GetClusterType()).To(Equal(ccproto.ClusterType_SHARED_POOL))
	g.Expect(req.req.GetComponentName()).To(Equal(ccproto.SHARED_POOL_COMPONENT_TIKV))
}

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

	g := NewWithT(t)
	fakeResourcePools := &fakeResourcePoolServiceClient{
		listResourcePoolsResp: &nextgenv1.ListResourcePoolsResponse{},
	}
	c := &ClusterController{
		coreSvcCli: &coresvccli.CoreSvcCli{
			Mgmt: &coresvccli.MgmtService{
				ResourcePool: fakeResourcePools,
			},
		},
		logger: logr.Discard(),
	}

	_, err := c.ListResourcePoolsCore(context.Background(), &clusterapi.ListResourcePoolsReq{
		View: "DETAIL",
	})

	g.Expect(err).NotTo(HaveOccurred())
	g.Expect(fakeResourcePools.listResourcePoolsReqs).To(HaveLen(1))
	req := fakeResourcePools.listResourcePoolsReqs[0]
	g.Expect(req.GetView()).To(Equal(nextgenv1.ListResourcePoolsRequest_DETAIL))
}

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

	g := NewWithT(t)
	fakeResourcePools := &fakeResourcePoolServiceClient{}
	c := &ClusterController{
		coreSvcCli: &coresvccli.CoreSvcCli{
			Mgmt: &coresvccli.MgmtService{
				ResourcePool: fakeResourcePools,
			},
		},
		logger: logr.Discard(),
	}

	_, err := c.ListResourcePoolsCore(context.Background(), &clusterapi.ListResourcePoolsReq{
		ResourcePoolIDs: []string{"invalid"},
	})

	g.Expect(err).To(HaveOccurred())
	g.Expect(fakeResourcePools.listResourcePoolsReqs).To(BeEmpty())
}

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

	g := NewWithT(t)
	fakeResourcePools := &fakeResourcePoolServiceClient{}
	c := &ClusterController{
		coreSvcCli: &coresvccli.CoreSvcCli{
			Mgmt: &coresvccli.MgmtService{
				ResourcePool: fakeResourcePools,
			},
		},
		logger: logr.Discard(),
	}

	_, err := c.ListResourcePoolsCore(context.Background(), &clusterapi.ListResourcePoolsReq{
		View: "FULL",
	})

	g.Expect(err).To(HaveOccurred())
	g.Expect(fakeResourcePools.listResourcePoolsReqs).To(BeEmpty())
}

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

	g := NewWithT(t)
	fakeResourcePools := &fakeResourcePoolServiceClient{
		listResourcePoolsResp: &nextgenv1.ListResourcePoolsResponse{
			ResourcePools: []*nextgenv1.ResourcePoolProfile{
				newResourcePoolProfile(101, 0, nextgenv1.ResourcePool_SHARED),
				newResourcePoolProfile(202, 0, nextgenv1.ResourcePool_SHARED),
			},
		},
	}
	c := &ClusterController{
		coreSvcCli: &coresvccli.CoreSvcCli{
			Mgmt: &coresvccli.MgmtService{
				ResourcePool: fakeResourcePools,
			},
		},
		mgmtHTTP: &fakeMgmtHTTPClient{
			nextGenRegionServer: &fakeResourcePoolRegionServer{err: errors.New("not found")},
			configCenterNgProxy: &fakeConfigCenterNgProxy{},
		},
		logger: logr.Discard(),
	}

	resp, err := c.ListSharedResourcePoolsCore(context.Background(), &clusterapi.ListSharedResourcePoolsReq{
		ResourcePoolIDs: []string{"101", "202"},
		Page:            2,
		PageSize:        20,
	})

	g.Expect(err).NotTo(HaveOccurred())
	g.Expect(resp.Clusters).To(HaveLen(2))
	g.Expect(fakeResourcePools.listResourcePoolsReqs).To(HaveLen(1))
	req := fakeResourcePools.listResourcePoolsReqs[0]
	g.Expect(req.GetResourcePoolIds()).To(Equal([]uint64{101, 202}))
	g.Expect(req.GetOwnerType()).To(Equal(nextgenv1.ResourcePool_SHARED))
	g.Expect(req.GetPage()).To(Equal(uint32(2)))
	g.Expect(req.GetPageSize()).To(Equal(uint32(20)))
	g.Expect(req.GetView()).To(Equal(nextgenv1.ListResourcePoolsRequest_DETAIL))
}

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

	g := NewWithT(t)
	fakeResourcePools := &fakeResourcePoolServiceClient{
		listResourcePoolsResp: &nextgenv1.ListResourcePoolsResponse{
			ResourcePools: []*nextgenv1.ResourcePoolProfile{
				newResourcePoolProfile(302, 0, nextgenv1.ResourcePool_SHARED),
				newResourcePoolProfile(301, 0, nextgenv1.ResourcePool_SHARED),
			},
		},
	}
	cfgCenter := &fakeConfigCenterNgProxy{
		resp: &ccproto.SearchClusterComponentsResponse{
			Total: 2,
			Components: []*ccproto.ClusterComponent{
				{
					ClusterType:       ccproto.ClusterType_SHARED_POOL,
					ClusterId:         "301",
					ComponentName:     ccproto.SHARED_POOL_COMPONENT_TIKV,
					EffectiveImageTag: "tikv:v301",
				},
				{
					ClusterType:       ccproto.ClusterType_SHARED_POOL,
					ClusterId:         "302",
					ComponentName:     ccproto.SHARED_POOL_COMPONENT_TIKV,
					EffectiveImageTag: "tikv:v302",
				},
			},
		},
	}
	c := &ClusterController{
		coreSvcCli: &coresvccli.CoreSvcCli{
			Mgmt: &coresvccli.MgmtService{
				ResourcePool: fakeResourcePools,
			},
		},
		mgmtHTTP: &fakeMgmtHTTPClient{
			nextGenRegionServer: &fakeResourcePoolRegionServer{err: errors.New("not found")},
			configCenterNgProxy: cfgCenter,
		},
		logger: logr.Discard(),
	}

	resp, err := c.ListSharedResourcePoolsCore(context.Background(), &clusterapi.ListSharedResourcePoolsReq{})

	g.Expect(err).NotTo(HaveOccurred())
	g.Expect(resp.Clusters).To(HaveLen(2))
	clustersByID := make(map[uint64]clusterapi.Cluster, len(resp.Clusters))
	for _, cluster := range resp.Clusters {
		clustersByID[cluster.ID] = cluster
	}
	g.Expect(clustersByID[301].ResourcePoolDetails.TiKVTag).To(Equal("tikv:v301"))
	g.Expect(clustersByID[302].ResourcePoolDetails.TiKVTag).To(Equal("tikv:v302"))
	g.Expect(cfgCenter.requests).To(HaveLen(1))
	req := cfgCenter.requests[0]
	g.Expect(req.req.GetClusterIds()).To(Equal([]string{"301", "302"}))
	g.Expect(req.req.GetClusterType()).To(Equal(ccproto.ClusterType_SHARED_POOL))
	g.Expect(req.req.GetComponentName()).To(Equal(ccproto.SHARED_POOL_COMPONENT_TIKV))
}

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

	g := NewWithT(t)
	fakeResourcePools := &fakeResourcePoolServiceClient{}
	c := &ClusterController{
		coreSvcCli: &coresvccli.CoreSvcCli{
			Mgmt: &coresvccli.MgmtService{
				ResourcePool: fakeResourcePools,
			},
		},
		logger: logr.Discard(),
	}

	_, err := c.ListSharedResourcePoolsCore(context.Background(), &clusterapi.ListSharedResourcePoolsReq{
		ResourcePoolIDs: []string{"invalid"},
	})

	g.Expect(err).To(HaveOccurred())
	g.Expect(fakeResourcePools.listResourcePoolsReqs).To(BeEmpty())
}

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

	g := NewWithT(t)
	cfgCenter := &fakeConfigCenterNgProxy{
		resp: &ccproto.SearchClusterComponentsResponse{
			Total: 2,
			Components: []*ccproto.ClusterComponent{
				{
					ClusterType:       ccproto.ClusterType_LOGICAL_CLUSTER,
					ClusterId:         "101",
					ComponentName:     ccproto.LOGICAL_CLUSTER_COMPONENT_TIDB,
					EffectiveImageTag: "tidb:v101",
				},
				{
					ClusterType:       ccproto.ClusterType_LOGICAL_CLUSTER,
					ClusterId:         "102",
					ComponentName:     ccproto.LOGICAL_CLUSTER_COMPONENT_TIDB,
					EffectiveImageTag: "tidb:v102",
				},
			},
		},
	}
	c := &ClusterController{
		mgmtHTTP: &fakeMgmtHTTPClient{configCenterNgProxy: cfgCenter},
		logger:   logr.Discard(),
	}
	apiClusters := []clusterapi.Cluster{
		{
			ID:             102,
			DeployType:     mgmtv1.DeployType_DEPLOY_TYPE_NEXTGEN.String(),
			NextGenDetails: &clusterapi.NextGenDetails{},
		},
		{
			ID:             101,
			DeployType:     mgmtv1.DeployType_DEPLOY_TYPE_NEXTGEN.String(),
			NextGenDetails: &clusterapi.NextGenDetails{},
		},
	}
	mgmtClusters := []*mgmtv1.Cluster{
		newNextGenMgmtCluster(102, "aws-us-east-1"),
		newNextGenMgmtCluster(101, "aws-us-east-1"),
	}

	got := c.injectTiDBTagsForList(context.Background(), apiClusters, mgmtClusters)

	g.Expect(got[0].NextGenDetails.TiDBTag).To(Equal("tidb:v102"))
	g.Expect(got[1].NextGenDetails.TiDBTag).To(Equal("tidb:v101"))
	g.Expect(cfgCenter.requests).To(HaveLen(1))
	req := cfgCenter.requests[0]
	g.Expect(req.logicalRegion).To(Equal("aws-us-east-1"))
	g.Expect(req.cloudProvider).To(Equal(commonv1.CloudProvider_AWS.String()))
	g.Expect(req.req.GetClusterType()).To(Equal(ccproto.ClusterType_LOGICAL_CLUSTER))
	g.Expect(req.req.GetComponentName()).To(Equal(ccproto.LOGICAL_CLUSTER_COMPONENT_TIDB))
	g.Expect(req.req.GetClusterIds()).To(Equal([]string{"101", "102"}))
	g.Expect(req.req.GetLogicalRegion()).To(Equal("aws-us-east-1"))
	g.Expect(req.req.GetPage()).To(Equal(int32(1)))
	g.Expect(req.req.GetPageSize()).To(Equal(clusterTagSearchPageSize))
}

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

	g := NewWithT(t)
	cfgCenter := &fakeConfigCenterNgProxy{
		resp: &ccproto.SearchClusterComponentsResponse{
			Total: 2,
			Components: []*ccproto.ClusterComponent{
				{
					ClusterType:       ccproto.ClusterType_SHARED_POOL,
					ClusterId:         "201",
					ComponentName:     ccproto.SHARED_POOL_COMPONENT_TIKV,
					EffectiveImageTag: "tikv:v201",
				},
				{
					ClusterType:       ccproto.ClusterType_SHARED_POOL,
					ClusterId:         "202",
					ComponentName:     ccproto.SHARED_POOL_COMPONENT_TIKV,
					EffectiveImageTag: "tikv:v202",
				},
			},
		},
	}
	c := &ClusterController{
		mgmtHTTP: &fakeMgmtHTTPClient{configCenterNgProxy: cfgCenter},
		logger:   logr.Discard(),
	}
	apiClusters := []clusterapi.Cluster{
		{
			ID:                  202,
			DeployType:          "DEPLOY_TYPE_RESOURCE_POOL",
			Provider:            commonv1.CloudProvider_AWS.String(),
			LogicalRegion:       "aws-us-east-1",
			ResourcePoolDetails: &clusterapi.ResourcePoolDetails{},
		},
		{
			ID:                  201,
			DeployType:          "DEPLOY_TYPE_RESOURCE_POOL",
			Provider:            commonv1.CloudProvider_AWS.String(),
			LogicalRegion:       "aws-us-east-1",
			ResourcePoolDetails: &clusterapi.ResourcePoolDetails{},
		},
	}

	got := c.injectTiKVTagsForList(context.Background(), apiClusters)

	g.Expect(got[0].ResourcePoolDetails.TiKVTag).To(Equal("tikv:v202"))
	g.Expect(got[1].ResourcePoolDetails.TiKVTag).To(Equal("tikv:v201"))
	g.Expect(cfgCenter.requests).To(HaveLen(1))
	req := cfgCenter.requests[0]
	g.Expect(req.logicalRegion).To(Equal("aws-us-east-1"))
	g.Expect(req.cloudProvider).To(Equal(commonv1.CloudProvider_AWS.String()))
	g.Expect(req.req.GetClusterType()).To(Equal(ccproto.ClusterType_SHARED_POOL))
	g.Expect(req.req.GetComponentName()).To(Equal(ccproto.SHARED_POOL_COMPONENT_TIKV))
	g.Expect(req.req.GetClusterIds()).To(Equal([]string{"201", "202"}))
	g.Expect(req.req.GetLogicalRegion()).To(Equal("aws-us-east-1"))
	g.Expect(req.req.GetPage()).To(Equal(int32(1)))
	g.Expect(req.req.GetPageSize()).To(Equal(clusterTagSearchPageSize))
}

func newNextGenMgmtCluster(clusterID uint64, logicalRegion string) *mgmtv1.Cluster {
	return &mgmtv1.Cluster{
		ClusterId:  clusterID,
		DeployType: mgmtv1.DeployType_DEPLOY_TYPE_NEXTGEN,
		ClusterInfo: &mgmtv1.Cluster_NextGenCluster{
			NextGenCluster: &nextgenv1.Cluster{
				CloudProviderId:   commonv1.CloudProvider_AWS,
				RegionLogicalName: logicalRegion,
			},
		},
	}
}

func newDetailedNextGenMgmtCluster(clusterID, tenantID uint64, sharedPoolID string) *mgmtv1.Cluster {
	return &mgmtv1.Cluster{
		ClusterId:      clusterID,
		OrganizationId: tenantID,
		ClusterName:    "logical-cluster",
		CloudProvider:  commonv1.CloudProvider_AWS.String(),
		Region:         commonv1.Region_AWS_US_EAST_1.String(),
		DeployType:     mgmtv1.DeployType_DEPLOY_TYPE_NEXTGEN,
		CreateTime:     timestamppb.New(time.Unix(123, 0)),
		ClusterInfo: &mgmtv1.Cluster_NextGenCluster{
			NextGenCluster: &nextgenv1.Cluster{
				CloudProviderId:   commonv1.CloudProvider_AWS,
				RegionLogicalName: "aws-us-east-1",
				Version:           "v8.5.0",
				State:             nextgenv1.Cluster_AVAILABLE,
				MinRcu:            100,
				MaxRcu:            1000,
				ServicePlan:       commonv1.ServicePlan_Premium,
				SharedPoolId:      sharedPoolID,
				DataplaneId:       "dp-1",
			},
		},
	}
}

func resourcePoolDetailsSharedPool() *mgmthttpcli.SharedPool {
	return &mgmthttpcli.SharedPool{
		Id:    "123",
		Phase: "AVAILABLE",
		ComponentGroups: []*mgmthttpcli.Component{
			{
				Id:            "tidb-1",
				ComponentType: "TIDB",
				NodeQuota:     &mgmthttpcli.NodeQuota{NodeCount: 1},
				Phase:         "AVAILABLE",
			},
			{
				Id:            "tiproxy-1",
				ComponentType: "TIPROXY",
				NodeQuota:     &mgmthttpcli.NodeQuota{NodeCount: 2},
				Phase:         "AVAILABLE",
			},
			{
				Id:            "tikv-1",
				ComponentType: "TIKV",
				NodeQuota:     &mgmthttpcli.NodeQuota{NodeCount: 3},
				Phase:         "AVAILABLE",
			},
		},
	}
}
