package tests

import (
	"strconv"
	"time"

	"github.com/google/uuid"
	"github.com/pingcap/dbaas/billing2/config"
	"github.com/pingcap/dbaas/billing2/constant"
	"github.com/pingcap/dbaas/billing2/db"
	"github.com/pingcap/dbaas/billing2/db/entity"
	"github.com/pingcap/dbaas/billing2/model/billingconfig"
	"github.com/pingcap/dbaas/billing2/model/costexplorer"
	"github.com/pingcap/dbaas/billing2/utils/tests/datainit"
	"github.com/shopspring/decimal"
)

func (suite *CostExplorerSuite) TestOpenCostsReturnsDailySummaryAndDetails() {
	assert := suite.Assert()
	unit := "open-costs-" + uuid.NewString()
	suite.createStreamTestUnit(unit)
	billingConfigID, err := (&billingconfig.BillingConfigModel{}).GetActiveBillingConfigID(1)
	assert.NoError(err)
	date := datainit.BillingDate
	suite.insertDetailCost(1, billingConfigID, date.Add(time.Hour).Unix(), unit, map[string]string{}, entity.ConsumedSource_Invoice, decimal.NewFromInt(10), decimal.NewFromInt(9), false)
	suite.insertDetailCost(1, billingConfigID, date.Add(2*time.Hour).Unix(), unit, map[string]string{}, entity.ConsumedSource_Credits, decimal.NewFromInt(2), decimal.NewFromInt(2), false)
	suite.insertDetailCost(1, billingConfigID, date.AddDate(0, 0, 1).Add(time.Hour).Unix(), unit, map[string]string{}, entity.ConsumedSource_Invoice, decimal.NewFromInt(100), decimal.NewFromInt(100), false)
	bindedPackageID, discountID := suite.createMatchedAdjustmentCatalog(1, billingConfigID)
	assert.NoError(db.GetDB().Model(&entity.UserUnitCost{}).
		Where("unit = ? AND consumed_source = ?", unit, entity.ConsumedSource_Invoice).
		Update("discount_id", discountID).Error)
	assert.NoError(db.GetDB().Model(&entity.UserUnitCost{}).
		Where("unit = ? AND consumed_source = ?", unit, entity.ConsumedSource_Credits).
		Update("binded_package_id", bindedPackageID).Error)

	res, err := (&costexplorer.CostExplorerModel{}).CalculateOpenCosts(1, billingConfigID, date)
	assert.NoError(err)
	suite.Require().NotNil(res)
	assert.Equal("legacy_open_costs", res.Meta.SourceMode)
	assert.Equal(date.Format("2006-01-02"), res.BillsOverView.BilledMonth)
	assert.True(decimal.NewFromInt(12).Equal(res.BillsOverView.RunningTotal), res.BillsOverView.RunningTotal.String())
	assert.True(decimal.NewFromInt(1).Equal(res.BillsOverView.Discounts), res.BillsOverView.Discounts.String())
	assert.True(decimal.NewFromInt(2).Equal(res.BillsOverView.Credits), res.BillsOverView.Credits.String())
	assert.True(decimal.NewFromInt(9).Equal(res.BillsOverView.TotalCost), res.BillsOverView.TotalCost.String())
	assert.True(decimal.NewFromInt(2).Equal(res.Meta.MatchedCredits), res.Meta.MatchedCredits.String())
	assert.True(decimal.NewFromInt(1).Equal(res.Meta.MatchedDiscounts), res.Meta.MatchedDiscounts.String())
	assert.Equal(strconv.FormatUint(uint64(bindedPackageID), 10), res.Meta.MatchedCreditPackageIDs)
	assert.Equal(strconv.FormatUint(uint64(discountID), 10), res.Meta.MatchedDiscountIDs)
	assert.Len(res.Meta.MatchedCreditItems, 1)
	suite.Require().NotNil(res.Meta.MatchedCreditItems[0].BindedPackageID)
	assert.Equal(bindedPackageID, *res.Meta.MatchedCreditItems[0].BindedPackageID)
	assert.Equal("rerun-credit-code", res.Meta.MatchedCreditItems[0].CreditsCode)
	assert.True(decimal.NewFromInt(100).Equal(res.Meta.MatchedCreditItems[0].TotalCredits), res.Meta.MatchedCreditItems[0].TotalCredits.String())
	assert.True(decimal.NewFromInt(98).Equal(res.Meta.MatchedCreditItems[0].RemainingCredits), res.Meta.MatchedCreditItems[0].RemainingCredits.String())
	assert.Len(res.Meta.MatchedDiscountItems, 1)
	assert.Equal(discountID, res.Meta.MatchedDiscountItems[0].DiscountID)
	assert.Equal("rerun-discount-code", res.Meta.MatchedDiscountItems[0].Code)
	assert.Len(res.BillsDetails.Details, 2)
	assert.Len(res.BillsServiceCosts, 1)
}

func (suite *CostExplorerSuite) TestOpenCostsReturnsRangeSummaryAndDetails() {
	assert := suite.Assert()
	unit := "open-costs-range-" + uuid.NewString()
	suite.createStreamTestUnit(unit)
	billingConfigID, err := (&billingconfig.BillingConfigModel{}).GetActiveBillingConfigID(1)
	assert.NoError(err)
	start := datainit.BillingDate
	end := start.AddDate(0, 0, 2)
	suite.insertDetailCost(1, billingConfigID, start.Add(time.Hour).Unix(), unit, map[string]string{}, entity.ConsumedSource_Invoice, decimal.NewFromInt(10), decimal.NewFromInt(9), false)
	suite.insertDetailCost(1, billingConfigID, start.AddDate(0, 0, 1).Add(time.Hour).Unix(), unit, map[string]string{}, entity.ConsumedSource_Invoice, decimal.NewFromInt(20), decimal.NewFromInt(18), false)
	suite.insertDetailCost(1, billingConfigID, end.Add(time.Hour).Unix(), unit, map[string]string{}, entity.ConsumedSource_Invoice, decimal.NewFromInt(100), decimal.NewFromInt(100), false)

	res, err := (&costexplorer.CostExplorerModel{}).CalculateOpenCostsForRange(1, billingConfigID, start, end)
	assert.NoError(err)
	suite.Require().NotNil(res)
	assert.Equal("legacy_open_costs", res.Meta.SourceMode)
	assert.Equal(start.Format("2006-01-02")+"/"+end.AddDate(0, 0, -1).Format("2006-01-02"), res.BillsOverView.BilledMonth)
	assert.Equal(start.Format(time.RFC3339), res.Meta.WindowStart)
	assert.Equal(end.Format(time.RFC3339), res.Meta.WindowEnd)
	assert.True(decimal.NewFromInt(30).Equal(res.BillsOverView.RunningTotal), res.BillsOverView.RunningTotal.String())
	assert.True(decimal.NewFromInt(3).Equal(res.BillsOverView.Discounts), res.BillsOverView.Discounts.String())
	assert.True(decimal.NewFromInt(27).Equal(res.BillsOverView.TotalCost), res.BillsOverView.TotalCost.String())
	assert.Len(res.BillsDetails.Details, 2)
	assert.Len(res.BillsServiceCosts, 1)
}

func (suite *CostExplorerSuite) TestOpenCostsIgnoresInvoiceIDBoundary() {
	assert := suite.Assert()
	unit := "open-costs-ignore-invoice-" + uuid.NewString()
	suite.createStreamTestUnit(unit)
	billingConfigID, err := (&billingconfig.BillingConfigModel{}).GetActiveBillingConfigID(1)
	assert.NoError(err)
	date := datainit.BillingDate

	for _, invoiceID := range []int{101, 102} {
		assert.NoError((&entity.UserUnitCost{
			CostID:                uuid.NewString(),
			TenantID:              1,
			BindedBillingConfigID: billingConfigID,
			CostTimeStamp:         date.Add(time.Hour).Unix(),
			Unit:                  unit,
			Cost:                  1,
			CostPrice:             decimal.RequireFromString("1.009"),
			RawPrice:              decimal.RequireFromString("1.009"),
			Labels:                entity.Labels{},
			ConsumedSource:        entity.ConsumedSource_Invoice,
			Status:                entity.UserUnitCostStatus_DONE,
			InvoiceID:             invoiceID,
			Internal:              false,
		}).Create(db.GetDB()))
	}

	res, err := (&costexplorer.CostExplorerModel{}).CalculateOpenCostsForRange(1, billingConfigID, date, date.AddDate(0, 0, 1))
	assert.NoError(err)
	suite.Require().NotNil(res)
	assert.True(decimal.RequireFromString("2.01").Equal(res.BillsOverView.RunningTotal), res.BillsOverView.RunningTotal.String())
	assert.True(decimal.RequireFromString("2.01").Equal(res.BillsOverView.TotalCost), res.BillsOverView.TotalCost.String())
	assert.Len(res.BillsDetails.Details, 1)
	assert.True(decimal.RequireFromString("2.01").Equal(res.BillsDetails.Details[0].Amount), res.BillsDetails.Details[0].Amount.String())
	assert.True(decimal.RequireFromString("2.01").Equal(res.BillsDetails.Details[0].BillableCost), res.BillsDetails.Details[0].BillableCost.String())
}

func (suite *CostExplorerSuite) TestOpenCostsReturnsSplitMergeRoundingAdjustment() {
	assert := suite.Assert()
	unit := "open-costs-rounding-" + uuid.NewString()
	suite.createStreamTestUnit(unit)
	billingConfigID, err := (&billingconfig.BillingConfigModel{}).GetActiveBillingConfigID(1)
	assert.NoError(err)
	date := datainit.BillingDate
	suite.insertDetailCost(1, billingConfigID, date.Add(time.Hour).Unix(), unit, map[string]string{}, entity.ConsumedSource_Invoice, decimal.RequireFromString("1.009"), decimal.RequireFromString("1.009"), false)
	suite.insertDetailCost(1, billingConfigID, date.Add(2*time.Hour).Unix(), unit, map[string]string{}, entity.ConsumedSource_Credits, decimal.RequireFromString("1.009"), decimal.RequireFromString("1.009"), false)

	res, err := (&costexplorer.CostExplorerModel{}).CalculateOpenCostsForRange(1, billingConfigID, date, date.AddDate(0, 0, 1))
	assert.NoError(err)
	suite.Require().NotNil(res)
	assert.True(decimal.RequireFromString("2.01").Equal(res.BillsOverView.RunningTotal), res.BillsOverView.RunningTotal.String())
	assert.True(decimal.RequireFromString("1").Equal(res.BillsOverView.Credits), res.BillsOverView.Credits.String())
	assert.True(decimal.RequireFromString("1").Equal(res.BillsOverView.TotalCost), res.BillsOverView.TotalCost.String())
	assert.True(decimal.RequireFromString("0.01").Equal(res.BillsOverView.RoundingAdjustment), res.BillsOverView.RoundingAdjustment.String())
	assert.Len(res.BillsServiceCosts, 1)
	assert.True(decimal.RequireFromString("0.01").Equal(res.BillsServiceCosts[0].RoundingAdjustment), res.BillsServiceCosts[0].RoundingAdjustment.String())
}

func (suite *CostExplorerSuite) TestRecalculateRunCostsReturnsFactsSummaryAndDetails() {
	assert := suite.Assert()
	config.Get().Set(config.SnapshotMeteringRawTenantWhitelist, []string{"1"})
	defer config.Get().Set(config.SnapshotMeteringRawTenantWhitelist, []string{})
	unit := "rerun-costs-" + uuid.NewString()
	suite.createStreamTestUnit(unit)
	billingConfigID, err := (&billingconfig.BillingConfigModel{}).GetActiveBillingConfigID(1)
	assert.NoError(err)
	windowStart := datainit.BillingDate
	windowEnd := windowStart.AddDate(0, 1, 0)

	bundle := entity.BillingSnapshotInputBundle{
		TenantID:        1,
		BillingConfigID: billingConfigID,
		PeriodMonth:     windowStart.Format("2006-01"),
		BundleType:      entity.BillingSnapshotInputBundleTypeRerunCandidate,
		InputRefs:       entity.Labels{},
		CreatedAt:       time.Now(),
	}
	assert.NoError(db.GetDB().Create(&bundle).Error)
	run := entity.BillingSnapshotRun{
		TenantID:        1,
		BillingConfigID: billingConfigID,
		PeriodMonth:     windowStart.Format("2006-01"),
		RunType:         entity.InvoiceType_MONTHLY,
		InvoiceDate:     windowStart,
		WindowStart:     windowStart,
		WindowEnd:       windowEnd,
		Status:          entity.BillingSnapshotRunStatusSuccess,
		InputBundleID:   bundle.BundleID,
		VersionSet:      entity.Labels{},
		SourceMode:      "raw_user_unit_costs",
		CreatedAt:       time.Now(),
	}
	assert.NoError(db.GetDB().Create(&run).Error)
	bindedPackageID, discountID := suite.createMatchedAdjustmentCatalog(1, billingConfigID)

	facts := []entity.BillingSnapshotSettlementFact{
		{
			RunID:           run.RunID,
			TenantID:        1,
			BillingConfigID: billingConfigID,
			BilledDate:      windowStart,
			Unit:            unit,
			Labels:          entity.Labels{"snapshot_discount_id": strconv.FormatUint(uint64(discountID), 10)},
			ConsumedSource:  entity.ConsumedSource_Invoice,
			RawPrice:        decimal.RequireFromString("10.009"),
			DiscountPrice:   decimal.NewFromInt(1),
			CreditPrice:     decimal.Zero,
			CostPrice:       decimal.RequireFromString("9.009"),
			CreatedAt:       time.Now(),
		},
		{
			RunID:           run.RunID,
			TenantID:        1,
			BillingConfigID: billingConfigID,
			BilledDate:      windowStart,
			Unit:            unit,
			Labels:          entity.Labels{"snapshot_binded_package_id": strconv.FormatUint(uint64(bindedPackageID), 10)},
			ConsumedSource:  entity.ConsumedSource_Credits,
			RawPrice:        decimal.RequireFromString("2.009"),
			DiscountPrice:   decimal.Zero,
			CreditPrice:     decimal.NewFromInt(2),
			CostPrice:       decimal.RequireFromString("2.009"),
			CreatedAt:       time.Now(),
		},
		{
			RunID:           run.RunID,
			TenantID:        1,
			BillingConfigID: billingConfigID,
			BilledDate:      windowStart,
			Unit:            unit,
			Labels:          entity.Labels{},
			ConsumedSource:  entity.ConsumedSource_Invoice,
			RawPrice:        decimal.NewFromInt(100),
			DiscountPrice:   decimal.Zero,
			CreditPrice:     decimal.Zero,
			CostPrice:       decimal.NewFromInt(100),
			IsInternal:      true,
			CreatedAt:       time.Now(),
		},
	}
	assert.NoError(db.GetDB().Create(&facts).Error)

	res, err := (&costexplorer.CostExplorerModel{}).CalculateRecalculateRunCosts(1, billingConfigID, run.RunID)
	assert.NoError(err)
	suite.Require().NotNil(res)
	assert.Equal("raw_user_unit_costs", res.Meta.SourceMode)
	assert.Equal(run.RunID, res.Meta.SourceRunID)
	assert.Equal(int64(3), res.Meta.FactCount)
	assert.True(decimal.RequireFromString("12.01").Equal(res.BillsOverView.RunningTotal), res.BillsOverView.RunningTotal.String())
	assert.True(decimal.NewFromInt(9).Equal(res.BillsOverView.TotalCost), res.BillsOverView.TotalCost.String())
	assert.True(decimal.RequireFromString("0.01").Equal(res.BillsOverView.RoundingAdjustment), res.BillsOverView.RoundingAdjustment.String())
	assert.True(decimal.NewFromInt(2).Equal(res.Meta.MatchedCredits), res.Meta.MatchedCredits.String())
	assert.True(decimal.NewFromInt(1).Equal(res.Meta.MatchedDiscounts), res.Meta.MatchedDiscounts.String())
	assert.Equal(strconv.FormatUint(uint64(bindedPackageID), 10), res.Meta.MatchedCreditPackageIDs)
	assert.Equal(strconv.FormatUint(uint64(discountID), 10), res.Meta.MatchedDiscountIDs)
	assert.Len(res.Meta.MatchedCreditItems, 1)
	suite.Require().NotNil(res.Meta.MatchedCreditItems[0].BindedPackageID)
	assert.Equal(bindedPackageID, *res.Meta.MatchedCreditItems[0].BindedPackageID)
	assert.Equal("rerun-credit-code", res.Meta.MatchedCreditItems[0].CreditsCode)
	assert.Len(res.Meta.MatchedDiscountItems, 1)
	assert.Equal(discountID, res.Meta.MatchedDiscountItems[0].DiscountID)
	assert.Equal("rerun-discount-code", res.Meta.MatchedDiscountItems[0].Code)
	assert.Len(res.BillsDetails.Details, 2)
	details, err := (&costexplorer.CostExplorerModel{}).GetRecalculateRunBillsDetails(1, billingConfigID, run.RunID)
	assert.NoError(err)
	suite.Require().NotNil(details)
	assert.Len(details.Details, 2)
	exportDetails, err := (&costexplorer.CostExplorerModel{}).GetRecalculateRunExportDetails(1, billingConfigID, run.RunID)
	assert.NoError(err)
	assert.Len(exportDetails, 2)
}

func (suite *CostExplorerSuite) createMatchedAdjustmentCatalog(tenantID uint, billingConfigID uint) (uint, uint) {
	now := datainit.BillingDate
	bindedPackage := entity.BindedPackage{
		TenantID:              tenantID,
		PackageID:             1,
		StartedDate:           &now,
		ExpiredDate:           ptrTime(now.AddDate(0, 1, 0)),
		RemainingCredits:      decimal.NewFromInt(98),
		Active:                entity.BindedPackageStatus_Active,
		InvoicePayable:        true,
		BindedBillingConfigID: &billingConfigID,
	}
	suite.Require().NoError(db.GetDB().Create(&bindedPackage).Error)
	creditsPackage := entity.CreditsPackage{
		TenantID:              tenantID,
		Status:                entity.CreditsPackageStatus_Active,
		CreditsCode:           "rerun-credit-code",
		StartDate:             now,
		ExpiredDate:           now.AddDate(0, 1, 0),
		BindedPackageID:       &bindedPackage.ID,
		TotalCredits:          decimal.NewFromInt(100),
		Type:                  entity.CreditsPackageType_General,
		Usage:                 entity.CreditsPackageUsage_Rerun,
		DiscountEnabled:       true,
		BindedBillingConfigID: billingConfigID,
	}
	suite.Require().NoError(db.GetDB().Create(&creditsPackage).Error)
	discount := entity.Discount{
		Code:                  "rerun-discount-code",
		TenantID:              tenantID,
		Value:                 decimal.RequireFromString("0.9"),
		StartedAt:             now,
		ExpiredAt:             now.AddDate(0, 1, 0),
		Status:                constant.DiscountStatus_Valid,
		Usage:                 entity.CreditsPackageUsage_Rerun,
		Description:           "rerun discount",
		BindedBillingConfigID: billingConfigID,
	}
	suite.Require().NoError(db.GetDB().Create(&discount).Error)
	return bindedPackage.ID, discount.ID
}

func ptrTime(t time.Time) *time.Time {
	return &t
}

func (suite *CostExplorerSuite) TestRecalculateRunCostsAggregatesBeforeTruncate() {
	assert := suite.Assert()
	config.Get().Set(config.SnapshotMeteringRawTenantWhitelist, []string{"1"})
	defer config.Get().Set(config.SnapshotMeteringRawTenantWhitelist, []string{})
	unit := "rerun-costs-aggregate-" + uuid.NewString()
	suite.createStreamTestUnit(unit)
	billingConfigID, err := (&billingconfig.BillingConfigModel{}).GetActiveBillingConfigID(1)
	assert.NoError(err)
	windowStart := datainit.BillingDate
	windowEnd := windowStart.AddDate(0, 1, 0)

	run := entity.BillingSnapshotRun{
		TenantID:        1,
		BillingConfigID: billingConfigID,
		PeriodMonth:     windowStart.Format("2006-01"),
		RunType:         entity.InvoiceType_MONTHLY,
		InvoiceDate:     windowStart,
		WindowStart:     windowStart,
		WindowEnd:       windowEnd,
		Status:          entity.BillingSnapshotRunStatusSuccess,
		InputBundleID:   322,
		VersionSet:      entity.Labels{},
		SourceMode:      "raw_user_unit_costs",
		CreatedAt:       time.Now(),
	}
	assert.NoError(db.GetDB().Create(&run).Error)

	facts := []entity.BillingSnapshotSettlementFact{
		{
			RunID:           run.RunID,
			TenantID:        1,
			BillingConfigID: billingConfigID,
			BilledDate:      windowStart,
			Unit:            unit,
			Labels: entity.Labels{
				"project":                        "project-a",
				"snapshot_source_item_count":     "1",
				"snapshot_source_item_refs_hash": "hash-a",
			},
			ConsumedSource: entity.ConsumedSource_Invoice,
			RawPrice:       decimal.RequireFromString("0.006"),
			DiscountPrice:  decimal.Zero,
			CreditPrice:    decimal.Zero,
			CostPrice:      decimal.RequireFromString("0.006"),
			CreatedAt:      time.Now(),
		},
		{
			RunID:           run.RunID,
			TenantID:        1,
			BillingConfigID: billingConfigID,
			BilledDate:      windowStart,
			Unit:            unit,
			Labels: entity.Labels{
				"project":                        "project-a",
				"snapshot_source_item_count":     "1",
				"snapshot_source_item_refs_hash": "hash-b",
			},
			ConsumedSource: entity.ConsumedSource_Invoice,
			RawPrice:       decimal.RequireFromString("0.006"),
			DiscountPrice:  decimal.Zero,
			CreditPrice:    decimal.Zero,
			CostPrice:      decimal.RequireFromString("0.006"),
			CreatedAt:      time.Now(),
		},
	}
	assert.NoError(db.GetDB().Create(&facts).Error)

	res, err := (&costexplorer.CostExplorerModel{}).CalculateRecalculateRunCosts(1, billingConfigID, run.RunID)
	assert.NoError(err)
	suite.Require().NotNil(res)
	assert.Equal(int64(2), res.Meta.FactCount)
	assert.True(decimal.RequireFromString("0.01").Equal(res.BillsOverView.RunningTotal), res.BillsOverView.RunningTotal.String())
	assert.True(decimal.RequireFromString("0.01").Equal(res.BillsOverView.TotalCost), res.BillsOverView.TotalCost.String())
	assert.True(decimal.Zero.Equal(res.BillsOverView.RoundingAdjustment), res.BillsOverView.RoundingAdjustment.String())
	assert.Len(res.BillsDetails.Details, 1)
	assert.True(decimal.RequireFromString("0.01").Equal(res.BillsDetails.Details[0].Amount), res.BillsDetails.Details[0].Amount.String())
	assert.True(decimal.RequireFromString("0.01").Equal(res.BillsDetails.Details[0].BillableCost), res.BillsDetails.Details[0].BillableCost.String())
}
