package tenant

import (
	"context"
	"testing"

	"github.com/gin-gonic/gin"
	_ "github.com/golang/mock/mockgen/model"
	"github.com/juju/errors"
	"github.com/pingcap/dbaas/central/config"
	"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/session"
	"github.com/pingcap/dbaas/central/model/tenant/handler"
	"github.com/pingcap/dbaas/central/test"
	"github.com/pingcap/log"
	. "github.com/smartystreets/goconvey/convey"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/suite"
	mailv2 "github.com/tidbcloud/pkgv2/mail/v2"
	panicIf "github.com/tidbcloud/pkgv2/panicif"
	"github.com/tidbcloud/pkgv2/salesforce"
	"github.com/tidbcloud/pkgv2/secret/encrypt"
	"github.com/tidbcloud/pkgv2/zendesk"
	"gopkg.in/auth0.v4/management"
	"k8s.io/utils/pointer"
)

var (
	testEmail = "test@pingcap.com"
)

type TenantTestSuite struct {
	test.CentralTestSuite
}

func newTokenSession() *session.Session {
	aesCrypt, err := encrypt.NewAESCrypt(make([]byte, 16))
	if err != nil {
		panicIf.Error(errors.Trace(err))
	}
	session := session.NewSession(aesCrypt)
	return session
}

type MockMailHandler struct {
	mock.Mock
}

func (m *MockMailHandler) SendMailWithTemplate(ctx context.Context, to []string, subject, templateName string, vars map[string]string, opts *mailv2.SendMailOpts) error {
	_ = m.Called(ctx, to, subject, templateName, vars, opts)
	return nil
}

func newMailMock() mailv2.Facade {
	return &MockMailHandler{}
}

type MockAuth0UserManagementHandler struct{}

func (*MockAuth0UserManagementHandler) Create(*management.User) error { return nil }
func (*MockAuth0UserManagementHandler) Delete(string) error           { return nil }
func (*MockAuth0UserManagementHandler) ListByEmail(string, ...management.ListOption) ([]*management.User, error) {
	connection := config.Global().Auth0Config.Auth0DatabaseConnectionName
	return []*management.User{
		{
			ID:    pointer.StringPtr("123"),
			Email: pointer.StringPtr(testEmail),
			Identities: []*management.UserIdentity{
				{
					Connection: pointer.StringPtr(connection),
				},
			},
		},
	}, nil
}
func (*MockAuth0UserManagementHandler) Update(id string, u *management.User) error { return nil }

func newAuth0Mock() Auth0UserManagementHandler {
	return &MockAuth0UserManagementHandler{}
}

type MockZendeskHandler struct{}

func (*MockZendeskHandler) CreateOrUpdateOrganization(input zendesk.Organization) (zendesk.Organization, error) {
	return input, nil
}

func (*MockZendeskHandler) CreateOrganization(input zendesk.Organization) (zendesk.Organization, error) {
	return input, nil
}

func newZendeskMock() ZendeskHandler {
	return &MockZendeskHandler{}
}

type MockSalesforceHandler struct {
	mock.Mock
}

func (m *MockSalesforceHandler) UpdateAccount(account salesforce.Account) error {
	_ = m.Called(account)
	return nil
}

func (m *MockSalesforceHandler) SyncLeads(lead salesforce.Lead) error {
	_ = m.Called(lead)
	return nil
}

func newSalesforceMock() SalesforceHandler {
	sales := &MockSalesforceHandler{}
	sales.On("UpdateAccount", mock.Anything).Return(nil)
	sales.On("SyncLeads", mock.Anything).Return(nil)
	return sales
}

func NewMockTenantModel() Model {
	mailMock := newMailMock()
	salesMock := newSalesforceMock()
	return NewModel(
		log.L(),
		newTokenSession(),
		mailMock,
		newAuth0Mock(),
		newZendeskMock(),
		salesMock,
		billing.NewModel(log.L(), mailMock, salesMock),
		nil,
	)
}

func (s *TenantTestSuite) TestTenantRegister() {
	assert := s.Assert()
	ctx := Context{
		Model: NewMockTenantModel(),
		log:   log.L(),
	}
	requestCtx := context.Background()
	params := &CreateOrgParams{
		Name:                      "pingcap",
		Email:                     testEmail,
		Country:                   "US",
		Password:                  "test_password",
		FirstName:                 "test",
		LastName:                  "user",
		Job:                       "Engineer",
		PhoneNumber:               "123456789",
		ConnectionType:            config.Auth0DatabaseConnection,
		ClientIP:                  "mock_user_ip",
		Auth0Token:                "",
		ThirdPartyAccountProvider: "",
		ThirdPartyAccountToken:    "",
	}

	mailMock := ctx.Model.mail.(*MockMailHandler)
	mailMock.On("SendMailWithTemplate", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
	err := ctx.CreateOrg(requestCtx, params)
	assert.NoError(err)
	mailMock.AssertExpectations(s.T())
}

func (s *TenantTestSuite) TestGetTenant() {
	assert := s.Assert()
	ctx := Context{
		Model: NewMockTenantModel(),
		tenantAndUser: &tenant.User{
			TenantID:       2,
			IsImpersonated: false,
		},
		log: log.L(),
	}

	_, err := ctx.GetTenant()
	assert.NotNil(err)
	params := &CreateOrgParams{
		Name:                      "pingcap",
		Email:                     testEmail,
		Country:                   "US",
		Password:                  "test_password",
		FirstName:                 "test",
		LastName:                  "user",
		Job:                       "Engineer",
		PhoneNumber:               "123456789",
		ConnectionType:            config.Auth0DatabaseConnection,
		ClientIP:                  "mock_user_ip",
		Auth0Token:                "",
		ThirdPartyAccountProvider: "",
		ThirdPartyAccountToken:    "",
	}
	requestCtx := context.Background()
	mailMock := ctx.Model.mail.(*MockMailHandler)
	mailMock.On("SendMailWithTemplate", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
	_ = ctx.CreateOrg(requestCtx, params)
	tenant, err := ctx.GetOrganization()
	assert.NoError(err)
	assert.Equal(tenant.ID.Uint64(), uint64(2))
	assert.Equal(tenant.HasDiscount, false)
	assert.EqualValues(tenant.Discount, 100)
	assert.EqualValues(tenant.RemainingCredit, 0)
	assert.Equal(tenant.DiscountDescription, "")
	mailMock.AssertExpectations(s.T())
}

type MockAccountHandler struct {
	MockProvider handler.AccountProvider
}

func (m *MockAccountHandler) GetAccountProvider(accountProvider string) (handler.AccountProvider, error) {
	return m.MockProvider, nil
}

func (m *MockAccountHandler) GetAllAccountProviders() ([]handler.AccountProvider, error) {
	return []handler.AccountProvider{
		m.MockProvider,
	}, nil
}

type MockAccountProvider struct {
	mock.Mock
}

func (m *MockAccountProvider) ResolveToken(token *string) (*string, error) {
	args := m.Called(token)
	res := args.String(0)
	return &res, args.Error(1)
}

func (*MockAccountProvider) GetTokenFromRedirect(c *gin.Context) (*string, error) {
	return nil, nil
}

func (*MockAccountProvider) ProcessTenantSubscribeChange(ctx context.Context, processor handler.ProcessTenantSubscribeChangeFunc) error {
	return nil
}

func (s *TenantTestSuite) TestRegisterWithThirdParty() {
	assert := s.Assert()
	accountProvider := &MockAccountProvider{}
	handler.SetAccountHandler(&MockAccountHandler{
		MockProvider: accountProvider,
	})
	requestCtx := context.Background()
	ctx := Context{
		Model: NewMockTenantModel(),
		log:   log.L(),
	}
	params := &CreateOrgParams{
		Name:                      "pingcap",
		Email:                     testEmail,
		Country:                   "CN",
		Password:                  "test_password",
		FirstName:                 "test",
		LastName:                  "user",
		Job:                       "Engineer",
		PhoneNumber:               "123456789",
		ConnectionType:            config.Auth0DatabaseConnection,
		ClientIP:                  "mock_user_ip",
		Auth0Token:                "",
		ThirdPartyAccountProvider: handler.AWSAccountProvider,
		ThirdPartyAccountToken:    "abc",
	}
	mailMock := ctx.Model.mail.(*MockMailHandler)
	mailMock.On("SendMailWithTemplate", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
	accountProvider.On("ResolveToken", mock.Anything).Return("ABC", nil)
	registrationErr := ctx.CreateOrg(requestCtx, params)
	accountProvider.AssertExpectations(s.T())
	assert.NoError(registrationErr)

	ctx.tenantAndUser = &tenant.User{
		TenantID:       2,
		IsImpersonated: false,
	}
	tenant, err := ctx.GetTenant()
	assert.NoError(err)
	assert.Equal(tenant.ID.Uint64(), uint64(2))
	assert.Equal(*tenant.ThirdPartyAccountProvider, handler.AWSAccountProvider)
	assert.Equal(*tenant.ThirdPartyAccountCode, "ABC")
}

type MockBillingHandler struct {
	billing2.BillingHandler
}

func (*MockBillingHandler) BindPackage(uint64, uint64) (uint64, error) {
	return 0, nil
}

func (*MockBillingHandler) CreatePayment(uint64, string) error {
	return nil
}

func (s *TenantTestSuite) TestRegisterWithThirdPartySyncSalesForce() {
	assert := s.Assert()
	config.Global().SalesforceConfig.EnableSalesforce = true
	accountProvider := &MockAccountProvider{}
	handler.SetAccountHandler(&MockAccountHandler{
		MockProvider: accountProvider,
	})
	billing2.SetHandler(&MockBillingHandler{})
	requestCtx := context.Background()
	ctx := Context{
		Model: NewMockTenantModel(),
		log:   log.L(),
	}
	params := &CreateOrgParams{
		Name:                      "pingcap",
		Email:                     testEmail,
		Country:                   "CN",
		Password:                  "test_password",
		FirstName:                 "test",
		LastName:                  "user",
		Job:                       "Engineer",
		PhoneNumber:               "123456789",
		ConnectionType:            config.Auth0DatabaseConnection,
		ClientIP:                  "mock_user_ip",
		Auth0Token:                "",
		ThirdPartyAccountProvider: handler.AWSAccountProvider,
		ThirdPartyAccountToken:    "abc",
	}
	mailMock := ctx.Model.mail.(*MockMailHandler)
	salesForceMock := ctx.Model.salesforceClient.(*MockSalesforceHandler)
	mailMock.On("SendMailWithTemplate", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
	accountProvider.On("ResolveToken", mock.Anything).Return("ABC", nil)

	registrationErr := ctx.CreateOrg(requestCtx, params)
	assert.NoError(registrationErr)
	// accountProvider.AssertExpectations(s.T())
	salesForceMock.AssertNotCalled(s.T(), "SyncLeads", mock.Anything)
	salesForceMock.AssertNotCalled(s.T(), "UpdateAccount", mock.Anything)
	token := mailMock.Calls[0].Arguments[4].(map[string]string)["token"]

	email, err := ctx.RegistrationVerification(requestCtx, token)
	assert.NoError(err)
	salesForceMock.AssertCalled(s.T(), "SyncLeads", mock.Anything)
	salesForceMock.AssertNotCalled(s.T(), "UpdateAccount", mock.Anything)
	assert.Equal(email, &testEmail)
}

func (s *TenantTestSuite) TestRegisterWithThirdPartyCodeAlreadyExists() {
	assert := s.Assert()
	accountProvider := &MockAccountProvider{}
	handler.SetAccountHandler(&MockAccountHandler{
		MockProvider: accountProvider,
	})
	requestCtx := context.Background()
	ctx := Context{
		Model: NewMockTenantModel(),
		log:   log.L(),
	}

	params := &CreateOrgParams{
		Name:                      "pingcap",
		Email:                     testEmail,
		Country:                   "CN",
		Password:                  "test_password",
		FirstName:                 "test",
		LastName:                  "user",
		Job:                       "Engineer",
		PhoneNumber:               "123456789",
		ConnectionType:            config.Auth0DatabaseConnection,
		ClientIP:                  "mock_user_ip",
		Auth0Token:                "",
		ThirdPartyAccountProvider: handler.AWSAccountProvider,
		ThirdPartyAccountToken:    "abc",
	}

	mailMock := ctx.Model.mail.(*MockMailHandler)
	mailMock.On("SendMailWithTemplate", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
	accountProvider.On("ResolveToken", mock.Anything).Return("ABC", nil)
	err := ctx.CreateOrg(requestCtx, params)
	assert.NoError(err)
	accountProvider.AssertExpectations(s.T())

	params2 := &CreateOrgParams{
		Name:                      "pingcap2",
		Email:                     "test2@pingcap.com",
		Country:                   "CN",
		Password:                  "test_password",
		FirstName:                 "test2",
		LastName:                  "user2",
		Job:                       "Engineer",
		PhoneNumber:               "123456789",
		ConnectionType:            config.Auth0DatabaseConnection,
		ClientIP:                  "mock_user_ip",
		Auth0Token:                "",
		ThirdPartyAccountProvider: handler.AWSAccountProvider,
		ThirdPartyAccountToken:    "abc",
	}
	err = ctx.CreateOrg(requestCtx, params2)
	assert.NotNil(err)
	assert.Equal(err.ErrCode(), config.ErrCodeDuplicatedThirdPartyAccount)
}

func TestTenantSuite(t *testing.T) {
	suite.Run(t, new(TenantTestSuite))
}

func TestTransferWhiteFlag(t *testing.T) {
	config.Global().ServerConfig.EnableDataStreamToKafka = []uint64{1, 3}
	config.Global().ServerConfig.EnableDataStreamToMysql = []uint64{2, 3}
	t1 := TransferWhiteFlag(uint64(1))
	t2 := TransferWhiteFlag(uint64(2))
	t3 := TransferWhiteFlag(uint64(3))
	assert.Equal(t, t1, uint32(2))
	assert.Equal(t, t2, uint32(1))
	assert.Equal(t, t3, uint32(3))
}

func TestGetGithubFirstAndLastName(t *testing.T) {
	Convey("name is empty", t, func() {
		name := ""
		nickName := "aaa"
		firstName, lastName := getGithubFirstAndLastName(name, nickName, testEmail)
		So(firstName, ShouldEqual, nickName)
		So(lastName, ShouldEqual, "")
	})

	Convey("name is the same as email", t, func() {
		name := testEmail
		nickName := "cde"
		firstName, lastName := getGithubFirstAndLastName(name, nickName, testEmail)
		So(firstName, ShouldEqual, nickName)
		So(lastName, ShouldEqual, "")
	})

	Convey("name has one word", t, func() {
		name := "abc"
		nickName := "bbb"
		firstName, lastName := getGithubFirstAndLastName(name, nickName, testEmail)
		So(firstName, ShouldEqual, name)
		So(lastName, ShouldEqual, "")
	})

	Convey("name has two words", t, func() {
		name := "aaa bbb"
		nickName := "ccc"
		firstName, lastName := getGithubFirstAndLastName(name, nickName, testEmail)
		So(firstName, ShouldEqual, "aaa")
		So(lastName, ShouldEqual, "bbb")
	})

	Convey("name has multi words", t, func() {
		name := "aaa bbb ccc"
		nickName := "ddd"
		firstName, lastName := getGithubFirstAndLastName(name, nickName, testEmail)
		So(firstName, ShouldEqual, "aaa bbb")
		So(lastName, ShouldEqual, "ccc")
	})
}
