package sqluser

import (
	"strconv"
	"testing"

	"github.com/smartystreets/goconvey/convey"
	"go.uber.org/mock/gomock"
	"google.golang.org/protobuf/proto"

	globalserverlessv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/serverless/v1"
	regionalserverlessv1 "github.com/tidbcloud/tidb-management-service/api/spec/regional/serverless/v1"
	"github.com/tidbcloud/tidb-management-service/serverless-global-service/domain/adapter/mock_adapter"
	"github.com/tidbcloud/tidb-management-service/serverless-global-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/serverless-global-service/domain/repository/mock_repository"
	"github.com/tidbcloud/tidb-management-service/serverless-global-service/domain/testdata"
	"github.com/tidbcloud/tidb-management-service/utilities/clusterproxy/mock_clusterproxy"
)

func TestCreateUserSvc(t *testing.T) {
	convey.Convey("TesCreateUserSvc", t, func() {
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()

		clusterProxy := mock_clusterproxy.NewMockClusterProxy(ctrl)
		clusterProxyProvider := mock_clusterproxy.NewMockProvider(ctrl)
		encryptSvc := mock_adapter.NewMockEncryptorService(ctrl)
		regionalSvc := mock_adapter.NewMockRegionalService(ctrl)
		clusterRepo := mock_repository.NewMockClusterRepo(ctrl)
		sys9Lifecycle := mock_adapter.NewMockSys9Lifecycle(ctrl)
		cluster := testdata.GetCluster()
		regionalCluster := testdata.GetRegionalCluster()

		clusterIdInt, err := strconv.ParseUint(cluster.ClusterId, 10, 64)
		convey.So(err, convey.ShouldBeNil)

		req := &globalserverlessv1.SqlUserServiceCreateSqlUserRequest{
			Operator:    testdata.GetDBUserOperator(),
			ClusterId:   cluster.ClusterId,
			UserType:    globalserverlessv1.SqlUser_TYPE_DELEGATE,
			Username:    "u1",
			Email:       proto.String("u1@domain.com"),
			AuthMethod:  globalserverlessv1.SqlUser_AUTH_METHOD_TOKEN,
			Credential:  proto.String("token"),
			BuiltinRole: globalserverlessv1.SqlUser_BUILTIN_ROLE_USER_ADMIN,
			CustomRoles: nil,
		}

		createUserInfo := &CreateUserInfo{
			UserType:    req.UserType,
			UserName:    req.Username,
			UserEmail:   *req.Email,
			AuthMethod:  req.AuthMethod,
			Credential:  *req.Credential,
			BuiltinRole: req.BuiltinRole,
			CustomRoles: req.CustomRoles,
		}
		params := &CreateUserParams{
			Operator: req.Operator,
			Cluster:  regionalCluster,
			Settings: &globalserverlessv1.SqlUserServiceCreateSqlUserRequest_Settings{
				IgnoreExists: false,
			},
			CreateUserInfo: createUserInfo,
		}

		createUserInfo2 := &CreateUserInfo{
			UserType:    globalserverlessv1.SqlUser_TYPE_USER_DEFINED,
			UserName:    "u2",
			UserEmail:   "u2@domain.com",
			AuthMethod:  globalserverlessv1.SqlUser_AUTH_METHOD_TOKEN,
			Credential:  "token2",
			BuiltinRole: globalserverlessv1.SqlUser_BUILTIN_ROLE_USER_ADMIN,
			CustomRoles: []string{"custom_role1"},
		}
		batchCreateParams := &BatchCreateUserParams{
			Operator: req.Operator,
			Cluster:  regionalCluster,
			ClusterAttributes: entity.ClusterAttributes{
				TokenBasedAuthEnabled: true,
			},
			Users: []*CreateUserInfo{
				createUserInfo,
				createUserInfo2,
			},
		}
		// enable token-based auth
		cluster.Attributes.TokenBasedAuthEnabled = true
		svc, ok := NewSqlUserService(encryptSvc, clusterProxyProvider, clusterRepo, regionalSvc, sys9Lifecycle, nil, nil).(*sqlUserSvcImpl)
		convey.So(ok, convey.ShouldBeTrue)

		convey.Convey("Create", func() {
			regionalSvc.EXPECT().
				ListCluster(gomock.Any(), gomock.Any(), gomock.Any()).
				Return([]*regionalserverlessv1.ServerlessRegionalServiceClusterInfo{regionalCluster}, nil)
			clusterRepo.EXPECT().Get(gomock.Any(), cluster.ClusterId, false).Return(cluster, nil)
			clusterProxyProvider.EXPECT().
				GetClusterProxy(gomock.Any(), clusterIdInt, regionalCluster.ProxyEndpoint, regionalCluster.Version, BuildClusterOperator(req.Operator)).
				Return(clusterProxy, nil)
			clusterProxy.EXPECT().ExecuteMultiSQL(gomock.Any(), []string{
				`CREATE USER IF NOT EXISTS 'u1' IDENTIFIED WITH tidb_auth_token REQUIRE TOKEN_ISSUER 'pingcap.com' ATTRIBUTE '{"email":"u1@domain.com"}';`,
				"GRANT 'role_admin' TO 'u1';",
				"SET DEFAULT ROLE ALL TO 'u1';",
			})

			err := svc.CreateSqlUser(testdata.Ctx, req)
			convey.So(err, convey.ShouldBeNil)
		})
		convey.Convey("BatchCreate", func() {
			clusterProxyProvider.EXPECT().
				GetClusterProxy(gomock.Any(), clusterIdInt, regionalCluster.ProxyEndpoint, regionalCluster.Version, BuildClusterOperator(req.Operator)).
				Return(clusterProxy, nil)

			clusterProxy.EXPECT().ExecuteMultiSQL(gomock.Any(), []string{
				`CREATE USER IF NOT EXISTS 'u1' IDENTIFIED WITH tidb_auth_token REQUIRE TOKEN_ISSUER 'pingcap.com' ATTRIBUTE '{"email":"u1@domain.com"}';`,
				"GRANT 'role_admin' TO 'u1';",
				"SET DEFAULT ROLE ALL TO 'u1';",

				`CREATE USER IF NOT EXISTS 'u2' IDENTIFIED WITH tidb_auth_token REQUIRE TOKEN_ISSUER 'pingcap.com' ATTRIBUTE '{"email":"u2@domain.com"}';`,
				"GRANT 'role_admin' TO 'u2';",
				"GRANT 'custom_role1' TO 'u2';",
				"SET DEFAULT ROLE ALL TO 'u2';",
			})

			err := svc.CreateSqlUsers(testdata.Ctx, batchCreateParams)
			convey.So(err, convey.ShouldBeNil)
		})
		convey.Convey("checkUser", func() {
			convey.Convey("should be ok if all is well", func() {
				err := checkUser(testdata.Ctx, createUserInfo)
				convey.So(err, convey.ShouldBeNil)
			})
			convey.Convey("should be ok if trying to create a token-based UserDefined user", func() {
				params.UserType = globalserverlessv1.SqlUser_TYPE_USER_DEFINED
				params.AuthMethod = globalserverlessv1.SqlUser_AUTH_METHOD_TOKEN

				err := checkUser(testdata.Ctx, createUserInfo)
				convey.So(err, convey.ShouldBeNil)
			})
			convey.Convey("should be ok if both builtin role and custom roles are not set", func() {
				params.BuiltinRole = globalserverlessv1.SqlUser_BUILTIN_ROLE_UNSPECIFIED
				params.CustomRoles = nil

				err := checkUser(testdata.Ctx, createUserInfo)
				convey.So(err, convey.ShouldBeNil)
			})
			convey.Convey("should return err if AuthMethod is not Token and UserType is delegate", func() {
				createUserInfo.UserType = globalserverlessv1.SqlUser_TYPE_DELEGATE
				createUserInfo.AuthMethod = globalserverlessv1.SqlUser_AUTH_METHOD_PASSWORD

				err := checkUser(testdata.Ctx, createUserInfo)
				convey.So(err, convey.ShouldNotBeNil)
				convey.So(err.Error(), convey.ShouldContainSubstring, "only support token")
			})
			convey.Convey("should be ok if AuthMethod is token but UserEmail is empty", func() {
				createUserInfo.AuthMethod = globalserverlessv1.SqlUser_AUTH_METHOD_TOKEN
				createUserInfo.UserEmail = ""

				err := checkUser(testdata.Ctx, createUserInfo)
				convey.So(err, convey.ShouldBeNil)
			})
			convey.Convey("should return err if AuthMethod is Password but the credential is empty", func() {
				createUserInfo.UserType = globalserverlessv1.SqlUser_TYPE_USER_DEFINED
				createUserInfo.AuthMethod = globalserverlessv1.SqlUser_AUTH_METHOD_PASSWORD
				createUserInfo.Credential = ""

				err := checkUser(testdata.Ctx, createUserInfo)
				convey.So(err, convey.ShouldNotBeNil)
				convey.So(err.Error(), convey.ShouldContainSubstring, "credential")
			})
			convey.Convey("should return err if custom roleName is builtin role", func() {
				createUserInfo.CustomRoles = []string{BuiltinRoleNameAdmin}

				err := checkUser(testdata.Ctx, createUserInfo)
				convey.So(err, convey.ShouldNotBeNil)
				convey.So(err.Error(), convey.ShouldContainSubstring, "builtin role")
			})
		})
		convey.Convey("skipCreationIfIgnoreExists", func() {
			convey.Convey("should do nothing is IgnoreExists is nil", func() {
				r1, err := svc.skipCreationIfIgnoreExists(testdata.Ctx, clusterProxy, params)
				convey.So(err, convey.ShouldBeNil)
				convey.So(r1, convey.ShouldBeFalse)
			})
			convey.Convey("should do nothing if params.Settings is nil", func() {
				params.Settings = nil

				r1, err := svc.skipCreationIfIgnoreExists(testdata.Ctx, clusterProxy, params)
				convey.So(err, convey.ShouldBeNil)
				convey.So(r1, convey.ShouldBeFalse)
			})
		})
		convey.Convey("buildCreateAndGrantSqls", func() {
			convey.Convey("token-based user", func() {
				createUserInfo.AuthMethod = globalserverlessv1.SqlUser_AUTH_METHOD_TOKEN

				r1, err := svc.buildCreateAndGrantSqls(testdata.Ctx, createUserInfo)
				convey.So(err, convey.ShouldBeNil)
				convey.So(r1, convey.ShouldResemble, []string{
					`CREATE USER IF NOT EXISTS 'u1' IDENTIFIED WITH tidb_auth_token REQUIRE TOKEN_ISSUER 'pingcap.com' ATTRIBUTE '{"email":"u1@domain.com"}';`,
					"GRANT 'role_admin' TO 'u1';",
					"SET DEFAULT ROLE ALL TO 'u1';",
				})
			})
			convey.Convey("password-based user", func() {
				createUserInfo.AuthMethod = globalserverlessv1.SqlUser_AUTH_METHOD_PASSWORD
				encryptSvc.EXPECT().
					DecryptBase64(gomock.Any(), createUserInfo.Credential).
					Return("plaintext password", nil)

				r1, err := svc.buildCreateAndGrantSqls(testdata.Ctx, createUserInfo)
				convey.So(err, convey.ShouldBeNil)
				convey.So(r1, convey.ShouldResemble, []string{
					`CREATE USER IF NOT EXISTS 'u1' IDENTIFIED BY 'plaintext password';`,
					"GRANT 'role_admin' TO 'u1';",
					"SET DEFAULT ROLE ALL TO 'u1';",
				})
			})
		})
		convey.Convey("buildCreateUserSql", func() {
			convey.Convey("token-based user", func() {
				createUserInfo.AuthMethod = globalserverlessv1.SqlUser_AUTH_METHOD_TOKEN

				r1, err := svc.buildCreateUserSql(testdata.Ctx, createUserInfo)
				convey.So(err, convey.ShouldBeNil)
				convey.So(
					r1,
					convey.ShouldEqual,
					`CREATE USER IF NOT EXISTS 'u1' IDENTIFIED WITH tidb_auth_token REQUIRE TOKEN_ISSUER 'pingcap.com' ATTRIBUTE '{"email":"u1@domain.com"}';`,
				)
			})
			convey.Convey("password-based user", func() {
				createUserInfo.AuthMethod = globalserverlessv1.SqlUser_AUTH_METHOD_PASSWORD
				encryptSvc.EXPECT().
					DecryptBase64(gomock.Any(), createUserInfo.Credential).
					Return("plaintext password", nil)

				r1, err := svc.buildCreateUserSql(testdata.Ctx, createUserInfo)
				convey.So(err, convey.ShouldBeNil)
				convey.So(r1, convey.ShouldEqual, `CREATE USER IF NOT EXISTS 'u1' IDENTIFIED BY 'plaintext password';`)
			})
		})
		convey.Convey("getDesiredRoles", func() {
			convey.Convey("should skip builtin role if createUserInfo.BuiltinRole is unspecified", func() {
				createUserInfo.BuiltinRole = globalserverlessv1.SqlUser_BUILTIN_ROLE_UNSPECIFIED
				createUserInfo.CustomRoles = []string{"r1", "r2"}

				r1 := svc.getCreateUserDesiredRoles(testdata.Ctx, createUserInfo)
				convey.So(r1, convey.ShouldResemble, createUserInfo.CustomRoles)
			})
			convey.Convey("should empty roles if both builtin role and custom roles are not set", func() {
				createUserInfo.BuiltinRole = globalserverlessv1.SqlUser_BUILTIN_ROLE_UNSPECIFIED
				createUserInfo.CustomRoles = nil

				r1 := svc.getCreateUserDesiredRoles(testdata.Ctx, createUserInfo)
				convey.So(r1, convey.ShouldBeEmpty)
			})
			convey.Convey("should return both builtin role and custom roles", func() {
				createUserInfo.BuiltinRole = globalserverlessv1.SqlUser_BUILTIN_ROLE_USER_ADMIN
				createUserInfo.CustomRoles = []string{"r1", "r2"}

				r1 := svc.getCreateUserDesiredRoles(testdata.Ctx, createUserInfo)
				convey.So(r1, convey.ShouldResemble, []string{BuiltinRoleNameAdmin, "r1", "r2"})
			})
		})
	})
}

func Test_buildDelegateUserAttribute(t *testing.T) {
	convey.Convey("Test_buildDelegateUserAttribute", t, func() {
		r1, _ := buildDelegateUserAttribute("user@domain.com")
		convey.So(r1, convey.ShouldEqual, `{"email":"user@domain.com"}`)

		convey.Convey("should be ok if userEmail is empty", func() {
			r1, _ := buildDelegateUserAttribute("")
			convey.So(r1, convey.ShouldEqual, `{"email":""}`)
		})
	})
}
