// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

package storetests

import (
	"fmt"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/mattermost/focalboard/server/model"
	"github.com/mattermost/focalboard/server/services/store"
	"github.com/mattermost/focalboard/server/utils"
)

//nolint:dupl
func StoreTestUserStore(t *testing.T, setup func(t *testing.T) (store.Store, func())) {
	t.Run("GetUsersByTeam", func(t *testing.T) {
		store, tearDown := setup(t)
		defer tearDown()
		testGetUsersByTeam(t, store)
	})

	t.Run("CreateAndGetUser", func(t *testing.T) {
		store, tearDown := setup(t)
		defer tearDown()
		testCreateAndGetUser(t, store)
	})

	t.Run("GetUsersList", func(t *testing.T) {
		store, tearDown := setup(t)
		defer tearDown()
		testGetUsersList(t, store)
	})

	t.Run("CreateAndUpdateUser", func(t *testing.T) {
		store, tearDown := setup(t)
		defer tearDown()
		testCreateAndUpdateUser(t, store)
	})

	t.Run("CreateAndGetRegisteredUserCount", func(t *testing.T) {
		store, tearDown := setup(t)
		defer tearDown()
		testCreateAndGetRegisteredUserCount(t, store)
	})

	t.Run("TestPatchUserProps", func(t *testing.T) {
		store, tearDown := setup(t)
		defer tearDown()
		testPatchUserProps(t, store)
	})
}

func testGetUsersByTeam(t *testing.T, store store.Store) {
	t.Run("GetTeamUsers", func(t *testing.T) {
		users, err := store.GetUsersByTeam("team_1", "", false, false)
		require.Equal(t, 0, len(users))
		require.NoError(t, err)

		userID := utils.NewID(utils.IDTypeUser)

		user, err := store.CreateUser(&model.User{
			ID:       userID,
			Username: "darth.vader",
		})
		require.NoError(t, err)
		require.NotNil(t, user)
		require.Equal(t, userID, user.ID)
		require.Equal(t, "darth.vader", user.Username)

		defer func() {
			_, _ = store.UpdateUser(&model.User{
				ID:       userID,
				DeleteAt: utils.GetMillis(),
			})
		}()

		users, err = store.GetUsersByTeam("team_1", "", false, false)
		require.Equal(t, 1, len(users))
		require.Equal(t, "darth.vader", users[0].Username)
		require.NoError(t, err)
	})
}

func testCreateAndGetUser(t *testing.T, store store.Store) {
	user := &model.User{
		ID:       utils.NewID(utils.IDTypeUser),
		Username: "damao",
		Email:    "mock@email.com",
	}

	t.Run("CreateUser", func(t *testing.T) {
		newUser, err := store.CreateUser(user)
		require.NoError(t, err)
		require.NotNil(t, newUser)
	})

	t.Run("GetUserByID", func(t *testing.T) {
		got, err := store.GetUserByID(user.ID)
		require.NoError(t, err)
		require.Equal(t, user.ID, got.ID)
		require.Equal(t, user.Username, got.Username)
		require.Equal(t, user.Email, got.Email)
	})

	t.Run("GetUserByID nonexistent", func(t *testing.T) {
		got, err := store.GetUserByID("nonexistent-id")
		var nf *model.ErrNotFound
		require.ErrorAs(t, err, &nf)
		require.Nil(t, got)
	})

	t.Run("GetUserByUsername", func(t *testing.T) {
		got, err := store.GetUserByUsername(user.Username)
		require.NoError(t, err)
		require.Equal(t, user.ID, got.ID)
		require.Equal(t, user.Username, got.Username)
		require.Equal(t, user.Email, got.Email)
	})

	t.Run("GetUserByUsername nonexistent", func(t *testing.T) {
		got, err := store.GetUserByID("nonexistent-username")
		var nf *model.ErrNotFound
		require.ErrorAs(t, err, &nf)
		require.Nil(t, got)
	})

	t.Run("GetUserByEmail", func(t *testing.T) {
		got, err := store.GetUserByEmail(user.Email)
		require.NoError(t, err)
		require.Equal(t, user.ID, got.ID)
		require.Equal(t, user.Username, got.Username)
		require.Equal(t, user.Email, got.Email)
	})

	t.Run("GetUserByEmail nonexistent", func(t *testing.T) {
		got, err := store.GetUserByID("nonexistent-email")
		var nf *model.ErrNotFound
		require.ErrorAs(t, err, &nf)
		require.Nil(t, got)
	})
}

func testGetUsersList(t *testing.T, store store.Store) {
	for _, id := range []string{"user1", "user2"} {
		user := &model.User{
			ID:       id,
			Username: fmt.Sprintf("%s-username", id),
			Email:    fmt.Sprintf("%s@sample.com", id),
		}
		newUser, err := store.CreateUser(user)
		require.NoError(t, err)
		require.NotNil(t, newUser)
	}

	testCases := []struct {
		Name          string
		UserIDs       []string
		ExpectedError bool
		ExpectedIDs   []string
	}{
		{
			Name:          "all of the IDs are found",
			UserIDs:       []string{"user1", "user2"},
			ExpectedError: false,
			ExpectedIDs:   []string{"user1", "user2"},
		},
		{
			Name:          "some of the IDs are found",
			UserIDs:       []string{"user2", "non-existent"},
			ExpectedError: true,
			ExpectedIDs:   []string{"user2"},
		},
		{
			Name:          "none of the IDs are found",
			UserIDs:       []string{"non-existent-1", "non-existent-2"},
			ExpectedError: true,
			ExpectedIDs:   []string{},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.Name, func(t *testing.T) {
			users, err := store.GetUsersList(tc.UserIDs, false, false)
			if tc.ExpectedError {
				require.Error(t, err)
				require.True(t, model.IsErrNotFound(err))
			} else {
				require.NoError(t, err)
			}

			userIDs := []string{}
			for _, user := range users {
				userIDs = append(userIDs, user.ID)
			}
			require.ElementsMatch(t, tc.ExpectedIDs, userIDs)
		})
	}
}

func testCreateAndUpdateUser(t *testing.T, store store.Store) {
	user := &model.User{
		ID: utils.NewID(utils.IDTypeUser),
	}
	newUser, err := store.CreateUser(user)
	require.NoError(t, err)
	require.NotNil(t, newUser)

	t.Run("UpdateUser", func(t *testing.T) {
		user.Username = "damao"
		user.Email = "mock@email.com"
		uUser, err := store.UpdateUser(user)
		require.NoError(t, err)
		require.NotNil(t, uUser)
		require.Equal(t, user.Username, uUser.Username)
		require.Equal(t, user.Email, uUser.Email)

		got, err := store.GetUserByID(user.ID)
		require.NoError(t, err)
		require.Equal(t, user.ID, got.ID)
		require.Equal(t, user.Username, got.Username)
		require.Equal(t, user.Email, got.Email)
	})

	t.Run("UpdateUserPassword", func(t *testing.T) {
		newPassword := utils.NewID(utils.IDTypeNone)
		err := store.UpdateUserPassword(user.Username, newPassword)
		require.NoError(t, err)

		got, err := store.GetUserByUsername(user.Username)
		require.NoError(t, err)
		require.Equal(t, user.Username, got.Username)
		require.Equal(t, newPassword, got.Password)
	})

	t.Run("UpdateUserPasswordByID", func(t *testing.T) {
		newPassword := utils.NewID(utils.IDTypeNone)
		err := store.UpdateUserPasswordByID(user.ID, newPassword)
		require.NoError(t, err)

		got, err := store.GetUserByID(user.ID)
		require.NoError(t, err)
		require.Equal(t, user.ID, got.ID)
		require.Equal(t, newPassword, got.Password)
	})
}

func testCreateAndGetRegisteredUserCount(t *testing.T, store store.Store) {
	randomN := int(time.Now().Unix() % 10)
	for i := 0; i < randomN; i++ {
		user, err := store.CreateUser(&model.User{
			ID: utils.NewID(utils.IDTypeUser),
		})
		require.NoError(t, err)
		require.NotNil(t, user)
	}

	got, err := store.GetRegisteredUserCount()
	require.NoError(t, err)
	require.Equal(t, randomN, got)
}

func testPatchUserProps(t *testing.T, store store.Store) {
	user := &model.User{
		ID: utils.NewID(utils.IDTypeUser),
	}
	newUser, err := store.CreateUser(user)
	require.NoError(t, err)
	require.NotNil(t, newUser)

	key1 := "new_key_1"
	key2 := "new_key_2"
	key3 := "new_key_3"

	// Only update props
	patch := model.UserPreferencesPatch{
		UpdatedFields: map[string]string{
			key1: "new_value_1",
			key2: "new_value_2",
			key3: "new_value_3",
		},
	}

	userPreferences, err := store.PatchUserPreferences(user.ID, patch)
	require.NoError(t, err)
	require.Equal(t, 3, len(userPreferences))

	for _, preference := range userPreferences {
		switch preference.Name {
		case key1:
			require.Equal(t, "new_value_1", preference.Value)
		case key2:
			require.Equal(t, "new_value_2", preference.Value)
		case key3:
			require.Equal(t, "new_value_3", preference.Value)
		}
	}

	// Delete a prop
	patch = model.UserPreferencesPatch{
		DeletedFields: []string{
			key1,
		},
	}

	userPreferences, err = store.PatchUserPreferences(user.ID, patch)
	require.NoError(t, err)

	for _, preference := range userPreferences {
		switch preference.Name {
		case key1:
			t.Errorf("new_key_1 shouldn't exist in user preference as we just deleted it")
		case key2:
			require.Equal(t, "new_value_2", preference.Value)
		case key3:
			require.Equal(t, "new_value_3", preference.Value)
		}
	}

	// update and delete together
	patch = model.UserPreferencesPatch{
		UpdatedFields: map[string]string{
			key3: "new_value_3_new_again",
		},
		DeletedFields: []string{
			key2,
		},
	}
	userPreferences, err = store.PatchUserPreferences(user.ID, patch)
	require.NoError(t, err)

	for _, preference := range userPreferences {
		switch preference.Name {
		case key1:
			t.Errorf("new_key_1 shouldn't exist in user preference as we just deleted it")
		case key2:
			t.Errorf("new_key_2 shouldn't exist in user preference as we just deleted it")
		case key3:
			require.Equal(t, "new_value_3_new_again", preference.Value)
		}
	}
}
