package database

import (
	"context"
	"testing"

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

	"github.com/grafana/grafana/pkg/components/simplejson"
	"github.com/grafana/grafana/pkg/infra/db"
	"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
	"github.com/grafana/grafana/pkg/services/dashboards"
	"github.com/grafana/grafana/pkg/services/featuremgmt"
	"github.com/grafana/grafana/pkg/services/folder/folderimpl"
	"github.com/grafana/grafana/pkg/services/org"
	"github.com/grafana/grafana/pkg/services/tag/tagimpl"
	"github.com/grafana/grafana/pkg/services/user"
	"github.com/grafana/grafana/pkg/setting"
	"github.com/grafana/grafana/pkg/util/testutil"
)

var testFeatureToggles = featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch)

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

	t.Run("Testing DB", func(t *testing.T) {
		var sqlStore db.DB
		var cfg *setting.Cfg
		var flder, dashInRoot, childDash *dashboards.Dashboard
		var currentUser *user.SignedInUser
		var dashboardStore dashboards.Store
		var folderStore *folderimpl.FolderStoreImpl

		setup := func() {
			sqlStore, cfg = db.InitTestDBWithCfg(t)
			var err error
			dashboardStore, err = ProvideDashboardStore(sqlStore, cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore))
			require.NoError(t, err)
			folderStore = folderimpl.ProvideStore(sqlStore)
			require.NoError(t, err)
			flder = insertTestDashFolder(t, dashboardStore, folderStore, "1 test dash folder", 1, 0, "", "prod", "webapp")
			dashInRoot = insertTestDashboard(t, dashboardStore, "test dash 67", 1, 0, "", false, "prod", "webapp")
			childDash = insertTestDashboard(t, dashboardStore, "test dash 23", 1, flder.ID, flder.UID, false, "prod", "webapp")
			insertTestDashboard(t, dashboardStore, "test dash 45", 1, flder.ID, flder.UID, false, "prod")
			currentUser = &user.SignedInUser{
				UserID:  1,
				OrgID:   1,
				OrgRole: org.RoleViewer,
			}
		}

		t.Run("Given one dashboard folder with two dashboards and one dashboard in the root folder", func(t *testing.T) {
			setup()

			t.Run("and user can read folders and dashboards", func(t *testing.T) {
				currentUser.Permissions = map[int64]map[string][]string{1: {dashboards.ActionDashboardsRead: []string{dashboards.ScopeDashboardsAll},
					dashboards.ActionFoldersRead: []string{dashboards.ScopeFoldersAll}}}
				actest.AddUserPermissionToDB(t, sqlStore, currentUser)

				t.Run("should return all dashboards and folders", func(t *testing.T) {
					query := &dashboards.FindPersistedDashboardsQuery{
						SignedInUser: currentUser,
						OrgId:        1,
						DashboardIds: []int64{flder.ID, dashInRoot.ID},
					}
					hits, err := testSearchDashboards(dashboardStore, query)
					require.NoError(t, err)
					require.Equal(t, len(hits), 2)
					require.Equal(t, hits[0].ID, flder.ID)
					require.Equal(t, hits[1].ID, dashInRoot.ID)
				})
			})

			t.Run("and user can only read dashboards", func(t *testing.T) {
				currentUser.Permissions = map[int64]map[string][]string{1: {dashboards.ActionDashboardsRead: []string{dashboards.ScopeDashboardsAll}}}
				actest.AddUserPermissionToDB(t, sqlStore, currentUser)

				t.Run("should not return folder", func(t *testing.T) {
					query := &dashboards.FindPersistedDashboardsQuery{
						SignedInUser: currentUser,
						OrgId:        1,
						DashboardIds: []int64{flder.ID, dashInRoot.ID},
					}
					hits, err := testSearchDashboards(dashboardStore, query)
					require.NoError(t, err)

					require.Equal(t, 1, len(hits))
					require.Equal(t, hits[0].ID, dashInRoot.ID)
				})
			})

			t.Run("and permissions are set for dashboard child and folder has all permissions removed", func(t *testing.T) {
				currentUser.Permissions = map[int64]map[string][]string{1: {dashboards.ActionDashboardsRead: {dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dashInRoot.UID)}}}
				actest.AddUserPermissionToDB(t, sqlStore, currentUser)

				t.Run("should not return folder or child", func(t *testing.T) {
					query := &dashboards.FindPersistedDashboardsQuery{
						SignedInUser: currentUser,
						DashboardIds: []int64{flder.ID, childDash.ID, dashInRoot.ID},
					}
					hits, err := testSearchDashboards(dashboardStore, query)
					require.NoError(t, err)
					require.Equal(t, 1, len(hits))
					require.Equal(t, hits[0].ID, dashInRoot.ID)
				})

				t.Run("when the user is given permission to child", func(t *testing.T) {
					currentUser.Permissions = map[int64]map[string][]string{1: {dashboards.ActionDashboardsRead: {dashboards.ScopeDashboardsAll}}}
					actest.AddUserPermissionToDB(t, sqlStore, currentUser)

					t.Run("should be able to search for child dashboard but not folder", func(t *testing.T) {
						query := &dashboards.FindPersistedDashboardsQuery{
							SignedInUser: currentUser,
							OrgId:        1,
							DashboardIds: []int64{flder.ID, childDash.ID, dashInRoot.ID},
						}
						hits, err := testSearchDashboards(dashboardStore, query)
						require.NoError(t, err)
						require.Equal(t, 2, len(hits))
						require.Equal(t, hits[0].ID, childDash.ID)
						require.Equal(t, hits[1].ID, dashInRoot.ID)
					})
				})
			})
		})

		t.Run("Given two dashboard folders with one dashboard each and one dashboard in the root folder", func(t *testing.T) {
			var sqlStore db.DB
			var folder1, folder2, dashInRoot, childDash1, childDash2 *dashboards.Dashboard
			var rootFolderId int64 = 0
			var currentUser *user.SignedInUser

			setup2 := func() {
				sqlStore, cfg = db.InitTestDBWithCfg(t)
				var err error
				require.NoError(t, err)
				folder1 = insertTestDashFolder(t, dashboardStore, folderStore, "1 test dash folder", 1, 0, "", "prod")
				folder2 = insertTestDashFolder(t, dashboardStore, folderStore, "2 test dash folder", 1, 0, "", "prod")
				dashInRoot = insertTestDashboard(t, dashboardStore, "test dash 67", 1, 0, "", false, "prod")
				childDash1 = insertTestDashboard(t, dashboardStore, "child dash 1", 1, folder1.ID, folder1.UID, false, "prod")
				childDash2 = insertTestDashboard(t, dashboardStore, "child dash 2", 1, folder2.ID, folder2.UID, false, "prod")

				currentUser = &user.SignedInUser{
					UserID:  1,
					OrgID:   1,
					OrgRole: org.RoleViewer,
				}
			}

			setup2()
			t.Run("and one folder is expanded, the other collapsed", func(t *testing.T) {
				currentUser.Permissions = map[int64]map[string][]string{1: {dashboards.ActionDashboardsRead: {dashboards.ScopeDashboardsAll}, dashboards.ActionFoldersRead: []string{dashboards.ScopeFoldersAll}}}
				actest.AddUserPermissionToDB(t, sqlStore, currentUser)

				t.Run("should return dashboards in root and expanded folder", func(t *testing.T) {
					query := &dashboards.FindPersistedDashboardsQuery{
						FolderIds: []int64{
							rootFolderId,
							folder1.ID,
						}, // nolint:staticcheck
						SignedInUser: currentUser,
						OrgId:        1,
					}
					hits, err := testSearchDashboards(dashboardStore, query)
					require.NoError(t, err)
					require.Equal(t, len(hits), 4)
					require.Equal(t, hits[0].ID, folder1.ID)
					require.Equal(t, hits[1].ID, folder2.ID)
					require.Equal(t, hits[2].ID, childDash1.ID)
					require.Equal(t, hits[3].ID, dashInRoot.ID)
				})
			})

			t.Run("and acl is set for one dashboard folder", func(t *testing.T) {
				t.Run("and a dashboard is moved from folder without acl to the folder with an acl", func(t *testing.T) {
					moveDashboard(t, dashboardStore, 1, childDash2.Data, folder1.ID, folder1.UID)
					currentUser.Permissions = map[int64]map[string][]string{1: {dashboards.ActionDashboardsRead: {dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder2.UID), dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dashInRoot.UID)}}}
					actest.AddUserPermissionToDB(t, sqlStore, currentUser)

					t.Run("should not return folder with acl or its children", func(t *testing.T) {
						query := &dashboards.FindPersistedDashboardsQuery{
							SignedInUser:  currentUser,
							OrgId:         1,
							DashboardIds:  []int64{folder1.ID, childDash1.ID, childDash2.ID, dashInRoot.ID},
							DashboardUIDs: []string{folder1.UID, childDash1.UID, childDash2.UID, dashInRoot.UID},
						}
						hits, err := testSearchDashboards(dashboardStore, query)
						require.NoError(t, err)
						require.Equal(t, len(hits), 1)
						require.Equal(t, hits[0].ID, dashInRoot.ID)
					})
				})
				t.Run("and a dashboard is moved from folder with acl to the folder without an acl", func(t *testing.T) {
					setup2()
					moveDashboard(t, dashboardStore, 1, childDash1.Data, folder2.ID, childDash2.UID)
					currentUser.Permissions = map[int64]map[string][]string{1: {dashboards.ActionDashboardsRead: {dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dashInRoot.UID), dashboards.ScopeDashboardsProvider.GetResourceScopeUID(folder2.UID), dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder2.UID)}, dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder2.UID)}}}
					actest.AddUserPermissionToDB(t, sqlStore, currentUser)

					t.Run("should return folder without acl and its children", func(t *testing.T) {
						query := &dashboards.FindPersistedDashboardsQuery{
							SignedInUser: currentUser,
							OrgId:        1,
							DashboardIds: []int64{folder2.ID, childDash1.ID, childDash2.ID, dashInRoot.ID},
						}
						hits, err := testSearchDashboards(dashboardStore, query)
						require.NoError(t, err)
						assert.Equal(t, 4, len(hits))
						assert.Equal(t, hits[0].ID, folder2.ID)
						assert.Equal(t, hits[1].ID, childDash1.ID)
						assert.Equal(t, hits[2].ID, childDash2.ID)
						assert.Equal(t, hits[3].ID, dashInRoot.ID)
					})
				})
			})
		})
	})
}

func moveDashboard(t *testing.T, dashboardStore dashboards.Store, orgId int64, dashboard *simplejson.Json,
	newFolderId int64, newFolderUID string) *dashboards.Dashboard {
	t.Helper()

	cmd := dashboards.SaveDashboardCommand{
		OrgID:     orgId,
		FolderID:  newFolderId, // nolint:staticcheck
		FolderUID: newFolderUID,
		Dashboard: dashboard,
		Overwrite: true,
	}
	dash, err := dashboardStore.SaveDashboard(context.Background(), cmd)
	require.NoError(t, err)

	return dash
}
