package sqluser

import (
	context2 "context"
	"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"
	"github.com/tidbcloud/tidb-management-service/serverless-global-service/domain/adapter/mock_adapter"
	"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/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/clusterproxy/mock_clusterproxy"
)

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

		clusterRepo := mock_repository.NewMockClusterRepo(ctrl)
		clusterProxy := mock_clusterproxy.NewMockClusterProxy(ctrl)
		clusterProxyProvider := mock_clusterproxy.NewMockProvider(ctrl)
		encryptSvc := mock_adapter.NewMockEncryptorService(ctrl)
		regionalSvc := mock_adapter.NewMockRegionalService(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.SqlUserServiceUpdateSqlUserRequest{
			Operator:    testdata.GetDBUserOperator(),
			ClusterId:   cluster.ClusterId,
			Username:    "u1",
			BuiltinRole: nil,
			CustomRoles: nil,
		}
		param := &UpdateUserParams{
			Operator:    req.Operator,
			Cluster:     regionalCluster,
			Username:    req.Username,
			BuiltinRole: req.BuiltinRole,
			CustomRoles: req.CustomRoles,
		}
		// 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("Update", func() {
			convey.Convey("have no any role change", func() {
				svc.roleGetter = func(ctx context2.Context, proxy adapter.ClusterProxy, username string) ([]string, bizerr.BizError) {
					return []string{BuiltinRoleNameAdmin}, nil
				}

				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)

				err := svc.UpdateSqlUser(testdata.Ctx, req)
				convey.So(err, convey.ShouldBeNil)
			})
			convey.Convey("both change builtin role and custom roles", func() {
				svc.roleGetter = func(ctx context2.Context, proxy adapter.ClusterProxy, username string) ([]string, bizerr.BizError) {
					return []string{BuiltinRoleNameAdmin, "r1"}, nil
				}
				newBuiltinRole := globalserverlessv1.SqlUser_BUILTIN_ROLE_CDC_ADMIN
				req.BuiltinRole = &newBuiltinRole
				req.CustomRoles = []string{"r2", "r3"}
				req.Password = proto.String("1234")

				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{
					"GRANT 'role_cdc_admin' TO 'u1';",
					"GRANT 'r2' TO 'u1';",
					"GRANT 'r3' TO 'u1';",
					"REVOKE 'role_admin' FROM 'u1';",
					"REVOKE 'r1' FROM 'u1';",
					`SET DEFAULT ROLE ALL TO 'u1';`,
					"ALTER USER 'u1' @'%' IDENTIFIED BY '1234';",
				})

				err := svc.UpdateSqlUser(testdata.Ctx, req)
				convey.So(err, convey.ShouldBeNil)
			})
			convey.Convey("both change builtin role and password, clear custom roles", func() {
				svc.roleGetter = func(ctx context2.Context, proxy adapter.ClusterProxy, username string) ([]string, bizerr.BizError) {
					return []string{BuiltinRoleNameAdmin, "r1"}, nil
				}
				newBuiltinRole := globalserverlessv1.SqlUser_BUILTIN_ROLE_CDC_ADMIN
				req.BuiltinRole = &newBuiltinRole
				req.ClearCustomRoles = true
				password := "1234"
				req.Password = &password

				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{
					"GRANT 'role_cdc_admin' TO 'u1';",
					"REVOKE 'role_admin' FROM 'u1';",
					"REVOKE 'r1' FROM 'u1';",
					`SET DEFAULT ROLE ALL TO 'u1';`,
					"ALTER USER 'u1' @'%' IDENTIFIED BY '1234';",
				})

				err := svc.UpdateSqlUser(testdata.Ctx, req)
				convey.So(err, convey.ShouldBeNil)
			})
		})
		convey.Convey("checkParams", func() {
			convey.Convey("should be ok if all is well", func() {
				err := updateSqlUserCheck(param, cluster)
				convey.So(err, convey.ShouldBeNil)
			})
			convey.Convey("should return err if custom roles contain builtin role", func() {
				param.CustomRoles = []string{BuiltinRoleNameAdmin}

				err := updateSqlUserCheck(param, cluster)
				convey.So(err, convey.ShouldNotBeNil)
				convey.So(err.Error(), convey.ShouldContainSubstring, "builtin role")
			})
		})
		convey.Convey("getDesiredRoles", func() {
			currentRoles := []string{BuiltinRoleNameAdmin}
			role1 := "r1"
			convey.Convey("should return the current builtin role if builtinRole is nil", func() {
				param.BuiltinRole = nil
				param.CustomRoles = []string{role1}

				r1 := svc.getUpdateUserDesiredRoles(testdata.Ctx, param, currentRoles)
				convey.So(r1, convey.ShouldResemble, []string{BuiltinRoleNameAdmin, role1})
			})
			convey.Convey("should return the current custom roles if customRoles is nil", func() {
				currentRoles = []string{BuiltinRoleNameAdmin, role1}
				param.BuiltinRole = nil
				param.CustomRoles = nil

				r1 := svc.getUpdateUserDesiredRoles(testdata.Ctx, param, currentRoles)
				convey.So(r1, convey.ShouldResemble, currentRoles)
			})
			convey.Convey("should return new builtin role and new custom roles if both set them", func() {
				currentRoles = []string{BuiltinRoleNameAdmin, role1}
				newBuiltinRole := globalserverlessv1.SqlUser_BUILTIN_ROLE_CDC_ADMIN
				param.BuiltinRole = &newBuiltinRole
				param.CustomRoles = []string{"r2", "r3"}
				desiredRoles := []string{BuiltinRoleNameCDCAdmin, "r2", "r3"}

				r1 := svc.getUpdateUserDesiredRoles(testdata.Ctx, param, currentRoles)
				convey.So(r1, convey.ShouldResemble, desiredRoles)
			})
			convey.Convey("should return custom roles if both set them", func() {
				currentRoles = []string{BuiltinRoleNameAdmin, role1}
				newBuiltinRole := globalserverlessv1.SqlUser_BUILTIN_ROLE_UNSPECIFIED
				param.BuiltinRole = &newBuiltinRole
				param.CustomRoles = []string{"r2", "r3"}
				desiredRoles := []string{"r2", "r3"}

				r1 := svc.getUpdateUserDesiredRoles(testdata.Ctx, param, currentRoles)
				convey.So(r1, convey.ShouldResemble, desiredRoles)
			})
		})
	})
}
