package accountcli

import (
	"errors"
	"strconv"
	"testing"

	"github.com/golang/mock/gomock"
	"github.com/stretchr/testify/require"
	accountsdk "github.com/tidbcloud/account/idl/pbgen/proto/account"
	"go.buf.build/protocolbuffers/go/linuxgit/tidbcloud/base"
	grpcmetadata "google.golang.org/grpc/metadata"

	"github.com/tidbcloud/tidb-management-service/utilities/grpc/metadata"
)

func TestIsUserInOrg(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name       string
		response   *accountsdk.ListOrgsByUserResponse
		accountErr error
		expected   bool
		errMessage string
	}{
		{
			name: "user belongs to organization",
			response: &accountsdk.ListOrgsByUserResponse{Orgs: []*accountsdk.Org{
				{Id: 99},
				{Id: 100},
			}},
			expected: true,
		},
		{
			name: "user does not belong to organization",
			response: &accountsdk.ListOrgsByUserResponse{Orgs: []*accountsdk.Org{
				{Id: 99},
			}},
		},
		{
			name:       "account RPC fails",
			accountErr: errors.New("account unavailable"),
			errMessage: "account unavailable",
		},
		{
			name: "account response contains an error",
			response: &accountsdk.ListOrgsByUserResponse{BaseResp: &base.BaseResp{
				ErrCode: int64(13),
				ErrMsg:  "account internal error",
			}},
			errMessage: "account internal error",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			ctrl := gomock.NewController(t)
			accountAPI := accountsdk.NewMockAccountAPIServiceClient(ctrl)
			accountAPI.EXPECT().ListOrgsByUser(gomock.Any(), &accountsdk.ListOrgsByUserRequest{
				Email: "migration@pingcap.com",
			}).Return(tt.response, tt.accountErr)

			inOrg, err := (&clientImpl{account: accountAPI}).IsUserInOrg(
				t.Context(),
				"migration@pingcap.com",
				100,
			)
			if tt.errMessage == "" {
				require.NoError(t, err)
				require.Equal(t, tt.expected, inOrg)
				return
			}
			require.ErrorContains(t, err, tt.errMessage)
		})
	}
}

func TestGetInstancePermissionScopeOfUser_UsesIdentityBindingsForAPIKey(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	accountAPI := accountsdk.NewMockAccountAPIServiceClient(ctrl)
	client := &clientImpl{account: accountAPI}

	ctx := grpcmetadata.NewIncomingContext(t.Context(), grpcmetadata.Pairs(
		metadata.OrgIDHeaderKey, "60205",
		metadata.UserIDHeaderKey, "70637",
		metadata.ApiKeyHeaderKeyId, "1837899",
		metadata.IsCheckPermissionKey, "true",
		metadata.DefaultProjectIDHeaderKey, "1",
		metadata.ProjectListHeaderKey, "1",
	))

	accountAPI.EXPECT().
		ListIdentityRoleBindings(gomock.Any(), &accountsdk.ListIdentityRoleBindingsReq{
			IdentType: accountsdk.OperatorType_OPERATOR_TYPE_APIKEY,
			IdentId:   1837899,
			OrgId:     60205,
		}).
		Return(&accountsdk.ListIdentityRoleBindingsRsp{
			OrgRole: &accountsdk.ScopeRoleBinding{Role: "org:owner"},
		}, nil)

	all, projectIDs, instanceIDs, err := client.GetInstancePermissionScopeOfUser(ctx, 60205, "70637")
	require.NoError(t, err)
	require.True(t, all)
	require.Empty(t, projectIDs)
	require.Empty(t, instanceIDs)
}

func TestGetInstancePermissionScopeOfUser_UsesUserBindingsForUserIdentity(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	accountAPI := accountsdk.NewMockAccountAPIServiceClient(ctrl)
	client := &clientImpl{account: accountAPI}

	ctx := grpcmetadata.NewIncomingContext(t.Context(), grpcmetadata.Pairs(
		metadata.OrgIDHeaderKey, "60205",
		metadata.UserIDHeaderKey, "70637",
		metadata.IsCheckPermissionKey, "true",
		metadata.DefaultProjectIDHeaderKey, "1",
		metadata.ProjectListHeaderKey, "1",
	))

	accountAPI.EXPECT().
		ListIdentityRoleBindings(gomock.Any(), &accountsdk.ListIdentityRoleBindingsReq{
			IdentType: accountsdk.OperatorType_OPERATOR_TYPE_USER,
			IdentId:   70637,
			OrgId:     60205,
		}).
		Return(&accountsdk.ListIdentityRoleBindingsRsp{
			OrgRole:      &accountsdk.ScopeRoleBinding{Role: "org:member"},
			ProjectRoles: []*accountsdk.ScopeRoleBinding{{ScopeId: 1001}, {ScopeId: 1002}, {ScopeId: 1001}},
			ClusterRoles: []*accountsdk.ScopeRoleBinding{{ScopeId: 2001}},
		}, nil)

	all, projectIDs, instanceIDs, err := client.GetInstancePermissionScopeOfUser(ctx, 60205, strconv.FormatUint(70637, 10))
	require.NoError(t, err)
	require.False(t, all)
	require.Equal(t, []uint64{1001, 1002}, projectIDs)
	require.Equal(t, []uint64{2001}, instanceIDs)
}

func TestCheckBackupMutationPermission_DeniesOrgMemberWithoutMutationRole(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	accountAPI := accountsdk.NewMockAccountAPIServiceClient(ctrl)
	client := &clientImpl{account: accountAPI}

	ctx := grpcmetadata.NewIncomingContext(t.Context(), grpcmetadata.Pairs(
		metadata.OrgIDHeaderKey, "60205",
		metadata.UserIDHeaderKey, "70637",
		metadata.IsCheckPermissionKey, "true",
		metadata.DefaultProjectIDHeaderKey, "1",
		metadata.ProjectListHeaderKey, "1",
	))

	accountAPI.EXPECT().
		ListIdentityRoleBindings(gomock.Any(), &accountsdk.ListIdentityRoleBindingsReq{
			IdentType: accountsdk.OperatorType_OPERATOR_TYPE_USER,
			IdentId:   70637,
			OrgId:     60205,
		}).
		Return(&accountsdk.ListIdentityRoleBindingsRsp{
			OrgRole:      &accountsdk.ScopeRoleBinding{Role: "org:member", ScopeId: 60205},
			ProjectRoles: nil,
			ClusterRoles: nil,
		}, nil)

	allowed, err := client.CheckBackupMutationPermission(ctx, 60205, 3201779, 1001, "70637")

	require.NoError(t, err)
	require.False(t, allowed)
}

func TestCheckBackupMutationPermission_FiltersReadOnlyRoles(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	accountAPI := accountsdk.NewMockAccountAPIServiceClient(ctrl)
	client := &clientImpl{account: accountAPI}

	ctx := grpcmetadata.NewIncomingContext(t.Context(), grpcmetadata.Pairs(
		metadata.OrgIDHeaderKey, "60205",
		metadata.UserIDHeaderKey, "70637",
		metadata.IsCheckPermissionKey, "true",
		metadata.DefaultProjectIDHeaderKey, "1",
		metadata.ProjectListHeaderKey, "1",
	))

	accountAPI.EXPECT().
		ListIdentityRoleBindings(gomock.Any(), &accountsdk.ListIdentityRoleBindingsReq{
			IdentType: accountsdk.OperatorType_OPERATOR_TYPE_USER,
			IdentId:   70637,
			OrgId:     60205,
		}).
		Return(&accountsdk.ListIdentityRoleBindingsRsp{
			OrgRole: &accountsdk.ScopeRoleBinding{Role: "org:member"},
			ProjectRoles: []*accountsdk.ScopeRoleBinding{
				{ScopeId: 3201779, Role: "project:viewer"},
			},
			ClusterRoles: []*accountsdk.ScopeRoleBinding{
				{ScopeId: 1001, Role: "cluster:viewer"},
			},
		}, nil)

	allowed, err := client.CheckBackupMutationPermission(ctx, 60205, 3201779, 1001, "70637")

	require.NoError(t, err)
	require.False(t, allowed)
}

func TestCheckBackupMutationPermission_DeniesControlPlaneProjectViewerAPIKey(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	accountAPI := accountsdk.NewMockAccountAPIServiceClient(ctrl)
	client := &clientImpl{account: accountAPI}

	ctx := grpcmetadata.NewIncomingContext(t.Context(), grpcmetadata.Pairs(
		metadata.OrgIDHeaderKey, "60205",
		metadata.UserIDHeaderKey, "70637",
		metadata.ApiKeyHeaderKeyId, "2404756",
		metadata.IsCheckPermissionKey, "true",
		metadata.DefaultProjectIDHeaderKey, "3202164",
		metadata.ProjectListHeaderKey, "3202164",
	))

	accountAPI.EXPECT().
		ListIdentityRoleBindings(gomock.Any(), &accountsdk.ListIdentityRoleBindingsReq{
			IdentType: accountsdk.OperatorType_OPERATOR_TYPE_APIKEY,
			IdentId:   2404756,
			OrgId:     60205,
		}).
		Return(&accountsdk.ListIdentityRoleBindingsRsp{
			OrgRole: &accountsdk.ScopeRoleBinding{Role: "org:member", ScopeId: 60205},
			ProjectRoles: []*accountsdk.ScopeRoleBinding{
				{ScopeId: 3202164, Role: "project:ctl_plane_viewer"},
			},
		}, nil)

	allowed, err := client.CheckBackupMutationPermission(ctx, 60205, 3202164, 1001, "70637")

	require.NoError(t, err)
	require.False(t, allowed)
}

func TestCheckBackupMutationPermission_AllowsProjectOwner(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	accountAPI := accountsdk.NewMockAccountAPIServiceClient(ctrl)
	client := &clientImpl{account: accountAPI}

	ctx := grpcmetadata.NewIncomingContext(t.Context(), grpcmetadata.Pairs(
		metadata.OrgIDHeaderKey, "60205",
		metadata.UserIDHeaderKey, "70637",
		metadata.IsCheckPermissionKey, "true",
		metadata.DefaultProjectIDHeaderKey, "1",
		metadata.ProjectListHeaderKey, "1",
	))

	accountAPI.EXPECT().
		ListIdentityRoleBindings(gomock.Any(), &accountsdk.ListIdentityRoleBindingsReq{
			IdentType: accountsdk.OperatorType_OPERATOR_TYPE_USER,
			IdentId:   70637,
			OrgId:     60205,
		}).
		Return(&accountsdk.ListIdentityRoleBindingsRsp{
			OrgRole: &accountsdk.ScopeRoleBinding{Role: "org:member"},
			ProjectRoles: []*accountsdk.ScopeRoleBinding{
				{ScopeId: 3201779, Role: "project:owner"},
			},
		}, nil)

	allowed, err := client.CheckBackupMutationPermission(ctx, 60205, 3201779, 1001, "70637")

	require.NoError(t, err)
	require.True(t, allowed)
}

func TestCheckBackupMutationPermission_AllowsClusterAdmin(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	accountAPI := accountsdk.NewMockAccountAPIServiceClient(ctrl)
	client := &clientImpl{account: accountAPI}

	ctx := grpcmetadata.NewIncomingContext(t.Context(), grpcmetadata.Pairs(
		metadata.OrgIDHeaderKey, "60205",
		metadata.UserIDHeaderKey, "70637",
		metadata.IsCheckPermissionKey, "true",
		metadata.DefaultProjectIDHeaderKey, "1",
		metadata.ProjectListHeaderKey, "1",
	))

	accountAPI.EXPECT().
		ListIdentityRoleBindings(gomock.Any(), &accountsdk.ListIdentityRoleBindingsReq{
			IdentType: accountsdk.OperatorType_OPERATOR_TYPE_USER,
			IdentId:   70637,
			OrgId:     60205,
		}).
		Return(&accountsdk.ListIdentityRoleBindingsRsp{
			OrgRole: &accountsdk.ScopeRoleBinding{Role: "org:member"},
			ClusterRoles: []*accountsdk.ScopeRoleBinding{
				{ScopeId: 1001, Role: "cluster:admin"},
			},
		}, nil)

	allowed, err := client.CheckBackupMutationPermission(ctx, 60205, 3201779, 1001, "70637")

	require.NoError(t, err)
	require.True(t, allowed)
}
