package auth

import (
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	authsdk "github.com/tidbcloud/tiinf/sdk/auth"
	"go.uber.org/mock/gomock"

	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-portal/domain/adapter/account/mock_account"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-portal/domain/entity"
)

func TestGetCurrentUserByAccessToken(t *testing.T) {
	// Init mock dependencies.
	ctrl := gomock.NewController(t)

	// init auth sdk
	accountApiCli := mock_account.NewMockApiCli(ctrl)
	accountApiCli.EXPECT().
		ListProjects(gomock.Any(), gomock.Any()).
		Return(&account.ListProjectsResponse{Projects: []*account.Project{{Id: 200}, {Id: 300}}}, nil).
		AnyTimes()
	accountApiCli.EXPECT().
		ListProjectsByOrgs(gomock.Any(), gomock.Any()).
		Return(&account.ListProjectsByOrgsResponse{Projects: []*account.OrgProject{{OrgId: 100, Projects: []*account.Project{{Id: 200}, {Id: 300}}}}}, nil).
		AnyTimes()
	authsdk.InitAccountClientByClient(accountApiCli, 10*time.Second)

	accountCli := mock_account.NewMockClient(ctrl)
	accountCli.EXPECT().
		ListProjects(gomock.Any(), gomock.Any()).
		Return([]*account.Project{{Id: 200}, {Id: 300}}, nil).
		AnyTimes()
	accountCli.EXPECT().
		GetAccountClient().
		Return(accountApiCli).
		AnyTimes()

	authService := authServiceImpl{
		account: accountCli,
	}

	testCases := []struct {
		name          string
		accessToken   *account.GetIdentityByAccessTokenResp
		expectedUser  *entity.User
		expectedError error
	}{
		{
			name: "valid access token",
			accessToken: &account.GetIdentityByAccessTokenResp{
				AccessTokenIdentity: &account.AccessTokenIdentity{
					UserId:         10,
					UserEmail:      "test@pingcap.com",
					RawAccessToken: "testRawAccessToken",
					OrgId:          100,
					ProjectIds:     []uint64{200, 300},
				},
			},
			expectedUser: &entity.User{
				User: &account.User{
					Id:    10,
					Email: "test@pingcap.com",
				},
				AccessToken: "testRawAccessToken",
				Org: &account.Org{
					Id: 100,
				},
				Projects: []*account.Project{
					{Id: 200},
					{Id: 300},
				},
				IdentityType: authsdk.AuthnIdentityTypeAccessToken,
			},
			expectedError: nil,
		},
		{
			name: "valid access token with grant all projects",
			accessToken: &account.GetIdentityByAccessTokenResp{
				AccessTokenIdentity: &account.AccessTokenIdentity{
					UserId:           10,
					UserEmail:        "test@pingcap.com",
					RawAccessToken:   "testRawAccessToken",
					OrgId:            100,
					GrantAllProjects: true,
					ProjectIds:       []uint64{},
				},
			},
			expectedUser: &entity.User{
				User: &account.User{
					Id:    10,
					Email: "test@pingcap.com",
				},
				AccessToken: "testRawAccessToken",
				Org: &account.Org{
					Id: 100,
				},
				Projects: []*account.Project{
					{Id: 200},
					{Id: 300},
				},
				IdentityType: authsdk.AuthnIdentityTypeAccessToken,
			},
			expectedError: nil,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			ctx := &gin.Context{
				Request: &http.Request{},
			}
			user, err := authService.getCurrentUserByAccessToken(ctx, tc.accessToken)

			assert.Equal(t, tc.expectedError, err)
			assert.Equal(t, authsdk.AuthnIdentityTypeAccessToken, tc.expectedUser.IdentityType)
			assert.Equal(t, tc.expectedUser.User.Id, user.User.Id)
			assert.Equal(t, tc.expectedUser.User.Email, user.User.Email)
			assert.Equal(t, tc.expectedUser.AccessToken, user.AccessToken)
			assert.Equal(t, tc.expectedUser.Org.Id, user.Org.Id)
			assert.Len(t, user.Projects, len(tc.expectedUser.Projects))
			assert.Equal(t, tc.expectedUser.Projects[0].Id, user.Projects[0].Id)
		})
	}
}

func TestGetCurrentUserByAPIKey(t *testing.T) {
	// Init mock dependencies.
	ctrl := gomock.NewController(t)
	accountCli := mock_account.NewMockClient(ctrl)
	accountCli.EXPECT().
		GetOrCreateSystemUser(gomock.Any(), gomock.Any()).
		Return(&account.User{Id: 1, Email: "100-system@pingcap.com"}, nil).
		AnyTimes()
	accountCli.EXPECT().
		ListProjects(gomock.Any(), []uint64{200}).
		Return([]*account.Project{{Id: 200}}, nil).
		AnyTimes()

	// init auth sdk
	accountApiCli := mock_account.NewMockApiCli(ctrl)
	accountApiCli.EXPECT().
		ListProjects(gomock.Any(), gomock.Any()).
		Return(&account.ListProjectsResponse{Projects: []*account.Project{{Id: 200}, {Id: 300}}}, nil).
		AnyTimes()
	accountApiCli.EXPECT().
		ListProjectsByOrgs(gomock.Any(), gomock.Any()).
		Return(&account.ListProjectsByOrgsResponse{Projects: []*account.OrgProject{{OrgId: 100, Projects: []*account.Project{{Id: 200}, {Id: 300}}}}}, nil).
		AnyTimes()
	authsdk.InitAccountClientByClient(accountApiCli, 10*time.Second)

	authService := authServiceImpl{
		account: accountCli,
	}

	testCases := []struct {
		name          string
		apiKeyAccess  *account.GetApiKeyByAccessKeyRsp
		expectedUser  *entity.User
		expectedError error
	}{
		{
			name: "org owner api key gets all org projects even with project bindings",
			apiKeyAccess: &account.GetApiKeyByAccessKeyRsp{
				ApiKey: &account.ApiKey{
					AccessKey: "testAccessKey",
				},
				ResourceInfos: []*account.ApiKeyResourceInfo{
					{
						ScopeType:  "ORG",
						ResourceId: 100,
						Role:       "org:owner",
					},
					{
						ScopeType:  "PROJECT",
						ResourceId: 200,
						Role:       "project:owner",
					},
				},
			},
			expectedUser: &entity.User{
				User: &account.User{
					Id:    1,
					Email: "100-system@pingcap.com",
				},
				PublicKey: "testAccessKey",
				Org: &account.Org{
					Id: 100,
				},
				Projects: []*account.Project{
					{Id: 200},
					{Id: 300},
				},
				IdentityType: authsdk.AuthnIdentityTypeApiKeyAK,
			},
			expectedError: nil,
		},
		{
			name: "non owner api key keeps project scoped projects",
			apiKeyAccess: &account.GetApiKeyByAccessKeyRsp{
				ApiKey: &account.ApiKey{
					AccessKey: "testAccessKey",
				},
				ResourceInfos: []*account.ApiKeyResourceInfo{
					{
						ScopeType:  "ORG",
						ResourceId: 100,
						Role:       "org:member",
					},
					{
						ScopeType:  "PROJECT",
						ResourceId: 200,
						Role:       "project:viewer",
					},
				},
			},
			expectedUser: &entity.User{
				User: &account.User{
					Id:    1,
					Email: "100-system@pingcap.com",
				},
				PublicKey: "testAccessKey",
				Org: &account.Org{
					Id: 100,
				},
				Projects: []*account.Project{
					{Id: 200},
				},
				IdentityType: authsdk.AuthnIdentityTypeApiKeyAK,
			},
			expectedError: nil,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			ctx := &gin.Context{
				Request: &http.Request{},
			}
			user, err := authService.getCurrentUserByAPIKey(ctx, tc.apiKeyAccess)

			assert.Equal(t, tc.expectedError, err)
			assert.Equal(t, authsdk.AuthnIdentityTypeApiKeyAK, tc.expectedUser.IdentityType)
			assert.Equal(t, tc.expectedUser.User.Id, user.User.Id)
			assert.Equal(t, tc.expectedUser.User.Email, user.User.Email)
			assert.Equal(t, tc.expectedUser.PublicKey, user.PublicKey)
			assert.Equal(t, tc.expectedUser.Org.Id, user.Org.Id)
			assert.Len(t, user.Projects, len(tc.expectedUser.Projects))
			assert.Equal(t, tc.expectedUser.Projects[0].Id, user.Projects[0].Id)
		})
	}
}

func TestTiDBXTransAPIKeyRspToUser(t *testing.T) {
	ctrl := gomock.NewController(t)

	accountCli := mock_account.NewMockClient(ctrl)
	accountCli.EXPECT().
		GetOrCreateSystemUser(gomock.Any(), uint64(100)).
		Return(&account.User{Id: 1, Email: "100-system@pingcap.com"}, nil).
		AnyTimes()
	accountCli.EXPECT().
		ListProjects(gomock.Any(), []uint64{200}).
		Return([]*account.Project{{Id: 200}}, nil).
		AnyTimes()

	accountApiCli := mock_account.NewMockApiCli(ctrl)
	accountApiCli.EXPECT().
		ListProjectsByOrgs(gomock.Any(), gomock.Any()).
		Return(&account.ListProjectsByOrgsResponse{Projects: []*account.OrgProject{{OrgId: 100, Projects: []*account.Project{{Id: 200}, {Id: 300}}}}}, nil).
		AnyTimes()
	authsdk.InitAccountClientByClient(accountApiCli, 10*time.Second)
	accountCli.EXPECT().
		GetAccountClient().
		Return(accountApiCli).
		AnyTimes()

	authService := tidbXAuthNServiceImpl{
		account: accountCli,
	}

	testCases := []struct {
		name         string
		apiKeyAccess *account.GetApiKeyByAccessKeyRsp
		projectIDs   []uint64
	}{
		{
			name: "org owner api key gets all org projects for tidbx",
			apiKeyAccess: &account.GetApiKeyByAccessKeyRsp{
				ApiKey: &account.ApiKey{
					Id:        10,
					AccessKey: "testAccessKey",
				},
				ResourceInfos: []*account.ApiKeyResourceInfo{
					{
						ScopeType:  "ORG",
						ResourceId: 100,
						Role:       "org:owner",
					},
					{
						ScopeType:  "PROJECT",
						ResourceId: 200,
						Role:       "project:owner",
					},
				},
			},
			projectIDs: []uint64{200, 300},
		},
		{
			name: "project scoped api key keeps scoped projects for tidbx",
			apiKeyAccess: &account.GetApiKeyByAccessKeyRsp{
				ApiKey: &account.ApiKey{
					Id:        11,
					AccessKey: "testAccessKey2",
				},
				ResourceInfos: []*account.ApiKeyResourceInfo{
					{
						ScopeType:  "ORG",
						ResourceId: 100,
						Role:       "org:member",
					},
					{
						ScopeType:  "PROJECT",
						ResourceId: 200,
						Role:       "project:viewer",
					},
				},
			},
			projectIDs: []uint64{200},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			ctx := &gin.Context{
				Request: httptest.NewRequest(http.MethodPost, "/v1beta2/tidbs", http.NoBody),
			}

			user, err := authService.transAPIKeyRspToUser(ctx, tc.apiKeyAccess)

			require.NoError(t, err)
			require.NotNil(t, user)
			assert.Equal(t, authsdk.AuthnIdentityTypeApiKeyAK, user.IdentityType)
			assert.Equal(t, tc.apiKeyAccess.GetApiKey().GetAccessKey(), user.PublicKey)
			assert.Equal(t, tc.apiKeyAccess.GetApiKey().GetId(), user.PublicKeyId)
			assert.Equal(t, uint64(100), user.Org.GetId())
			assert.Equal(t, uint64(1), user.User.GetId())
			assert.Equal(t, "100-system@pingcap.com", user.User.GetEmail())
			assert.Equal(t, tc.projectIDs, projectIDsFromProjects(user.Projects))
		})
	}
}

func TestGetCurrentUserByAuth0(t *testing.T) {
	testCases := []struct {
		name          string
		userResponse  *account.GetUserByTokenResponse
		orgId         string
		projectId     string
		expectedUser  *entity.User
		expectedError error
	}{
		{
			name: "valid user and orgId",
			userResponse: &account.GetUserByTokenResponse{
				User: &account.User{
					Id:    10,
					Email: "test@pingcap.com",
				},
			},
			orgId:     "100",
			projectId: "",
			expectedUser: &entity.User{
				User: &account.User{
					Id:    10,
					Email: "test@pingcap.com",
				},
				Org: &account.Org{
					Id: 100,
				},
				IdentityType: authsdk.AuthnIdentityTypeUserToken,
			},
			expectedError: nil,
		},
		{
			name:          "nil user response",
			userResponse:  nil,
			orgId:         "100",
			projectId:     "",
			expectedUser:  nil,
			expectedError: errors.New("failed to get user"),
		},
		{
			name: "invalid orgId",
			userResponse: &account.GetUserByTokenResponse{
				User: &account.User{
					Id:    10,
					Email: "test@pingcap.com",
				},
			},
			orgId:         "invalid",
			projectId:     "",
			expectedUser:  nil,
			expectedError: fmt.Errorf("failed to get orgId from query param"),
		},
		// Add more test cases as needed
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			// Create a new gin context for testing
			w := httptest.NewRecorder()
			ctx, _ := gin.CreateTestContext(w)
			ctx.Request, _ = http.NewRequest(http.MethodGet, "/", http.NoBody)
			ctx.Request.URL.RawQuery = fmt.Sprintf("orgId=%s", tc.orgId)
			if tc.projectId != "" {
				ctx.Request.URL.RawQuery = fmt.Sprintf("orgId=%s&projectId=%s", tc.orgId, tc.projectId)
			}

			authService := authServiceImpl{}
			user, err := authService.getCurrentUserByAuth0(ctx, tc.userResponse)

			if tc.expectedError != nil {
				require.Error(t, err)
				assert.Contains(t, err.Error(), tc.expectedError.Error())
			} else {
				require.NoError(t, err)
				assert.Equal(t, tc.expectedUser.IdentityType, user.IdentityType)
				assert.Equal(t, tc.expectedUser.User.Id, user.User.Id)
				assert.Equal(t, tc.expectedUser.User.Email, user.User.Email)
				assert.Equal(t, tc.expectedUser.Org.Id, user.Org.Id)
				if tc.projectId != "" {
					assert.Equal(t, tc.expectedUser.Projects[0].Id, user.Projects[0].Id)
				}
			}
		})
	}
}
