package tests

import (
	"fmt"
	"testing"

	"github.com/pingcap/dbaas/billing2/billing_errors"
	"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/forms"
	"github.com/pingcap/dbaas/billing2/handler/centralhandler"
	"github.com/pingcap/dbaas/billing2/handler/eventbushandler"
	"github.com/pingcap/dbaas/billing2/handler/paymenthandler"
	"github.com/pingcap/dbaas/billing2/model/payment/paymentsub"
	"github.com/pingcap/dbaas/billing2/utils/tests"
	"github.com/pingcap/dbaas/billing2/utils/tests/datainit"
	"github.com/shopspring/decimal"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/suite"
)

var paymentModel = &paymentsub.PaymentModel{}

type PaymentSuite struct {
	tests.BillingTestSuite
}

func TestPaymentSuite(t *testing.T) {
	suite.Run(t, new(PaymentSuite))
}

func (suite *PaymentSuite) SetupTest() {
	suite.BillingTestSuite.SetupTest()
	datainit.CreateBillingConfig(1, entity.BillingConfigType_DirectCustomer)

}

func (suite *PaymentSuite) TestGetPayment() {
	assert := suite.Assert()
	var tenantID uint = 1
	billingConfigID := uint(1)
	_, err := paymentModel.GetActivePaymentMethod(tenantID, 1)
	assert.ErrorIs(err, billing_errors.ObjectNotExistsError)

	tests.MockPaymentHandler()
	tests.MockCentralHandler()
	paymentModel.CreatePaymentMethodAndActive(
		tenantID,
		&forms.PaymentMethodCreateQuery{
			PaymentType:           "mock",
			BindedBillingConfigID: &billingConfigID,
		},
	)
	info, err := paymentModel.GetActivePaymentMethod(tenantID, 1)
	assert.NoError(err)
	assert.NotNil(info)
	assert.Equal(info.PaymentType, "mock")
}

func (suite *PaymentSuite) TestCreatePayment() {
	tests.MockPaymentHandler()
	tests.MockCentralHandler()
	assert := suite.Assert()
	var tenantID uint = 1
	billingConfigID := uint(1)
	info, err := paymentModel.CreatePaymentMethodAndActive(
		tenantID,
		&forms.PaymentMethodCreateQuery{
			PaymentType:           "mock",
			BindedBillingConfigID: &billingConfigID,
		},
	)
	assert.NoError(err)
	assert.Equal(info.PaymentType, "mock")
	assert.NotEmpty(info.PaymentCode)
}

type MockCentralHandlerThirdPartyImpl struct {
	centralhandler.CentralHandler
}

func (*MockCentralHandlerThirdPartyImpl) GetTenant(tenantID uint) (*forms.Tenant, error) {
	return &forms.Tenant{
		TenantID:                  tenantID,
		ThirdPartyAccountProvider: constant.AwsAccountProvider,
		Status:                    centralhandler.TenantStatusActive,
		ThirdPartyAccountCode:     fmt.Sprintf("mock_%d", tenantID),
	}, nil
}

func (*MockCentralHandlerThirdPartyImpl) GetProjectInfo(projectID uint) (*forms.Project, error) {
	return &forms.Project{
		ID:   projectID,
		Name: fmt.Sprintf("project_%d", projectID),
	}, nil
}

func (suite *PaymentSuite) TestCreatePaymentMethodAfterMarketplaceBind() {
	tests.MockPaymentHandler()
	centralhandler.SetCentralHandler(&MockCentralHandlerThirdPartyImpl{})
	assert := suite.Assert()
	var tenantID uint = 1
	err := paymentModel.CreateOrReplacePaymentMethodAfterMarketplaceBind(
		tenantID,
	)
	assert.NoError(err)

	info, err := paymentModel.GetActivePaymentMethod(tenantID, 1)
	assert.NoError(err)

	assert.Equal(info.PaymentType, entity.PaymentType_AWS)
	assert.NotEmpty(info.PaymentCode)
}

func (suite *PaymentSuite) TestReplacePaymentMethodAfterMarketplaceBind() {
	tests.MockPaymentHandler()
	centralhandler.SetCentralHandler(&MockCentralHandlerThirdPartyImpl{})
	assert := suite.Assert()
	var tenantID uint = 1
	billingConfigID := uint(1)
	_, err := paymentModel.CreatePaymentMethodAndActive(tenantID, &forms.PaymentMethodCreateQuery{
		PaymentType:           entity.PaymentType_STRIPE,
		BindedBillingConfigID: &billingConfigID,
	})
	assert.NoError(err)

	err = paymentModel.CreateOrReplacePaymentMethodAfterMarketplaceBind(
		tenantID,
	)
	assert.NoError(err)

	info, err := paymentModel.GetActivePaymentMethod(tenantID, 1)
	assert.NoError(err)

	assert.Equal(info.PaymentType, entity.PaymentType_AWS)
	assert.NotEmpty(info.PaymentCode)
}

func (suite *PaymentSuite) TestReplacePaymentMethodAfterMarketplaceBindFailed() {
	tests.MockPaymentHandler()
	centralhandler.SetCentralHandler(&MockCentralHandlerThirdPartyImpl{})
	assert := suite.Assert()
	var tenantID uint = 1
	billingConfigID := uint(1)

	_, err := paymentModel.CreatePaymentMethodAndActive(tenantID, &forms.PaymentMethodCreateQuery{
		PaymentType:           entity.PaymentType_GCP,
		BindedBillingConfigID: &billingConfigID,
	})
	assert.NoError(err)

	err = paymentModel.CreateOrReplacePaymentMethodAfterMarketplaceBind(
		tenantID,
	)
	assert.NoError(err)

	info, err := paymentModel.GetActivePaymentMethod(tenantID, 1)
	assert.NoError(err)

	assert.Equal(info.PaymentType, entity.PaymentType_GCP)
	assert.NotEmpty(info.PaymentCode)
}

func (suite *PaymentSuite) TestPaymentMethodActiveRecord() {
	tests.MockPaymentHandler()
	centralhandler.SetCentralHandler(&MockCentralHandlerThirdPartyImpl{})
	assert := suite.Assert()
	var tenantID uint = 1
	billingConfigID := uint(1)

	payment1, err := paymentModel.CreatePaymentMethodAndActive(tenantID, &forms.PaymentMethodCreateQuery{
		PaymentType:           entity.PaymentType_STRIPE,
		BindedBillingConfigID: &billingConfigID,
	})
	assert.NoError(err)
	var activeRecords []entity.PaymentMethodActiveRecord
	err = entity.NewPaymentMethodActiveRecordQuerySet(db.GetDB()).TenantIDEq(tenantID).All(&activeRecords)
	assert.NoError(err)
	assert.Len(activeRecords, 1)
	assert.Equal(payment1.PaymentMethodID, activeRecords[0].PaymentMethodID)

	err = paymentModel.CreateOrReplacePaymentMethodAfterMarketplaceBind(
		tenantID,
	)
	assert.NoError(err)

	payment2, err := paymentModel.GetActivePaymentMethod(tenantID, 1)
	assert.NoError(err)

	err = entity.NewPaymentMethodActiveRecordQuerySet(db.GetDB()).TenantIDEq(tenantID).OrderAscByCreatedAt().All(&activeRecords)
	assert.NoError(err)
	assert.Len(activeRecords, 2)

	assert.Equal(payment1.PaymentMethodID, activeRecords[0].PaymentMethodID)
	assert.Equal(payment2.PaymentMethodID, activeRecords[1].PaymentMethodID)
}

func (suite *PaymentSuite) TestGetPayableTenantIDs() {
	tests.MockPaymentHandler()
	tests.MockCentralHandler()
	assert := suite.Assert()
	var tenantID uint = 1
	billingConfigID := uint(1)

	_, err := paymentModel.CreatePaymentMethodAndActive(
		tenantID,
		&forms.PaymentMethodCreateQuery{
			PaymentType:           "mock",
			BindedBillingConfigID: &billingConfigID,
		},
	)
	assert.NoError(err)

	tenantIDs, err := paymentModel.GetAllPayableTenantIDs()
	assert.NoError(err)
	assert.Len(tenantIDs, 1)
}

func (suite *PaymentSuite) TestPayable() {
	tests.MockCentralHandler()
	tests.MockPaymentHandler()
	assert := suite.Assert()
	var tenantID uint = 1
	res, err := paymentModel.Payable(tenantID)
	assert.NoError(err)
	assert.False(res)
	billingConfigID := uint(1)

	_, err = paymentModel.CreatePaymentMethodAndActive(
		tenantID,
		&forms.PaymentMethodCreateQuery{
			PaymentType:           "mock",
			BindedBillingConfigID: &billingConfigID,
		},
	)

	assert.NoError(err)

	res, err = paymentModel.Payable(tenantID)
	assert.NoError(err)
	assert.True(res)
}

func (suite *PaymentSuite) TestSyncPayable() {
	tests.MockCentralHandler()
	tests.MockPaymentHandler()
	assert := suite.Assert()
	var tenantID uint = 1
	billingConfigID := uint(1)

	payment, err := paymentModel.CreatePaymentMethodAndActive(
		tenantID,
		&forms.PaymentMethodCreateQuery{
			PaymentType:           "mock",
			BindedBillingConfigID: &billingConfigID,
		},
	)
	assert.NoError(err)
	err = entity.NewPaymentMethodQuerySet(db.GetDB()).IDEq(payment.PaymentMethodID).GetUpdater().SetPayable(false).Update()
	assert.NoError(err)

	res, err := paymentModel.Payable(tenantID)
	assert.NoError(err)
	assert.False(res)

	err = entity.NewPaymentMethodQuerySet(db.GetDB()).IDEq(payment.PaymentMethodID).GetUpdater().SetManagedBySales(true).Update()
	assert.NoError(err)

	res, err = paymentModel.Payable(tenantID)
	assert.NoError(err)
	assert.False(res)

	err = paymentModel.SyncPayable(tenantID)
	assert.NoError(err)

	res, err = paymentModel.Payable(tenantID)
	assert.NoError(err)
	assert.True(res)
}

func (suite *PaymentSuite) TestIsHourlyPayment() {
	tests.MockCentralHandler()
	tests.MockPaymentHandler()
	assert := suite.Assert()
	var tenantID uint = 1
	res, err := paymentModel.Payable(tenantID)
	assert.NoError(err)
	assert.False(res)
	billingConfigID := uint(1)

	_, err = paymentModel.CreatePaymentMethodAndActive(
		tenantID,
		&forms.PaymentMethodCreateQuery{
			PaymentType:           "mock",
			BindedBillingConfigID: &billingConfigID,
		},
	)
	assert.NoError(err)

	isHourlyPayment, err := paymentModel.IsHourlyPayment(tenantID, 1)
	assert.NoError(err)
	assert.False(isHourlyPayment)

	tenantID = 2
	billingConfigID = uint(2)
	datainit.CreateBillingConfig(tenantID, entity.BillingConfigType_DirectCustomer)
	_, err = paymentModel.CreatePaymentMethodAndActive(
		tenantID,
		&forms.PaymentMethodCreateQuery{
			PaymentType:           entity.PaymentType_AWS,
			BindedBillingConfigID: &billingConfigID,
		},
	)
	assert.NoError(err)

	isHourlyPayment, err = paymentModel.IsHourlyPayment(tenantID, 2)
	assert.NoError(err)
	assert.True(isHourlyPayment)

}

func (suite *PaymentSuite) TestPay() {
	tests.MockPaymentHandler()
	tests.MockCentralHandler()
	assert := suite.Assert()
	var tenantID uint = 1
	billingConfigID := uint(1)

	company := &forms.BillingCompany{
		Name:           "default-company-name",
		CompanyKey:     config.Get().GetString(config.DefaultBillingCompanyKey),
		PaymentSubCode: "",
	}
	billingInfoModel := paymentsub.BillingInformationModel{}
	err := billingInfoModel.CreateBillingCompany(company)
	assert.NoError(err)

	payment, err := paymentModel.CreatePaymentMethodAndActive(
		tenantID,
		&forms.PaymentMethodCreateQuery{
			PaymentType:           "mock",
			BindedBillingConfigID: &billingConfigID,
		},
	)
	assert.NoError(err)
	assert.Equal(payment.PaymentType, "mock")

	provider, err := paymenthandler.GetPaymentHandler().GetProvider("mock")
	assert.NoError(err)
	providerImpl, ok := provider.(*tests.MockPaymentProviderImpl)
	assert.True(ok)

	invoice := &forms.Invoice{}
	invoice.CostPrice = decimal.RequireFromString("1.1")
	result, err := paymentModel.Pay(tenantID, 1, *invoice)
	assert.NoError(err)
	assert.Equal(payment.PaymentMethodID, result.PaymentMethodID)
	invoice.PaymentMethodID = &result.PaymentMethodID
	invoice.Meta.PaymentMeta = forms.PaymentMeta{EnlargerDiscount: decimal.NewFromInt(1)}
	invoice.ChannelLoss = decimal.RequireFromString("0.1")

	providerImpl.AssertCalled(suite.T(), "Pay", payment, company, invoice)

	providerImpl.Calls = nil

	company = &forms.BillingCompany{
		Name:           "company-name2",
		CompanyKey:     "company-key2",
		PaymentSubCode: "payment_sub_code",
	}
	err = billingInfoModel.CreateBillingCompany(company)
	assert.NoError(err)
	_, err = billingInfoModel.UpdateTenantBillingCompany(tenantID, &forms.TenantBillingCompany{
		CompanyKey: "company-key2",
	})
	assert.NoError(err)

	result, err = paymentModel.Pay(tenantID, 1, *invoice)
	assert.NoError(err)
	invoice.PaymentMethodID = &result.PaymentMethodID
	invoice.Meta.PaymentMeta = forms.PaymentMeta{EnlargerDiscount: decimal.NewFromInt(1)}
	invoice.ChannelLoss = decimal.RequireFromString("0.1")
	providerImpl.AssertCalled(suite.T(), "Pay", payment, company, invoice)
}

func (suite *PaymentSuite) TestNeedMultiInoviceSendOnce() {
	tests.MockPaymentHandler()
	tests.MockCentralHandler()
	assert := suite.Assert()
	var tenantID uint = 1
	billingConfigID := uint(1)

	company := &forms.BillingCompany{
		Name:           "default-company-name",
		CompanyKey:     config.Get().GetString(config.DefaultBillingCompanyKey),
		PaymentSubCode: "",
	}
	billingInfoModel := paymentsub.BillingInformationModel{}
	err := billingInfoModel.CreateBillingCompany(company)
	assert.NoError(err)

	payment, err := paymentModel.CreatePaymentMethodAndActive(
		tenantID,
		&forms.PaymentMethodCreateQuery{
			PaymentType:           "mock",
			BindedBillingConfigID: &billingConfigID,
		},
	)
	assert.NoError(err)
	assert.Equal(payment.PaymentType, "mock")

	provider, err := paymenthandler.GetPaymentHandler().GetProvider("mock")
	assert.NoError(err)
	providerImpl, ok := provider.(*tests.MockPaymentProviderImpl)
	assert.True(ok)

	invoice := &forms.Invoice{ChannelLoss: decimal.Zero}
	invoice.CostPrice = decimal.RequireFromString("2.1")

	result, err := paymentModel.Pay(tenantID, 1, *invoice)
	assert.NoError(err)
	invoice.PaymentMethodID = &result.PaymentMethodID
	invoice.Meta.PaymentMeta = forms.PaymentMeta{EnlargerDiscount: decimal.NewFromInt(1)}
	invoice.ChannelLoss = decimal.RequireFromString("0.1")
	providerImpl.AssertCalled(suite.T(), "Pay", payment, company, invoice)
}

func (suite *PaymentSuite) TestPayMultiInvoiceOnce() {
	tests.MockPaymentHandler()
	tests.MockCentralHandler()
	assert := suite.Assert()
	var tenantID uint = 1
	billingConfigID := uint(1)

	company := &forms.BillingCompany{
		Name:           "default-company-name",
		CompanyKey:     config.Get().GetString(config.DefaultBillingCompanyKey),
		PaymentSubCode: "",
	}
	billingInfoModel := paymentsub.BillingInformationModel{}
	err := billingInfoModel.CreateBillingCompany(company)
	assert.NoError(err)

	info, err := paymentModel.CreatePaymentMethodAndActive(
		tenantID,
		&forms.PaymentMethodCreateQuery{
			PaymentType:           "mock",
			BindedBillingConfigID: &billingConfigID,
		},
	)
	assert.NoError(err)
	assert.Equal(info.PaymentType, "mock")

	provider, err := paymenthandler.GetPaymentHandler().GetProvider("mock")
	assert.NoError(err)
	providerImpl, ok := provider.(*tests.MockPaymentProviderImpl)
	assert.True(ok)
	providerImpl.ExpectedCalls = nil
	providerImpl.On("NeedMultiInvoiceSendOnce").Return(true)
	providerImpl.On("PayMultiInvoice", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)

}

func (suite *PaymentSuite) TestPayMultiInvoiceManageBySales() {
	tests.MockPaymentHandler()
	tests.MockCentralHandler()
	assert := suite.Assert()
	var tenantID uint = 1
	billingConfigID := uint(1)

	company := &forms.BillingCompany{
		Name:           "default-company-name",
		CompanyKey:     config.Get().GetString(config.DefaultBillingCompanyKey),
		PaymentSubCode: "",
	}
	billingInfoModel := paymentsub.BillingInformationModel{}
	err := billingInfoModel.CreateBillingCompany(company)
	assert.NoError(err)

	info, err := paymentModel.CreatePaymentMethodAndActive(
		tenantID,
		&forms.PaymentMethodCreateQuery{
			PaymentType:           "mock",
			BindedBillingConfigID: &billingConfigID,
		},
	)
	assert.Equal(info.PaymentType, "mock")
	provider, err := paymenthandler.GetPaymentHandler().GetProvider("mock")
	assert.NoError(err)

	err = entity.NewPaymentMethodQuerySet(db.GetDB()).TenantIDEq(1).GetUpdater().SetManagedBySales(true).Update()
	assert.NoError(err)

	invoices := []forms.Invoice{
		{InvoiceID: 1,
			TenantID: 1,
			Status:   entity.InvoiceStatus_GENERATED,
			Type:     entity.InvoiceType_COMMITMENT},
		{InvoiceID: 2,
			TenantID: 1,
			Status:   entity.InvoiceStatus_GENERATED,
			Type:     entity.InvoiceType_HOURLY},
	}

	providerImpl, ok := provider.(*tests.MockPaymentProviderImpl)
	assert.True(ok)
	providerImpl.ExpectedCalls = nil
	providerImpl.On("NeedMultiInvoiceSendOnce").Return(true)
	providerImpl.On("PayMultiInvoice", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)

	_, err = paymentModel.Pay(tenantID, 1, invoices...)
	assert.NoError(err)
}

type PayableMockPaymentHandler struct {
	payable bool
	tests.MockPaymentHandlerImpl
}

func (h *PayableMockPaymentHandler) GetProvider(provider string) (paymenthandler.PaymentProvider, error) {
	return &AWSMockPaymentProvider{payable: h.payable}, nil
}

type AWSMockPaymentProvider struct {
	payable bool
	tests.MockPaymentProviderImpl
}

func (p *AWSMockPaymentProvider) Payable(tenant uint, code string) (bool, error) {
	return p.payable, nil
}

func (suite *PaymentSuite) TestMarketplaceReSub() {
	handler := &PayableMockPaymentHandler{payable: true}
	paymenthandler.SetPaymentHandler(handler)
	centralhandler.SetCentralHandler(&MockCentralHandlerThirdPartyImpl{})
	assert := suite.Assert()
	var tenantID uint = 1
	var err error
	err = paymentModel.CreateOrReplacePaymentMethodAfterMarketplaceBind(tenantID)
	assert.NoError(err)

	eb, ok := eventbushandler.Get().(*tests.LocalEventBusHandlerImpl)
	assert.True(ok)

	events := eb.GetAllTxEvents(paymentsub.TenantPayableEventType)
	assert.Len(events, 1)

	info, err := paymentModel.GetActivePaymentMethod(tenantID, 1)
	assert.NoError(err)
	assert.Equal(info.PaymentType, entity.PaymentType_AWS)
	assert.NotEmpty(info.PaymentCode)

	err = paymentModel.MarkMarketplacePaymentMethodUnsubscribed(tenantID)
	assert.NoError(err)
	events = eb.GetAllTxEvents(paymentsub.TenantPayableEventType)
	assert.Len(events, 1)

	handler.payable = false

	err = paymentModel.MarkMarketplacePaymentMethodUnsubscribed(tenantID)
	assert.NoError(err)
	events = eb.GetAllTxEvents(paymentsub.TenantPayableEventType)
	assert.Len(events, 2)

	info, err = paymentModel.GetActivePaymentMethod(tenantID, 1)
	assert.NoError(err)
	assert.NotNil(info.UnpayableAt)

	handler.payable = true
	err = paymentModel.CreateOrReplacePaymentMethodAfterMarketplaceBind(tenantID)
	assert.NoError(err)

	info, err = paymentModel.GetActivePaymentMethod(tenantID, 1)
	assert.NoError(err)
	assert.Nil(info.UnpayableAt)
}

func (suite *PaymentSuite) TestCreateEndorsedPayment() {
	assert := suite.Assert()
	tests.MockCentralHandler()
	tests.MockPaymentHandler()
	tenantID := uint(1)
	payable, err := paymentModel.Payable(tenantID)
	assert.NoError(err)
	assert.False(payable)
	_, err = paymentModel.ActiveWireTransferPayment(tenantID, 1)
	assert.NoError(err)
	info, err := paymentModel.GetActivePaymentMethod(tenantID, 1)
	assert.NoError(err)
	assert.Equal(info.PaymentType, entity.PaymentType_WireTransfer)
	assert.True(info.Payable)
	payable, err = paymentModel.Payable(tenantID)
	assert.NoError(err)
	assert.True(payable)
}
