// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package user

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"forgejo.org/models"
	asymkey_model "forgejo.org/models/asymkey"
	"forgejo.org/models/auth"
	"forgejo.org/models/db"
	"forgejo.org/models/moderation"
	"forgejo.org/models/organization"
	repo_model "forgejo.org/models/repo"
	"forgejo.org/models/unittest"
	user_model "forgejo.org/models/user"
	"forgejo.org/modules/json"
	"forgejo.org/modules/optional"
	"forgejo.org/modules/setting"
	"forgejo.org/modules/test"
	"forgejo.org/modules/timeutil"
	"forgejo.org/services/auth/source/oauth2"
	redirect_service "forgejo.org/services/redirect"

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

func TestMain(m *testing.M) {
	unittest.MainTest(m)
}

func TestDeleteUser(t *testing.T) {
	test := func(userID int64) {
		require.NoError(t, unittest.PrepareTestDatabase())
		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID})

		ownedRepos := make([]*repo_model.Repository, 0, 10)
		require.NoError(t, db.GetEngine(db.DefaultContext).Find(&ownedRepos, &repo_model.Repository{OwnerID: userID}))
		if len(ownedRepos) > 0 {
			err := DeleteUser(db.DefaultContext, user, false)
			require.Error(t, err)
			assert.True(t, models.IsErrUserOwnRepos(err))
			return
		}

		orgUsers := make([]*organization.OrgUser, 0, 10)
		require.NoError(t, db.GetEngine(db.DefaultContext).Find(&orgUsers, &organization.OrgUser{UID: userID}))
		for _, orgUser := range orgUsers {
			if err := models.RemoveOrgUser(db.DefaultContext, orgUser.OrgID, orgUser.UID); err != nil {
				assert.True(t, organization.IsErrLastOrgOwner(err))
				return
			}
		}
		require.NoError(t, DeleteUser(db.DefaultContext, user, false))
		unittest.AssertNotExistsBean(t, &user_model.User{ID: userID})
		unittest.CheckConsistencyFor(t, &user_model.User{}, &repo_model.Repository{})
	}
	test(2)
	test(4)
	test(8)
	test(11)

	org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
	require.Error(t, DeleteUser(db.DefaultContext, org, false))
}

func TestPurgeUser(t *testing.T) {
	defer unittest.OverrideFixtures("services/user/TestPurgeUser")()
	require.NoError(t, unittest.PrepareTestDatabase())
	defer test.MockVariableValue(&setting.SSH.RootPath, t.TempDir())()
	defer test.MockVariableValue(&setting.SSH.CreateAuthorizedKeysFile, true)()
	defer test.MockVariableValue(&setting.SSH.CreateAuthorizedPrincipalsFile, true)()
	defer test.MockVariableValue(&setting.SSH.StartBuiltinServer, false)()
	require.NoError(t, asymkey_model.RewriteAllPublicKeys(db.DefaultContext))
	require.NoError(t, asymkey_model.RewriteAllPrincipalKeys(db.DefaultContext))

	test := func(userID int64, modifySSHKey bool) {
		require.NoError(t, unittest.PrepareTestDatabase())
		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID})

		fAuthorizedKeys, err := os.Open(filepath.Join(setting.SSH.RootPath, "authorized_keys"))
		require.NoError(t, err)
		authorizedKeysStatBefore, err := fAuthorizedKeys.Stat()
		require.NoError(t, err)
		fAuthorizedPrincipals, err := os.Open(filepath.Join(setting.SSH.RootPath, "authorized_principals"))
		require.NoError(t, err)
		authorizedPrincipalsBefore, err := fAuthorizedPrincipals.Stat()
		require.NoError(t, err)

		require.NoError(t, DeleteUser(db.DefaultContext, user, true))

		unittest.AssertNotExistsBean(t, &user_model.User{ID: userID})
		unittest.CheckConsistencyFor(t, &user_model.User{}, &repo_model.Repository{})

		fAuthorizedKeys, err = os.Open(filepath.Join(setting.SSH.RootPath, "authorized_keys"))
		require.NoError(t, err)
		fAuthorizedPrincipals, err = os.Open(filepath.Join(setting.SSH.RootPath, "authorized_principals"))
		require.NoError(t, err)

		authorizedKeysStatAfter, err := fAuthorizedKeys.Stat()
		require.NoError(t, err)
		authorizedPrincipalsAfter, err := fAuthorizedPrincipals.Stat()
		require.NoError(t, err)

		if modifySSHKey {
			assert.Greater(t, authorizedKeysStatAfter.ModTime(), authorizedKeysStatBefore.ModTime())
			assert.Greater(t, authorizedPrincipalsAfter.ModTime(), authorizedPrincipalsBefore.ModTime())
		} else {
			assert.Equal(t, authorizedKeysStatAfter.ModTime(), authorizedKeysStatBefore.ModTime())
			assert.Equal(t, authorizedPrincipalsAfter.ModTime(), authorizedPrincipalsBefore.ModTime())
		}
	}
	test(2, true)
	test(4, false)
	test(8, false)
	test(11, false)

	org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
	require.Error(t, DeleteUser(db.DefaultContext, org, false))
}

func TestCreateUser(t *testing.T) {
	user := &user_model.User{
		Name:               "GiteaBot",
		Email:              "GiteaBot@gitea.io",
		Passwd:             ";p['////..-++']",
		IsAdmin:            false,
		Theme:              setting.UI.DefaultTheme,
		MustChangePassword: false,
	}

	require.NoError(t, user_model.CreateUser(db.DefaultContext, user))

	require.NoError(t, DeleteUser(db.DefaultContext, user, false))
}

func TestRenameUser(t *testing.T) {
	defer unittest.OverrideFixtures("models/user/fixtures/")()
	require.NoError(t, unittest.PrepareTestDatabase())
	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 21})

	t.Run("Same username", func(t *testing.T) {
		require.NoError(t, RenameUser(db.DefaultContext, user, user.Name))
	})

	t.Run("Non usable username", func(t *testing.T) {
		usernames := []string{"--diff", ".well-known", "gitea-actions", "aaa.atom", "aa.png"}
		for _, username := range usernames {
			require.Error(t, user_model.IsUsableUsername(username), "non-usable username: %s", username)
			require.Error(t, RenameUser(db.DefaultContext, user, username), "non-usable username: %s", username)
		}
	})

	t.Run("Only capitalization", func(t *testing.T) {
		caps := strings.ToUpper(user.Name)
		unittest.AssertNotExistsBean(t, &user_model.User{ID: user.ID, Name: caps})
		unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: user.ID, OwnerName: user.Name})

		require.NoError(t, RenameUser(db.DefaultContext, user, caps))

		unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: user.ID, Name: caps})
		unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: user.ID, OwnerName: caps})
	})

	t.Run("Already exists", func(t *testing.T) {
		existUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})

		require.ErrorIs(t, RenameUser(db.DefaultContext, user, existUser.Name), user_model.ErrUserAlreadyExist{Name: existUser.Name})
		require.ErrorIs(t, RenameUser(db.DefaultContext, user, existUser.LowerName), user_model.ErrUserAlreadyExist{Name: existUser.LowerName})
		newUsername := fmt.Sprintf("uSEr%d", existUser.ID)
		require.ErrorIs(t, RenameUser(db.DefaultContext, user, newUsername), user_model.ErrUserAlreadyExist{Name: newUsername})
	})

	t.Run("Normal", func(t *testing.T) {
		oldUsername := user.Name
		newUsername := "User_Rename"

		require.NoError(t, RenameUser(db.DefaultContext, user, newUsername))
		unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: user.ID, Name: newUsername, LowerName: strings.ToLower(newUsername)})

		redirectUID, err := redirect_service.LookupUserRedirect(db.DefaultContext, user, oldUsername)
		require.NoError(t, err)
		assert.Equal(t, user.ID, redirectUID)

		unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: user.ID, OwnerName: user.Name})
	})

	t.Run("Keep N redirects", func(t *testing.T) {
		defer test.MockProtect(&setting.Service.MaxUserRedirects)()
		// Start clean
		unittest.AssertSuccessfulDelete(t, &user_model.Redirect{RedirectUserID: user.ID})

		setting.Service.MaxUserRedirects = 1

		require.NoError(t, RenameUser(db.DefaultContext, user, "redirect-1"))
		unittest.AssertExistsIf(t, true, &user_model.Redirect{LowerName: "user_rename"})

		// The granularity of created_unix is a second.
		test.SleepTillNextSecond()
		require.NoError(t, RenameUser(db.DefaultContext, user, "redirect-2"))
		unittest.AssertExistsIf(t, false, &user_model.Redirect{LowerName: "user_rename"})
		unittest.AssertExistsIf(t, true, &user_model.Redirect{LowerName: "redirect-1"})

		setting.Service.MaxUserRedirects = 2
		test.SleepTillNextSecond()
		require.NoError(t, RenameUser(db.DefaultContext, user, "redirect-3"))
		unittest.AssertExistsIf(t, true, &user_model.Redirect{LowerName: "redirect-1"})
		unittest.AssertExistsIf(t, true, &user_model.Redirect{LowerName: "redirect-2"})
	})

	t.Run("Non-local", func(t *testing.T) {
		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1041, LoginSource: 1001})
		authSource := unittest.AssertExistsAndLoadBean(t, &auth.Source{ID: user.LoginSource})
		assert.False(t, user.IsLocal())
		assert.True(t, user.IsOAuth2())

		t.Run("Allowed", func(t *testing.T) {
			require.NoError(t, RenameUser(t.Context(), user, "I-am-a-local-username"))
		})

		t.Run("Not allowed", func(t *testing.T) {
			authSourceCfg := authSource.Cfg.(*oauth2.Source)
			authSourceCfg.AllowUsernameChange = false
			authSource.Cfg = authSourceCfg
			_, err := db.GetEngine(t.Context()).Cols("cfg").ID(authSource.ID).Update(authSource)
			require.NoError(t, err)

			require.ErrorIs(t, RenameUser(t.Context(), user, "Another-username-change"), user_model.ErrUserIsNotLocal{UID: user.ID, Name: user.Name})
			t.Run("Admin", func(t *testing.T) {
				require.NoError(t, AdminRenameUser(t.Context(), user, "Another-username-change"))
			})
		})
	})
}

func TestCreateUser_Issue5882(t *testing.T) {
	// Init settings
	_ = setting.Admin

	passwd := ".//.;1;;//.,-=_"

	tt := []struct {
		user               *user_model.User
		disableOrgCreation bool
	}{
		{&user_model.User{Name: "GiteaBot", Email: "GiteaBot@gitea.io", Passwd: passwd, MustChangePassword: false}, false},
		{&user_model.User{Name: "GiteaBot2", Email: "GiteaBot2@gitea.io", Passwd: passwd, MustChangePassword: false}, true},
	}

	setting.Service.DefaultAllowCreateOrganization = true

	for _, v := range tt {
		setting.Admin.DisableRegularOrgCreation = v.disableOrgCreation

		require.NoError(t, user_model.CreateUser(db.DefaultContext, v.user))

		u, err := user_model.GetUserByEmail(db.DefaultContext, v.user.Email)
		require.NoError(t, err)

		assert.Equal(t, !u.AllowCreateOrganization, v.disableOrgCreation)

		require.NoError(t, DeleteUser(db.DefaultContext, v.user, false))
	}
}

func TestDeleteInactiveUsers(t *testing.T) {
	require.NoError(t, unittest.PrepareTestDatabase())
	// Add an inactive user older than a minute, with an associated email_address record.
	oldUser := &user_model.User{Name: "OldInactive", LowerName: "oldinactive", Email: "old@example.com", CreatedUnix: timeutil.TimeStampNow().Add(-120)}
	_, err := db.GetEngine(db.DefaultContext).NoAutoTime().Insert(oldUser)
	require.NoError(t, err)
	oldEmail := &user_model.EmailAddress{UID: oldUser.ID, IsPrimary: true, Email: "old@example.com", LowerEmail: "old@example.com"}
	err = db.Insert(db.DefaultContext, oldEmail)
	require.NoError(t, err)

	// Add an inactive user that's not older than a minute, with an associated email_address record.
	newUser := &user_model.User{Name: "NewInactive", LowerName: "newinactive", Email: "new@example.com"}
	err = db.Insert(db.DefaultContext, newUser)
	require.NoError(t, err)
	newEmail := &user_model.EmailAddress{UID: newUser.ID, IsPrimary: true, Email: "new@example.com", LowerEmail: "new@example.com"}
	err = db.Insert(db.DefaultContext, newEmail)
	require.NoError(t, err)

	err = DeleteInactiveUsers(db.DefaultContext, time.Minute)
	require.NoError(t, err)

	// User older than a minute should be deleted along with their email address.
	unittest.AssertExistsIf(t, false, oldUser)
	unittest.AssertExistsIf(t, false, oldEmail)

	// User not older than a minute shouldn't be deleted and their emaill address should still exist.
	unittest.AssertExistsIf(t, true, newUser)
	unittest.AssertExistsIf(t, true, newEmail)
}

func TestCreateShadowCopyOnUserUpdate(t *testing.T) {
	defer unittest.OverrideFixtures("models/fixtures/ModerationFeatures")()
	require.NoError(t, unittest.PrepareTestDatabase())

	userAlexSmithID := int64(1002)
	abuseReportID := int64(2)     // submitted for @alexsmith
	newDummyValue := "[REDACTED]" // used for updating profile text fields

	// Retrieve the abusive user (@alexsmith) and the abuse report already created for this user.
	abuser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userAlexSmithID})
	report := unittest.AssertExistsAndLoadBean(t, &moderation.AbuseReport{
		ID:          abuseReportID,
		ContentType: moderation.ReportedContentTypeUser,
		ContentID:   abuser.ID,
	})
	// The report should not already have a shadow copy linked.
	assert.False(t, report.ShadowCopyID.Valid)

	// Keep a copy of old field values before updating them.
	oldUserData := user_model.UserData{
		FullName:    abuser.FullName,
		Location:    abuser.Location,
		Website:     abuser.Website,
		Pronouns:    abuser.Pronouns,
		Description: abuser.Description,
	}

	// The abusive user is updating their profile.
	opts := &UpdateOptions{
		FullName:    optional.Some(newDummyValue),
		Location:    optional.Some(newDummyValue),
		Website:     optional.Some(newDummyValue),
		Pronouns:    optional.Some(newDummyValue),
		Description: optional.Some(newDummyValue),
	}
	require.NoError(t, UpdateUser(t.Context(), abuser, opts))

	// Reload the report.
	report = unittest.AssertExistsAndLoadBean(t, &moderation.AbuseReport{ID: report.ID})
	// A shadow copy should have been created and linked to our report.
	assert.True(t, report.ShadowCopyID.Valid)
	// Retrieve the newly created shadow copy and unmarshal the stored JSON so that we can check the values.
	shadowCopy := unittest.AssertExistsAndLoadBean(t, &moderation.AbuseReportShadowCopy{ID: report.ShadowCopyID.Int64})
	shadowCopyUserData := new(user_model.UserData)
	require.NoError(t, json.Unmarshal([]byte(shadowCopy.RawValue), &shadowCopyUserData))
	// Check to see if the initial field values of the user were stored within the shadow copy.
	assert.Equal(t, oldUserData.FullName, shadowCopyUserData.FullName)
	assert.Equal(t, oldUserData.Location, shadowCopyUserData.Location)
	assert.Equal(t, oldUserData.Website, shadowCopyUserData.Website)
	assert.Equal(t, oldUserData.Pronouns, shadowCopyUserData.Pronouns)
	assert.Equal(t, oldUserData.Description, shadowCopyUserData.Description)
}
