package store

import (
	"context"
	"testing"

	"github.com/google/uuid"
	"github.com/stretchr/testify/require"

	"github.com/grafana/grafana/pkg/infra/log"
	"github.com/grafana/grafana/pkg/services/dashboards"
	"github.com/grafana/grafana/pkg/services/featuremgmt"
	"github.com/grafana/grafana/pkg/services/folder"
	"github.com/grafana/grafana/pkg/services/org"
	"github.com/grafana/grafana/pkg/services/user"
	"github.com/grafana/grafana/pkg/util"

	"github.com/grafana/grafana/pkg/infra/db"
	"github.com/grafana/grafana/pkg/setting"
)

func TestIntegration_GetUserVisibleNamespaces(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping integration test")
	}

	sqlStore := db.InitTestDB(t)
	cfg := setting.NewCfg()
	folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures())
	b := &fakeBus{}
	logger := log.New("test-dbstore")
	store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b)

	admin := &user.SignedInUser{
		UserID:         1,
		OrgID:          1,
		OrgRole:        org.RoleAdmin,
		IsGrafanaAdmin: true,
	}

	folders := []struct {
		uid       string
		title     string
		parentUid string
	}{
		{uid: uuid.NewString(), title: "folder1", parentUid: ""},
		{uid: uuid.NewString(), title: "folder2", parentUid: ""},
		{uid: uuid.NewString(), title: "nested/folder", parentUid: ""},
	}

	for _, f := range folders {
		createFolder(t, store, f.uid, f.title, 1, f.parentUid)
	}

	t.Run("returns all folders", func(t *testing.T) {
		namespaces, err := store.GetUserVisibleNamespaces(context.Background(), 1, admin)
		require.NoError(t, err)
		require.Len(t, namespaces, len(folders))
	})

	t.Run("returns empty list for a non existing org", func(t *testing.T) {
		emptyOrgID := int64(999)
		namespaces, err := store.GetUserVisibleNamespaces(context.Background(), emptyOrgID, admin)
		require.NoError(t, err)
		require.Empty(t, namespaces)
	})
}

func TestIntegration_GetNamespaceByUID(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping integration test")
	}

	sqlStore := db.InitTestDB(t)
	cfg := setting.NewCfg()
	folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures())
	b := &fakeBus{}
	logger := log.New("test-dbstore")
	store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b)

	u := &user.SignedInUser{
		UserID:         1,
		OrgID:          1,
		OrgRole:        org.RoleAdmin,
		IsGrafanaAdmin: true,
	}

	uid := uuid.NewString()
	parentUid := uuid.NewString()
	title := "folder/title"
	parentTitle := "parent-title"
	createFolder(t, store, parentUid, parentTitle, 1, "")
	createFolder(t, store, uid, title, 1, parentUid)

	actual, err := store.GetNamespaceByUID(context.Background(), uid, 1, u)
	require.NoError(t, err)
	require.Equal(t, title, actual.Title)
	require.Equal(t, uid, actual.UID)
	require.Equal(t, title, actual.Fullpath)

	t.Run("error when user does not have permissions", func(t *testing.T) {
		someUser := &user.SignedInUser{
			UserID:  2,
			OrgID:   1,
			OrgRole: org.RoleViewer,
		}
		_, err = store.GetNamespaceByUID(context.Background(), uid, 1, someUser)
		require.ErrorIs(t, err, dashboards.ErrFolderAccessDenied)
	})

	t.Run("error when folder does not exist", func(t *testing.T) {
		nonExistentUID := uuid.NewString()
		_, err := store.GetNamespaceByUID(context.Background(), nonExistentUID, 1, u)
		require.ErrorIs(t, err, dashboards.ErrFolderAccessDenied)
	})

	t.Run("when nested folders are enabled full path should be populated with correct value", func(t *testing.T) {
		store.FolderService = setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders))
		actual, err := store.GetNamespaceByUID(context.Background(), uid, 1, u)
		require.NoError(t, err)
		require.Equal(t, title, actual.Title)
		require.Equal(t, uid, actual.UID)
		require.Equal(t, "parent-title/folder\\/title", actual.Fullpath)
	})
}

func TestIntegration_GetNamespaceByTitle(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping integration test")
	}

	sqlStore := db.InitTestDB(t)
	cfg := setting.NewCfg()
	folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures())
	b := &fakeBus{}
	logger := log.New("test-dbstore")
	store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b)
	store.FolderService = setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders))

	u := &user.SignedInUser{
		UserID:         1,
		OrgID:          1,
		OrgRole:        org.RoleAdmin,
		IsGrafanaAdmin: true,
	}

	// Create parent folder
	parentUID := uuid.NewString()
	parentTitle := "parent-folder"
	createFolder(t, store, parentUID, parentTitle, 1, "")

	// Create child folder under parent
	childUID := uuid.NewString()
	childTitle := "child-folder"
	createFolder(t, store, childUID, childTitle, 1, parentUID)

	// Create another folder with same title but under root
	sameTitleInRoot := uuid.NewString()
	createFolder(t, store, sameTitleInRoot, childTitle, 1, "")

	t.Run("should find folder by title and parent UID", func(t *testing.T) {
		actual, err := store.GetNamespaceByTitle(context.Background(), childTitle, 1, u, parentUID)
		require.NoError(t, err)
		require.Equal(t, childTitle, actual.Title)
		require.Equal(t, childUID, actual.UID)
		require.Equal(t, parentUID, actual.ParentUID)
	})

	t.Run("should find folder by title in root", func(t *testing.T) {
		actual, err := store.GetNamespaceByTitle(context.Background(), childTitle, 1, u, folder.RootFolderUID)
		require.NoError(t, err)
		require.Equal(t, childTitle, actual.Title)
		require.Equal(t, sameTitleInRoot, actual.UID)
		require.Equal(t, folder.RootFolderUID, actual.ParentUID)
	})

	t.Run("should return ErrFolderNotFound when folder with title doesn't exist under specified parent", func(t *testing.T) {
		nonExistentTitle := "non-existent-folder"
		f, err := store.GetNamespaceByTitle(context.Background(), nonExistentTitle, 1, u, parentUID)
		require.Nil(t, f)
		require.ErrorIs(t, err, dashboards.ErrFolderNotFound)
	})
}

func TestIntegration_GetOrCreateNamespaceByTitle(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping integration test")
	}

	u := &user.SignedInUser{
		UserID:         1,
		OrgID:          1,
		OrgRole:        org.RoleAdmin,
		IsGrafanaAdmin: true,
	}

	setupStore := func(t *testing.T) *DBstore {
		sqlStore := db.InitTestDB(t)
		cfg := setting.NewCfg()
		folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures())
		b := &fakeBus{}
		logger := log.New("test-dbstore")
		store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b)
		store.FolderService = setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders))

		return store
	}

	t.Run("should create folder when it does not exist", func(t *testing.T) {
		store := setupStore(t)

		f, err := store.GetOrCreateNamespaceByTitle(context.Background(), "new folder", 1, u, folder.RootFolderUID)
		require.NoError(t, err)
		require.Equal(t, "new folder", f.Title)
		require.NotEmpty(t, f.UID)
		require.Equal(t, folder.RootFolderUID, f.ParentUID)

		folders, err := store.FolderService.GetFolders(
			context.Background(),
			folder.GetFoldersQuery{
				OrgID:        1,
				WithFullpath: true,
				SignedInUser: u,
			},
		)
		require.NoError(t, err)
		require.Len(t, folders, 1)
	})

	t.Run("should return existing folder when it exists", func(t *testing.T) {
		store := setupStore(t)

		title := "existing folder"
		createFolder(t, store, "", title, 1, "")
		f, err := store.GetOrCreateNamespaceByTitle(context.Background(), title, 1, u, folder.RootFolderUID)
		require.NoError(t, err)
		require.Equal(t, title, f.Title)

		folders, err := store.FolderService.GetFolders(
			context.Background(),
			folder.GetFoldersQuery{
				OrgID:        1,
				WithFullpath: true,
				SignedInUser: u,
			},
		)
		require.NoError(t, err)
		require.Len(t, folders, 1)
	})

	t.Run("should create folder under specified parent when it does not exist", func(t *testing.T) {
		store := setupStore(t)

		// Create parent folder first
		parentTitle := "parent folder"
		parentFolder, err := store.GetOrCreateNamespaceByTitle(context.Background(), parentTitle, 1, u, folder.RootFolderUID)
		require.NoError(t, err)

		// Now create a child folder under the parent
		childTitle := "child folder"
		childFolder, err := store.GetOrCreateNamespaceByTitle(context.Background(), childTitle, 1, u, parentFolder.UID)
		require.NoError(t, err)

		// Verify the child folder was created under the parent
		folders, err := store.FolderService.GetChildren(context.Background(), &folder.GetChildrenQuery{UID: parentFolder.UID, OrgID: 1, SignedInUser: u})
		require.NoError(t, err)
		require.Len(t, folders, 1)
		require.Equal(t, childFolder.UID, folders[0].UID)

		folders, err = store.FolderService.GetChildren(context.Background(), &folder.GetChildrenQuery{UID: folder.RootFolderUID, OrgID: 1, SignedInUser: u})
		require.NoError(t, err)
		require.Len(t, folders, 1)
		require.Equal(t, parentFolder.UID, folders[0].UID)
	})

	t.Run("should get correct folder when same title exists under different parents", func(t *testing.T) {
		store := setupStore(t)

		// Create first parent folder
		parent1Title := "parent folder 1"
		parent1, err := store.GetOrCreateNamespaceByTitle(context.Background(), parent1Title, 1, u, folder.RootFolderUID)
		require.NoError(t, err)

		// Create second parent folder
		parent2Title := "parent folder 2"
		parent2, err := store.GetOrCreateNamespaceByTitle(context.Background(), parent2Title, 1, u, folder.RootFolderUID)
		require.NoError(t, err)

		// Create folders with same title under different parents
		sameTitle := "same title folder"

		// Create under first parent
		folder1, err := store.GetOrCreateNamespaceByTitle(context.Background(), sameTitle, 1, u, parent1.UID)
		require.NoError(t, err)

		// Create under second parent
		folder2, err := store.GetOrCreateNamespaceByTitle(context.Background(), sameTitle, 1, u, parent2.UID)
		require.NoError(t, err)

		// Create under root
		folder3, err := store.GetOrCreateNamespaceByTitle(context.Background(), sameTitle, 1, u, folder.RootFolderUID)
		require.NoError(t, err)

		// Verify we get the correct folders when specifying the parent
		gotFolder1, err := store.GetOrCreateNamespaceByTitle(context.Background(), sameTitle, 1, u, parent1.UID)
		require.NoError(t, err)
		require.Equal(t, folder1.UID, gotFolder1.UID)
		require.Equal(t, parent1.UID, gotFolder1.ParentUID)

		gotFolder2, err := store.GetOrCreateNamespaceByTitle(context.Background(), sameTitle, 1, u, parent2.UID)
		require.NoError(t, err)
		require.Equal(t, folder2.UID, gotFolder2.UID)
		require.Equal(t, parent2.UID, gotFolder2.ParentUID)

		gotFolder3, err := store.GetOrCreateNamespaceByTitle(context.Background(), sameTitle, 1, u, folder.RootFolderUID)
		require.NoError(t, err)
		require.Equal(t, folder3.UID, gotFolder3.UID)
		require.Equal(t, folder.RootFolderUID, gotFolder3.ParentUID)
	})
}

func TestIntegration_GetNamespaceChildren(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping integration test")
	}

	sqlStore := db.InitTestDB(t)
	cfg := setting.NewCfg()
	folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures())
	b := &fakeBus{}
	logger := log.New("test-dbstore")
	store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b)
	store.FolderService = setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders))

	admin := &user.SignedInUser{
		UserID:         1,
		OrgID:          1,
		OrgRole:        org.RoleAdmin,
		IsGrafanaAdmin: true,
	}

	// Create root folders
	rootFolder1 := uuid.NewString()
	rootFolder2 := uuid.NewString()
	createFolder(t, store, rootFolder1, "Root Folder 1", 1, "")
	createFolder(t, store, rootFolder2, "Root Folder 2", 1, "")

	// Create child folders under root folder 1
	child1 := uuid.NewString()
	child2 := uuid.NewString()
	createFolder(t, store, child1, "Child Folder 1", 1, rootFolder1)
	createFolder(t, store, child2, "Child Folder 2", 1, rootFolder1)

	// Create nested child under child1
	nestedChild := uuid.NewString()
	createFolder(t, store, nestedChild, "Nested Child", 1, child1)

	differentOrgID := int64(999)
	createFolder(t, store, util.GenerateShortUID(), "Root Folder 1", differentOrgID, "")

	/*
	 * Folder structure:
	 *
	 * Root Folder 1
	 *  - Child Folder 1
	 *    - Nested Child
	 *  - Child Folder 2
	 * Root Folder 2
	 */

	t.Run("should return direct children of a folder", func(t *testing.T) {
		children, err := store.GetNamespaceChildren(context.Background(), rootFolder1, 1, admin)
		require.NoError(t, err)
		require.Len(t, children, 2)

		require.ElementsMatch(t, []string{child1, child2}, []string{children[0].UID, children[1].UID})

		// Verify parent UID
		for _, child := range children {
			require.Equal(t, rootFolder1, child.ParentUID)
		}
	})

	t.Run("should return direct children of a nested folder", func(t *testing.T) {
		children, err := store.GetNamespaceChildren(context.Background(), child1, 1, admin)
		require.NoError(t, err)
		require.Len(t, children, 1)
		require.Equal(t, nestedChild, children[0].UID)
		require.Equal(t, child1, children[0].ParentUID)
	})

	t.Run("should return nil when folder does not exist", func(t *testing.T) {
		nonExistentUID := uuid.NewString()
		children, err := store.GetNamespaceChildren(context.Background(), nonExistentUID, 1, admin)
		require.NotNil(t, children)
		require.Empty(t, children)
		require.Nil(t, err)
	})

	t.Run("should return empty array for folders with no children", func(t *testing.T) {
		children, err := store.GetNamespaceChildren(context.Background(), rootFolder2, 1, admin)
		require.Empty(t, children)
		require.NotNil(t, children)
		require.Nil(t, err)
	})

	t.Run("should return no children for a different org", func(t *testing.T) {
		children, err := store.GetNamespaceChildren(context.Background(), rootFolder1, differentOrgID, admin)
		require.Empty(t, children)
		require.Nil(t, err)
	})

	t.Run("should return children from root folder", func(t *testing.T) {
		children, err := store.GetNamespaceChildren(context.Background(), "", 1, admin)
		require.NoError(t, err)
		require.Equal(t, len(children), 2)
		require.ElementsMatch(t, []string{rootFolder1, rootFolder2}, []string{children[0].UID, children[1].UID})
	})
}
