package tests

import (
	"encoding/json"
	"fmt"
	"net/http"
	"slices"
	"testing"
	"time"

	"github.com/pingcap/dbaas/billing2/constant"
	"github.com/pingcap/dbaas/billing2/handler/paymenthandler"
	"github.com/pingcap/dbaas/billing2/handler/paymenthandler/impl"
	"github.com/pingcap/dbaas/billing2/model/payment/paymentsub"

	"github.com/gin-gonic/gin"
	"github.com/pingcap/dbaas/billing2/api"
	"github.com/pingcap/dbaas/billing2/config"
	"github.com/pingcap/dbaas/billing2/db"
	"github.com/pingcap/dbaas/billing2/db/entity"
	"github.com/pingcap/dbaas/billing2/forms"
	"github.com/pingcap/dbaas/billing2/model/billing"
	"github.com/pingcap/dbaas/billing2/model/marketing"
	"github.com/pingcap/dbaas/billing2/model/marketing/plan"
	"github.com/pingcap/dbaas/billing2/model/marketing/unit_filter"
	"github.com/pingcap/dbaas/billing2/utils/ginutils"
	"github.com/pingcap/dbaas/billing2/utils/tests"
	"github.com/pingcap/dbaas/billing2/utils/tests/datainit"
	"github.com/pingcap/dbaas/billing2/utils/timeutils"
	"github.com/shopspring/decimal"
	"github.com/stretchr/testify/suite"
)

type CreditsTestSuite struct {
	tests.BillingTestSuite
	router            *gin.Engine
	generalUnitFilter *forms.UnitFilter
}

var creditsPackageModel = &marketing.CreditsPackageModel{}

func (suite *CreditsTestSuite) SetupTest() {
	suite.BillingTestSuite.SetupTest()
	_, err := (&billing.UnitModel{}).CreateUnit(&forms.UnitInfo{
		UnitKey: datainit.TestUnit,
	})
	assert := suite.Assert()
	assert.NoError(err)
	suite.generalUnitFilter, err = (&unit_filter.Model{}).CreateUnitFilter(&forms.UnitFilter{
		UnitFilterSlim: forms.UnitFilterSlim{Name: "General", Description: "General"},
		General:        true,
	})
	config.Get().Set(config.GeneralCreditTypeID, suite.generalUnitFilter.FilterID)
	assert.NoError(err)
	packageInfo, err := (&billing.PackageModel{}).CreatePackage(&forms.PackageInfo{
		UnitPrices: forms.UnitPrices{
			datainit.TestUnit: {Price: decimal.NewFromInt(1)},
		},
		Discount:      decimal.NewFromInt(1),
		CreditsEnable: true,
		CreditsOnly:   true,
	})
	assert.NoError(err)
	config.Get().Set(config.CreditsPricePackage, packageInfo.PackageID)
	suite.router = gin.New()
	suite.router.Use(ginutils.ErrorMiddleWare)
	tests.MockCentralHandler()
	api.InitializeCreditsRouter(ginutils.MyRoute{Route: suite.router})
}

func TestCreditsSuite(t *testing.T) {
	suite.Run(t, new(CreditsTestSuite))
}

func (suite *CreditsTestSuite) TestListAllcredits() {
	assert := suite.Assert()
	datainit.CreateBillingConfig(1, entity.BillingConfigType_DirectCustomer)

	w := tests.PerformGetRequest(suite.router, "/tenants/1/credits")
	assert.Equal(http.StatusOK, w.Code)
	credits := []forms.CreditsPackage{}
	err := json.NewDecoder(w.Body).Decode(&credits)
	assert.NoError(err)
	assert.Len(credits, 0)

	w1 := tests.PerformGetRequest(suite.router, "/tenants/1/billing_config_id/1/credits")
	assert.Equal(http.StatusOK, w1.Code)
}

func (suite *CreditsTestSuite) TestCreateCredits() {
	assert := suite.Assert()
	datainit.CreateBillingConfig(1, entity.BillingConfigType_DirectCustomer)
	w := tests.PerformRequest(suite.router, "POST", "/tenants/1/credits", &forms.CreateCreditsPackageReq{
		StartDate:    datainit.BillingDate,
		ExpiredDate:  datainit.BillingDate.AddDate(0, 1, 0),
		TotalCredits: decimal.NewFromInt(100),
		Usage:        entity.CreditsPackageUsage_POC,
		BindTypes:    []*forms.UnitFilterSlim{{TypeID: suite.generalUnitFilter.FilterID}},
	})
	assert.Equal(http.StatusOK, w.Code)
	credits := forms.CreditsPackage{}
	err := json.NewDecoder(w.Body).Decode(&credits)
	assert.NoError(err)
	assert.Equal(credits.TotalCredits, decimal.NewFromInt(100))
	assert.NotZero(credits.CreditsPackageID)

	info, err := creditsPackageModel.GetCreditsPackage(credits.CreditsPackageID)
	assert.NoError(err)
	assert.Equal(info.TotalCredits, decimal.NewFromInt(100))
	actionRecord := &entity.AuditRecord{}
	db.GetDB().Where("action = ?", entity.AuditRecordActionCreateCredit).Find(actionRecord)
	assert.NotZero(actionRecord.ID)
	assert.Equal(fmt.Sprintf(`{"credits_package_id": %d}`, info.CreditsPackageID), actionRecord.ExtraInfo)
	billingConfigID := uint(2)
	datainit.CreateBillingConfig(100, entity.BillingConfigType_MSP)
	paymenthandler.SetPaymentHandler(&impl.PaymentHandlerImpl{})
	_, err = (&paymentsub.PaymentModel{}).CreatePaymentMethodAndActive(
		100,
		&forms.PaymentMethodCreateQuery{
			PaymentType:           entity.PaymentType_MSP,
			BindedBillingConfigID: &billingConfigID,
		},
	)
	r := tests.PerformRequest(suite.router, "POST", "/tenants/100/credits", &forms.CreateCreditsPackageReq{
		StartDate:    datainit.BillingDate,
		ExpiredDate:  datainit.BillingDate.AddDate(0, 1, 0),
		TotalCredits: decimal.NewFromInt(100),
		Usage:        entity.CreditsPackageUsage_POC,
		BindTypes:    []*forms.UnitFilterSlim{{TypeID: suite.generalUnitFilter.FilterID}},
	})
	assert.Equal(http.StatusBadRequest, r.Code)
	ress := gin.H{}
	json.Unmarshal(r.Body.Bytes(), &ress)
	assert.NotNil(ress["error"])
	assert.Equal(ress["error"], "Invalid Input Error: MSP Customer  don't support credits.")

}

func (suite *CreditsTestSuite) TestCreateCreditsOnAssignCustomLabelsFilter() {
	assert := suite.Assert()
	tenantID := uint(1)
	datainit.CreateBillingConfig(tenantID, entity.BillingConfigType_DirectCustomer)
	assert.NoError(datainit.CreatePaymentMock(tenantID))
	assert.NoError((&plan.TenantPlanModel{}).EnableTenantOnDemandPlan(tenantID))
	timeutils.SetMockNow(datainit.BillingDate.AddDate(0, 0, 1))
	customers := []string{"134", "222"}
	w := tests.PerformRequest(suite.router, "POST", "/tenants/1/credits", &forms.CreateCreditsPackageReq{
		StartDate:       datainit.BillingDate,
		ExpiredDate:     datainit.BillingDate.AddDate(0, 1, 0),
		TotalCredits:    decimal.NewFromInt(100),
		Usage:           entity.CreditsPackageUsage_POC,
		DiscountEnabled: true,
		BindTypes:       []*forms.UnitFilterSlim{{TypeID: suite.generalUnitFilter.FilterID}},
		CopyUnitFilterArgs: forms.CopyUnitFilterArgs{
			Customers: customers,
		},
	})
	assert.Equal(http.StatusOK, w.Code)
	credits := forms.CreditsPackage{}
	err := json.NewDecoder(w.Body).Decode(&credits)
	assert.NoError(err)
	assert.Equal(credits.TotalCredits, decimal.NewFromInt(100))
	assert.NotZero(credits.CreditsPackageID)
	assert.Len(credits.UnitFilters, len(customers))
	assert.True(credits.DiscountEnabled)
	measureModel := &billing.MeasureModel{}
	for _, cus := range customers {
		c1, err := measureModel.CostUnits(1, &forms.UnitCosts{
			Unit:          datainit.TestUnit,
			Cost:          1,
			CostTimeStamp: timeutils.GetNow().Unix(),
			Labels:        map[string]string{constant.MSPCustomerIDLabel: cus},
		})
		assert.NoError(err)
		assert.Equal(entity.ConsumedSource_Credits, c1.ConsumedSource)
		assert.Equal(*credits.BindedPackageID, c1.BindedPackageID)
	}
	c1, err := measureModel.CostUnits(1, &forms.UnitCosts{
		Unit:          datainit.TestUnit,
		Cost:          1,
		CostTimeStamp: timeutils.GetNow().Unix(),
	})
	assert.NoError(err)
	assert.Equal(entity.ConsumedSource_Invoice, c1.ConsumedSource)
	assert.NotEqual(*credits.BindedPackageID, c1.BindedPackageID)

	w = tests.PerformRequest(suite.router, "POST", "/tenants/1/credits", &forms.CreateCreditsPackageReq{
		StartDate:    datainit.BillingDate,
		ExpiredDate:  datainit.BillingDate.AddDate(0, 1, 0),
		TotalCredits: decimal.NewFromInt(100),
		Usage:        entity.CreditsPackageUsage_POC,
		BindTypes:    []*forms.UnitFilterSlim{{TypeID: suite.generalUnitFilter.FilterID}},
		CopyUnitFilterArgs: forms.CopyUnitFilterArgs{
			Customers: customers,
			Projects:  []string{"999"},
		},
	})
	assert.Equal(http.StatusBadRequest, w.Code)
	unit, err := (&billing.UnitModel{}).CreateUnit(&forms.UnitInfo{
		UnitKey: "node:u:us-west-2:xxx",
		Region:  "us-west-2",
		Price:   decimal.NewFromInt(1),
	})
	assert.NoError(err)
	c1, err = measureModel.CostUnits(1, &forms.UnitCosts{
		Unit:          unit.UnitKey,
		Cost:          1,
		CostTimeStamp: timeutils.GetNow().Unix(),
		Labels:        map[string]string{constant.MSPCustomerIDLabel: customers[0]},
	})
	assert.NoError(err)
	assert.Equal(entity.ConsumedSource_Credits, c1.ConsumedSource)
	assert.Equal(*credits.BindedPackageID, c1.BindedPackageID)
	info := &forms.UnitFilter{
		UnitFilterSlim: forms.UnitFilterSlim{Name: "US-WEST-2", Description: "US-WEST-2", ParentID: suite.generalUnitFilter.FilterID},
		General:        false, Region: "us-west-2", UnitKeyPattern: "[\\w\\-]+:[\\w\\-]+:us-west-2:[\\w\\-]+",
	}
	unitFilterModel := &unit_filter.Model{}
	t, err := unitFilterModel.CreateUnitFilter(info)
	assert.NoError(err)
	prepayCustomers := []string{"888"}
	w = tests.PerformRequest(suite.router, "POST", "/tenants/1/credits", &forms.CreateCreditsPackageReq{
		StartDate:    datainit.BillingDate,
		ExpiredDate:  datainit.BillingDate.AddDate(0, 1, 0),
		TotalCredits: decimal.NewFromInt(100),
		Usage:        entity.CreditsPackageUsage_Prepayment,
		BindTypes:    []*forms.UnitFilterSlim{{TypeID: t.FilterID}},
		CopyUnitFilterArgs: forms.CopyUnitFilterArgs{
			Customers: prepayCustomers,
		},
	})
	assert.Equal(http.StatusOK, w.Code)
	prepayCredits := forms.CreditsPackage{}
	err = json.NewDecoder(w.Body).Decode(&prepayCredits)
	assert.NoError(err)
	c2, err := measureModel.CostUnits(1, &forms.UnitCosts{
		Unit:          unit.UnitKey,
		Cost:          1,
		CostTimeStamp: timeutils.GetNow().Unix(),
	})
	assert.NoError(err)
	assert.Equal(entity.ConsumedSource_Invoice, c2.ConsumedSource)
	c3, err := measureModel.CostUnits(1, &forms.UnitCosts{
		Unit:          datainit.TestUnit,
		Cost:          1,
		CostTimeStamp: timeutils.GetNow().Unix(),
		Labels:        map[string]string{constant.MSPCustomerIDLabel: prepayCustomers[0]},
	})
	assert.NoError(err)
	assert.Equal(entity.ConsumedSource_Invoice, c3.ConsumedSource)
	c4, err := measureModel.CostUnits(1, &forms.UnitCosts{
		Unit:          unit.UnitKey,
		Cost:          1,
		CostTimeStamp: timeutils.GetNow().Unix(),
		Labels:        map[string]string{constant.MSPCustomerIDLabel: prepayCustomers[0]},
	})
	assert.NoError(err)
	assert.Equal(entity.ConsumedSource_Credits, c4.ConsumedSource)
	assert.Equal(*prepayCredits.BindedPackageID, c4.BindedPackageID)
}

func (suite *CreditsTestSuite) TestGetCredits() {
	assert := suite.Assert()
	datainit.CreateBillingConfig(1, entity.BillingConfigType_DirectCustomer)
	w := tests.PerformRequest(suite.router, "POST", "/tenants/1/credits", &forms.CreateCreditsPackageReq{
		StartDate:    datainit.BillingDate,
		ExpiredDate:  datainit.BillingDate.AddDate(0, 1, 0),
		TotalCredits: decimal.NewFromInt(100),
		Usage:        entity.CreditsPackageUsage_POC,
		BindTypes:    []*forms.UnitFilterSlim{{TypeID: suite.generalUnitFilter.FilterID}},
	})
	assert.Equal(http.StatusOK, w.Code)
	credits := forms.CreditsPackage{}
	err := json.NewDecoder(w.Body).Decode(&credits)
	assert.NoError(err)
	assert.Equal(credits.TotalCredits, decimal.NewFromInt(100))
	assert.NotZero(credits.CreditsPackageID)

	info, err := creditsPackageModel.GetCreditsPackage(credits.CreditsPackageID)
	assert.NoError(err)
	assert.Equal(info.TotalCredits, decimal.NewFromInt(100))
}

func (suite *CreditsTestSuite) TestReclaimPackage() {
	assert := suite.Assert()

	tenantID := uint(1)
	datainit.CreateBillingConfig(tenantID, entity.BillingConfigType_DirectCustomer)
	billingConfigID := uint(1)

	timeutils.SetNowFunc(func() time.Time {
		return datainit.BillingDate
	})
	creditsPackage := &forms.CreditsPackage{
		StartDate:             datainit.BillingDate,
		ExpiredDate:           datainit.BillingDate.AddDate(0, 1, 0),
		TotalCredits:          decimal.NewFromInt(100),
		Usage:                 entity.CreditsPackageUsage_POC,
		BindTypes:             []*forms.UnitFilterSlim{{TypeID: suite.generalUnitFilter.FilterID, FilterID: suite.generalUnitFilter.FilterID}},
		BindedBillingConfigID: &billingConfigID,
	}
	info, err := creditsPackageModel.CreateCreditsPackage(tenantID, creditsPackage)
	assert.NoError(err)

	w := tests.PerformRequest(suite.router, "POST", fmt.Sprintf("/tenants/1/credits/%d/actions/reclaim", info.CreditsPackageID), nil)
	assert.Equal(http.StatusOK, w.Code)

	info, err = creditsPackageModel.GetCreditsPackage(info.CreditsPackageID)
	assert.NoError(err)
	assert.Equal(info.Status, entity.CreditsPackageStatus_InActive)

	actionRecord := &entity.AuditRecord{}
	db.GetDB().Where("action = ?", entity.AuditRecordActionReclaimCredit).Find(actionRecord)
	assert.NotZero(actionRecord.ID)
	assert.Equal(fmt.Sprintf(`{"credits_package_id": %d}`, info.CreditsPackageID), actionRecord.ExtraInfo)
}

func (suite *CreditsTestSuite) TestGetOpenedCreditsTenantIDs() {
	assert := suite.Assert()

	tenantID := uint(1)
	datainit.CreateBillingConfig(tenantID, entity.BillingConfigType_DirectCustomer)
	billingConfigID := uint(1)

	timeutils.SetNowFunc(func() time.Time {
		return datainit.BillingDate
	})
	creditsPackage := &forms.CreditsPackage{
		StartDate:             datainit.BillingDate,
		ExpiredDate:           datainit.BillingDate.AddDate(0, 1, 0),
		TotalCredits:          decimal.NewFromInt(100),
		Usage:                 entity.CreditsPackageUsage_POC,
		BindedBillingConfigID: &billingConfigID,
		BindTypes:             []*forms.UnitFilterSlim{{TypeID: suite.generalUnitFilter.FilterID}},
	}

	w := tests.PerformGetRequest(suite.router, "/opened_credits/tenant_ids")
	assert.Equal(http.StatusOK, w.Code)
	tenantIDs := []forms.TenantID{}
	err := json.NewDecoder(w.Body).Decode(&tenantIDs)
	assert.NoError(err)
	assert.Len(tenantIDs, 0)

	_, err = creditsPackageModel.CreateCreditsPackage(tenantID, creditsPackage)
	assert.NoError(err)

	w = tests.PerformGetRequest(suite.router, "/opened_credits/tenant_ids")
	assert.Equal(http.StatusOK, w.Code)
	err = json.NewDecoder(w.Body).Decode(&tenantIDs)
	assert.NoError(err)
	assert.Len(tenantIDs, 1)
	assert.Equal(tenantIDs[0].TenantID, tenantID)
}

func (suite *CreditsTestSuite) TestCreatePOCCredits() {
	suite.T().Skip("poc check has been removed")
	assert := suite.Assert()
	asiaUnitFilter, err := (&unit_filter.Model{}).CreateUnitFilter(&forms.UnitFilter{
		UnitFilterSlim: forms.UnitFilterSlim{Name: "Asia", Description: "Asia"},
		General:        false, Region: "Asia",
	})
	asiaUnitFilter2, err := (&unit_filter.Model{}).CreateUnitFilter(&forms.UnitFilter{
		UnitFilterSlim: forms.UnitFilterSlim{Name: "Asia-1", Description: "Asia-1"},
		General:        false, Region: "Asia",
	})
	assert.NoError(err)
	datainit.CreateBillingConfig(1, entity.BillingConfigType_DirectCustomer)

	reqBody := &forms.CreateCreditsPackageReq{
		StartDate:    datainit.BillingDate,
		ExpiredDate:  datainit.BillingDate.AddDate(0, 1, 0),
		TotalCredits: decimal.NewFromInt(100),
		Usage:        entity.CreditsPackageUsage_POC,
		BindTypes:    []*forms.UnitFilterSlim{{TypeID: asiaUnitFilter.FilterID}},
	}
	w := tests.PerformRequest(suite.router, "POST", "/tenants/1/credits", reqBody)
	assert.Equal(http.StatusBadRequest, w.Code)

	reqBody.BindTypes = []*forms.UnitFilterSlim{{FilterID: suite.generalUnitFilter.FilterID}, {FilterID: asiaUnitFilter.FilterID}, {FilterID: asiaUnitFilter2.FilterID}}
	w = tests.PerformRequest(suite.router, "POST", "/tenants/1/credits", reqBody)
	assert.Equal(http.StatusBadRequest, w.Code)

	reqBody.BindTypes = []*forms.UnitFilterSlim{{FilterID: asiaUnitFilter.FilterID}, {FilterID: asiaUnitFilter2.FilterID}}
	w = tests.PerformRequest(suite.router, "POST", "/tenants/1/credits", reqBody)
	assert.Equal(http.StatusBadRequest, w.Code)

	reqBody.BindTypes = []*forms.UnitFilterSlim{{TypeID: suite.generalUnitFilter.FilterID}}

	w = tests.PerformRequest(suite.router, "POST", "/tenants/1/credits", reqBody)
	assert.Equal(http.StatusOK, w.Code)
}

func (suite *CreditsTestSuite) TestCreateNoPOCCredits() {
	assert := suite.Assert()
	tenantID := uint(1)
	datainit.CreateBillingConfig(tenantID, entity.BillingConfigType_DirectCustomer)
	billingConfigID := uint(1)

	reqBody := &forms.CreateCreditsPackageReq{
		StartDate:    datainit.BillingDate,
		ExpiredDate:  datainit.BillingDate.AddDate(0, 1, 0),
		TotalCredits: decimal.NewFromInt(100),
		Usage:        "Other",
		BindTypes:    []*forms.UnitFilterSlim{{TypeID: suite.generalUnitFilter.FilterID}},
	}
	w := tests.PerformRequest(suite.router, "POST", "/tenants/1/credits", reqBody)
	assert.Equal(http.StatusBadRequest, w.Code)
	tests.MockPaymentHandler()
	tests.MockCentralHandler()
	_, err := paymentModel.CreatePaymentMethodAndActive(
		tenantID,
		&forms.PaymentMethodCreateQuery{
			PaymentType:           "mock",
			BindedBillingConfigID: &billingConfigID,
		},
	)
	assert.NoError(err)
	packageInfo, err := packageModel.CreatePackage(
		&forms.PackageInfo{
			UnitPrices:           forms.UnitPrices{},
			Discount:             decimal.NewFromInt(1),
			TenantMaxBindedTimes: 1,
		})
	assert.NoError(err)
	config.Get().Set(config.OnDemandPackage, packageInfo.PackageID)
	err = (&plan.TenantPlanModel{}).EnableTenantOnDemandPlan(tenantID)
	assert.NoError(err)
	w = tests.PerformRequest(suite.router, "POST", "/tenants/1/credits", reqBody)
	assert.Equal(http.StatusOK, w.Code)
}

func (suite *CreditsTestSuite) TestCopyCredits() {
	assert := suite.Assert()
	asiaUnitFilter, err := (&unit_filter.Model{}).CreateUnitFilter(&forms.UnitFilter{
		UnitFilterSlim: forms.UnitFilterSlim{Name: "Asia", Description: "Asia"},
		General:        false, Region: "Asia",
	})
	assert.NoError(err)
	tenantID := uint(1)
	b, err := datainit.CreateBillingConfig(tenantID, entity.BillingConfigType_DirectCustomer)
	packageInfo, err := packageModel.CreatePackage(
		&forms.PackageInfo{
			UnitPrices:           forms.UnitPrices{},
			Discount:             decimal.NewFromInt(1),
			TenantMaxBindedTimes: 1,
		})
	assert.NoError(err)
	tests.MockPaymentHandler()
	tests.MockCentralHandler()
	_, err = paymentModel.CreatePaymentMethodAndActive(
		tenantID,
		&forms.PaymentMethodCreateQuery{
			PaymentType:           "mock",
			BindedBillingConfigID: &b.BillingConfigID,
		},
	)
	assert.Nil(err)
	config.Get().Set(config.OnDemandPackage, packageInfo.PackageID)
	err = (&plan.TenantPlanModel{}).EnableTenantOnDemandPlan(tenantID)
	assert.NoError(err)

	reqBody := &forms.CreateCreditsPackageReq{
		StartDate:    datainit.BillingDate,
		ExpiredDate:  datainit.BillingDate.AddDate(0, 1, 0),
		TotalCredits: decimal.NewFromInt(100),
		Usage:        entity.CreditsPackageUsage_Prepayment,
		BindTypes:    []*forms.UnitFilterSlim{{TypeID: asiaUnitFilter.FilterID}},
	}
	w := tests.PerformRequest(suite.router, "POST", "/tenants/1/credits", reqBody)
	assert.Equal(http.StatusOK, w.Code)
	var credits forms.CreditsPackage
	err = json.NewDecoder(w.Body).Decode(&credits)
	assert.NoError(err)
	assert.Equal(forms.Provider_All, *credits.Provider)
	provider := forms.Provider_AWS
	reqBody.Provider = &provider
	reqBody.Customers = []string{"3", "4", "7", "8"}
	w = tests.PerformRequest(suite.router, "POST", "/tenants/1/credits", reqBody)
	assert.Equal(http.StatusOK, w.Code)
	err = json.NewDecoder(w.Body).Decode(&credits)
	assert.NoError(err)
	assert.Equal(*reqBody.Provider, *credits.Provider)
	assert.Equal(reqBody.Customers, credits.Customers)
	assert.Len(credits.UnitFilters, len(reqBody.Customers))
	cusList := make([]string, len(credits.UnitFilters))
	for i, ufs := range credits.UnitFilters {
		uf, err := (&unit_filter.Model{}).GetUnitFilter(ufs.FilterID)
		assert.NoError(err)
		assert.Equal(string(*reqBody.Provider), uf.Provider)
		cusList[i] = uf.Labels[constant.MSPCustomerIDLabel]
	}
	slices.Sort(cusList)
	assert.Equal(cusList, reqBody.Customers)
}
