package auth

import (
	"context"
	"errors"
	"testing"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	authv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/api/core-server/auth/v1"
	userapiv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/api/core-server/user/v1"
	iamv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/entity/core-server/iam/v1"
	userv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/entity/core-server/user/v1"
	"github.com/tidbcloud/devops/pkg/infra/clients/opscore"
	"github.com/tidbcloud/devops/pkg/infra/utils/test"
	"go.uber.org/mock/gomock"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
)

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

var _ = Describe("[Auth]", func() {
	var fw = test.Framework()

	var (
		a        *Auth
		mockAuth *opscore.MockAuthServiceClient
	)

	BeforeEach(func() {
		mockAuth = opscore.NewMockAuthServiceClient(fw.Ctrl)
		a = &Auth{
			authCli: mockAuth,
		}
	})

	Describe("[Authenticate]", func() {
		It("maps access-token authentication into the legacy user profile", func() {
			token := "access-token"
			mockAuth.EXPECT().
				Authenticate(gomock.Any(), &authv1.AuthenticateRequest{AccessToken: &token}).
				Return(&authv1.AuthenticateResponse{
					User: &userv1.User{
						Email:        "user@pingcap.com",
						LarkUserName: "Unit Tester",
					},
					PrincipalProfile: &userapiv1.PrincipalProfile{
						OperatorType: "user",
						OperatorName: "user@pingcap.com",
						UserEmail:    "user@pingcap.com",
						AggregatedRoleScope: &userapiv1.PrincipalRoleScope{
							AllowedRegions:   []string{"APAC"},
							AllowedTenantIds: []uint64{101},
						},
						Roles: map[string]*userapiv1.PrincipalRoleScope{
							"role-b": {},
							"role-a": {},
						},
					},
				}, nil)

			result, skipAuthz, err := a.AuthnFromAccessToken(context.Background(), token)
			Expect(err).ToNot(HaveOccurred())
			Expect(skipAuthz).To(BeFalse())
			Expect(result.User.UserEmail).To(Equal("user@pingcap.com"))
			Expect(result.User.Roles).To(HaveKey("role-a"))
			Expect(result.User.Roles).To(HaveKey("role-b"))
			Expect(result.User.AggregatedRoleScope.AllowedRegions).To(Equal([]string{"APAC"}))
		})

		It("returns the core-server unauthenticated error directly", func() {
			token := "access-token"
			mockAuth.EXPECT().
				Authenticate(gomock.Any(), &authv1.AuthenticateRequest{AccessToken: &token}).
				Return(nil, status.Error(codes.Unauthenticated, "invalid token"))

			result, skipAuthz, err := a.AuthnFromAccessToken(context.Background(), token)
			Expect(status.Code(err)).To(Equal(codes.Unauthenticated))
			Expect(skipAuthz).To(BeFalse())
			Expect(result).To(BeNil())
		})

		It("returns non-gRPC core-server errors directly", func() {
			token := "access-token"
			mockAuth.EXPECT().
				Authenticate(gomock.Any(), &authv1.AuthenticateRequest{AccessToken: &token}).
				Return(nil, errors.New("invalid token"))

			result, skipAuthz, err := a.AuthnFromAccessToken(context.Background(), token)
			Expect(err).To(MatchError("invalid token"))
			Expect(skipAuthz).To(BeFalse())
			Expect(result).To(BeNil())
		})

		It("forwards the trusted client IP for API-token authentication", func() {
			token := "api-token"
			apiToken := &userv1.APIToken{
				Id:        "token-id",
				Owner:     "owner@pingcap.com",
				Privilege: userv1.APITokenPrivilege_API_TOKEN_PRIVILEGE_READONLY,
			}
			mockAuth.EXPECT().
				Authenticate(gomock.Any(), &authv1.AuthenticateRequest{ApiToken: &token}).
				DoAndReturn(func(ctx context.Context, _ *authv1.AuthenticateRequest, _ ...grpc.CallOption) (*authv1.AuthenticateResponse, error) {
					md, ok := metadata.FromOutgoingContext(ctx)
					Expect(ok).To(BeTrue())
					Expect(md.Get(clientIPKey)).To(Equal([]string{"10.0.0.8"}))
					return &authv1.AuthenticateResponse{
						User:     &userv1.User{Email: "owner@pingcap.com"},
						ApiToken: apiToken,
						PrincipalProfile: &userapiv1.PrincipalProfile{
							OperatorType:        "api_token",
							OperatorName:        "owner@pingcap.com",
							UserEmail:           "owner@pingcap.com",
							AggregatedRoleScope: &userapiv1.PrincipalRoleScope{},
						},
					}, nil
				})

			result, skipAuthz, err := a.AuthnFromAPIToken(context.Background(), token, "10.0.0.8")
			Expect(err).ToNot(HaveOccurred())
			Expect(skipAuthz).To(BeFalse())
			Expect(result.User.UserEmail).To(Equal("owner@pingcap.com"))
			Expect(result.APIToken).To(Equal(apiToken))
		})

		It("maps API-key identity and tenant scope", func() {
			key := "raw-key"
			mockAuth.EXPECT().
				Authenticate(gomock.Any(), &authv1.AuthenticateRequest{ApiKey: &key}).
				Return(&authv1.AuthenticateResponse{
					ApiKey: &iamv1.APIKey{
						Id:    10,
						Name:  "key-a",
						Owner: "owner@pingcap.com",
					},
					PrincipalProfile: &userapiv1.PrincipalProfile{
						OperatorType: "api_key",
						OperatorName: "key-a",
						UserEmail:    "owner@pingcap.com",
						AggregatedRoleScope: &userapiv1.PrincipalRoleScope{
							AllowGlobalRegion: true,
							AllowedTenantIds:  []uint64{101, 102},
						},
						Roles: map[string]*userapiv1.PrincipalRoleScope{"role-a": {}},
					},
				}, nil)

			result, skipAuthz, err := a.AuthnFromAPIKey(context.Background(), key)
			Expect(err).ToNot(HaveOccurred())
			Expect(skipAuthz).To(BeFalse())
			Expect(result.APIKey.Name).To(Equal("key-a"))
			Expect(result.User.OperatorName).To(Equal("key-a"))
			Expect(result.User.AggregatedRoleScope.AllowGlobalRegion).To(BeTrue())
			Expect(result.PrincipalProfile.AggregatedRoleScope.AllowedTenantIds).To(Equal([]uint64{101, 102}))
		})
	})

	Describe("[Authz]", func() {
		It("uses user email for user principals", func() {
			result := &AuthnResult{
				PrincipalProfile: &userapiv1.PrincipalProfile{
					OperatorType: "user",
					UserEmail:    "user@pingcap.com",
				},
			}
			email := "user@pingcap.com"
			mockAuth.EXPECT().
				Authorize(gomock.Any(), &authv1.AuthorizeRequest{
					UserEmail: &email,
					Uri:       "/clusters",
					Method:    "POST",
				}).
				Return(&authv1.AuthorizeResponse{Authorized: true}, nil)

			allow, err := a.Authz(context.Background(), result, "/clusters", "post")
			Expect(err).ToNot(HaveOccurred())
			Expect(allow).To(BeTrue())
		})

		DescribeTable("passes API-token privilege to core-server authorization",
			func(privilege userv1.APITokenPrivilege) {
				result := &AuthnResult{
					APIToken: &userv1.APIToken{Privilege: privilege},
					PrincipalProfile: &userapiv1.PrincipalProfile{
						OperatorType: "api_token",
						UserEmail:    "owner@pingcap.com",
					},
				}
				email := "owner@pingcap.com"
				mockAuth.EXPECT().
					Authorize(gomock.Any(), &authv1.AuthorizeRequest{
						UserEmail:         &email,
						ApiTokenPrivilege: &privilege,
						Uri:               "/clusters",
						Method:            "POST",
					}).
					Return(&authv1.AuthorizeResponse{Authorized: true}, nil)

				allow, err := a.Authz(context.Background(), result, "/clusters", "post")
				Expect(err).ToNot(HaveOccurred())
				Expect(allow).To(BeTrue())
			},
			Entry("readonly", userv1.APITokenPrivilege_API_TOKEN_PRIVILEGE_READONLY),
			Entry("readwrite", userv1.APITokenPrivilege_API_TOKEN_PRIVILEGE_READWRITE),
		)

		It("uses API-key name for API-key principals", func() {
			result := &AuthnResult{
				PrincipalProfile: &userapiv1.PrincipalProfile{
					OperatorType: "api_key",
					OperatorName: "key-a",
				},
			}
			name := "key-a"
			mockAuth.EXPECT().
				Authorize(gomock.Any(), &authv1.AuthorizeRequest{
					ApiKeyName: &name,
					Uri:        "/clusters",
					Method:     "GET",
				}).
				Return(&authv1.AuthorizeResponse{}, nil)

			allow, err := a.Authz(context.Background(), result, "/clusters", "get")
			Expect(err).ToNot(HaveOccurred())
			Expect(allow).To(BeFalse())
		})
	})

})
