package cluster_svc

import (
	"context"
	"testing"

	"github.com/shopspring/decimal"
	. "github.com/smartystreets/goconvey/convey"
	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/pkgv2/db/mock_db"
	"github.com/tidbcloud/serverless-service/internal/config"
	"github.com/tidbcloud/serverless-service/internal/domain/constant"
	"github.com/tidbcloud/serverless-service/internal/domain/entity"
	"github.com/tidbcloud/serverless-service/internal/domain/facade"
	"github.com/tidbcloud/serverless-service/internal/domain/facade/mock_facade"
	"github.com/tidbcloud/serverless-service/internal/domain/repo/mock_repo"
	"github.com/tidbcloud/serverless-service/internal/infra/eventbus"
	globalserverlessv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/serverless/v1"
	regionalserverlessv1 "github.com/tidbcloud/tidb-management-service/api/spec/regional/serverless/v1"
	tidbmgmtservicev1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"go.uber.org/mock/gomock"
)

func init() {
	config.Global().ServerConfig.EnableLarkMsg = false
}

func Test_SyncTidbPoolService_syncOneCluster(t *testing.T) {
	ctx := context.Background()
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	accountFacade := mock_facade.NewMockAccountFacade(ctrl)
	billingFacade := mock_facade.NewMockBillingFacade(ctrl)
	mgmtFacade := mock_facade.NewMockMgmtSvcFacade(ctrl)
	clusterRepo := mock_repo.NewMockClusterRepo(ctrl)
	clusterTidbPoolRepo := mock_repo.NewMockClusterTidbPoolRepo(ctrl)
	canaryRepo := mock_repo.NewMockCanaryRepo(ctrl)
	eventbusService := eventbus.NewMockEventBusService(ctrl)
	dbProvider := mock_db.NewMockTransProvider(ctrl)
	branchRepo := mock_repo.NewMockBranchRepo(ctrl)
	infraFacade := mock_facade.NewMockInfraFacade(ctrl)

	svc := NewSyncTidbPoolService(
		billingFacade,
		mgmtFacade,
		clusterRepo,
		clusterTidbPoolRepo,
		canaryRepo,
		accountFacade,
		eventbusService,
		dbProvider,
		branchRepo,
		infraFacade,
	)
	config.Global().ServerConfig.EnableLarkMsg = false

	record := &entity.ClusterTidbPool{
		EventID:          0,
		Type:             constant.TypeSpendLimit,
		ClusterID:        1001,
		SpendLimitStatus: constant.SpendLimitEventStatusHandy,
		Status:           constant.StatusUnsynced,
	}

	dbProvider.EXPECT().Transaction(gomock.Any(), gomock.Any()).DoAndReturn(func(
		ctx context.Context, cb func(ctx context.Context) error,
	) error {
		_, err := svc.handleCanaryEventImpl(ctx, record)
		return err
	})

	cluster := &entity.Cluster{CanaryMark: false}
	mgmtCluster := &tidbmgmtservicev1.Cluster{
		ClusterId: 1001, OrganizationId: 1, ProjectId: 101,
		ClusterInfo: &tidbmgmtservicev1.Cluster_ServerlessCluster{
			ServerlessCluster: &globalserverlessv1.ServerlessServiceCluster{
				UserStatus: globalserverlessv1.UserClusterStatus_USER_CLUSTER_STATUS_NORMAL,
				Cluster: &regionalserverlessv1.ServerlessRegionalServiceClusterInfo{
					Annotations: map[string]string{
						constant.AnnoTidbPoolSelector: "vip-pool",
						constant.AnnoRuLimitMode:      constant.RuLimitModeStandard,
					},
				},
			},
		},
	}
	spSetting := &facade.SpendLimitSetting{
		BizData: `{"free_credits": 6, "invoice_limit": 5, "active_total_credits": 6}`,
	}
	spInfo := &facade.SpendLimitInfo{Costs: decimal.NewFromFloat(0)}

	Convey("Test_syncOneCluster", t, func() {
		Convey("should return err if failed to get mgmt cluster", func() {
			e := bizerr.New(bizerr.ErrCodeUnknown, "failed")
			mgmtFacade.EXPECT().
				GetCluster(gomock.Any(), uint64(1001), tidbmgmtservicev1.ClusterFilter_CLUSTER_VIEW_BASIC).
				Return(nil, e)
			So(svc.syncOneCluster(ctx, record), ShouldNotBeNil)
		})

		Convey("should return err if core cluster not found", func() {
			e := bizerr.New(bizerr.ErrCodeRecordNotFound, "failed")
			mgmtFacade.EXPECT().
				GetCluster(gomock.Any(), uint64(1001), tidbmgmtservicev1.ClusterFilter_CLUSTER_VIEW_BASIC).
				Return(nil, e)
			clusterTidbPoolRepo.EXPECT().RemoveAllEvents(gomock.Any(), uint64(1001)).Return(nil)
			So(svc.syncOneCluster(ctx, record), ShouldNotBeNil)
		})

		Convey("should return nil if cluster not running ", func() {
			cls := &tidbmgmtservicev1.Cluster{
				ClusterId: 1001, OrganizationId: 1, ProjectId: 101,
				ClusterInfo: &tidbmgmtservicev1.Cluster_ServerlessCluster{
					ServerlessCluster: &globalserverlessv1.ServerlessServiceCluster{
						UserStatus: globalserverlessv1.UserClusterStatus_USER_CLUSTER_STATUS_CREATING,
					},
				},
			}
			mgmtFacade.EXPECT().
				GetCluster(gomock.Any(), uint64(1001), tidbmgmtservicev1.ClusterFilter_CLUSTER_VIEW_BASIC).
				Return(cls, nil)
			So(svc.syncOneCluster(ctx, record), ShouldBeNil)
		})
		Convey("should succeed for handy event", func() {
			mgmtFacade.EXPECT().
				GetCluster(gomock.Any(), uint64(1001), tidbmgmtservicev1.ClusterFilter_CLUSTER_VIEW_BASIC).
				Return(mgmtCluster, nil)
			billingFacade.EXPECT().
				GetSpendLimit(gomock.Any(), uint64(1), uint64(1001)).
				Return(spSetting, nil)
			billingFacade.EXPECT().GetSpendLimitInfo(gomock.Any(), spSetting).Return(spInfo, nil)
			clusterRepo.EXPECT().GetClusterByID(gomock.Any(), uint64(1001)).Return(cluster, nil)
			clusterRepo.EXPECT().Save(gomock.Any(), gomock.Any()).Return(nil)
			mgmtFacade.EXPECT().
				UpdateServerlessClusterAnnotations(gomock.Any(), uint64(1001), gomock.Any()).
				Return(mgmtCluster, nil)
			eventbusService.EXPECT().Publish(gomock.Any()).Return(nil)
			branchRepo.EXPECT().
				ListBranchesByMainBranchID(gomock.Any(), "1001", nil).
				Return([]*entity.Branch{}, nil)
			clusterTidbPoolRepo.EXPECT().MarkAsSynced(gomock.Any(), gomock.Any()).Return(nil)
			So(svc.syncOneCluster(ctx, record), ShouldBeNil)
		})

		Convey("should succeed for excessive event", func() {
			record := &entity.ClusterTidbPool{
				EventID:          0,
				Type:             constant.TypeSpendLimit,
				ClusterID:        1001,
				SpendLimitStatus: constant.SpendLimitEventStatusExcessive,
				Status:           constant.StatusUnsynced,
			}
			spSetting := &facade.SpendLimitSetting{
				BizData: `{"free_credits": 6, "invoice_limit": 0, "active_total_credits": 6}`,
			}
			spInfo := &facade.SpendLimitInfo{Costs: decimal.NewFromFloat(6.1)}
			mgmtFacade.EXPECT().
				GetCluster(gomock.Any(), uint64(1001), tidbmgmtservicev1.ClusterFilter_CLUSTER_VIEW_BASIC).
				Return(mgmtCluster, nil)
			billingFacade.EXPECT().
				GetSpendLimit(gomock.Any(), uint64(1), uint64(1001)).
				Return(spSetting, nil)
			billingFacade.EXPECT().GetSpendLimitInfo(gomock.Any(), spSetting).Return(spInfo, nil)
			clusterRepo.EXPECT().
				UpdateRUMode(gomock.Any(), uint64(1001), constant.ClusterRUMode("throttled")).
				Return(nil)
			clusterRepo.EXPECT().GetClusterByID(gomock.Any(), uint64(1001)).Return(cluster, nil)
			mgmtFacade.EXPECT().
				UpdateServerlessClusterAnnotations(gomock.Any(), uint64(1001), gomock.Any()).
				Return(mgmtCluster, nil)
			accountFacade.EXPECT().GetTenant(gomock.Any(), uint64(1)).Return(nil, nil)
			eventbusService.EXPECT().Publish(gomock.Any()).Return(nil)
			branchRepo.EXPECT().
				ListBranchesByMainBranchID(gomock.Any(), "1001", nil).
				Return([]*entity.Branch{}, nil)
			clusterTidbPoolRepo.EXPECT().MarkAsSynced(gomock.Any(), gomock.Any()).Return(nil)
			So(svc.syncOneCluster(ctx, record), ShouldBeNil)
		})

		Convey("should succeed and alert for inconsistent excessive event", func() {
			record := &entity.ClusterTidbPool{
				EventID:          0,
				Type:             constant.TypeSpendLimit,
				ClusterID:        1001,
				SpendLimitStatus: constant.SpendLimitEventStatusFit,
				Status:           constant.StatusUnsynced,
			}
			spSetting := &facade.SpendLimitSetting{
				BizData: `{"free_credits": 6, "invoice_limit": 0, "active_total_credits": 6}`,
			}
			spInfo := &facade.SpendLimitInfo{Costs: decimal.NewFromFloat(6.1)}
			mgmtFacade.EXPECT().
				GetCluster(gomock.Any(), uint64(1001), tidbmgmtservicev1.ClusterFilter_CLUSTER_VIEW_BASIC).
				Return(mgmtCluster, nil)
			billingFacade.EXPECT().
				GetSpendLimit(gomock.Any(), uint64(1), uint64(1001)).
				Return(spSetting, nil)
			billingFacade.EXPECT().GetSpendLimitInfo(gomock.Any(), spSetting).Return(spInfo, nil)
			clusterRepo.EXPECT().
				UpdateRUMode(gomock.Any(), uint64(1001), constant.ClusterRUMode("throttled")).
				Return(nil)
			clusterRepo.EXPECT().GetClusterByID(gomock.Any(), uint64(1001)).Return(cluster, nil)
			mgmtFacade.EXPECT().
				UpdateServerlessClusterAnnotations(gomock.Any(), uint64(1001), gomock.Any()).
				Return(mgmtCluster, nil)
			accountFacade.EXPECT().GetTenant(gomock.Any(), uint64(1)).Return(nil, nil)
			accountFacade.EXPECT().GetTenant(gomock.Any(), uint64(1)).Return(nil, nil)
			eventbusService.EXPECT().Publish(gomock.Any()).Return(nil)
			branchRepo.EXPECT().
				ListBranchesByMainBranchID(gomock.Any(), "1001", nil).
				Return([]*entity.Branch{}, nil)
			clusterTidbPoolRepo.EXPECT().MarkAsSynced(gomock.Any(), gomock.Any()).Return(nil)
			So(svc.syncOneCluster(ctx, record), ShouldBeNil)
		})

		Convey("should succeed for canary event", func() {
			record := &entity.ClusterTidbPool{
				EventID:    0,
				Type:       constant.TypeCanary,
				ClusterID:  1001,
				CanaryMark: true,
				Status:     constant.StatusUnsynced,
			}
			mgmtFacade.EXPECT().
				GetCluster(gomock.Any(), uint64(1001), tidbmgmtservicev1.ClusterFilter_CLUSTER_VIEW_BASIC).
				Return(mgmtCluster, nil)
			billingFacade.EXPECT().
				GetSpendLimit(gomock.Any(), uint64(1), uint64(1001)).
				Return(spSetting, nil)
			billingFacade.EXPECT().GetSpendLimitInfo(gomock.Any(), spSetting).Return(spInfo, nil)
			clusterRepo.EXPECT().Save(gomock.Any(), gomock.Any()).Return(nil)
			clusterRepo.EXPECT().GetClusterByID(gomock.Any(), uint64(1001)).Return(cluster, nil)
			mgmtFacade.EXPECT().
				UpdateServerlessClusterAnnotations(gomock.Any(), uint64(1001), gomock.Any()).
				Return(mgmtCluster, nil)
			accountFacade.EXPECT().GetTenant(gomock.Any(), uint64(1)).Return(nil, nil)
			branchRepo.EXPECT().
				ListBranchesByMainBranchID(gomock.Any(), "1001", nil).
				Return([]*entity.Branch{}, nil)
			clusterTidbPoolRepo.EXPECT().MarkAsSynced(gomock.Any(), gomock.Any()).Return(nil)
			So(svc.syncOneCluster(ctx, record), ShouldBeNil)
		})
	})
}

func Test_SyncTidbPoolService_syncOneClusterV2_Essential(t *testing.T) {
	ctx := context.Background()

	newEssentialCluster := func(ruLimitMode string) *tidbmgmtservicev1.Cluster {
		autoScaling := &regionalserverlessv1.ServerlessRegionalServiceClusterInfo_AutoScaling{
			MinRcu: 2000,
			MaxRcu: 4000,
		}
		return &tidbmgmtservicev1.Cluster{
			ClusterId: 1001, OrganizationId: 1, ProjectId: 101,
			ClusterInfo: &tidbmgmtservicev1.Cluster_ServerlessCluster{
				ServerlessCluster: &globalserverlessv1.ServerlessServiceCluster{
					UserStatus: globalserverlessv1.UserClusterStatus_USER_CLUSTER_STATUS_NORMAL,
					Cluster: &regionalserverlessv1.ServerlessRegionalServiceClusterInfo{
						Annotations: map[string]string{
							constant.AnnoTidbPoolSelector: "vip-tidb-pool",
							constant.AnnoRuLimitMode:      ruLimitMode,
						},
						AutoScaling: autoScaling,
					},
				},
			},
		}
	}

	setup := func() (
		*SyncTidbPoolService,
		*mock_facade.MockBillingFacade,
		*mock_facade.MockMgmtSvcFacade,
		*mock_repo.MockClusterRepo,
		*mock_repo.MockClusterTidbPoolRepo,
		*mock_repo.MockBranchRepo,
	) {
		ctrl := gomock.NewController(t)
		billingFacade := mock_facade.NewMockBillingFacade(ctrl)
		mgmtFacade := mock_facade.NewMockMgmtSvcFacade(ctrl)
		clusterRepo := mock_repo.NewMockClusterRepo(ctrl)
		clusterTidbPoolRepo := mock_repo.NewMockClusterTidbPoolRepo(ctrl)
		canaryRepo := mock_repo.NewMockCanaryRepo(ctrl)
		accountFacade := mock_facade.NewMockAccountFacade(ctrl)
		eventbusService := eventbus.NewMockEventBusService(ctrl)
		dbProvider := mock_db.NewMockTransProvider(ctrl)
		branchRepo := mock_repo.NewMockBranchRepo(ctrl)
		infraFacade := mock_facade.NewMockInfraFacade(ctrl)
		svc := NewSyncTidbPoolService(
			billingFacade,
			mgmtFacade,
			clusterRepo,
			clusterTidbPoolRepo,
			canaryRepo,
			accountFacade,
			eventbusService,
			dbProvider,
			branchRepo,
			infraFacade,
		)
		return svc, billingFacade, mgmtFacade, clusterRepo, clusterTidbPoolRepo, branchRepo
	}

	Convey("Test_syncOneClusterV2 essential billing events", t, func() {
		Convey("should throttle CR when fit event and EffectivePlan is closing", func() {
			svc, billingFacade, mgmtFacade, clusterRepo, clusterTidbPoolRepo, branchRepo := setup()
			record := &entity.ClusterTidbPool{
				Type:             constant.TypeSpendLimit,
				ClusterID:        1001,
				SpendLimitStatus: constant.SpendLimitEventStatusFit,
				Status:           constant.StatusUnsynced,
			}
			mgmtCluster := newEssentialCluster(constant.RuLimitModeStandard)
			mgmtFacade.EXPECT().
				GetClusterBypassCache(
					gomock.Any(),
					uint64(1001),
					tidbmgmtservicev1.ClusterFilter_CLUSTER_VIEW_BASIC,
				).
				Return(mgmtCluster, nil)
			billingFacade.EXPECT().GetTenantPlan(gomock.Any(), uint64(1)).
				Return(&facade.TenantPlanInfo{
					EffectivePlan: constant.CreditControlEventStatusClosing,
				}, nil)
			mgmtFacade.EXPECT().
				UpdateServerlessClusterAnnotations(gomock.Any(), uint64(1001), map[string]string{
					constant.AnnoRuLimitMode: constant.RuLimitModeThrottled,
					constant.AnnoTenantPlan:  constant.CreditControlEventStatusClosing,
				}).
				Return(mgmtCluster, nil)
			branchRepo.EXPECT().
				ListBranchesByMainBranchID(gomock.Any(), "1001", nil).
				Return([]*entity.Branch{}, nil)
			clusterRepo.EXPECT().Save(gomock.Any(), gomock.Any()).DoAndReturn(
				func(_ context.Context, c *entity.Cluster) bizerr.BizError {
					So(c.RuMode, ShouldEqual, constant.ClusterRUModeThrottled)
					return nil
				},
			)
			clusterTidbPoolRepo.EXPECT().MarkAsSynced(gomock.Any(), gomock.Any()).DoAndReturn(
				func(_ context.Context, r *entity.ClusterTidbPool) bizerr.BizError {
					So(r.RuLimitMode, ShouldEqual, constant.RuLimitModeThrottled)
					return nil
				},
			)
			So(svc.syncOneClusterV2(ctx, record), ShouldBeNil)
		})

		Convey("should restore CR to standard when recovered event and cluster is throttled", func() {
			svc, billingFacade, mgmtFacade, clusterRepo, clusterTidbPoolRepo, branchRepo := setup()
			record := &entity.ClusterTidbPool{
				Type:             constant.TypeSpendLimit,
				ClusterID:        1001,
				SpendLimitStatus: constant.CreditControlEventStatusRecovered,
				Status:           constant.StatusUnsynced,
			}
			mgmtCluster := newEssentialCluster(constant.RuLimitModeThrottled)
			mgmtFacade.EXPECT().
				GetClusterBypassCache(
					gomock.Any(),
					uint64(1001),
					tidbmgmtservicev1.ClusterFilter_CLUSTER_VIEW_BASIC,
				).
				Return(mgmtCluster, nil)
			billingFacade.EXPECT().GetTenantPlan(gomock.Any(), uint64(1)).
				Return(&facade.TenantPlanInfo{EffectivePlan: string(constant.TenantPlanOnDemand)}, nil)
			mgmtFacade.EXPECT().
				UpdateServerlessClusterAnnotations(gomock.Any(), uint64(1001), map[string]string{
					constant.AnnoRuLimitMode: constant.RuLimitModeStandard,
					constant.AnnoTenantPlan:  string(constant.TenantPlanOnDemand),
				}).
				Return(mgmtCluster, nil)
			branchRepo.EXPECT().
				ListBranchesByMainBranchID(gomock.Any(), "1001", nil).
				Return([]*entity.Branch{}, nil)
			clusterRepo.EXPECT().Save(gomock.Any(), gomock.Any()).DoAndReturn(
				func(_ context.Context, c *entity.Cluster) bizerr.BizError {
					So(c.RuMode, ShouldEqual, constant.ClusterRUModeStandard)
					return nil
				},
			)
			clusterTidbPoolRepo.EXPECT().MarkAsSynced(gomock.Any(), gomock.Any()).DoAndReturn(
				func(_ context.Context, r *entity.ClusterTidbPool) bizerr.BizError {
					So(r.RuLimitMode, ShouldEqual, constant.RuLimitModeStandard)
					return nil
				},
			)
			So(svc.syncOneClusterV2(ctx, record), ShouldBeNil)
		})

		Convey("should not update CR when already standard and EffectivePlan is not closing", func() {
			svc, billingFacade, mgmtFacade, clusterRepo, clusterTidbPoolRepo, _ := setup()
			record := &entity.ClusterTidbPool{
				Type:             constant.TypeSpendLimit,
				ClusterID:        1001,
				SpendLimitStatus: constant.SpendLimitEventStatusFit,
				Status:           constant.StatusUnsynced,
			}
			mgmtCluster := newEssentialCluster(constant.RuLimitModeStandard)
			mgmtCluster.GetServerlessCluster().GetCluster().
				Annotations[constant.AnnoTenantPlan] = string(constant.TenantPlanOnDemand)
			mgmtFacade.EXPECT().
				GetClusterBypassCache(
					gomock.Any(),
					uint64(1001),
					tidbmgmtservicev1.ClusterFilter_CLUSTER_VIEW_BASIC,
				).
				Return(mgmtCluster, nil)
			billingFacade.EXPECT().GetTenantPlan(gomock.Any(), uint64(1)).
				Return(&facade.TenantPlanInfo{EffectivePlan: string(constant.TenantPlanOnDemand)}, nil)
			clusterRepo.EXPECT().Save(gomock.Any(), gomock.Any()).DoAndReturn(
				func(_ context.Context, c *entity.Cluster) bizerr.BizError {
					So(c.RuMode, ShouldEqual, constant.ClusterRUModeStandard)
					return nil
				},
			)
			clusterTidbPoolRepo.EXPECT().MarkAsSynced(gomock.Any(), gomock.Any()).Return(nil)
			So(svc.syncOneClusterV2(ctx, record), ShouldBeNil)
		})

		Convey("should skip GetTenantPlan for create_cluster event", func() {
			svc, _, mgmtFacade, clusterRepo, clusterTidbPoolRepo, _ := setup()
			record := &entity.ClusterTidbPool{
				Type:             constant.TypeSpendLimit,
				ClusterID:        1001,
				SpendLimitStatus: constant.SpendLimitEventStatusCreateCluster,
				Status:           constant.StatusUnsynced,
			}
			mgmtCluster := newEssentialCluster(constant.RuLimitModeStandard)
			mgmtFacade.EXPECT().
				GetClusterBypassCache(
					gomock.Any(),
					uint64(1001),
					tidbmgmtservicev1.ClusterFilter_CLUSTER_VIEW_BASIC,
				).
				Return(mgmtCluster, nil)
			clusterRepo.EXPECT().Save(gomock.Any(), gomock.Any()).Return(nil)
			clusterTidbPoolRepo.EXPECT().MarkAsSynced(gomock.Any(), gomock.Any()).Return(nil)
			So(svc.syncOneClusterV2(ctx, record), ShouldBeNil)
		})

		Convey("should return err and not mark synced if GetTenantPlan failed", func() {
			svc, billingFacade, mgmtFacade, _, _, _ := setup()
			record := &entity.ClusterTidbPool{
				Type:             constant.TypeSpendLimit,
				ClusterID:        1001,
				SpendLimitStatus: constant.SpendLimitEventStatusExcessive,
				Status:           constant.StatusUnsynced,
			}
			mgmtCluster := newEssentialCluster(constant.RuLimitModeStandard)
			mgmtFacade.EXPECT().
				GetClusterBypassCache(
					gomock.Any(),
					uint64(1001),
					tidbmgmtservicev1.ClusterFilter_CLUSTER_VIEW_BASIC,
				).
				Return(mgmtCluster, nil)
			billingFacade.EXPECT().GetTenantPlan(gomock.Any(), uint64(1)).
				Return(nil, bizerr.New(bizerr.ErrCodeUnknown, "failed"))
			So(svc.syncOneClusterV2(ctx, record), ShouldNotBeNil)
		})
	})
}

func Test_SyncTidbPoolService_syncOneClusterV2_Starter(t *testing.T) {
	ctx := context.Background()

	newStarterCluster := func(ruLimitMode string) *tidbmgmtservicev1.Cluster {
		return &tidbmgmtservicev1.Cluster{
			ClusterId: 1001, OrganizationId: 1, ProjectId: 101, ClusterName: "cluster1",
			CloudProvider: "aws", Region: "us-west-2",
			ClusterInfo: &tidbmgmtservicev1.Cluster_ServerlessCluster{
				ServerlessCluster: &globalserverlessv1.ServerlessServiceCluster{
					UserStatus: globalserverlessv1.UserClusterStatus_USER_CLUSTER_STATUS_NORMAL,
					Cluster: &regionalserverlessv1.ServerlessRegionalServiceClusterInfo{
						Annotations: map[string]string{
							constant.AnnoTidbPoolSelector: constant.TidbPoolNameVip,
							constant.AnnoRuLimitMode:      ruLimitMode,
						},
					},
				},
			},
		}
	}

	Convey("Test_syncOneClusterV2 starter billing events", t, func() {
		Convey("should write tenant-plan when updating CR annotations", func() {
			ctrl := gomock.NewController(t)
			billingFacade := mock_facade.NewMockBillingFacade(ctrl)
			mgmtFacade := mock_facade.NewMockMgmtSvcFacade(ctrl)
			clusterRepo := mock_repo.NewMockClusterRepo(ctrl)
			clusterTidbPoolRepo := mock_repo.NewMockClusterTidbPoolRepo(ctrl)
			accountFacade := mock_facade.NewMockAccountFacade(ctrl)
			eventbusService := eventbus.NewMockEventBusService(ctrl)
			branchRepo := mock_repo.NewMockBranchRepo(ctrl)
			svc := NewSyncTidbPoolService(
				billingFacade,
				mgmtFacade,
				clusterRepo,
				clusterTidbPoolRepo,
				mock_repo.NewMockCanaryRepo(ctrl),
				accountFacade,
				eventbusService,
				mock_db.NewMockTransProvider(ctrl),
				branchRepo,
				mock_facade.NewMockInfraFacade(ctrl),
			)
			record := &entity.ClusterTidbPool{
				Type:             constant.TypeSpendLimit,
				ClusterID:        1001,
				SpendLimitStatus: constant.SpendLimitEventStatusExcessive,
				Status:           constant.StatusUnsynced,
			}
			mgmtCluster := newStarterCluster(constant.RuLimitModeStandard)
			spSetting := &facade.SpendLimitSetting{
				Limit:   decimal.NewFromFloat(5),
				BizData: `{"free_credits": 6, "invoice_limit": 5, "active_total_credits": 6}`,
			}
			spInfo := &facade.SpendLimitInfo{
				SpendLimitSetting: facade.SpendLimitSetting{
					Status: constant.SpendLimitEventStatusExcessive,
				},
				Costs: decimal.NewFromFloat(6.1),
			}
			mgmtFacade.EXPECT().
				GetClusterBypassCache(
					gomock.Any(),
					uint64(1001),
					tidbmgmtservicev1.ClusterFilter_CLUSTER_VIEW_BASIC,
				).
				Return(mgmtCluster, nil)
			billingFacade.EXPECT().
				GetSpendLimit(gomock.Any(), uint64(1), uint64(1001)).
				Return(spSetting, nil)
			billingFacade.EXPECT().GetSpendLimitInfo(gomock.Any(), spSetting).Return(spInfo, nil)
			clusterRepo.EXPECT().
				GetClusterByID(gomock.Any(), uint64(1001)).
				Return(&entity.Cluster{CanaryMark: false}, nil)
			mgmtFacade.EXPECT().
				UpdateServerlessClusterAnnotations(gomock.Any(), uint64(1001), map[string]string{
					constant.AnnoRuLimitMode: constant.RuLimitModeThrottled,
					constant.AnnoTenantPlan:  constant.SpendLimitEventStatusExcessive,
				}).
				Return(mgmtCluster, nil)
			clusterRepo.EXPECT().
				GetClusterByID(gomock.Any(), uint64(1001)).
				Return(&entity.Cluster{CanaryMark: false}, nil)
			clusterRepo.EXPECT().Save(gomock.Any(), gomock.Any()).Return(nil)
			eventbusService.EXPECT().Publish(gomock.Any()).Return(nil)
			clusterRepo.EXPECT().
				UpdateRUMode(gomock.Any(), uint64(1001), constant.ClusterRUMode("throttled")).
				Return(nil)
			accountFacade.EXPECT().GetTenant(gomock.Any(), uint64(1)).Return(nil, nil)
			branchRepo.EXPECT().
				ListBranchesByMainBranchID(gomock.Any(), "1001", nil).
				Return([]*entity.Branch{}, nil)
			clusterTidbPoolRepo.EXPECT().MarkAsSynced(gomock.Any(), gomock.Any()).Return(nil)
			So(svc.syncOneClusterV2(ctx, record), ShouldBeNil)
		})
	})
}
