package tests

import (
	"errors"
	"testing"
	"time"

	"github.com/pingcap/dbaas/billing2/constant"
	"github.com/pingcap/dbaas/billing2/db"
	"github.com/pingcap/dbaas/billing2/handler/centralhandler"
	"github.com/pingcap/dbaas/billing2/model/billingconfig"
	"github.com/pingcap/dbaas/billing2/utils/tests/datainit"
	"github.com/pingcap/dbaas/billing2/utils/timeutils"
	"github.com/stripe/stripe-go/v76"

	"github.com/pingcap/dbaas/billing2/api"
	"github.com/pingcap/dbaas/billing2/billing_errors"
	"github.com/pingcap/dbaas/billing2/config"
	"github.com/pingcap/dbaas/billing2/db/entity"
	"github.com/pingcap/dbaas/billing2/forms"
	"github.com/pingcap/dbaas/billing2/handler/paymenthandler"
	"github.com/pingcap/dbaas/billing2/model/creditriskcontrol"
	"github.com/pingcap/dbaas/billing2/utils/tests"
	"github.com/shopspring/decimal"
	"github.com/stretchr/testify/suite"
)

type CreditCardVerifyTestSuite struct {
	tests.BillingTestSuite
}

func TestCreditCardVerifySuite(t *testing.T) {
	suite.Run(t, new(CreditCardVerifyTestSuite))
}

type MockStripeHandler struct {
	paymenthandler.PaymentProvider
	paymenthandler.StripePaymentHandler
}

func (*MockStripeHandler) ConfirmPaymentIntent(amount decimal.Decimal, paymentMethodID string, customerId string) (string, error) {
	if paymentMethodID == "pm_fail_1000" {
		return "", billing_errors.NewCardVerificationError(nil, "invalid_number")
	}
	return "", nil
}

func (*MockStripeHandler) RefundBackground(paymentIntentID string, refundPrice decimal.Decimal) (string, error) {
	if paymentIntentID == "pm_refund_fail" {
		return "", billing_errors.SystemError
	}
	return "re_1", nil
}

func (*MockStripeHandler) CancelPaymentIntent(paymentIntentID string) error {
	if paymentIntentID == "pm_cancel_fail" {
		return billing_errors.SystemError
	}
	return nil
}
func (*MockStripeHandler) QueryRefundStatus(refundId string) (string, error) {
	return "", nil
}

func (*MockStripeHandler) Setup(tenant *forms.Tenant) (string, error) {
	return "", nil
}
func (*MockStripeHandler) Pay(paymentInfo *forms.PaymentMethod, billingCompany *forms.BillingCompany, invoice *forms.Invoice) (*forms.Invoice, error) {
	return invoice, nil
}
func (*MockStripeHandler) PayMultiInvoice(paymentInfo *forms.PaymentMethod, billingCompany *forms.BillingCompany, invoices []forms.Invoice) ([]forms.Invoice, error) {
	return invoices, nil
}
func (*MockStripeHandler) Payable(tenantID uint, userCode string) (bool, error) {
	return false, nil
}
func (*MockStripeHandler) NeedMultiInvoiceSendOnce() bool {
	return false
}

func (m *MockStripeHandler) GetThirdPartyName() string {
	return "mock"
}

func (m *MockStripeHandler) EnableReconciliation() bool {
	return false
}

func (m *MockStripeHandler) GetDailyRevenue(date time.Time) ([]*paymenthandler.TenantRevenue, error) {
	return nil, nil
}

func (m *MockStripeHandler) GetMonthlyRevenue(date time.Time) ([]*paymenthandler.TenantRevenue, error) {
	return nil, nil
}

func (m *MockStripeHandler) Init3DSPaymentIntent(cent decimal.Decimal, paymentMethodID string, customerId string) (*stripe.PaymentIntent, error) {
	if paymentMethodID == "pm_fail_1000" {
		return nil, billing_errors.NewCardVerificationError(nil, "invalid_number")
	}
	return nil, nil
}

func (m *MockStripeHandler) GetPaymentIntent(paymentIntentID string) (*stripe.PaymentIntent, error) {
	if paymentIntentID == "pm_cancel_fail" || paymentIntentID == "pm_cancel_suc" {
		return &stripe.PaymentIntent{
			Status: stripe.PaymentIntentStatusRequiresAction,
		}, nil
	}
	return &stripe.PaymentIntent{
		Status: stripe.PaymentIntentStatusSucceeded,
	}, nil
}

func (*MockStripeHandler) GetPaymentMethod(methodId string) (*forms.StripePaymentMethod, error) {
	return &forms.StripePaymentMethod{
		ID: "mock_id",
		Card: &forms.Card{
			Brand: string(stripe.CardBrandDiscover),
		},
		BillingDetails: &forms.BillingDetails{Address: &forms.Address{
			Line1:      "2469 Chicago Avenue",
			City:       "Fresno",
			State:      "CA",
			Country:    "US",
			PostalCode: "93721"}},
	}, nil
}

type MockPaymentHandlerImpl struct {
}

func (*MockPaymentHandlerImpl) GetProvider(string) (paymenthandler.PaymentProvider, error) {
	return &MockStripeHandler{}, nil
}

func (m *MockPaymentHandlerImpl) AllProviders() []string {
	return []string{entity.PaymentType_STRIPE}
}

type MockCentralHandlerImpl struct {
	tests.MockCentralHandlerImpl
}

func (*MockCentralHandlerImpl) GetTenant(tenantID uint) (*forms.Tenant, error) {
	switch tenantID {
	case 100:
		return &forms.Tenant{
			TenantID:                  tenantID,
			ThirdPartyAccountProvider: constant.MSPAccountProvider,
			Status:                    centralhandler.TenantStatusActive,
			Kind:                      forms.TenantKindInternal,
		}, nil
	case 200:
		return &forms.Tenant{
			TenantID:                  tenantID,
			Email:                     "xx@pingcap.com",
			ThirdPartyAccountProvider: constant.MSPAccountProvider,
			Status:                    centralhandler.TenantStatusActive,
			Kind:                      forms.TenantKindExternal,
		}, nil

	default:
		return &forms.Tenant{
			TenantID:                  tenantID,
			ThirdPartyAccountProvider: constant.GCPAccountProvider,
			Status:                    centralhandler.TenantStatusActive,
			Kind:                      forms.TenantKindExternal,
		}, nil
	}
}
func MockPaymentHandler() {
	paymenthandler.SetPaymentHandler(&MockPaymentHandlerImpl{})
}

var creditCardVerifyModel = &creditriskcontrol.CreditCardVerifyModel{}

func (s *CreditCardVerifyTestSuite) TestCreditCardValidate() {
	var err error
	assert := s.Assert()
	datainit.CreateBillingConfig(1, entity.BillingConfigType_DirectCustomer)
	MockPaymentHandler()
	centralhandler.SetCentralHandler(&MockCentralHandlerImpl{})
	// create test stripe customer firstly
	assert.NoError(api.CreateStripeIfNotExists(1, 1))
	// test case 1
	config.Get().SetDefault(config.IsCreditCardValidateAllowed, false)
	assert.False(creditCardVerifyModel.IsCreditCardValidationRequired(uint(1)))

	// test case 2
	config.Get().SetDefault(config.IsCreditCardValidateAllowed, true)
	config.Get().SetDefault(config.CreditCardValidateAllowedTenants, "1#2")
	assert.False(creditCardVerifyModel.IsCreditCardValidationRequired(uint(3)))

	// test case 3
	_, err = creditCardVerifyModel.CreditCardValidate(uint(1), 1, "pm_fail_1000")
	assert.Equal(true, errors.Is(err, billing_errors.NewCardVerificationError(nil, "invalid_number")))

	// test case 4
	_, err = creditCardVerifyModel.CreditCardValidate(uint(1), 1, "pm_suc")
	assert.Equal(true, err == nil)

	config.Get().SetDefault(config.CreditCardValidateAllowedTenants, "all")
	datainit.CreateBillingConfig(100, entity.BillingConfigType_DirectCustomer)
	datainit.CreateBillingConfig(200, entity.BillingConfigType_DirectCustomer)
	assert.NoError(api.CreateStripeIfNotExists(100, 1))
	billingConfigModel := billingconfig.BillingConfigModel{}
	bid, err := billingConfigModel.GetActiveBillingConfigID(100)
	assert.NoError(err)
	assert.NoError(api.CreateStripeIfNotExists(100, bid))
	_, err = creditCardVerifyModel.CreditCardValidate(uint(100), bid, "pm_suc")
	assert.NoError(err)
	config.Get().Set(config.RestrictInternalUserToBindCreditCard, true)
	_, err = creditCardVerifyModel.CreditCardValidate(uint(100), bid, "pm_suc")
	assert.Error(err)

	bid, err = billingConfigModel.GetActiveBillingConfigID(200)
	assert.NoError(err)
	assert.NoError(api.CreateStripeIfNotExists(200, bid))
	_, err = creditCardVerifyModel.CreditCardValidate(uint(200), bid, "pm_suc")
	assert.NoError(err)
	config.Get().Set(config.RestrictedEmailDomainsForBindingCreditCard, []string{"pingcap.com"})
	_, err = creditCardVerifyModel.CreditCardValidate(uint(200), 1, "pm_suc")
	assert.Error(err)
}

func (s *CreditCardVerifyTestSuite) TestRefundForCreditCardVerify() {
	assert := s.Assert()
	MockPaymentHandler()

	// test case 1
	err := creditCardVerifyModel.RefundForCreditCardVerify()
	assert.Equal(true, err == nil)

	// create record
	err = creditCardVerifyModel.CreateRefundRecord(entity.CreditCardValidateRefundStatus_Init, uint(1), "pm_refund_suc", decimal.NewFromInt(100))
	assert.NoError(err)
	err = creditCardVerifyModel.CreateRefundRecord(entity.CreditCardValidateRefundStatus_Init, uint(1), "pm_refund_fail", decimal.NewFromInt(100))
	assert.NoError(err)
	err = creditCardVerifyModel.CreateRefundRecord(entity.CreditCardValidateCancelStatus_Init, uint(1), "pm_cancel_suc", decimal.NewFromInt(100))
	assert.NoError(err)
	err = creditCardVerifyModel.CreateRefundRecord(entity.CreditCardValidateCancelStatus_Init, uint(1), "pm_cancel_fail", decimal.NewFromInt(100))
	assert.NoError(err)
	err = entity.NewCreditCardValidateRefundRecordQuerySet(db.GetDB()).PaymentIntentIDIn("pm_cancel_suc", "pm_cancel_fail").GetUpdater().SetCreatedAt(timeutils.GetNow().Add(config.Get().GetDuration(config.WaitingCustomerActionDurationSeconds)*-time.Second - time.Minute)).Update()
	assert.NoError(err)
	// test case 2
	creditCardVerifyModel.RefundForCreditCardVerify()
	refundsList, err := creditCardVerifyModel.QueryUnfinishedRefundRecordsWithLimit(5)
	assert.NoError(err)
	// should only get record-1
	assert.Len(refundsList, 2)
	for _, refundItem := range refundsList {
		if refundItem.PaymentIntentID == "pm_refund_fail" {
			assert.Equal(uint(1), refundItem.RetryCount)
			assert.Equal(entity.CreditCardValidateRefundStatus_Fail, refundItem.Status)
		}
		if refundItem.PaymentIntentID == "pm_cancel_fail" {
			assert.Equal(uint(1), refundItem.RetryCount)
			assert.Equal(entity.CreditCardValidateCancelStatus_Fail, refundItem.Status)
		}
	}
	refundsList, err = creditCardVerifyModel.QuerySuccessfulRefundRecords()
	assert.NoError(err)
	assert.Len(refundsList, 1)
	assert.Equal("pm_refund_suc", refundsList[0].PaymentIntentID)
	assert.Equal(uint(1), refundsList[0].RetryCount)
}

func (s *CreditCardVerifyTestSuite) TestCreditCardVerifyModel() {
	assert := s.Assert()
	MockPaymentHandler()

	// create record-1
	err := creditCardVerifyModel.CreateRefundRecord(entity.CreditCardValidateRefundStatus_Init, uint(1), "pm_test_1", decimal.NewFromInt(100))
	assert.NoError(err)

	// create record-2
	err = creditCardVerifyModel.CreateRefundRecord(entity.CreditCardValidateRefundStatus_Success, uint(1), "pm_test_2", decimal.NewFromInt(100))
	assert.NoError(err)

	// create record-3
	err = creditCardVerifyModel.CreateRefundRecord(entity.CreditCardValidateCancelStatus_Success, uint(1), "pm_test_3", decimal.NewFromInt(100))
	assert.NoError(err)

	// query all record
	refundsList, err := creditCardVerifyModel.QueryUnfinishedRefundRecordsWithLimit(5)
	assert.NoError(err)
	// should only get record-1
	assert.Equal(true, len(refundsList) == 1)

	for _, refundItem := range refundsList {
		if refundItem.PaymentIntentID == "pm_test_1" {
			assert.Equal(uint(0), refundItem.RetryCount)
			assert.Equal(entity.CreditCardValidateRefundStatus_Init, refundItem.Status)
			// update record
			err := creditCardVerifyModel.UpdateStatusAndAddRefundRetryCount(entity.CreditCardValidateRefundStatus_Fail, "", refundItem)
			assert.NoError(err)
			break
		}
	}
	refundsList, err = creditCardVerifyModel.QueryUnfinishedRefundRecordsWithLimit(5)
	assert.NoError(err)
	// should only get record-1
	assert.Equal(true, len(refundsList) == 1)
	for _, refundItem := range refundsList {
		if refundItem.PaymentIntentID == "pm_test_1" {
			assert.Equal(uint(1), refundItem.RetryCount)
			assert.Equal(entity.CreditCardValidateRefundStatus_Fail, refundItem.Status)
			// update record
			err := creditCardVerifyModel.UpdateStatusAndAddRefundRetryCount(entity.CreditCardValidateRefundStatus_Success, "refund_1", refundItem)
			assert.NoError(err)
			break
		}
	}

	refundsList, err = creditCardVerifyModel.QueryUnfinishedRefundRecordsWithLimit(5)
	assert.NoError(err)
	assert.Equal(true, len(refundsList) == 0)

	// should get record-1 & record-2
	refundsList, err = creditCardVerifyModel.QuerySuccessfulRefundRecords()
	assert.NoError(err)
	assert.Equal(true, len(refundsList) == 2)

	for _, refundItem := range refundsList {
		if refundItem.PaymentIntentID == "pm_test_1" {
			assert.Equal("refund_1", refundItem.RefundID)
		}
		break
	}
}
