package user

import (
	"context"
	"errors"
	"strings"
	"testing"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	authapiv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/api/core-server/auth/v1"
	iamapiv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/api/core-server/iam/v1"
	userv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/api/core-server/user/v1"
	iamentv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/entity/core-server/iam/v1"
	coreuser "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/entity/core-server/user/v1"
	"go.uber.org/mock/gomock"
	"google.golang.org/grpc"

	auth "github.com/tidbcloud/devops/idl/proto/devops_server/auth/v1"
	"github.com/tidbcloud/devops/pkg/infra/clients/opscore"
	"github.com/tidbcloud/devops/pkg/infra/utils/test"
)

func TestAuthController(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "Auth Controller Suite", test.ReporterConfig())
}

var _ = Describe("[AuthController]", func() {
	var fw = test.Framework()
	var userSvc *opscore.MockUserServiceClient
	var authSvc *opscore.MockAuthServiceClient
	var iamSvc *opscore.MockIAMServiceClient
	var authController *AuthController

	BeforeEach(func() {
		userSvc = opscore.NewMockUserServiceClient(fw.Ctrl)
		authSvc = opscore.NewMockAuthServiceClient(fw.Ctrl)
		iamSvc = opscore.NewMockIAMServiceClient(fw.Ctrl)
		authController = &AuthController{
			userCli: userSvc,
			authCli: authSvc,
			iam:     iamSvc,
		}
	})

	Describe("[HasAccess]", func() {
		It("uses Ops Core Auth Service authorization", func() {
			orgUser := &coreuser.User{
				Email:     "unittest@pingcap.com",
				Available: true,
			}
			testCases := []struct {
				uri       string
				method    string
				hasAccess bool
			}{
				{uri: "/clusters", method: "get", hasAccess: true},
				{uri: "/clusters/123", method: "POST", hasAccess: false},
			}

			for _, testCase := range testCases {
				userSvc.EXPECT().GetUser(gomock.Any(), gomock.Any()).
					DoAndReturn(func(_ context.Context, req *userv1.GetUserRequest, _ ...grpc.CallOption) (*userv1.GetUserResponse, error) {
						Expect(req.Email).To(Equal(orgUser.Email))
						return &userv1.GetUserResponse{User: orgUser}, nil
					})
				authSvc.EXPECT().
					Authorize(gomock.Any(), gomock.Any()).
					DoAndReturn(func(_ context.Context, req *authapiv1.AuthorizeRequest, _ ...grpc.CallOption) (*authapiv1.AuthorizeResponse, error) {
						Expect(req.UserEmail).To(Equal(&orgUser.Email))
						Expect(req.Uri).To(Equal(testCase.uri))
						Expect(req.Method).To(Equal(strings.ToUpper(testCase.method)))
						return &authapiv1.AuthorizeResponse{
							Authorized: testCase.hasAccess,
						}, nil
					})
				resp, err := authController.HasAccess(context.Background(), &auth.HasAccessReq{
					Email:  orgUser.Email,
					Uri:    testCase.uri,
					Method: testCase.method,
				})
				Expect(err).NotTo(HaveOccurred())
				Expect(resp.HasAccess).To(Equal(testCase.hasAccess))
				Expect(resp.BaseResp).NotTo(BeNil())
			}
		})

		It("returns authorization errors", func() {
			userSvc.EXPECT().GetUser(gomock.Any(), gomock.Any()).Return(&userv1.GetUserResponse{User: &coreuser.User{
				Email:     "unittest@pingcap.com",
				Available: true,
			}}, nil)
			authSvc.EXPECT().
				Authorize(gomock.Any(), gomock.Any()).
				Return(nil, errors.New("authorize failed"))

			resp, err := authController.HasAccess(context.Background(), &auth.HasAccessReq{
				Email:  "unittest@pingcap.com",
				Uri:    "/test",
				Method: "GET",
			})
			Expect(err).To(MatchError("authorize failed"))
			Expect(resp).To(BeNil())
		})

		It("does not authorize unavailable users", func() {
			userSvc.EXPECT().GetUser(gomock.Any(), gomock.Any()).Return(&userv1.GetUserResponse{User: &coreuser.User{
				Email:     "unittest@pingcap.com",
				Available: false,
			}}, nil)

			resp, err := authController.HasAccess(context.Background(), &auth.HasAccessReq{
				Email:  "unittest@pingcap.com",
				Uri:    "/test",
				Method: "GET",
			})
			Expect(err).To(MatchError(ContainSubstring("user unittest@pingcap.com is not available")))
			Expect(resp).To(BeNil())
		})
	})

	Describe("[GetUser]", func() {
		It("returns all effective IAM roles and region info", func() {
			userSvc.EXPECT().GetUser(gomock.Any(), gomock.Any()).
				DoAndReturn(func(_ context.Context, req *userv1.GetUserRequest, _ ...grpc.CallOption) (*userv1.GetUserResponse, error) {
					Expect(req.Email).To(Equal("user@pingcap.com"))
					return &userv1.GetUserResponse{User: &coreuser.User{
						Email:              "user@pingcap.com",
						Available:          true,
						LarkUserId:         "ou_user",
						LarkUserName:       "Unit Test",
						LarkDepartmentId:   "od_dept",
						LarkDepartmentName: "Testing",
					}}, nil
				})
			iamSvc.EXPECT().
				ResolveEntityPermissions(gomock.Any(), gomock.Any()).
				DoAndReturn(func(_ context.Context, req *iamapiv1.ResolveEntityPermissionsRequest, _ ...grpc.CallOption) (*iamapiv1.ResolveEntityPermissionsResponse, error) {
					Expect(req.Target).To(Equal(&iamentv1.RoleBindingTarget{
						Type: iamentv1.RoleBindingTargetType_ROLE_BINDING_TARGET_TYPE_USER,
						Name: "user@pingcap.com",
					}))
					return &iamapiv1.ResolveEntityPermissionsResponse{
						Permissions: []*iamentv1.GrantedPermissions{
							{GrantPath: []*iamentv1.GrantSource{{Role: &iamentv1.Role{Name: "team-tse-apac"}}}},
							{GrantPath: []*iamentv1.GrantSource{{Role: &iamentv1.Role{Name: "team-tse-apac"}}}},
							{GrantPath: []*iamentv1.GrantSource{
								{Role: &iamentv1.Role{Name: "department-tse"}},
								{Entity: &iamentv1.RoleBindingTarget{
									Type: iamentv1.RoleBindingTargetType_ROLE_BINDING_TARGET_TYPE_USER_GROUP,
									Name: "tse",
								}},
							}},
						},
					}, nil
				})

			resp, err := authController.GetUser(context.Background(), &auth.GetUserReq{Email: "user@pingcap.com"})
			Expect(err).NotTo(HaveOccurred())
			Expect(resp.User.Roles).To(Equal([]string{"team-tse-apac", "department-tse"}))
			Expect(resp.User.RegionInfo.Global).To(BeTrue())
			Expect(resp.User.RegionInfo.Regions).To(Equal([]string{"APAC"}))
			Expect(resp.User.LarkUserId).To(Equal("ou_user"))
			Expect(resp.User.LarkDepartmentId).To(Equal("od_dept"))
		})

		It("returns IAM permission resolution errors", func() {
			userSvc.EXPECT().GetUser(gomock.Any(), gomock.Any()).Return(&userv1.GetUserResponse{User: &coreuser.User{
				Email:     "user@pingcap.com",
				Available: true,
			}}, nil)
			iamSvc.EXPECT().
				ResolveEntityPermissions(gomock.Any(), gomock.Any()).
				Return(nil, errors.New("resolve permissions failed"))

			resp, err := authController.GetUser(context.Background(), &auth.GetUserReq{Email: "user@pingcap.com"})
			Expect(err).To(MatchError("resolve permissions failed"))
			Expect(resp).To(BeNil())
		})
	})
})
