package acimpl

import (
	"context"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/grafana/grafana/pkg/apimachinery/identity"
	"github.com/grafana/grafana/pkg/infra/db"
	"github.com/grafana/grafana/pkg/infra/localcache"
	"github.com/grafana/grafana/pkg/infra/log"
	"github.com/grafana/grafana/pkg/infra/tracing"
	"github.com/grafana/grafana/pkg/plugins"
	"github.com/grafana/grafana/pkg/services/accesscontrol"
	"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
	"github.com/grafana/grafana/pkg/services/accesscontrol/database"
	"github.com/grafana/grafana/pkg/services/accesscontrol/permreg"
	"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
	"github.com/grafana/grafana/pkg/services/featuremgmt"
	"github.com/grafana/grafana/pkg/services/licensing"
	"github.com/grafana/grafana/pkg/services/user"
	"github.com/grafana/grafana/pkg/setting"
	"github.com/grafana/grafana/pkg/tests/testsuite"
	"github.com/grafana/grafana/pkg/util/testutil"
)

func TestMain(m *testing.M) {
	testsuite.Run(m)
}

func setupTestEnv(t testing.TB, registerRoles bool) *Service {
	t.Helper()
	cfg := setting.NewCfg()

	ac := &Service{
		cache:          localcache.ProvideService(),
		cfg:            cfg,
		features:       featuremgmt.WithFeatures(),
		log:            log.New("accesscontrol"),
		registrations:  accesscontrol.RegistrationList{},
		roles:          accesscontrol.BuildBasicRoleDefinitions(),
		store:          database.ProvideService(db.InitTestDB(t)),
		permRegistry:   permreg.ProvidePermissionRegistry(),
		actionResolver: resourcepermissions.NewActionSetService(),
	}

	if registerRoles {
		require.NoError(t, ac.RegisterFixedRoles(context.Background()))
	}

	return ac
}

func TestIntegrationUsageMetrics(t *testing.T) {
	testutil.SkipIntegrationTestInShortMode(t)

	tests := []struct {
		name          string
		expectedValue int
	}{
		{
			name:          "Expecting metric with value 1",
			expectedValue: 1,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cfg := setting.NewCfg()

			s := ProvideOSSService(
				cfg,
				database.ProvideService(db.InitTestDB(t)),
				&resourcepermissions.FakeActionSetSvc{},
				localcache.ProvideService(),
				featuremgmt.WithFeatures(),
				tracing.InitializeTracerForTest(),
				nil,
				permreg.ProvidePermissionRegistry(),
				nil,
			)
			assert.Equal(t, tt.expectedValue, s.GetUsageStats(context.Background())["stats.oss.accesscontrol.enabled.count"])
		})
	}
}

func TestIntegrationService_DeclareFixedRoles(t *testing.T) {
	testutil.SkipIntegrationTestInShortMode(t)

	tests := []struct {
		name          string
		registrations []accesscontrol.RoleRegistration
		wantErr       bool
		err           error
	}{
		{
			name:    "should work with empty list",
			wantErr: false,
		},
		{
			name: "should add registration",
			registrations: []accesscontrol.RoleRegistration{
				{
					Role:   accesscontrol.RoleDTO{Name: "fixed:test:test"},
					Grants: []string{"Admin"},
				},
			},
			wantErr: false,
		},
		{
			name: "should fail registration invalid role name",
			registrations: []accesscontrol.RoleRegistration{
				{
					Role:   accesscontrol.RoleDTO{Name: "custom:test:test"},
					Grants: []string{"Admin"},
				},
			},
			wantErr: true,
			err:     accesscontrol.ErrFixedRolePrefixMissing,
		},
		{
			name: "should fail registration invalid basic role assignment",
			registrations: []accesscontrol.RoleRegistration{
				{
					Role:   accesscontrol.RoleDTO{Name: "fixed:test:test"},
					Grants: []string{"WrongAdmin"},
				},
			},
			wantErr: true,
			err:     accesscontrol.ErrInvalidBuiltinRole.Build(accesscontrol.ErrInvalidBuiltinRoleData("WrongAdmin")),
		},
		{
			name: "should add multiple registrations at once",
			registrations: []accesscontrol.RoleRegistration{
				{
					Role:   accesscontrol.RoleDTO{Name: "fixed:test:test"},
					Grants: []string{"Admin"},
				},
				{
					Role:   accesscontrol.RoleDTO{Name: "fixed:test2:test2"},
					Grants: []string{"Admin"},
				},
			},
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ac := setupTestEnv(t, true)

			// Reset the registations
			ac.registrations = accesscontrol.RegistrationList{}

			// Test
			err := ac.DeclareFixedRoles(tt.registrations...)
			if tt.wantErr {
				require.Error(t, err)
				assert.ErrorIs(t, err, tt.err)
				return
			}
			require.NoError(t, err)

			registrationCnt := 0
			ac.registrations.Range(func(registration accesscontrol.RoleRegistration) bool {
				registrationCnt++
				return true
			})
			assert.Equal(t, len(tt.registrations), registrationCnt,
				"expected service registration list to contain all test registrations")
		})
	}
}

func TestIntegrationService_DeclarePluginRoles(t *testing.T) {
	testutil.SkipIntegrationTestInShortMode(t)

	tests := []struct {
		name          string
		pluginID      string
		registrations []plugins.RoleRegistration
		wantErr       bool
		err           error
	}{
		{
			name:    "should work with empty list",
			wantErr: false,
		},
		{
			name:     "should add registration",
			pluginID: "test-app",
			registrations: []plugins.RoleRegistration{
				{
					Role:   plugins.Role{Name: "Tester"},
					Grants: []string{"Admin"},
				},
			},
			wantErr: false,
		},
		{
			name:     "should add registration with valid permissions",
			pluginID: "test-app",
			registrations: []plugins.RoleRegistration{
				{
					Role: plugins.Role{
						Name: "Tester",
						Permissions: []plugins.Permission{
							{Action: "plugins.app:access", Scope: "plugins:id:test-app"},
							{Action: "test-app:read"},
							{Action: "test-app.resource:read"},
						},
					},
					Grants: []string{"Admin"},
				},
			},
			wantErr: false,
		},
		{
			name:     "should fail registration invalid permission action",
			pluginID: "test-app",
			registrations: []plugins.RoleRegistration{
				{
					Role: plugins.Role{
						Name: "Tester",
						Permissions: []plugins.Permission{
							{Action: "invalid.test-app.resource:read"},
						},
					},
					Grants: []string{"Admin"},
				},
			},
			wantErr: true,
			err:     &accesscontrol.ErrorInvalidRole{},
		},
		{
			name:     "should fail registration invalid basic role assignment",
			pluginID: "test-app",
			registrations: []plugins.RoleRegistration{
				{
					Role:   plugins.Role{Name: "Tester"},
					Grants: []string{"WrongAdmin"},
				},
			},
			wantErr: true,
			err:     accesscontrol.ErrInvalidBuiltinRole.Build(accesscontrol.ErrInvalidBuiltinRoleData("WrongAdmin")),
		},
		{
			name:     "should add multiple registrations at once",
			pluginID: "test-app",
			registrations: []plugins.RoleRegistration{
				{
					Role:   plugins.Role{Name: "Tester"},
					Grants: []string{"Admin"},
				},
				{
					Role:   plugins.Role{Name: "Tester2"},
					Grants: []string{"Admin"},
				},
			},
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ac := setupTestEnv(t, true)

			// Reset the registations
			ac.registrations = accesscontrol.RegistrationList{}

			// Test
			err := ac.DeclarePluginRoles(context.Background(), tt.pluginID, tt.pluginID, tt.registrations)
			if tt.wantErr {
				require.Error(t, err)
				assert.ErrorIs(t, err, tt.err)
				return
			}
			require.NoError(t, err)

			registrationCnt := 0
			ac.registrations.Range(func(registration accesscontrol.RoleRegistration) bool {
				registrationCnt++
				return true
			})
			assert.Equal(t, len(tt.registrations), registrationCnt,
				"expected service registration list to contain all test registrations")
		})
	}
}

func TestIntegrationService_RegisterFixedRoles(t *testing.T) {
	testutil.SkipIntegrationTestInShortMode(t)

	tests := []struct {
		name          string
		token         licensing.Licensing
		registrations []accesscontrol.RoleRegistration
		wantErr       bool
	}{
		{
			name: "should work with empty list",
		},
		{
			name: "should register and assign role",
			registrations: []accesscontrol.RoleRegistration{
				{
					Role: accesscontrol.RoleDTO{
						Name:        "fixed:test:test",
						Permissions: []accesscontrol.Permission{{Action: "test:test"}},
					},
					Grants: []string{"Editor"},
				},
			},
			wantErr: false,
		},
		{
			name: "should register and assign multiple roles",
			registrations: []accesscontrol.RoleRegistration{
				{
					Role: accesscontrol.RoleDTO{
						Name:        "fixed:test:test",
						Permissions: []accesscontrol.Permission{{Action: "test:test"}},
					},
					Grants: []string{"Editor"},
				},
				{
					Role: accesscontrol.RoleDTO{
						Name: "fixed:test2:test2",
						Permissions: []accesscontrol.Permission{
							{Action: "test:test2"},
							{Action: "test:test3", Scope: "test:*"},
						},
					},
					Grants: []string{"Viewer"},
				},
			},
			wantErr: false,
		},
		{
			name: "should register and assign fixed and plugins roles",
			registrations: []accesscontrol.RoleRegistration{
				{
					Role: accesscontrol.RoleDTO{
						Name:        accesscontrol.PluginRolePrefix + "test-app:tester",
						DisplayName: "Tester",
						Permissions: []accesscontrol.Permission{{Action: "test-app:test"}},
					},
					Grants: []string{"Editor"},
				},
				{
					Role: accesscontrol.RoleDTO{
						Name: "fixed:test2:test2",
						Permissions: []accesscontrol.Permission{
							{Action: "test:test2"},
							{Action: "test:test3", Scope: "test:*"},
						},
					},
					Grants: []string{"Viewer"},
				},
			},
			wantErr: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ac := setupTestEnv(t, true)

			ac.registrations.Append(tt.registrations...)

			// Test
			err := ac.RegisterFixedRoles(context.Background())
			if tt.wantErr {
				require.Error(t, err)
				return
			}
			require.NoError(t, err)

			// Check
			for _, registration := range tt.registrations {
				// Check builtin roles (parents included) have been granted with the permissions
				for br := range accesscontrol.BuiltInRolesWithParents(registration.Grants) {
					builtinRole, ok := ac.roles[br]
					assert.True(t, ok)
					for _, expectedPermission := range registration.Role.Permissions {
						expectedPermission.Kind, expectedPermission.Attribute, expectedPermission.Identifier = accesscontrol.SplitScope(expectedPermission.Scope)
						assert.Contains(t, builtinRole.Permissions, expectedPermission)
					}
				}
			}
		})
	}
}

func TestIntegrationService_DeclarePluginRoles_DynamicRegistration(t *testing.T) {
	testutil.SkipIntegrationTestInShortMode(t)

	tests := []struct {
		name             string
		postRegister     bool
		pluginID         string
		registrations    []plugins.RoleRegistration
		wantGrantedRoles []string
		wantPermissions  []string
		wantPermCount    int
	}{
		{
			name:         "should register plugin roles before initialization",
			postRegister: false,
			pluginID:     "test-app",
			registrations: []plugins.RoleRegistration{
				{
					Role: plugins.Role{
						Name:        "Reader",
						Description: "Test reader role",
						Permissions: []plugins.Permission{
							{Action: "test-app:read", Scope: "test-app:*"},
						},
					},
					Grants: []string{string(identity.RoleViewer)},
				},
			},
			wantGrantedRoles: []string{string(identity.RoleViewer)},
			wantPermissions:  []string{"test-app:read"},
			wantPermCount:    1,
		},
		{
			name:         "should register plugin roles dynamically after initialization",
			postRegister: true,
			pluginID:     "dynamic-app",
			registrations: []plugins.RoleRegistration{
				{
					Role: plugins.Role{
						Name:        "DynamicReader",
						Description: "Dynamic reader role",
						Permissions: []plugins.Permission{
							{Action: "dynamic-app:read", Scope: "dynamic-app:*"},
							{Action: "dynamic-app:query", Scope: "dynamic-app:*"},
						},
					},
					Grants: []string{string(identity.RoleViewer)},
				},
			},
			wantGrantedRoles: []string{string(identity.RoleViewer)},
			wantPermissions:  []string{"dynamic-app:read", "dynamic-app:query"},
			wantPermCount:    2,
		},
		{
			name:         "should grant to multiple roles dynamically",
			postRegister: true,
			pluginID:     "multi-app",
			registrations: []plugins.RoleRegistration{
				{
					Role: plugins.Role{
						Name:        "MultiReader",
						Description: "Multi reader role",
						Permissions: []plugins.Permission{
							{Action: "multi-app:read", Scope: "multi-app:*"},
						},
					},
					Grants: []string{string(identity.RoleViewer), string(identity.RoleEditor)},
				},
			},
			wantGrantedRoles: []string{string(identity.RoleViewer), string(identity.RoleEditor)},
			wantPermissions:  []string{"multi-app:read"},
			wantPermCount:    1,
		},
		{
			name:         "should register multiple permissions dynamically",
			postRegister: true,
			pluginID:     "batch-app",
			registrations: []plugins.RoleRegistration{
				{
					Role: plugins.Role{
						Name: "BatchReader",
						Permissions: []plugins.Permission{
							{Action: "batch-app:read"},
							{Action: "batch-app:write"},
							{Action: "batch-app:delete"},
						},
					},
					Grants: []string{string(identity.RoleEditor)},
				},
			},
			wantGrantedRoles: []string{string(identity.RoleEditor)},
			wantPermissions:  []string{"batch-app:read", "batch-app:write", "batch-app:delete"},
			wantPermCount:    3,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ac := setupTestEnv(t, tt.postRegister)
			initialPermCounts := make(map[string]int)
			for _, roleName := range tt.wantGrantedRoles {
				if role, ok := ac.roles[roleName]; ok {
					initialPermCounts[roleName] = len(role.Permissions)
				}
			}

			err := ac.DeclarePluginRoles(context.Background(), tt.pluginID, tt.pluginID, tt.registrations)
			require.NoError(t, err)

			if !tt.postRegister {
				err = ac.RegisterFixedRoles(context.Background())
				require.NoError(t, err)
			}

			// Verify permissions were added to expected roles
			for _, roleName := range tt.wantGrantedRoles {
				role, ok := ac.roles[roleName]
				require.True(t, ok, "Role %s should exist", roleName)

				expectedCount := initialPermCounts[roleName] + tt.wantPermCount
				assert.Equal(t, expectedCount, len(role.Permissions))

				actions := make([]string, 0, len(role.Permissions))
				for _, perm := range role.Permissions {
					actions = append(actions, perm.Action)
				}

				for _, wantAction := range tt.wantPermissions {
					assert.Contains(t, actions, wantAction, "Role %s should have permission %s", roleName, wantAction)
				}
			}
		})
	}
}

func TestIntegrationService_DeclarePluginRoles_UserPermissions(t *testing.T) {
	testutil.SkipIntegrationTestInShortMode(t)

	t.Run("dynamic registration should be reflected in user permissions", func(t *testing.T) {
		ac := setupTestEnv(t, true)

		// Create a test user with Viewer role
		testUser := &user.SignedInUser{
			UserID:  1,
			OrgID:   1,
			OrgRole: identity.RoleViewer,
		}

		// Get initial permissions
		initialPerms, err := ac.getUserPermissions(context.Background(), testUser, accesscontrol.Options{})
		require.NoError(t, err)
		initialCount := len(initialPerms)

		// Register plugin roles dynamically
		err = ac.DeclarePluginRoles(context.Background(), "perm-test-app", "Perm Test App", []plugins.RoleRegistration{
			{
				Role: plugins.Role{
					Name:        "PermTester",
					Description: "Permission test role",
					Permissions: []plugins.Permission{
						{Action: "perm-test-app:test", Scope: "perm-test-app:*"},
					},
				},
				Grants: []string{string(identity.RoleViewer)},
			},
		})
		require.NoError(t, err)

		updatedPerms, err := ac.getUserPermissions(context.Background(), testUser, accesscontrol.Options{})
		require.NoError(t, err)

		require.Greater(t, len(updatedPerms), initialCount)
		hasNewPerm := false
		hasNewScope := false
		for _, perm := range updatedPerms {
			if perm.Action == "perm-test-app:test" {
				hasNewPerm = true
			}
			if perm.Scope == "perm-test-app:*" {
				hasNewScope = true
			}
		}
		assert.True(t, hasNewPerm)
		assert.True(t, hasNewScope)
	})
}

func TestIntegrationService_SearchUsersPermissions(t *testing.T) {
	testutil.SkipIntegrationTestInShortMode(t)

	searchOption := accesscontrol.SearchOptions{ActionPrefix: "teams"}
	ctx := context.Background()
	listAllPerms := map[string][]string{accesscontrol.ActionUsersPermissionsRead: {"users:*"}}
	listSomePerms := map[string][]string{accesscontrol.ActionUsersPermissionsRead: {"users:id:2"}}
	tests := []struct {
		name           string
		siuPermissions map[string][]string
		searchOption   accesscontrol.SearchOptions
		ramRoles       map[string]*accesscontrol.RoleDTO    // BasicRole => RBAC BasicRole
		storedPerms    map[int64][]accesscontrol.Permission // UserID => Permissions
		storedRoles    map[int64][]string                   // UserID => Roles
		want           map[int64][]accesscontrol.Permission
		wantErr        bool
	}{
		{
			name:           "ram only",
			siuPermissions: listAllPerms,
			searchOption:   searchOption,
			ramRoles: map[string]*accesscontrol.RoleDTO{
				string(identity.RoleAdmin): {Permissions: []accesscontrol.Permission{
					{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
				}},
				accesscontrol.RoleGrafanaAdmin: {Permissions: []accesscontrol.Permission{
					{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"},
				}},
			},
			storedRoles: map[int64][]string{
				1: {string(identity.RoleEditor)},
				2: {string(identity.RoleAdmin), accesscontrol.RoleGrafanaAdmin},
			},
			want: map[int64][]accesscontrol.Permission{
				2: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
					{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"}},
			},
		},
		{
			name:           "stored only",
			siuPermissions: listAllPerms,
			searchOption:   searchOption,
			storedPerms: map[int64][]accesscontrol.Permission{
				1: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"}},
				2: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
					{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"}},
			},
			storedRoles: map[int64][]string{
				1: {string(identity.RoleEditor)},
				2: {string(identity.RoleAdmin), accesscontrol.RoleGrafanaAdmin},
			},
			want: map[int64][]accesscontrol.Permission{
				1: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"}},
				2: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
					{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"}},
			},
		},
		{
			name:           "ram and stored",
			siuPermissions: listAllPerms,
			searchOption:   searchOption,
			ramRoles: map[string]*accesscontrol.RoleDTO{
				string(identity.RoleAdmin): {Permissions: []accesscontrol.Permission{
					{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
				}},
				accesscontrol.RoleGrafanaAdmin: {Permissions: []accesscontrol.Permission{
					{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"},
				}},
			},
			storedPerms: map[int64][]accesscontrol.Permission{
				1: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"}},
				2: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"},
					{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:id:1"}},
			},
			storedRoles: map[int64][]string{
				1: {string(identity.RoleEditor)},
				2: {string(identity.RoleAdmin), accesscontrol.RoleGrafanaAdmin},
			},
			want: map[int64][]accesscontrol.Permission{
				1: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"}},
				2: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"},
					{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:id:1"},
					{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
					{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"}},
			},
		},
		{
			name:           "ram only search on scope",
			siuPermissions: listAllPerms,
			searchOption:   accesscontrol.SearchOptions{Scope: "teams:id:2"},
			ramRoles: map[string]*accesscontrol.RoleDTO{
				string(identity.RoleAdmin): {Permissions: []accesscontrol.Permission{
					{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
				}},
			},
			storedRoles: map[int64][]string{
				1: {string(identity.RoleEditor)},
				2: {string(identity.RoleAdmin), accesscontrol.RoleGrafanaAdmin},
			},
			want: map[int64][]accesscontrol.Permission{
				2: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"}},
			},
		},
		{
			name:           "view permission on subset of users only",
			siuPermissions: listSomePerms,
			searchOption:   searchOption,
			ramRoles: map[string]*accesscontrol.RoleDTO{
				accesscontrol.RoleGrafanaAdmin: {Permissions: []accesscontrol.Permission{
					{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"},
				}},
			},
			storedPerms: map[int64][]accesscontrol.Permission{
				1: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"}},
				2: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"},
					{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:id:1"}},
			},
			storedRoles: map[int64][]string{
				1: {string(identity.RoleEditor)},
				2: {accesscontrol.RoleGrafanaAdmin},
			},
			want: map[int64][]accesscontrol.Permission{
				2: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"},
					{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:id:1"},
					{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"}},
			},
		},
		{
			name:           "check action filter on RAM permissions works correctly",
			siuPermissions: listAllPerms,
			searchOption:   searchOption,
			ramRoles: map[string]*accesscontrol.RoleDTO{
				accesscontrol.RoleGrafanaAdmin: {Permissions: []accesscontrol.Permission{
					{Action: accesscontrol.ActionUsersCreate},
					{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"},
				}},
			},
			storedRoles: map[int64][]string{1: {accesscontrol.RoleGrafanaAdmin}},
			want: map[int64][]accesscontrol.Permission{
				1: {{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"}},
			},
		},
		{
			name:           "check empty action filter on RAM permissions works correctly",
			siuPermissions: listAllPerms,
			searchOption:   accesscontrol.SearchOptions{},
			ramRoles: map[string]*accesscontrol.RoleDTO{
				accesscontrol.RoleGrafanaAdmin: {Permissions: []accesscontrol.Permission{
					{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
					{Action: accesscontrol.ActionUsersCreate},
					{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"},
					{Action: accesscontrol.ActionAnnotationsRead, Scope: "annotations:*"},
				}},
			},
			storedRoles: map[int64][]string{1: {accesscontrol.RoleGrafanaAdmin}},
			want: map[int64][]accesscontrol.Permission{
				1: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
					{Action: accesscontrol.ActionUsersCreate},
					{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"},
					{Action: accesscontrol.ActionAnnotationsRead, Scope: "annotations:*"},
				},
			},
		},
		{
			// This test is not exactly representative as normally the store would return
			// only the user's basic roles and the user's stored permissions
			name:           "check namespacedId filter works correctly",
			siuPermissions: listAllPerms,
			searchOption:   accesscontrol.SearchOptions{UserID: 1},
			ramRoles: map[string]*accesscontrol.RoleDTO{
				string(identity.RoleEditor): {Permissions: []accesscontrol.Permission{
					{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
				}},
				string(identity.RoleAdmin): {Permissions: []accesscontrol.Permission{
					{Action: accesscontrol.ActionTeamsWrite, Scope: "teams:*"},
				}},
				accesscontrol.RoleGrafanaAdmin: {Permissions: []accesscontrol.Permission{
					{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"},
				}},
			},
			storedPerms: map[int64][]accesscontrol.Permission{
				1: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"}},
				2: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"},
					{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:id:1"}},
			},
			storedRoles: map[int64][]string{
				1: {string(identity.RoleEditor)},
				2: {string(identity.RoleAdmin), accesscontrol.RoleGrafanaAdmin},
			},
			want: map[int64][]accesscontrol.Permission{
				1: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"}, {Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"}},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ac := setupTestEnv(t, true)

			ac.roles = tt.ramRoles
			ac.store = actest.FakeStore{
				ExpectedUsersPermissions: tt.storedPerms,
				ExpectedUsersRoles:       tt.storedRoles,
			}

			siu := &user.SignedInUser{OrgID: 2, Permissions: map[int64]map[string][]string{2: tt.siuPermissions}}
			got, err := ac.SearchUsersPermissions(ctx, siu, tt.searchOption)
			if tt.wantErr {
				require.NotNil(t, err)
				return
			}
			require.Nil(t, err)

			require.Len(t, got, len(tt.want), "expected more users permissions")
			for userID, wantPerm := range tt.want {
				gotPerm, ok := got[userID]
				require.True(t, ok, "expected permissions for user", userID)

				require.ElementsMatch(t, gotPerm, wantPerm)
			}
		})
	}
}

func TestIntegrationService_SearchUserPermissions(t *testing.T) {
	testutil.SkipIntegrationTestInShortMode(t)

	ctx := context.Background()
	tests := []struct {
		name           string
		searchOption   accesscontrol.SearchOptions
		withActionSets bool
		actionSets     map[string][]string
		ramRoles       map[string]*accesscontrol.RoleDTO    // BasicRole => RBAC BasicRole
		storedPerms    map[int64][]accesscontrol.Permission // UserID => Permissions
		storedRoles    map[int64][]string                   // UserID => Roles
		want           []accesscontrol.Permission
		wantErr        bool
	}{
		{
			name: "ram only",
			searchOption: accesscontrol.SearchOptions{
				ActionPrefix: "teams",
				UserID:       2,
			},
			ramRoles: map[string]*accesscontrol.RoleDTO{
				string(identity.RoleEditor): {Permissions: []accesscontrol.Permission{
					{Action: accesscontrol.ActionTeamsCreate},
				}},
				string(identity.RoleAdmin): {Permissions: []accesscontrol.Permission{
					{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
				}},
				accesscontrol.RoleGrafanaAdmin: {Permissions: []accesscontrol.Permission{
					{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"},
				}},
			},
			storedRoles: map[int64][]string{
				1: {string(identity.RoleEditor)},
				2: {string(identity.RoleAdmin), accesscontrol.RoleGrafanaAdmin},
			},
			want: []accesscontrol.Permission{
				{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
				{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"}},
		},
		{
			name: "stored only",
			searchOption: accesscontrol.SearchOptions{
				ActionPrefix: "teams",
				UserID:       2,
			},
			storedPerms: map[int64][]accesscontrol.Permission{
				1: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"}},
				2: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
					{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"}},
			},
			storedRoles: map[int64][]string{
				1: {string(identity.RoleEditor)},
				2: {string(identity.RoleAdmin), accesscontrol.RoleGrafanaAdmin},
			},
			want: []accesscontrol.Permission{
				{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
				{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"},
			},
		},
		{
			name: "ram and stored",
			searchOption: accesscontrol.SearchOptions{
				ActionPrefix: "teams",
				UserID:       2,
			},
			ramRoles: map[string]*accesscontrol.RoleDTO{
				string(identity.RoleAdmin): {Permissions: []accesscontrol.Permission{
					{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
				}},
				accesscontrol.RoleGrafanaAdmin: {Permissions: []accesscontrol.Permission{
					{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"},
				}},
			},
			storedPerms: map[int64][]accesscontrol.Permission{
				1: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"}},
				2: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"},
					{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:id:1"}},
			},
			storedRoles: map[int64][]string{
				1: {string(identity.RoleEditor)},
				2: {string(identity.RoleAdmin), accesscontrol.RoleGrafanaAdmin},
			},
			want: []accesscontrol.Permission{
				{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"},
				{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:id:1"},
				{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
				{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"},
			},
		},
		{
			name: "check action prefix filter works correctly",
			searchOption: accesscontrol.SearchOptions{
				ActionPrefix: "teams",
				UserID:       1,
			},
			ramRoles: map[string]*accesscontrol.RoleDTO{
				string(identity.RoleEditor): {Permissions: []accesscontrol.Permission{
					{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
					{Action: accesscontrol.ActionUsersCreate},
					{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"},
					{Action: accesscontrol.ActionAnnotationsRead, Scope: "annotations:*"},
				}},
			},
			storedRoles: map[int64][]string{
				1: {string(identity.RoleEditor)},
			},
			want: []accesscontrol.Permission{
				{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
				{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"},
			},
		},
		{
			name: "check action filter works correctly",
			searchOption: accesscontrol.SearchOptions{
				Action: accesscontrol.ActionTeamsRead,
				UserID: 1,
			},
			ramRoles: map[string]*accesscontrol.RoleDTO{
				string(identity.RoleEditor): {Permissions: []accesscontrol.Permission{
					{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
					{Action: accesscontrol.ActionUsersCreate},
					{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"},
					{Action: accesscontrol.ActionAnnotationsRead, Scope: "annotations:*"},
				}},
			},
			storedRoles: map[int64][]string{
				1: {string(identity.RoleEditor)},
			},
			want: []accesscontrol.Permission{
				{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"},
				{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
			},
		},
		{
			name: "check action sets are correctly included if an action is specified",
			searchOption: accesscontrol.SearchOptions{
				Action: "dashboards:read",
				UserID: 1,
			},
			withActionSets: true,
			actionSets: map[string][]string{
				"dashboards:view": {"dashboards:read"},
				"dashboards:edit": {"dashboards:read", "dashboards:write", "dashboards:read-advanced"},
			},
			ramRoles: map[string]*accesscontrol.RoleDTO{
				string(identity.RoleEditor): {Permissions: []accesscontrol.Permission{
					{Action: "dashboards:read", Scope: "dashboards:uid:ram"},
				}},
			},
			storedRoles: map[int64][]string{
				1: {string(identity.RoleEditor)},
			},
			storedPerms: map[int64][]accesscontrol.Permission{
				1: {
					{Action: "dashboards:read", Scope: "dashboards:uid:stored"},
					{Action: "dashboards:edit", Scope: "dashboards:uid:stored2"},
					{Action: "dashboards:view", Scope: "dashboards:uid:stored3"},
				},
			},
			want: []accesscontrol.Permission{
				{Action: "dashboards:read", Scope: "dashboards:uid:ram"},
				{Action: "dashboards:read", Scope: "dashboards:uid:stored"},
				{Action: "dashboards:read", Scope: "dashboards:uid:stored2"},
				{Action: "dashboards:read", Scope: "dashboards:uid:stored3"},
			},
		},
		{
			name: "check action sets are correctly included if an action prefix is specified",
			searchOption: accesscontrol.SearchOptions{
				ActionPrefix: "dashboards",
				UserID:       1,
			},
			withActionSets: true,
			actionSets: map[string][]string{
				"dashboards:view": {"dashboards:read"},
				"folders:view":    {"dashboards:read", "folders:read"},
				"dashboards:edit": {"dashboards:read", "dashboards:write"},
			},
			ramRoles: map[string]*accesscontrol.RoleDTO{
				string(identity.RoleEditor): {Permissions: []accesscontrol.Permission{
					{Action: "dashboards:read", Scope: "dashboards:uid:ram"},
				}},
			},
			storedRoles: map[int64][]string{
				1: {string(identity.RoleEditor)},
			},
			storedPerms: map[int64][]accesscontrol.Permission{
				1: {
					{Action: "dashboards:read", Scope: "dashboards:uid:stored"},
					{Action: "folders:view", Scope: "folders:uid:stored2"},
					{Action: "dashboards:edit", Scope: "dashboards:uid:stored3"},
				},
			},
			want: []accesscontrol.Permission{
				{Action: "dashboards:read", Scope: "dashboards:uid:ram"},
				{Action: "dashboards:read", Scope: "dashboards:uid:stored"},
				{Action: "dashboards:read", Scope: "folders:uid:stored2"},
				{Action: "dashboards:read", Scope: "dashboards:uid:stored3"},
				{Action: "dashboards:write", Scope: "dashboards:uid:stored3"},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ac := setupTestEnv(t, true)
			if tt.withActionSets {
				actionSetSvc := resourcepermissions.NewActionSetService()
				for set, actions := range tt.actionSets {
					actionSetName := resourcepermissions.GetActionSetName(strings.Split(set, ":")[0], strings.Split(set, ":")[1])
					actionSetSvc.StoreActionSet(actionSetName, actions)
				}
				ac.actionResolver = actionSetSvc
			}

			ac.roles = tt.ramRoles
			ac.store = actest.FakeStore{
				ExpectedUsersPermissions: tt.storedPerms,
				ExpectedUsersRoles:       tt.storedRoles,
			}

			got, err := ac.searchUserPermissions(ctx, 1, tt.searchOption)
			if tt.wantErr {
				require.NotNil(t, err)
				return
			}
			require.Nil(t, err)

			assert.ElementsMatch(t, got, tt.want)
		})
	}
}

func TestIntegrationService_SaveExternalServiceRole(t *testing.T) {
	testutil.SkipIntegrationTestInShortMode(t)

	type run struct {
		cmd     accesscontrol.SaveExternalServiceRoleCommand
		wantErr bool
	}
	tests := []struct {
		name string
		runs []run
	}{
		{
			name: "can create a role",
			runs: []run{
				{
					cmd: accesscontrol.SaveExternalServiceRoleCommand{
						AssignmentOrgID:   2,
						ServiceAccountID:  2,
						ExternalServiceID: "App 1",
						Permissions:       []accesscontrol.Permission{{Action: "users:read", Scope: "users:id:1"}},
					},
					wantErr: false,
				},
			},
		},
		{
			name: "can update a role",
			runs: []run{
				{
					cmd: accesscontrol.SaveExternalServiceRoleCommand{
						AssignmentOrgID:   1,
						ServiceAccountID:  2,
						ExternalServiceID: "App 1",
						Permissions:       []accesscontrol.Permission{{Action: "users:read", Scope: "users:id:1"}},
					},
					wantErr: false,
				},
				{
					cmd: accesscontrol.SaveExternalServiceRoleCommand{
						AssignmentOrgID:   1,
						ServiceAccountID:  2,
						ExternalServiceID: "App 1",
						Permissions: []accesscontrol.Permission{
							{Action: "users:write", Scope: "users:id:1"},
							{Action: "users:write", Scope: "users:id:2"},
						},
					},
					wantErr: false,
				},
			},
		},
		{
			name: "test command validity - no service account ID",
			runs: []run{
				{
					cmd: accesscontrol.SaveExternalServiceRoleCommand{
						AssignmentOrgID:   2,
						ExternalServiceID: "App 1",
						Permissions:       []accesscontrol.Permission{{Action: "users:read", Scope: "users:id:1"}},
					},
					wantErr: true,
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := context.Background()
			ac := setupTestEnv(t, true)
			ac.cfg.ManagedServiceAccountsEnabled = true
			ac.features = featuremgmt.WithFeatures(featuremgmt.FlagExternalServiceAccounts)
			for _, r := range tt.runs {
				err := ac.SaveExternalServiceRole(ctx, r.cmd)
				if r.wantErr {
					require.Error(t, err)
					continue
				}
				require.NoError(t, err)

				// Check that the permissions and assignment are stored correctly
				perms, errGetPerms := ac.getUserPermissions(ctx, &user.SignedInUser{OrgID: r.cmd.AssignmentOrgID, UserID: 2}, accesscontrol.Options{})
				require.NoError(t, errGetPerms)

				// Only compare action and scope
				expPerms := make([]accesscontrol.Permission, len(r.cmd.Permissions))
				for i, p := range r.cmd.Permissions {
					expPerms[i] = accesscontrol.Permission{Action: p.Action, Scope: p.Scope}
				}
				// shared with me is added by default for all users in pkg/services/accesscontrol/acimpl/service.go
				assert.Equal(t, append([]accesscontrol.Permission{{Action: "folders:read", Scope: "folders:uid:sharedwithme"}}, expPerms...), perms)
			}
		})
	}
}

func TestIntegrationService_DeleteExternalServiceRole(t *testing.T) {
	testutil.SkipIntegrationTestInShortMode(t)

	tests := []struct {
		name              string
		initCmd           *accesscontrol.SaveExternalServiceRoleCommand
		externalServiceID string
		wantErr           bool
	}{
		{
			name:              "handles deleting role that doesn't exist",
			externalServiceID: "App 1",
			wantErr:           false,
		},
		{
			name: "handles deleting role that exists",
			initCmd: &accesscontrol.SaveExternalServiceRoleCommand{
				AssignmentOrgID:   1,
				ServiceAccountID:  2,
				ExternalServiceID: "App 1",
				Permissions:       []accesscontrol.Permission{{Action: "users:read", Scope: "users:id:1"}},
			},
			externalServiceID: "App 1",
			wantErr:           false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := context.Background()
			ac := setupTestEnv(t, true)
			ac.cfg.ManagedServiceAccountsEnabled = true
			ac.features = featuremgmt.WithFeatures(featuremgmt.FlagExternalServiceAccounts)

			if tt.initCmd != nil {
				err := ac.SaveExternalServiceRole(ctx, *tt.initCmd)
				require.NoError(t, err)
			}

			err := ac.DeleteExternalServiceRole(ctx, tt.externalServiceID)
			if tt.wantErr {
				require.Error(t, err)
				return
			}
			require.NoError(t, err)

			if tt.initCmd != nil {
				// Check that the permissions and assignment are removed correctly
				perms, errGetPerms := ac.getUserPermissions(ctx, &user.SignedInUser{OrgID: tt.initCmd.AssignmentOrgID, UserID: 2}, accesscontrol.Options{})
				require.NoError(t, errGetPerms)
				// shared with me is added by default for all users in pkg/services/accesscontrol/acimpl/service.go
				assert.Equal(t, []accesscontrol.Permission{{Action: "folders:read", Scope: "folders:uid:sharedwithme"}}, perms)
			}
		})
	}
}

func TestIntegrationService_GetRoleByName(t *testing.T) {
	testutil.SkipIntegrationTestInShortMode(t)

	t.Parallel()

	ctx := context.Background()

	t.Run("when the role does not exists, it returns an error", func(t *testing.T) {
		t.Parallel()

		ac := setupTestEnv(t, true)
		ac.registrations = accesscontrol.RegistrationList{}

		role, err := ac.GetRoleByName(ctx, 0, "not-found-role")
		require.ErrorIs(t, err, accesscontrol.ErrRoleNotFound)
		require.Nil(t, role)
	})

	t.Run("when the role exists, it is returned", func(t *testing.T) {
		t.Parallel()

		roleName := "fixed:test:test"

		ac := setupTestEnv(t, true)
		ac.registrations = accesscontrol.RegistrationList{}
		ac.registrations.Append(accesscontrol.RoleRegistration{
			Role:   accesscontrol.RoleDTO{Name: roleName},
			Grants: []string{"Admin"},
		})

		role, err := ac.GetRoleByName(ctx, 0, roleName)
		require.NoError(t, err)
		require.NotNil(t, role)
		require.Equal(t, roleName, role.Name)
	})
}
