package tests

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/pingcap/dbaas/central/config"
	"github.com/pingcap/dbaas/central/db"
	"github.com/pingcap/dbaas/central/db/dbutil"
	"github.com/pingcap/dbaas/central/db/entity"
	"github.com/pingcap/dbaas/central/db/tenant"
	"github.com/pingcap/dbaas/central/handler/billing2"
	"github.com/pingcap/dbaas/central/model/billing"
	"github.com/pingcap/dbaas/central/model/billing/worker"
	"github.com/pingcap/dbaas/central/model/tenant/handler"
	"github.com/pingcap/dbaas/central/test"
	"github.com/pingcap/log"
	"github.com/shopspring/decimal"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/suite"
	"github.com/tidbcloud/pkgv2/salesforce"
	"gorm.io/gorm"
)

type BillingOnDemandTestSuite struct {
	test.CentralTestSuite
	tenant            *entity.Tenant
	model             billing.Model
	context           billing.Context
	db                *gorm.DB
	billingHandler    *MockBillingHandlerOnDemand
	salesforceHandler *MockSalesforceHandlerOnDemand
}

func TestOnDemandSuite(t *testing.T) {
	suite.Run(t, new(BillingOnDemandTestSuite))
}

func (s *BillingOnDemandTestSuite) SetupTest() {
	s.CentralTestSuite.SetupTest()
	s.createTenantAndAdminUser()
	s.createModelAndContext()
}

type MockBillingHandlerOnDemand struct {
	billing2.BillingHandler
	mock.Mock
}

type MockSalesforceHandlerOnDemand struct {
	mock.Mock
}

func (m *MockSalesforceHandlerOnDemand) UpdateAccount(account salesforce.Account) error {
	return m.Called().Error(0)
}

func (m *MockSalesforceHandlerOnDemand) SyncLeads(lead salesforce.Lead) error {
	return m.Called().Error(0)
}

func (m *MockBillingHandlerOnDemand) BindPackage(tenantID, packageID uint64) (uint64, error) {
	return m.Called().Get(0).(uint64), nil
}

func (m *MockBillingHandlerOnDemand) GetPackageDiscount(packageID uint64) (*decimal.Decimal, error) {
	return m.Called().Get(0).(*decimal.Decimal), nil
}

func (m *MockBillingHandlerOnDemand) UpdatePackageDiscount(packageID uint64, discount decimal.Decimal) error {
	m.Called(discount)
	return nil
}

func (m *MockBillingHandlerOnDemand) GetBindedPackageDiscount(tenantID, bindedPackageID uint64) (*decimal.Decimal, error) {
	return m.Called().Get(0).(*decimal.Decimal), nil
}
func (m *MockBillingHandlerOnDemand) UpdateBindedPackageDiscount(tenantID, bindedPackageID uint64, discount *decimal.Decimal) error {
	m.Called(discount)
	return nil
}

func (m *MockBillingHandlerOnDemand) Payable(tenantID uint64) (bool, error) {
	return true, nil
}

func (m *MockBillingHandlerOnDemand) ListPayments(tenantID uint64) ([]billing2.PaymentMethod, error) {
	return []billing2.PaymentMethod{
		{Payable: true},
	}, nil
}

func (m *MockBillingHandlerOnDemand) CreatePayment(tenantID uint64, paymentType string) error {
	m.Called(paymentType)
	return nil
}

func (s *BillingOnDemandTestSuite) createBillingHandler() {
	s.billingHandler = &MockBillingHandlerOnDemand{}
	s.billingHandler.On("BindPackage").Return(uint64(1), nil)
	billing2.SetHandler(s.billingHandler)

}

func (s *BillingOnDemandTestSuite) createModelAndContext() {
	mailHandler := &MockMailHandler{}
	mailHandler.On("SendMailWithTemplate").Return().Maybe()
	s.salesforceHandler = &MockSalesforceHandlerOnDemand{}
	s.salesforceHandler.On("UpdateAccount").Return(nil).Maybe()
	s.salesforceHandler.On("SyncLeads").Return(nil).Maybe()
	s.model = billing.NewModel(log.L(), mailHandler, s.salesforceHandler)
	s.context = s.model.User(&tenant.User{TenantID: s.tenant.ID})
	s.createBillingHandler()
}

func (s *BillingOnDemandTestSuite) createTenantAndAdminUser() {
	dbProvider := db.NewProvider()
	ctx := context.Background()
	s.db = dbProvider.WriteDB(ctx)
	tenant := &entity.Tenant{
		Name:          "unittest tenant",
		Email:         "unittest@pingcap.com",
		Country:       "CN",
		TimeZone:      "8",
		Status:        entity.TenantStatusActive,
		Plan:          entity.TenantPlanNewUser,
		AcceptService: false,
		ZendeskID:     0,
		Quotas:        nil,
		BillingEmail:  "",
	}
	err := tenant.Create(s.db)
	if err != nil {
		s.FailNow("failed to create tenant for test %s", err)
	}
	// init fake user
	dbUser := &entity.User{
		TenantID:  tenant.ID,
		FirstName: "FirstName",
		LastName:  "LastName",
		Email:     "unittest@pingcap.com",
		Status:    entity.UserStatusActive,
	}

	err = dbUser.Create(s.db)
	if err != nil {
		s.FailNow("failed to create tenant for user %s", err)
	}

	registrationLog := &entity.RegistrationLog{
		Email:       "unittest@pingcap.com",
		IPAddress:   "mock_ip",
		InvitedAt:   time.Now(),
		InvitedByID: dbUser.ID,
		UserID:      dbUser.ID,
	}
	err = registrationLog.Create(s.db)
	if err != nil {
		s.FailNow("failed to create registration log for user %s", err)
	}
	s.tenant = tenant
}

func (s *BillingOnDemandTestSuite) TestUpdateTenantDiscount() {
	assert := s.Assert()
	assert.NoError(s.context.EnableOrgOnDemandPlan())

	discount := int64(50)

	s.billingHandler.On("UpdateBindedPackageDiscount", mock.Anything).Return(nil)
	s.billingHandler.Calls = nil

	assert.NoError(s.context.UpdateTenantOnDemandDiscount(&discount))
	s.billingHandler.AssertExpectations(s.T())
	assert.Zero(s.billingHandler.Calls[0].Arguments[0].(*decimal.Decimal).Cmp(decimal.RequireFromString("0.5")))

	s.billingHandler.ExpectedCalls = nil
	s.billingHandler.On("UpdateBindedPackageDiscount", (*decimal.Decimal)(nil)).Return(nil)
	s.billingHandler.Calls = nil
	assert.NoError(s.context.UpdateTenantOnDemandDiscount(nil))
	s.billingHandler.AssertExpectations(s.T())
}

func (s *BillingOnDemandTestSuite) TestGetTenantDiscount() {
	assert := s.Assert()
	assert.NoError(s.context.EnableOrgOnDemandPlan())
	discount := decimal.RequireFromString("0.5")

	s.billingHandler.On("GetBindedPackageDiscount").Return(&discount)
	res, err := s.context.GetTenantOnDemandDiscount()
	assert.NoError(err)
	assert.EqualValues(*res, 50)

	s.billingHandler.ExpectedCalls = nil
	s.billingHandler.On("GetBindedPackageDiscount").Return((*decimal.Decimal)(nil))
	res, err = s.context.GetTenantOnDemandDiscount()
	assert.NoError(err)
	assert.Nil(res)
}

func (s *BillingOnDemandTestSuite) TestEnableOnDemand() {
	assert := s.Assert()
	err := s.context.EnableOrgOnDemandPlan()
	assert.NoError(err)
	tenant, err := dbutil.GetTenantByID(s.db, s.tenant.ID)
	assert.NoError(err)
	assert.EqualValues(tenant.OnDemandBindedPackageID, 1)
}

func (s *BillingOnDemandTestSuite) TestEnableOnDemandWithoutSalesforceSync() {
	assert := s.Assert()
	err := s.context.EnableOrgOnDemandPlanWithoutSalesforceSync()
	assert.NoError(err)
	tenant, err := dbutil.GetTenantByID(s.db, s.tenant.ID)
	assert.NoError(err)
	assert.EqualValues(tenant.OnDemandBindedPackageID, 1)
}

func (s *BillingOnDemandTestSuite) TestIsEnableOnDemand() {
	assert := s.Assert()
	enable, err := s.context.IsOnDemandPlanEnabled()
	assert.NoError(err)
	assert.False(enable)

	s.tenant.Plan = entity.TenantPlanOnDemand
	assert.NoError(s.tenant.Update(s.db, entity.TenantDBSchema.Plan))

	enable, err = s.context.IsOnDemandPlanEnabled()
	assert.NoError(err)
	assert.True(enable)
}

func (s *BillingOnDemandTestSuite) TestGetGlobalDiscount() {
	assert := s.Assert()
	value := decimal.RequireFromString("0.5")
	s.billingHandler.On("GetPackageDiscount").Return(&value)
	res, err := s.model.GetGlobalOnDemandDiscount()
	assert.NoError(err)
	assert.EqualValues(res, 50)
}

func (s *BillingOnDemandTestSuite) TestUpdateGlobalDiscount() {
	assert := s.Assert()
	value := decimal.NewFromInt(1)
	s.billingHandler.On("UpdatePackageDiscount", mock.Anything).Return(&value)
	err := s.model.UpdateGlobalOnDemandDiscount(50)
	assert.NoError(err)
	assert.Zero(decimal.RequireFromString("0.5").Cmp(s.billingHandler.Calls[0].Arguments[0].(decimal.Decimal)))
}

func (s *BillingOnDemandTestSuite) TestGetOnDemandEffectiveDiscount() {
	assert := s.Assert()
	value := decimal.RequireFromString("0.5")
	s.billingHandler.On("GetPackageDiscount").Return(&value)
	discount := decimal.RequireFromString("0.3")
	s.billingHandler.On("GetBindedPackageDiscount").Return(&discount)

	res, err := s.context.GetOnDemandEffectiveDiscount()
	assert.NoError(err)
	assert.EqualValues(res, 100)

	assert.NoError(s.context.EnableOrgOnDemandPlan())
	res, err = s.context.GetOnDemandEffectiveDiscount()
	assert.NoError(err)
	assert.EqualValues(res, 30)
}

func (s *BillingOnDemandTestSuite) TestSyncOnDemand() {
	assert := s.Assert()
	context := worker.BgModel{Model: s.model}.BgContext(s.tenant.ID)
	assert.NoError(context.SyncEnableOnDemandByPayable())
	enable, err := s.context.IsOnDemandPlanEnabled()
	assert.NoError(err)
	assert.True(enable)
}

func (s *BillingOnDemandTestSuite) TestCreatePayment() {
	assert := s.Assert()
	assert.NoError(s.context.CreatePayment())

	provider := handler.AWSAccountProvider

	s.tenant.ThirdPartyAccountProvider = &provider
	assert.NoError(s.tenant.Update(s.db, entity.TenantDBSchema.ThirdPartyAccountProvider))
	s.billingHandler.On("CreatePayment", "aws").Return()
	assert.NoError(s.context.CreatePayment())
}

func (s *BillingOnDemandTestSuite) TestSyncSalesforceFailed() {
	config.Global().SalesforceConfig.EnableSalesforce = true
	assert := s.Assert()
	_ = assert
	s.salesforceHandler.ExpectedCalls = nil
	mock_err := errors.New("mock_error")
	s.salesforceHandler.On("SyncLeads").Return(mock_err)
	err := s.context.SyncSalesforce()
	s.salesforceHandler.AssertExpectations(s.T())
	assert.Error(err)
	assert.ErrorIs(err, mock_err)
	record := entity.TenantSalesforceSyncRecord{}
	err = entity.NewTenantSalesforceSyncRecordQuerySet(s.db).TenantIDEq(s.tenant.ID).One(&record)
	assert.NoError(err)
	assert.False(record.Synced)

	s.salesforceHandler.ExpectedCalls = nil
	s.salesforceHandler.On("SyncLeads").Return(nil)
	err = s.context.SyncSalesforce()
	assert.NoError(err)
	err = entity.NewTenantSalesforceSyncRecordQuerySet(s.db).TenantIDEq(s.tenant.ID).One(&record)
	assert.NoError(err)
	assert.True(record.Synced)
}
