package sqlstore

import (
	"context"
	"fmt"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/grafana/grafana/pkg/components/simplejson"
	"github.com/grafana/grafana/pkg/models"
	"github.com/grafana/grafana/pkg/services/accesscontrol"
	"github.com/grafana/grafana/pkg/services/dashboards"
	dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
	"github.com/grafana/grafana/pkg/services/org"
	"github.com/grafana/grafana/pkg/services/user"
	"github.com/grafana/grafana/pkg/util"
)

func TestIntegrationAccountDataAccess(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping integration test")
	}
	t.Run("Testing Account DB Access", func(t *testing.T) {
		sqlStore := InitTestDB(t)
		testUser := &user.SignedInUser{
			Permissions: map[int64]map[string][]string{
				1: {accesscontrol.ActionOrgUsersRead: []string{accesscontrol.ScopeUsersAll}},
			},
		}

		t.Run("Given we have organizations, we can query them by IDs", func(t *testing.T) {
			var err error
			var cmd *models.CreateOrgCommand
			ids := []int64{}

			for i := 1; i < 4; i++ {
				cmd = &models.CreateOrgCommand{Name: fmt.Sprint("Org #", i)}
				err = sqlStore.CreateOrg(context.Background(), cmd)
				require.NoError(t, err)

				ids = append(ids, cmd.Result.Id)
			}

			query := &models.SearchOrgsQuery{Ids: ids}
			err = sqlStore.SearchOrgs(context.Background(), query)

			require.NoError(t, err)
			require.Equal(t, len(query.Result), 3)
		})

		t.Run("Given we have organizations, we can limit and paginate search", func(t *testing.T) {
			sqlStore = InitTestDB(t)
			for i := 1; i < 4; i++ {
				cmd := &models.CreateOrgCommand{Name: fmt.Sprint("Org #", i)}
				err := sqlStore.CreateOrg(context.Background(), cmd)
				require.NoError(t, err)
			}

			t.Run("Should be able to search with defaults", func(t *testing.T) {
				query := &models.SearchOrgsQuery{}
				err := sqlStore.SearchOrgs(context.Background(), query)

				require.NoError(t, err)
				require.Equal(t, len(query.Result), 3)
			})

			t.Run("Should be able to limit search", func(t *testing.T) {
				query := &models.SearchOrgsQuery{Limit: 1}
				err := sqlStore.SearchOrgs(context.Background(), query)

				require.NoError(t, err)
				require.Equal(t, len(query.Result), 1)
			})

			t.Run("Should be able to limit and paginate search", func(t *testing.T) {
				query := &models.SearchOrgsQuery{Limit: 2, Page: 1}
				err := sqlStore.SearchOrgs(context.Background(), query)

				require.NoError(t, err)
				require.Equal(t, len(query.Result), 1)
			})
		})

		t.Run("Given single org mode", func(t *testing.T) {
			sqlStore.Cfg.AutoAssignOrg = true
			sqlStore.Cfg.AutoAssignOrgId = 1
			sqlStore.Cfg.AutoAssignOrgRole = "Viewer"

			t.Run("Users should be added to default organization", func(t *testing.T) {
				ac1cmd := user.CreateUserCommand{Login: "ac1", Email: "ac1@test.com", Name: "ac1 name"}
				ac2cmd := user.CreateUserCommand{Login: "ac2", Email: "ac2@test.com", Name: "ac2 name"}

				ac1, err := sqlStore.CreateUser(context.Background(), ac1cmd)
				require.NoError(t, err)
				ac2, err := sqlStore.CreateUser(context.Background(), ac2cmd)
				require.NoError(t, err)

				q1 := models.GetUserOrgListQuery{UserId: ac1.ID}
				q2 := models.GetUserOrgListQuery{UserId: ac2.ID}
				err = sqlStore.GetUserOrgList(context.Background(), &q1)
				require.NoError(t, err)
				err = sqlStore.GetUserOrgList(context.Background(), &q2)
				require.NoError(t, err)

				require.Equal(t, q1.Result[0].OrgId, q2.Result[0].OrgId)
				require.Equal(t, string(q1.Result[0].Role), "Viewer")
			})
		})

		t.Run("Given single org and 2 users inserted", func(t *testing.T) {
			sqlStore = InitTestDB(t)
			sqlStore.Cfg.AutoAssignOrg = true
			sqlStore.Cfg.AutoAssignOrgId = 1
			sqlStore.Cfg.AutoAssignOrgRole = "Viewer"

			ac1cmd := user.CreateUserCommand{Login: "ac1", Email: "ac1@test.com", Name: "ac1 name"}
			ac2cmd := user.CreateUserCommand{Login: "ac2", Email: "ac2@test.com", Name: "ac2 name"}

			ac1, err := sqlStore.CreateUser(context.Background(), ac1cmd)
			testUser.OrgID = ac1.OrgID
			require.NoError(t, err)
			_, err = sqlStore.CreateUser(context.Background(), ac2cmd)
			require.NoError(t, err)

			t.Run("Can get organization users paginated with query", func(t *testing.T) {
				query := models.SearchOrgUsersQuery{
					OrgID: ac1.OrgID,
					Page:  1,
					User:  testUser,
				}
				err = sqlStore.SearchOrgUsers(context.Background(), &query)

				require.NoError(t, err)
				require.Equal(t, len(query.Result.OrgUsers), 2)
			})

			t.Run("Can get organization users paginated and limited", func(t *testing.T) {
				query := models.SearchOrgUsersQuery{
					OrgID: ac1.OrgID,
					Limit: 1,
					Page:  1,
					User:  testUser,
				}
				err = sqlStore.SearchOrgUsers(context.Background(), &query)

				require.NoError(t, err)
				require.Equal(t, len(query.Result.OrgUsers), 1)
			})
		})

		t.Run("Given two saved users", func(t *testing.T) {
			sqlStore = InitTestDB(t)
			sqlStore.Cfg.AutoAssignOrg = false

			ac1cmd := user.CreateUserCommand{Login: "ac1", Email: "ac1@test.com", Name: "ac1 name"}
			ac2cmd := user.CreateUserCommand{Login: "ac2", Email: "ac2@test.com", Name: "ac2 name", IsAdmin: true}
			serviceaccountcmd := user.CreateUserCommand{Login: "serviceaccount", Email: "service@test.com", Name: "serviceaccount name", IsAdmin: true, IsServiceAccount: true}

			ac1, err := sqlStore.CreateUser(context.Background(), ac1cmd)
			require.NoError(t, err)
			ac2, err := sqlStore.CreateUser(context.Background(), ac2cmd)
			require.NoError(t, err)
			// user only used for making sure we filter out the service accounts
			_, err = sqlStore.CreateUser(context.Background(), serviceaccountcmd)
			require.NoError(t, err)

			t.Run("Should be able to read user info projection", func(t *testing.T) {
				query := models.GetUserProfileQuery{UserId: ac1.ID}
				err = sqlStore.GetUserProfile(context.Background(), &query)

				require.NoError(t, err)
				require.Equal(t, query.Result.Email, "ac1@test.com")
				require.Equal(t, query.Result.Login, "ac1")
			})

			t.Run("Can search users", func(t *testing.T) {
				query := models.SearchUsersQuery{Query: "", SignedInUser: &user.SignedInUser{
					OrgID: 1,
					Permissions: map[int64]map[string][]string{
						1: {accesscontrol.ActionUsersRead: {accesscontrol.ScopeGlobalUsersAll}},
					},
				}}
				err := sqlStore.SearchUsers(context.Background(), &query)

				require.NoError(t, err)
				require.Len(t, query.Result.Users, 2)
				require.Equal(t, query.Result.Users[0].Email, "ac1@test.com")
				require.Equal(t, query.Result.Users[1].Email, "ac2@test.com")
			})

			t.Run("Given an added org user", func(t *testing.T) {
				cmd := models.AddOrgUserCommand{
					OrgId:  ac1.OrgID,
					UserId: ac2.ID,
					Role:   org.RoleViewer,
				}

				err := sqlStore.AddOrgUser(context.Background(), &cmd)
				t.Run("Should have been saved without error", func(t *testing.T) {
					require.NoError(t, err)
				})

				t.Run("Can update org user role", func(t *testing.T) {
					updateCmd := models.UpdateOrgUserCommand{OrgId: ac1.OrgID, UserId: ac2.ID, Role: org.RoleAdmin}
					err = sqlStore.UpdateOrgUser(context.Background(), &updateCmd)
					require.NoError(t, err)

					orgUsersQuery := models.GetOrgUsersQuery{
						OrgId: ac1.OrgID,
						User: &user.SignedInUser{
							OrgID:       ac1.OrgID,
							Permissions: map[int64]map[string][]string{ac1.OrgID: {accesscontrol.ActionOrgUsersRead: {accesscontrol.ScopeUsersAll}}},
						},
					}
					err = sqlStore.GetOrgUsers(context.Background(), &orgUsersQuery)
					require.NoError(t, err)

					require.EqualValues(t, orgUsersQuery.Result[1].Role, org.RoleAdmin)
				})

				t.Run("Can get logged in user projection", func(t *testing.T) {
					query := models.GetSignedInUserQuery{UserId: ac2.ID}
					err := sqlStore.GetSignedInUser(context.Background(), &query)

					require.NoError(t, err)
					require.Equal(t, query.Result.Email, "ac2@test.com")
					require.Equal(t, query.Result.OrgID, ac2.OrgID)
					require.Equal(t, query.Result.Name, "ac2 name")
					require.Equal(t, query.Result.Login, "ac2")
					require.EqualValues(t, query.Result.OrgRole, "Admin")
					require.Equal(t, query.Result.OrgName, "ac2@test.com")
					require.Equal(t, query.Result.IsGrafanaAdmin, true)
				})

				t.Run("Can get user organizations", func(t *testing.T) {
					query := models.GetUserOrgListQuery{UserId: ac2.ID}
					err := sqlStore.GetUserOrgList(context.Background(), &query)

					require.NoError(t, err)
					require.Equal(t, len(query.Result), 2)
				})

				t.Run("Can get organization users", func(t *testing.T) {
					query := models.GetOrgUsersQuery{
						OrgId: ac1.OrgID,
						User: &user.SignedInUser{
							OrgID:       ac1.OrgID,
							Permissions: map[int64]map[string][]string{ac1.OrgID: {accesscontrol.ActionOrgUsersRead: {accesscontrol.ScopeUsersAll}}},
						},
					}
					err := sqlStore.GetOrgUsers(context.Background(), &query)

					require.NoError(t, err)
					require.Equal(t, len(query.Result), 2)
					require.Equal(t, query.Result[0].Role, "Admin")
				})

				t.Run("Can get organization users with query", func(t *testing.T) {
					query := models.GetOrgUsersQuery{
						OrgId: ac1.OrgID,
						Query: "ac1",
						User: &user.SignedInUser{
							OrgID:       ac1.OrgID,
							Permissions: map[int64]map[string][]string{ac1.OrgID: {accesscontrol.ActionOrgUsersRead: {accesscontrol.ScopeUsersAll}}},
						},
					}
					err := sqlStore.GetOrgUsers(context.Background(), &query)

					require.NoError(t, err)
					require.Equal(t, len(query.Result), 1)
					require.Equal(t, query.Result[0].Email, ac1.Email)
				})

				t.Run("Can get organization users with query and limit", func(t *testing.T) {
					query := models.GetOrgUsersQuery{
						OrgId: ac1.OrgID,
						Query: "ac",
						Limit: 1,
						User: &user.SignedInUser{
							OrgID:       ac1.OrgID,
							Permissions: map[int64]map[string][]string{ac1.OrgID: {accesscontrol.ActionOrgUsersRead: {accesscontrol.ScopeUsersAll}}},
						},
					}
					err := sqlStore.GetOrgUsers(context.Background(), &query)

					require.NoError(t, err)
					require.Equal(t, len(query.Result), 1)
					require.Equal(t, query.Result[0].Email, ac1.Email)
				})

				t.Run("Can set using org", func(t *testing.T) {
					cmd := models.SetUsingOrgCommand{UserId: ac2.ID, OrgId: ac1.OrgID}
					err := sqlStore.SetUsingOrg(context.Background(), &cmd)
					require.NoError(t, err)

					t.Run("SignedInUserQuery with a different org", func(t *testing.T) {
						query := models.GetSignedInUserQuery{UserId: ac2.ID}
						err := sqlStore.GetSignedInUser(context.Background(), &query)

						require.NoError(t, err)
						require.Equal(t, query.Result.OrgID, ac1.OrgID)
						require.Equal(t, query.Result.Email, "ac2@test.com")
						require.Equal(t, query.Result.Name, "ac2 name")
						require.Equal(t, query.Result.Login, "ac2")
						require.Equal(t, query.Result.OrgName, "ac1@test.com")
					})

					t.Run("Should set last org as current when removing user from current", func(t *testing.T) {
						remCmd := models.RemoveOrgUserCommand{OrgId: ac1.OrgID, UserId: ac2.ID}
						err := sqlStore.RemoveOrgUser(context.Background(), &remCmd)
						require.NoError(t, err)

						query := models.GetSignedInUserQuery{UserId: ac2.ID}
						err = sqlStore.GetSignedInUser(context.Background(), &query)

						require.NoError(t, err)
						require.Equal(t, query.Result.OrgID, ac2.OrgID)
					})
				})

				t.Run("Removing user from org should delete user completely if in no other org", func(t *testing.T) {
					// make sure ac2 has no org
					err := sqlStore.DeleteOrg(context.Background(), &models.DeleteOrgCommand{Id: ac2.OrgID})
					require.NoError(t, err)

					// remove ac2 user from ac1 org
					remCmd := models.RemoveOrgUserCommand{OrgId: ac1.OrgID, UserId: ac2.ID, ShouldDeleteOrphanedUser: true}
					err = sqlStore.RemoveOrgUser(context.Background(), &remCmd)
					require.NoError(t, err)
					require.True(t, remCmd.UserWasDeleted)

					err = sqlStore.GetSignedInUser(context.Background(), &models.GetSignedInUserQuery{UserId: ac2.ID})
					require.Equal(t, err, user.ErrUserNotFound)
				})

				t.Run("Cannot delete last admin org user", func(t *testing.T) {
					cmd := models.RemoveOrgUserCommand{OrgId: ac1.OrgID, UserId: ac1.ID}
					err := sqlStore.RemoveOrgUser(context.Background(), &cmd)
					require.Equal(t, err, models.ErrLastOrgAdmin)
				})

				t.Run("Cannot update role so no one is admin user", func(t *testing.T) {
					cmd := models.UpdateOrgUserCommand{OrgId: ac1.OrgID, UserId: ac1.ID, Role: org.RoleViewer}
					err := sqlStore.UpdateOrgUser(context.Background(), &cmd)
					require.Equal(t, err, models.ErrLastOrgAdmin)
				})

				t.Run("Given an org user with dashboard permissions", func(t *testing.T) {
					ac3cmd := user.CreateUserCommand{Login: "ac3", Email: "ac3@test.com", Name: "ac3 name", IsAdmin: false}
					ac3, err := sqlStore.CreateUser(context.Background(), ac3cmd)
					require.NoError(t, err)

					orgUserCmd := models.AddOrgUserCommand{
						OrgId:  ac1.OrgID,
						UserId: ac3.ID,
						Role:   org.RoleViewer,
					}

					err = sqlStore.AddOrgUser(context.Background(), &orgUserCmd)
					require.NoError(t, err)

					query := models.GetOrgUsersQuery{
						OrgId: ac1.OrgID,
						User: &user.SignedInUser{
							OrgID:       ac1.OrgID,
							Permissions: map[int64]map[string][]string{ac1.OrgID: {accesscontrol.ActionOrgUsersRead: {accesscontrol.ScopeUsersAll}}},
						},
					}
					err = sqlStore.GetOrgUsers(context.Background(), &query)
					require.NoError(t, err)
					// require.Equal(t, len(query.Result), 3)

					dash1 := insertTestDashboard(t, sqlStore, "1 test dash", ac1.OrgID, 0, false, "prod", "webapp")
					dash2 := insertTestDashboard(t, sqlStore, "2 test dash", ac3.OrgID, 0, false, "prod", "webapp")

					err = updateDashboardACL(t, sqlStore, dash1.Id, &models.DashboardACL{
						DashboardID: dash1.Id, OrgID: ac1.OrgID, UserID: ac3.ID, Permission: models.PERMISSION_EDIT,
					})
					require.NoError(t, err)

					err = updateDashboardACL(t, sqlStore, dash2.Id, &models.DashboardACL{
						DashboardID: dash2.Id, OrgID: ac3.OrgID, UserID: ac3.ID, Permission: models.PERMISSION_EDIT,
					})
					require.NoError(t, err)

					t.Run("When org user is deleted", func(t *testing.T) {
						cmdRemove := models.RemoveOrgUserCommand{OrgId: ac1.OrgID, UserId: ac3.ID}
						err := sqlStore.RemoveOrgUser(context.Background(), &cmdRemove)
						require.NoError(t, err)

						t.Run("Should remove dependent permissions for deleted org user", func(t *testing.T) {
							permQuery := &models.GetDashboardACLInfoListQuery{DashboardID: dash1.Id, OrgID: ac1.OrgID}

							err = getDashboardACLInfoList(sqlStore, permQuery)
							require.NoError(t, err)

							require.Equal(t, len(permQuery.Result), 0)
						})

						t.Run("Should not remove dashboard permissions for same user in another org", func(t *testing.T) {
							permQuery := &models.GetDashboardACLInfoListQuery{DashboardID: dash2.Id, OrgID: ac3.OrgID}

							err = getDashboardACLInfoList(sqlStore, permQuery)
							require.NoError(t, err)

							require.Equal(t, len(permQuery.Result), 1)
							require.Equal(t, permQuery.Result[0].OrgId, ac3.OrgID)
							require.Equal(t, permQuery.Result[0].UserId, ac3.ID)
						})
					})
				})
			})
		})
	})
}

//TODO: Use FakeDashboardStore when org has its own service
func insertTestDashboard(t *testing.T, sqlStore *SQLStore, title string, orgId int64,
	folderId int64, isFolder bool, tags ...interface{}) *models.Dashboard {
	t.Helper()
	cmd := models.SaveDashboardCommand{
		OrgId:    orgId,
		FolderId: folderId,
		IsFolder: isFolder,
		Dashboard: simplejson.NewFromAny(map[string]interface{}{
			"id":    nil,
			"title": title,
			"tags":  tags,
		}),
	}

	var dash *models.Dashboard
	err := sqlStore.WithDbSession(context.Background(), func(sess *DBSession) error {
		dash = cmd.GetDashboardModel()
		dash.SetVersion(1)
		dash.Created = time.Now()
		dash.Updated = time.Now()
		dash.Uid = util.GenerateShortUID()
		_, err := sess.Insert(dash)
		return err
	})

	require.NoError(t, err)
	require.NotNil(t, dash)
	dash.Data.Set("id", dash.Id)
	dash.Data.Set("uid", dash.Uid)

	err = sqlStore.WithDbSession(context.Background(), func(sess *DBSession) error {
		dashVersion := &dashver.DashboardVersion{
			DashboardID:   dash.Id,
			ParentVersion: dash.Version,
			RestoredFrom:  cmd.RestoredFrom,
			Version:       dash.Version,
			Created:       time.Now(),
			CreatedBy:     dash.UpdatedBy,
			Message:       cmd.Message,
			Data:          dash.Data,
		}
		require.NoError(t, err)

		if affectedRows, err := sess.Insert(dashVersion); err != nil {
			return err
		} else if affectedRows == 0 {
			return dashboards.ErrDashboardNotFound
		}

		return nil
	})
	require.NoError(t, err)

	return dash
}

//TODO: Use FakeDashboardStore when org has its own service
func updateDashboardACL(t *testing.T, sqlStore *SQLStore, dashboardID int64, items ...*models.DashboardACL) error {
	t.Helper()

	err := sqlStore.WithDbSession(context.Background(), func(sess *DBSession) error {
		_, err := sess.Exec("DELETE FROM dashboard_acl WHERE dashboard_id=?", dashboardID)
		if err != nil {
			return fmt.Errorf("deleting from dashboard_acl failed: %w", err)
		}

		for _, item := range items {
			item.Created = time.Now()
			item.Updated = time.Now()
			if item.UserID == 0 && item.TeamID == 0 && (item.Role == nil || !item.Role.IsValid()) {
				return models.ErrDashboardACLInfoMissing
			}

			if item.DashboardID == 0 {
				return models.ErrDashboardPermissionDashboardEmpty
			}

			sess.Nullable("user_id", "team_id")
			if _, err := sess.Insert(item); err != nil {
				return err
			}
		}

		// Update dashboard HasACL flag
		dashboard := models.Dashboard{HasACL: true}
		_, err = sess.Cols("has_acl").Where("id=?", dashboardID).Update(&dashboard)
		return err
	})
	return err
}

// This function was copied from pkg/services/dashboards/database to circumvent
// import cycles. When this org-related code is refactored into a service the
// tests can the real GetDashboardACLInfoList functions
func getDashboardACLInfoList(s *SQLStore, query *models.GetDashboardACLInfoListQuery) error {
	outerErr := s.WithDbSession(context.Background(), func(dbSession *DBSession) error {
		query.Result = make([]*models.DashboardACLInfoDTO, 0)
		falseStr := dialect.BooleanStr(false)

		if query.DashboardID == 0 {
			sql := `SELECT
		da.id,
		da.org_id,
		da.dashboard_id,
		da.user_id,
		da.team_id,
		da.permission,
		da.role,
		da.created,
		da.updated,
		'' as user_login,
		'' as user_email,
		'' as team,
		'' as title,
		'' as slug,
		'' as uid,` +
				falseStr + ` AS is_folder,` +
				falseStr + ` AS inherited
		FROM dashboard_acl as da
		WHERE da.dashboard_id = -1`
			return dbSession.SQL(sql).Find(&query.Result)
		}

		rawSQL := `
			-- get permissions for the dashboard and its parent folder
			SELECT
				da.id,
				da.org_id,
				da.dashboard_id,
				da.user_id,
				da.team_id,
				da.permission,
				da.role,
				da.created,
				da.updated,
				u.login AS user_login,
				u.email AS user_email,
				ug.name AS team,
				ug.email AS team_email,
				d.title,
				d.slug,
				d.uid,
				d.is_folder,
				CASE WHEN (da.dashboard_id = -1 AND d.folder_id > 0) OR da.dashboard_id = d.folder_id THEN ` + dialect.BooleanStr(true) + ` ELSE ` + falseStr + ` END AS inherited
			FROM dashboard as d
				LEFT JOIN dashboard folder on folder.id = d.folder_id
				LEFT JOIN dashboard_acl AS da ON
				da.dashboard_id = d.id OR
				da.dashboard_id = d.folder_id OR
				(
					-- include default permissions -->
					da.org_id = -1 AND (
					  (folder.id IS NOT NULL AND folder.has_acl = ` + falseStr + `) OR
					  (folder.id IS NULL AND d.has_acl = ` + falseStr + `)
					)
				)
				LEFT JOIN ` + dialect.Quote("user") + ` AS u ON u.id = da.user_id
				LEFT JOIN team ug on ug.id = da.team_id
			WHERE d.org_id = ? AND d.id = ? AND da.id IS NOT NULL
			ORDER BY da.id ASC
			`

		return dbSession.SQL(rawSQL, query.OrgID, query.DashboardID).Find(&query.Result)
	})

	if outerErr != nil {
		return outerErr
	}

	for _, p := range query.Result {
		p.PermissionName = p.Permission.String()
	}

	return nil
}
