package service

import (
	"context"
	"testing"

	"github.com/shopspring/decimal"
	. "github.com/smartystreets/goconvey/convey"
	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/serverless-service/internal/config"
	"github.com/tidbcloud/serverless-service/internal/domain/constant"
	"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/vo"
	"github.com/tidbcloud/serverless-service/internal/errorcode"
	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 Test_BillingService_PrefillSkuPriceCache(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()
	billingFacade := mock_facade.NewMockBillingFacade(ctrl)
	o11yFacade := mock_facade.NewMockO11yFacade(ctrl)

	skuPrice := map[string]*facade.BillingCharge{
		"serverless-row-storage:aws:xxx":    {RawPrice: decimal.NewFromFloat(0.0001953125)},
		"serverless-column-storage:aws:xxx": {RawPrice: decimal.NewFromFloat(0.0001953125)},
		"serverless-request-unit:aws:xxx":   {RawPrice: decimal.NewFromFloat(0.0000001)},
	}

	Convey("Test_PrefillSkuPriceCache", t, func() {
		Convey("should retry if calc fee failed", func() {
			e := bizerr.New(bizerr.ErrCodeUnknown, "failed")
			config.Global().WorkerConfig.RegionalConfig = []config.RegionalConfig{{Provider: "aws", Region: "test1"}}
			billingFacade.EXPECT().CalcFee(gomock.Any(), gomock.Any()).Return(nil, e)
			billingFacade.EXPECT().CalcFee(gomock.Any(), gomock.Any()).Return(nil, e)
			billingFacade.EXPECT().CalcFee(gomock.Any(), gomock.Any()).Return(nil, e)
			_ = NewBillingService(billingFacade, o11yFacade)
		})

		Convey("should succeed", func() {
			config.Global().WorkerConfig.RegionalConfig = []config.RegionalConfig{
				{Provider: "aws", Region: "test1"},
				{Provider: "alicloud", Region: "test2"},
			}
			billingFacade.EXPECT().CalcFee(gomock.Any(), gomock.Any()).Return(skuPrice, nil)
			billingFacade.EXPECT().CalcFee(gomock.Any(), gomock.Any()).Return(skuPrice, nil)
			_ = NewBillingService(billingFacade, o11yFacade)
		})
	})
}

func Test_BillingService_GetTenantSpendLimitsSettings(t *testing.T) {
	ctx := context.Background()
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()
	billingFacade := mock_facade.NewMockBillingFacade(ctrl)
	o11yFacade := mock_facade.NewMockO11yFacade(ctrl)
	svc := NewBillingService(billingFacade, o11yFacade)

	tenantID := uint64(1)
	clusterMap := map[uint64]*tidbmgmtservicev1.Cluster{
		1000: {ClusterId: 1000, OrganizationId: 1},
		1001: {ClusterId: 1001, OrganizationId: 1},
	}

	Convey("Test_GetAllSpendLimits", t, func() {
		Convey("should return err if list clusters failed", func() {
			e := bizerr.New(bizerr.ErrCodeUnknown, "failed")
			billingFacade.EXPECT().GetSpendLimitSettings(ctx, tenantID).Return(nil, e)
			_, err := svc.GetTenantSpendLimitsSettings(ctx, tenantID, clusterMap, true)
			So(err, ShouldNotBeNil)
		})
		Convey("should return err if get unmarshalable bizdata", func() {
			settings := []*facade.SpendLimitSetting{
				{
					TenantID:  1,
					RangeType: constant.SpendLimitRangeTypeCluster,
					RangeID:   1000,
					BizData:   `{"free_credits": 6, "invoice_limit": 5, "active_total_credits": 6}`,
				},
				{TenantID: 1, RangeType: constant.SpendLimitRangeTypeCluster, RangeID: 1001, BizData: `unmarshalable`},
			}
			billingFacade.EXPECT().GetSpendLimitSettings(ctx, tenantID).Return(settings, nil)
			_, err := svc.GetTenantSpendLimitsSettings(ctx, tenantID, clusterMap, true)
			So(err, ShouldNotBeNil)
		})
		// TODO wjz: review if the comparation is needed
		// Convey("should succeed if number of cluster Spending Limit not match", func() {
		//	settings := []*facade.SpendLimitSetting{
		//		{TenantID: 1, RangeType: constant.SpendLimitRangeTypeCluster, RangeID: 1000, BizData: `{"free_credits": 6, "invoice_limit": 5, "active_total_credits": 6}`},
		//		{TenantID: 1, RangeType: constant.SpendLimitRangeTypeCluster, RangeID: 1002, BizData: `{"free_credits": 6, "invoice_limit": 1, "active_total_credits": 6}`},
		//	}
		//	billingFacade.EXPECT().GetSpendLimitSettings(ctx, tenantID).Return(settings, nil)
		//	_, err := svc.GetTenantSpendLimitsSettings(ctx, tenantID, clusterMap)
		//	So(err, ShouldNotBeNil)
		// })
		Convey("should succeed", func() {
			settings := []*facade.SpendLimitSetting{
				{
					TenantID:  1,
					RangeType: constant.SpendLimitRangeTypeCluster,
					RangeID:   1000,
					BizData:   `{"free_credits": 6, "invoice_limit": 5, "active_total_credits": 6}`,
				},
				{
					TenantID:  1,
					RangeType: constant.SpendLimitRangeTypeCluster,
					RangeID:   1001,
					BizData:   `{"free_credits": 6, "invoice_limit": 1, "active_total_credits": 6}`,
				},
			}
			billingFacade.EXPECT().GetSpendLimitSettings(ctx, tenantID).Return(settings, nil)
			_, err := svc.GetTenantSpendLimitsSettings(ctx, tenantID, clusterMap, true)
			So(err, ShouldBeNil)
		})
		Convey("should succeed if range type is not cluster", func() {
			settings := []*facade.SpendLimitSetting{
				{TenantID: 1, RangeType: constant.SpendLimitRangeTypeProject, RangeID: 101, BizData: ``},
				{
					TenantID:  1,
					RangeType: constant.SpendLimitRangeTypeCluster,
					RangeID:   1000,
					BizData:   `{"free_credits": 6, "invoice_limit": 5, "active_total_credits": 6}`,
				},
				{
					TenantID:  1,
					RangeType: constant.SpendLimitRangeTypeCluster,
					RangeID:   1001,
					BizData:   `{"free_credits": 6, "invoice_limit": 1, "active_total_credits": 6}`,
				},
			}
			billingFacade.EXPECT().GetSpendLimitSettings(ctx, tenantID).Return(settings, nil)
			_, err := svc.GetTenantSpendLimitsSettings(ctx, tenantID, clusterMap, true)
			So(err, ShouldBeNil)
		})
		Convey("should succeed if invalid cluster Spending Limit encountered", func() {
			settings := []*facade.SpendLimitSetting{
				{
					TenantID:  1,
					RangeType: constant.SpendLimitRangeTypeCluster,
					RangeID:   1000,
					BizData:   `{"free_credits": 6, "invoice_limit": 5, "active_total_credits": 6}`,
				},
				{
					TenantID:  1,
					RangeType: constant.SpendLimitRangeTypeCluster,
					RangeID:   1001,
					BizData:   `{"free_credits": 6, "invoice_limit": 1, "active_total_credits": 6}`,
				},
				{
					TenantID:  1,
					RangeType: constant.SpendLimitRangeTypeCluster,
					RangeID:   1002,
					BizData:   `{"free_credits": 6, "invoice_limit": 1, "active_total_credits": 6}`,
				},
			}
			billingFacade.EXPECT().GetSpendLimitSettings(ctx, tenantID).Return(settings, nil)
			_, err := svc.GetTenantSpendLimitsSettings(ctx, tenantID, clusterMap, true)
			So(err, ShouldBeNil)
		})
	})
}

func Test_BillingService_GetSkuUnitPrices(t *testing.T) {
	ctx := context.Background()
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()
	billingFacade := mock_facade.NewMockBillingFacade(ctrl)
	o11yFacade := mock_facade.NewMockO11yFacade(ctrl)
	svc := NewBillingService(billingFacade, o11yFacade)

	provider := config.ProviderAWS
	region := "us-east-1"
	tenantID := uint64(1)
	skuPrice := map[string]*facade.BillingCharge{
		"serverless-row-storage:aws:us-east-1:starter":             {RawPrice: decimal.NewFromFloat(0.0001953125)},
		"serverless-column-storage:aws:us-east-1:starter":          {RawPrice: decimal.NewFromFloat(0.0001953125)},
		"serverless-request-unit:aws:us-east-1:starter":            {RawPrice: decimal.NewFromFloat(0.0000001)},
		"serverless-request-capacity-unit:aws:us-east-1:essential": {RawPrice: decimal.NewFromFloat(0.0000001)},
	}
	starterOnlySkuPrice := map[string]*facade.BillingCharge{
		"serverless-row-storage:aws:sa-east-1:starter":    {RawPrice: decimal.NewFromFloat(0.0001953125)},
		"serverless-column-storage:aws:sa-east-1:starter": {RawPrice: decimal.NewFromFloat(0.0001953125)},
		"serverless-request-unit:aws:sa-east-1:starter":   {RawPrice: decimal.NewFromFloat(0.0000001)},
	}

	Convey("Test_GetSkuUnitPrices", t, func() {
		Convey("should return err if calc fee failed", func() {
			e := bizerr.New(bizerr.ErrCodeUnknown, "failed")
			billingFacade.EXPECT().CalcFee(ctx, gomock.Any()).Return(nil, e)
			_, err := svc.GetSkuUnitPrices(ctx, provider, region, tenantID)
			So(err, ShouldNotBeNil)
		})
		Convey("should succeed w/ cache miss", func() {
			billingFacade.EXPECT().CalcFee(ctx, gomock.Any()).Return(skuPrice, nil)
			res, err := svc.GetSkuUnitPrices(ctx, provider, region, tenantID)
			So(err, ShouldBeNil)
			So(res[constant.SkuTypeRowStorage].RawPrice.String(), ShouldEqual, "8.5546875")
			So(res[constant.SkuTypeColumnStorage].RawPrice.String(), ShouldEqual, "8.5546875")
			So(res[constant.SkuTypeRequestUnit].RawPrice.String(), ShouldEqual, "0.0000001")
		})
		Convey("should succeed w/ cache hit", func() {
			res, err := svc.GetSkuUnitPrices(ctx, provider, region, tenantID)
			So(err, ShouldBeNil)
			So(res[constant.SkuTypeRowStorage].RawPrice.String(), ShouldEqual, "8.5546875")
			So(res[constant.SkuTypeColumnStorage].RawPrice.String(), ShouldEqual, "8.5546875")
			So(res[constant.SkuTypeRequestUnit].RawPrice.String(), ShouldEqual, "0.0000001")
		})
		Convey("should retry without essential when essential sku not exists", func() {
			starterOnlyRegion := "sa-east-1"
			RegionalSkuPriceCache.Delete(provider + "-" + starterOnlyRegion)
			config.Global().WorkerConfig.RegionalConfig = []config.RegionalConfig{
				{Provider: provider, Region: starterOnlyRegion},
			}
			essentialMissing := bizerr.New(
				bizerr.ErrCodeSvcBilling,
				`http status code != 200, resp: {"code":"ObjectNotExists","error":"Object Not Exists: unit serverless-request-capacity-unit:aws:sa-east-1:essential"}`,
			)
			billingFacade.EXPECT().CalcFee(ctx, gomock.Any()).Return(nil, essentialMissing)
			billingFacade.EXPECT().CalcFee(ctx, gomock.Any()).DoAndReturn(
				func(_ context.Context, reports []*facade.BillingReport) (map[string]*facade.BillingCharge, bizerr.BizError) {
					So(len(reports), ShouldEqual, 3)
					for _, report := range reports {
						So(report.Sku, ShouldNotContainSubstring, "essential")
					}
					return starterOnlySkuPrice, nil
				},
			)
			res, err := svc.GetSkuUnitPrices(ctx, provider, starterOnlyRegion, tenantID)
			So(err, ShouldBeNil)
			So(res[constant.SkuTypeRowStorage].RawPrice.String(), ShouldEqual, "8.5546875")
			So(res[constant.SkuTypeColumnStorage].RawPrice.String(), ShouldEqual, "8.5546875")
			So(res[constant.SkuTypeRequestUnit].RawPrice.String(), ShouldEqual, "0.0000001")
			_, hasEssential := res[constant.SkuTypeRequestCapacityUnit]
			So(hasEssential, ShouldBeFalse)
		})
		Convey("should fallback to aws us-east-1 when region sku is missing", func() {
			originalLark := config.Global().ServerConfig.EnableLarkMsg
			config.Global().ServerConfig.EnableLarkMsg = false
			defer func() {
				config.Global().ServerConfig.EnableLarkMsg = originalLark
			}()

			missingProvider := config.ProviderGCP
			missingRegion := "us-east-1"
			RegionalSkuPriceCache.Delete(missingProvider + "-" + missingRegion)
			RegionalSkuPriceCache.Delete(config.ProviderAWS + "-" + defaultSkuRegion)
			config.Global().WorkerConfig.RegionalConfig = []config.RegionalConfig{
				{Provider: missingProvider, Region: missingRegion},
				{Provider: config.ProviderAWS, Region: defaultSkuRegion},
			}
			missing := bizerr.New(
				bizerr.ErrCodeSvcBilling,
				`http status code != 200, resp: {"code":"ObjectNotExists"}`,
			)
			fallbackSkuPrice := map[string]*facade.BillingCharge{
				"serverless-row-storage:aws:us-east-1:starter": {
					RawPrice: decimal.NewFromFloat(0.0001953125),
				},
				"serverless-column-storage:aws:us-east-1:starter": {
					RawPrice: decimal.NewFromFloat(0.0001953125),
				},
				"serverless-request-unit:aws:us-east-1:starter": {
					RawPrice: decimal.NewFromFloat(0.0000001),
				},
				"serverless-request-capacity-unit:aws:us-east-1:essential": {
					RawPrice: decimal.NewFromFloat(0.0000001),
				},
			}
			billingFacade.EXPECT().CalcFee(gomock.Any(), gomock.Any()).Return(nil, missing)
			billingFacade.EXPECT().CalcFee(gomock.Any(), gomock.Any()).Return(nil, missing)
			billingFacade.EXPECT().CalcFee(gomock.Any(), gomock.Any()).Return(fallbackSkuPrice, nil)

			res, err := svc.GetSkuUnitPrices(ctx, missingProvider, missingRegion, tenantID)
			So(err, ShouldBeNil)
			So(res[constant.SkuTypeRowStorage].RawPrice.String(), ShouldEqual, "8.5546875")
			So(res[constant.SkuTypeColumnStorage].RawPrice.String(), ShouldEqual, "8.5546875")
			So(res[constant.SkuTypeRequestUnit].RawPrice.String(), ShouldEqual, "0.0000001")
		})
	})
}

func Test_BillingService_GetSkuStorageUsage(t *testing.T) {
	ctx := context.Background()
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()
	billingFacade := mock_facade.NewMockBillingFacade(ctrl)
	o11yFacade := mock_facade.NewMockO11yFacade(ctrl)
	svc := NewBillingService(billingFacade, o11yFacade)

	cluster := &tidbmgmtservicev1.Cluster{
		ClusterId:      1001,
		OrganizationId: 1,
		ProjectId:      101,
		CloudProvider:  "aws",
		Region:         "us-east-1",
		ClusterInfo: &tidbmgmtservicev1.Cluster_ServerlessCluster{
			ServerlessCluster: &globalserverlessv1.ServerlessServiceCluster{
				Cluster: &regionalserverlessv1.ServerlessRegionalServiceClusterInfo{
					KeyspaceId: 2333,
					Kubernetes: "dev-us-east-f01",
				},
				UserStatus: globalserverlessv1.UserClusterStatus_USER_CLUSTER_STATUS_NORMAL,
			},
		},
	}

	Convey("Test_GetSkuStorageUsage", t, func() {
		Convey("should return err if get o11y addr failed", func() {
			config.Global().WorkerConfig.RegionalConfig = nil
			_, err := svc.GetSkuStorageUsage(ctx, cluster)
			So(err, ShouldNotBeNil)
		})

		Convey("should return err if get storage usage failed", func() {
			e := bizerr.New(bizerr.ErrCodeUnknown, "failed")
			config.Global().WorkerConfig.RegionalConfig = []config.RegionalConfig{
				{
					Provider:  "aws",
					Region:    "us-east-1",
					K8sConfig: []config.K8sConfig{{Kubernetes: "dev-us-east-f01", O11yUrl: "http://test"}},
				},
			}
			o11yFacade.EXPECT().GetClusterSkuUsageByRegion(ctx, "1001", uint32(2333), "http://test").Return(nil, e)
			_, err := svc.GetSkuStorageUsage(ctx, cluster)
			So(err, ShouldNotBeNil)
		})

		Convey("get sku storage", func() {
			usages := map[string]float64{"row_storage": 512}
			config.Global().WorkerConfig.RegionalConfig = []config.RegionalConfig{
				{
					Provider:  "aws",
					Region:    "us-east-1",
					K8sConfig: []config.K8sConfig{{Kubernetes: "dev-us-east-f01", O11yUrl: "http://test"}},
				},
			}
			o11yFacade.EXPECT().GetClusterSkuUsageByRegion(ctx, "1001", uint32(2333), "http://test").Return(usages, nil)
			res, err := svc.GetSkuStorageUsage(ctx, cluster)
			So(err, ShouldBeNil)
			So(res[constant.SkuTypeRowStorage], ShouldEqual, 512)
		})
	})
}

func Test_BillingService_GetBranchSkuUsage(t *testing.T) {
	ctx := context.Background()
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()
	billingFacade := mock_facade.NewMockBillingFacade(ctrl)
	o11yFacade := mock_facade.NewMockO11yFacade(ctrl)
	svc := NewBillingService(billingFacade, o11yFacade)

	branch := &vo.Branch{
		ID:                "1001",
		MainBranchID:      "2002",
		KeyspaceId:        2334,
		TenantID:          1,
		RegionName:        "cn-east-1",
		CloudProviderName: "aws",
		Kubernetes:        "dev-cn-east-f01",
		AutoScaling: vo.AutoScaling{
			MinRcu: 0,
			MaxRcu: 0,
		},
	}

	Convey("Test_GetBranchSkuUsage", t, func() {
		Convey("should return err if get o11y addr failed", func() {
			config.Global().WorkerConfig.RegionalConfig = nil
			_, err := svc.GetBranchSkuUsage(ctx, branch)
			So(err, ShouldNotBeNil)
		})

		Convey("should return err if essential plan", func() {
			branch.AutoScaling = vo.AutoScaling{
				MinRcu: 1,
				MaxRcu: 1,
			}
			_, err := svc.GetBranchSkuUsage(ctx, branch)
			So(err.Error(), ShouldContainSubstring, errorcode.ErrMsgInvalidClusterPlan)
			branch.AutoScaling = vo.AutoScaling{
				MinRcu: 0,
				MaxRcu: 0,
			}
		})

		Convey("should return err if get storage usage failed", func() {
			e := bizerr.New(bizerr.ErrCodeUnknown, "failed")
			config.Global().WorkerConfig.RegionalConfig = []config.RegionalConfig{
				{
					Provider:  "aws",
					Region:    "cn-east-1",
					K8sConfig: []config.K8sConfig{{Kubernetes: "dev-cn-east-f01", O11yUrl: "http://test"}},
				},
			}
			o11yFacade.EXPECT().GetClusterSkuUsageByRegion(ctx, "1001", uint32(2334), "http://test").Return(nil, e)
			_, err := svc.GetBranchSkuUsage(ctx, branch)
			So(err, ShouldNotBeNil)
		})

		Convey("should succeed w/ branch billing", func() {
			usages := map[string]float64{"row_storage": 512}
			cost := &facade.CompleteCostExplorerResult{
				InternalCosts: facade.CostExplorerResult{
					CreditsCosts: facade.Costs{
						KindCategoryUnitsCost: map[string]facade.CategoryItemCosts{
							"TiDB Cloud Starter": {
								CategoryItemUnitsCosts: map[string]facade.CategoryCosts{
									"Request Units": {
										Price: facade.Price{
											RawPrice: decimal.NewFromFloat(1.0),
										},
									},
								},
							},
						},
					},
				},
			}
			skuPrice := map[string]*facade.BillingCharge{
				"serverless-request-unit:aws:cn-east-1:starter": {RawPrice: decimal.NewFromFloat(0.0000001)},
			}
			config.Global().WorkerConfig.RegionalConfig = []config.RegionalConfig{
				{
					Provider:  "aws",
					Region:    "cn-east-1",
					K8sConfig: []config.K8sConfig{{Kubernetes: "dev-cn-east-f01", O11yUrl: "http://test"}},
				},
			}
			o11yFacade.EXPECT().GetClusterSkuUsageByRegion(ctx, "1001", uint32(2334), "http://test").Return(usages, nil)
			billingFacade.EXPECT().CalcFee(ctx, gomock.Any()).Return(skuPrice, nil)
			billingFacade.EXPECT().CostExplorerV2(gomock.Any(), uint64(1), gomock.Any()).Return(cost, nil)
			res, err := svc.GetBranchSkuUsage(ctx, branch)
			So(err, ShouldBeNil)
			So(res[constant.SkuTypeRowStorage], ShouldEqual, 512)
			So(res[constant.SkuTypeRequestUnit], ShouldEqual, 10000000)
		})
	})
}

func Test_BillingService_HasFreeCreditQuota(t *testing.T) {
	ctx := context.Background()
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()
	billingFacade := mock_facade.NewMockBillingFacade(ctrl)
	o11yFacade := mock_facade.NewMockO11yFacade(ctrl)
	svc := NewBillingService(billingFacade, o11yFacade)

	spendlimits := map[uint64]*facade.ServerlessSpendLimitBizData{
		1000: {FreeCredits: decimal.NewFromInt(6)},
		1001: {FreeCredits: decimal.NewFromInt(6)},
	}

	Convey("Test_HasFreeCreditQuota", t, func() {
		Convey("should return false if exceeds max count", func() {
			config.Global().BizConfig.ServerlessFreeQuotaMaxCount = 1
			res := svc.HasFreeCreditQuota(ctx, spendlimits)
			So(res, ShouldBeFalse)
		})
		Convey("should return true", func() {
			config.Global().BizConfig.ServerlessFreeQuotaMaxCount = 3
			res := svc.HasFreeCreditQuota(ctx, spendlimits)
			So(res, ShouldBeTrue)
		})
	})
}

func Test_BillingService_CalculateClusterCostThisMonth(t *testing.T) {
	ctx := context.Background()
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()
	billingFacade := mock_facade.NewMockBillingFacade(ctrl)
	o11yFacade := mock_facade.NewMockO11yFacade(ctrl)
	svc := NewBillingService(billingFacade, o11yFacade)

	cost := &facade.CompleteCostExplorerResult{
		InternalCosts: facade.CostExplorerResult{
			CreditsCosts: facade.Costs{
				KindCategoryUnitsCost: map[string]facade.CategoryItemCosts{
					"TiDB Cloud Starter": {
						CategoryItemUnitsCosts: map[string]facade.CategoryCosts{
							"Request Units": {
								Order: 1,
								Price: facade.Price{
									RawPrice: decimal.NewFromFloat(0.4),
								},
							},
							"Row-based Storage": {
								Order: 1,
								Price: facade.Price{
									RawPrice: decimal.NewFromFloat(0.2),
								},
							},
						},
					},
				},
			},
		},
		ExternalCosts: facade.CostExplorerResult{
			TotalCosts: facade.Costs{
				KindCategoryUnitsCost: map[string]facade.CategoryItemCosts{
					"TiDB Cloud Starter": {
						CategoryItemUnitsCosts: map[string]facade.CategoryCosts{
							"Request Units": {
								Order: 1,
								Price: facade.Price{
									RawPrice: decimal.NewFromFloat(1.4),
								},
							},
							"Row-based Storage": {
								Order: 1,
								Price: facade.Price{
									RawPrice: decimal.NewFromFloat(1.2),
								},
							},
						},
					},
				},
			},
		},
	}

	Convey("Test_CalculateClusterCostThisMonth", t, func() {
		Convey("should return err if calculate cost failed", func(c C) {
			e := bizerr.New(bizerr.ErrCodeUnknown, "failed")
			billingFacade.EXPECT().CostExplorerV2(ctx, uint64(1), gomock.Any()).DoAndReturn(
				func(ctx context.Context, tenantID uint64, query *facade.CostExplorerInternalQuery) (*facade.CompleteCostExplorerResult, bizerr.BizError) {
					c.So(query.Labels["clusterID"], ShouldEqual, "1001")
					c.So(query.Units[0], ShouldEqual, "serverless-row-storage:aws:us-east-1:starter")
					return nil, e
				},
			)
			_, err := svc.CalculateClusterCostThisMonth(
				ctx,
				uint64(1),
				"1001",
				constant.ClusterTypeCluster,
				[]constant.SkuType{constant.SkuTypeRowStorage},
				config.ProviderAWS,
				"us-east-1",
			)
			So(err, ShouldNotBeNil)
		})
		Convey("should succeed", func(c C) {
			billingFacade.EXPECT().CostExplorerV2(ctx, uint64(1), gomock.Any()).Return(cost, nil)
			cost, err := svc.CalculateClusterCostThisMonth(
				ctx,
				uint64(1),
				"1001",
				constant.ClusterTypeCluster,
				[]constant.SkuType{constant.SkuTypeRowStorage},
				config.ProviderAWS,
				"us-east-1",
			)
			res := svc.GetCostExplorerServerlessSkuCost(cost)
			So(err, ShouldBeNil)
			So(res.RuFreeCost, ShouldEqual, decimal.NewFromFloat(0.4))
			So(res.RowStoreFreeCost, ShouldEqual, decimal.NewFromFloat(0.2))
			So(res.RuSpentCost, ShouldEqual, decimal.NewFromFloat(1.4))
			So(res.RowStoreSpentCost, ShouldEqual, decimal.NewFromFloat(1.2))
		})
	})
}

func TestValidateSpendLimitMax(t *testing.T) {
	Convey("ValidateSpendLimitMax", t, func() {
		prev := config.Global().BizConfig.SpendLimitMaxOverrideTenants
		defer func() { config.Global().BizConfig.SpendLimitMaxOverrideTenants = prev }()

		Convey("rejects value above default max", func() {
			config.Global().BizConfig.SpendLimitMaxOverrideTenants = nil
			err := ValidateSpendLimitMax(1, constant.MaxSpendLimitValue+1)
			So(err, ShouldNotBeNil)
			So(err.Error(), ShouldContainSubstring, "10000")
		})

		Convey("allows value within override max", func() {
			config.Global().BizConfig.SpendLimitMaxOverrideTenants = map[uint64]uint32{42: 50000000}
			err := ValidateSpendLimitMax(42, 2000000)
			So(err, ShouldBeNil)
		})

		Convey("rejects value above override max", func() {
			config.Global().BizConfig.SpendLimitMaxOverrideTenants = map[uint64]uint32{42: 50000000}
			err := ValidateSpendLimitMax(42, 50000001)
			So(err, ShouldNotBeNil)
			So(err.Error(), ShouldContainSubstring, "500000")
		})

		Convey("zero override falls back to default", func() {
			config.Global().BizConfig.SpendLimitMaxOverrideTenants = map[uint64]uint32{42: 0}
			err := ValidateSpendLimitMax(42, constant.MaxSpendLimitValue+1)
			So(err, ShouldNotBeNil)
		})
	})
}
