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

package auth

import (
	"html/template"
	"net/http"
	"net/http/httptest"
	"net/url"
	"testing"

	auth_model "code.gitea.io/gitea/models/auth"
	"code.gitea.io/gitea/models/unittest"
	user_model "code.gitea.io/gitea/models/user"
	"code.gitea.io/gitea/modules/optional"
	"code.gitea.io/gitea/modules/session"
	"code.gitea.io/gitea/modules/setting"
	"code.gitea.io/gitea/modules/test"
	"code.gitea.io/gitea/modules/util"
	"code.gitea.io/gitea/modules/web"
	"code.gitea.io/gitea/services/auth/source/oauth2"
	"code.gitea.io/gitea/services/contexttest"
	"code.gitea.io/gitea/services/forms"

	"github.com/markbates/goth"
	"github.com/markbates/goth/gothic"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestActivateAccountRequestValidationCode(t *testing.T) {
	require.NoError(t, unittest.PrepareTestDatabase())
	defer test.MockVariableValue(&setting.Service.RegisterManualConfirm, true)()

	user := &user_model.User{
		Name:   "account-request-activate-user",
		Email:  "Account-Request-Activate@example.com",
		Passwd: "password",
	}
	require.NoError(t, user_model.CreateUser(t.Context(), user, &user_model.Meta{}))
	require.NoError(t, user_model.ResetAccountRequestToPendingEmailValidation(t.Context(), user))

	code := user_model.GenerateAccountRequestValidationCode(user)
	require.NoError(t, user_model.SetAccountRequestValidationCode(t.Context(), user.ID, code))
	require.NoError(t, user_model.DeleteUserSetting(t.Context(), user.ID, user_model.SettingsKeyAccountRequestStatus))
	user.Rands = "changed-rands-after-email"
	require.NoError(t, user_model.UpdateUserCols(t.Context(), user, "rands"))

	ctx, resp := contexttest.MockContext(t, "/user/activate?code="+code)
	Activate(ctx)

	assert.Equal(t, http.StatusOK, resp.Code)
	assert.EqualValues(t, "auth.account_request_validated_waiting_admin", ctx.Data["ActivationPromptMessage"])

	status, err := user_model.GetAccountRequestStatus(t.Context(), user)
	require.NoError(t, err)
	assert.Equal(t, user_model.AccountRequestStatusPendingAdminReview, status)
}

func TestSignUpPostExistingAccountRequestBeforeUsernameTaken(t *testing.T) {
	require.NoError(t, unittest.PrepareTestDatabase())
	defer test.MockVariableValue(&setting.Service.RegisterManualConfirm, true)()

	user := &user_model.User{
		Name:   "account-request-signup-user",
		Email:  "account-request-signup-user@example.com",
		Passwd: "password",
	}
	require.NoError(t, user_model.CreateUser(t.Context(), user, &user_model.Meta{}))
	require.NoError(t, user_model.ResetAccountRequestToPendingEmailValidation(t.Context(), user))
	require.NoError(t, user_model.RecordAccountRequestValidationMailAttempt(t.Context(), user.ID, true))
	require.NoError(t, user_model.DeleteUserSetting(t.Context(), user.ID, user_model.SettingsKeyAccountRequestStatus))

	form := &forms.RegisterForm{
		UserName: user.Name,
		Email:    user.Email,
		Password: "password",
		Retype:   "password",
	}
	ctx, resp := contexttest.MockContext(t, "POST /user/sign_up")
	web.SetForm(ctx.Data, form)
	ctx.Data["HasError"] = true
	ctx.Data["ErrorMsg"] = "The username is already taken."

	SignUpPost(ctx)

	assert.Equal(t, http.StatusOK, resp.Code)
	assert.Equal(t, user.Email, ctx.Data["PendingAccountRequestEmail"])
	assert.Equal(t, true, ctx.Data["ShowResendAccountRequestButton"])
	assert.Contains(t, string(ctx.Data["PendingAccountRequestMessage"].(template.HTML)), user.Email)
}

func TestSignUpPostRegisterEmailConfirmShowsResendPrompt(t *testing.T) {
	require.NoError(t, unittest.PrepareTestDatabase())
	defer test.MockVariableValue(&setting.Service.RegisterEmailConfirm, true)()
	defer test.MockVariableValue(&setting.Service.RegisterManualConfirm, false)()

	form := &forms.RegisterForm{
		UserName: "email-confirm-signup-user",
		Email:    "email-confirm-signup-user@example.com",
		Password: "abc123ABC!=$",
		Retype:   "abc123ABC!=$",
	}
	ctx, resp := contexttest.MockContext(t, "POST /user/sign_up")
	web.SetForm(ctx.Data, form)

	SignUpPost(ctx)

	assert.Equal(t, http.StatusOK, resp.Code)
	assert.Equal(t, form.Email, ctx.Data["PendingAccountRequestEmail"])
	assert.Equal(t, true, ctx.Data["ShowResendAccountRequestButton"])
	assert.Contains(t, string(ctx.Data["ActivationPromptMessage"].(template.HTML)), form.Email)

	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: form.UserName})
	assert.False(t, user.IsActive)
	status, err := user_model.GetActivationMailAttemptStatus(t.Context(), user)
	require.NoError(t, err)
	assert.Equal(t, user_model.AccountRequestValidationMailStatusSucceeded, status)
}

func TestSignUpPostExistingEmailConfirmationBeforeUsernameTaken(t *testing.T) {
	require.NoError(t, unittest.PrepareTestDatabase())
	defer test.MockVariableValue(&setting.Service.RegisterEmailConfirm, true)()
	defer test.MockVariableValue(&setting.Service.RegisterManualConfirm, false)()

	user := &user_model.User{
		Name:   "email-confirm-existing-user",
		Email:  "email-confirm-existing-user@example.com",
		Passwd: "password",
	}
	require.NoError(t, user_model.CreateUser(t.Context(), user, &user_model.Meta{}))
	require.NoError(t, user_model.RecordActivationMailAttempt(t.Context(), user.ID, true))
	require.NoError(t, user_model.SetActivationResendHits(t.Context(), user.ID, 0))

	form := &forms.RegisterForm{
		UserName: user.Name,
		Email:    user.Email,
		Password: "password",
		Retype:   "password",
	}
	ctx, resp := contexttest.MockContext(t, "POST /user/sign_up")
	web.SetForm(ctx.Data, form)

	SignUpPost(ctx)

	assert.Equal(t, http.StatusOK, resp.Code)
	assert.Equal(t, user.Email, ctx.Data["PendingAccountRequestEmail"])
	assert.Equal(t, true, ctx.Data["ShowResendAccountRequestButton"])
	assert.Contains(t, string(ctx.Data["PendingAccountRequestMessage"].(template.HTML)), user.Email)
}

func TestSignInPostPendingAccountRequestEmailValidation(t *testing.T) {
	require.NoError(t, unittest.PrepareTestDatabase())
	defer test.MockVariableValue(&setting.Service.EnablePasswordSignInForm, true)()
	defer test.MockVariableValue(&setting.Service.RegisterManualConfirm, true)()

	user := &user_model.User{
		Name:   "account-request-login-validation-user",
		Email:  "account-request-login-validation-user@example.com",
		Passwd: "password",
	}
	require.NoError(t, user_model.CreateUser(t.Context(), user, &user_model.Meta{}))
	require.False(t, user.IsActive)
	require.NoError(t, user_model.ResetAccountRequestToPendingEmailValidation(t.Context(), user))
	require.NoError(t, user_model.RecordAccountRequestValidationMailAttempt(t.Context(), user.ID, true))

	for _, login := range []string{user.Name, user.Email} {
		t.Run(login, func(t *testing.T) {
			sessionStore := session.NewMockMemStore("account-request-login-validation")
			ctx, resp := contexttest.MockContext(t, "POST /user/login", contexttest.MockContextOption{SessionStore: sessionStore})
			web.SetForm(ctx.Data, &forms.SignInForm{UserName: login, Password: "password"})

			SignInPost(ctx)

			assert.Equal(t, http.StatusOK, resp.Code)
			assert.Equal(t, false, ctx.Data["IsSigned"])
			assert.Nil(t, ctx.Session.Get(session.KeyUID))
			assert.Equal(t, user.Email, ctx.Data["PendingAccountRequestEmail"])
			assert.Equal(t, true, ctx.Data["ShowResendAccountRequestButton"])
			assert.Contains(t, string(ctx.Data["ActivationPromptMessage"].(template.HTML)), "auth.account_request_validation_pending_login")
		})
	}
}

func TestSignInPostPendingAccountRequestAdminReview(t *testing.T) {
	require.NoError(t, unittest.PrepareTestDatabase())
	defer test.MockVariableValue(&setting.Service.EnablePasswordSignInForm, true)()
	defer test.MockVariableValue(&setting.Service.RegisterManualConfirm, true)()

	user := &user_model.User{
		Name:   "account-request-login-review-user",
		Email:  "account-request-login-review-user@example.com",
		Passwd: "password",
	}
	require.NoError(t, user_model.CreateUser(t.Context(), user, &user_model.Meta{}))
	require.False(t, user.IsActive)
	require.NoError(t, user_model.ResetAccountRequestToPendingEmailValidation(t.Context(), user))
	require.NoError(t, user_model.MarkAccountRequestPendingAdminReview(t.Context(), user))

	sessionStore := session.NewMockMemStore("account-request-login-review")
	ctx, resp := contexttest.MockContext(t, "POST /user/login", contexttest.MockContextOption{SessionStore: sessionStore})
	web.SetForm(ctx.Data, &forms.SignInForm{UserName: user.Name, Password: "password"})

	SignInPost(ctx)

	assert.Equal(t, http.StatusOK, resp.Code)
	assert.Equal(t, false, ctx.Data["IsSigned"])
	assert.Nil(t, ctx.Session.Get(session.KeyUID))
	assert.Empty(t, ctx.Data["PendingAccountRequestEmail"])
	assert.EqualValues(t, "auth.account_request_validated_waiting_admin", ctx.Data["ActivationPromptMessage"])
}

func TestSignInPostPendingEmailConfirmation(t *testing.T) {
	require.NoError(t, unittest.PrepareTestDatabase())
	defer test.MockVariableValue(&setting.Service.EnablePasswordSignInForm, true)()
	defer test.MockVariableValue(&setting.Service.RegisterEmailConfirm, true)()
	defer test.MockVariableValue(&setting.Service.RegisterManualConfirm, false)()

	user := &user_model.User{
		Name:   "email-confirm-login-user",
		Email:  "email-confirm-login-user@example.com",
		Passwd: "password",
	}
	require.NoError(t, user_model.CreateUser(t.Context(), user, &user_model.Meta{}))
	require.False(t, user.IsActive)
	require.NoError(t, user_model.RecordActivationMailAttempt(t.Context(), user.ID, true))
	require.NoError(t, user_model.SetActivationResendHits(t.Context(), user.ID, 0))

	for _, login := range []string{user.Name, user.Email} {
		t.Run(login, func(t *testing.T) {
			sessionStore := session.NewMockMemStore("email-confirm-login")
			ctx, resp := contexttest.MockContext(t, "POST /user/login", contexttest.MockContextOption{SessionStore: sessionStore})
			web.SetForm(ctx.Data, &forms.SignInForm{UserName: login, Password: "password"})

			SignInPost(ctx)

			assert.Equal(t, http.StatusOK, resp.Code)
			assert.Equal(t, false, ctx.Data["IsSigned"])
			assert.Nil(t, ctx.Session.Get(session.KeyUID))
			assert.Equal(t, user.Email, ctx.Data["PendingAccountRequestEmail"])
			assert.Equal(t, true, ctx.Data["ShowResendAccountRequestButton"])
			assert.Contains(t, string(ctx.Data["ActivationPromptMessage"].(template.HTML)), user.Email)
		})
	}
}

func TestSignInPostPendingEmailConfirmationHidesResendAfterLimit(t *testing.T) {
	require.NoError(t, unittest.PrepareTestDatabase())
	defer test.MockVariableValue(&setting.Service.EnablePasswordSignInForm, true)()
	defer test.MockVariableValue(&setting.Service.RegisterEmailConfirm, true)()
	defer test.MockVariableValue(&setting.Service.RegisterManualConfirm, false)()

	user := &user_model.User{
		Name:   "email-confirm-login-limit-user",
		Email:  "email-confirm-login-limit-user@example.com",
		Passwd: "password",
	}
	require.NoError(t, user_model.CreateUser(t.Context(), user, &user_model.Meta{}))
	require.False(t, user.IsActive)
	require.NoError(t, user_model.RecordActivationMailAttempt(t.Context(), user.ID, true))
	require.NoError(t, user_model.SetActivationResendHits(t.Context(), user.ID, user_model.ActivationMaxResendHits))

	sessionStore := session.NewMockMemStore("email-confirm-login-limit")
	ctx, resp := contexttest.MockContext(t, "POST /user/login", contexttest.MockContextOption{SessionStore: sessionStore})
	web.SetForm(ctx.Data, &forms.SignInForm{UserName: user.Name, Password: "password"})

	SignInPost(ctx)

	assert.Equal(t, http.StatusOK, resp.Code)
	assert.Equal(t, false, ctx.Data["ShowResendAccountRequestButton"])
	assert.Equal(t, user.Email, ctx.Data["PendingAccountRequestEmail"])
}

func TestResendAccountRequestValidationPostClassicActivation(t *testing.T) {
	require.NoError(t, unittest.PrepareTestDatabase())
	defer test.MockVariableValue(&setting.Service.RegisterEmailConfirm, true)()
	defer test.MockVariableValue(&setting.Service.RegisterManualConfirm, false)()

	user := &user_model.User{
		Name:   "email-confirm-resend-user",
		Email:  "email-confirm-resend-user@example.com",
		Passwd: "password",
	}
	require.NoError(t, user_model.CreateUser(t.Context(), user, &user_model.Meta{}))
	require.NoError(t, user_model.RecordActivationMailAttempt(t.Context(), user.ID, true))
	require.NoError(t, user_model.SetActivationResendHits(t.Context(), user.ID, 0))

	ctx, resp := contexttest.MockContext(t, "POST /user/sign_up/resend")
	ctx.SetFormString("email", user.Email)
	ctx.SetFormString("source", "signup")
	ctx.SetFormString("user_name", user.Name)

	ResendAccountRequestValidationPost(ctx)

	assert.Equal(t, http.StatusOK, resp.Code)
	assert.Equal(t, "success", ctx.Data["AccountRequestResendButtonState"])
	assert.Equal(t, user.Email, ctx.Data["PendingAccountRequestEmail"])

	hits, err := user_model.GetActivationResendHits(t.Context(), user)
	require.NoError(t, err)
	assert.Equal(t, 1, hits)
}

func TestSignInPostPendingAdminInviteRedirectsToInvitationPrompt(t *testing.T) {
	require.NoError(t, unittest.PrepareTestDatabase())
	defer test.MockVariableValue(&setting.Service.EnablePasswordSignInForm, true)()

	user := &user_model.User{
		Name:   "pending-admin-invite-user",
		Email:  "pending-admin-invite-user@example.com",
		Passwd: "password",
	}
	require.NoError(t, user_model.CreateUser(t.Context(), user, &user_model.Meta{}, &user_model.CreateUserOverwriteOptions{IsActive: optional.Some(false)}))
	require.NoError(t, user_model.SetAdminInvitePending(t.Context(), user.ID, true))
	require.NoError(t, user_model.SetUserSetting(t.Context(), user.ID, user_model.SettingsKeyAdminInviteCreatedByName, "admin-user"))

	for _, login := range []string{user.Name, user.Email} {
		t.Run(login, func(t *testing.T) {
			sessionStore := session.NewMockMemStore("pending-admin-invite")
			ctx, resp := contexttest.MockContext(t, "POST /user/login", contexttest.MockContextOption{SessionStore: sessionStore})
			web.SetForm(ctx.Data, &forms.SignInForm{UserName: login, Password: "password"})

			SignInPost(ctx)

			assert.Equal(t, http.StatusSeeOther, resp.Code)
			assert.Equal(t, "/user/invitation?email="+url.QueryEscape(user.Email), test.RedirectURL(resp))
			assert.Nil(t, ctx.Session.Get(session.KeyUID))
		})
	}
}

func TestSignUpPostPendingAdminInviteRedirectsToInvitationPrompt(t *testing.T) {
	require.NoError(t, unittest.PrepareTestDatabase())
	defer test.MockVariableValue(&setting.Service.DisableRegistration, false)()
	defer test.MockVariableValue(&setting.Service.AllowOnlyExternalRegistration, false)()

	user := &user_model.User{
		Name:   "signup-pending-admin-invite-user",
		Email:  "signup-pending-admin-invite-user@example.com",
		Passwd: "password",
	}
	require.NoError(t, user_model.CreateUser(t.Context(), user, &user_model.Meta{}, &user_model.CreateUserOverwriteOptions{IsActive: optional.Some(false)}))
	require.NoError(t, user_model.SetAdminInvitePending(t.Context(), user.ID, true))
	require.NoError(t, user_model.SetUserSetting(t.Context(), user.ID, user_model.SettingsKeyAdminInviteCreatedBy, "1"))

	form := &forms.RegisterForm{
		UserName: user.Name,
		Email:    user.Email,
		Password: "password",
		Retype:   "password",
	}
	ctx, resp := contexttest.MockContext(t, "POST /user/sign_up")
	web.SetForm(ctx.Data, form)

	SignUpPost(ctx)

	assert.Equal(t, http.StatusSeeOther, resp.Code)
	assert.Equal(t, "/user/invitation?email="+url.QueryEscape(user.Email), test.RedirectURL(resp))
}

func TestSignUpPostExistingAdminCreatedNotifyAccountSignsInNormally(t *testing.T) {
	require.NoError(t, unittest.PrepareTestDatabase())
	defer test.MockVariableValue(&setting.Service.DisableRegistration, false)()
	defer test.MockVariableValue(&setting.Service.AllowOnlyExternalRegistration, false)()

	user := &user_model.User{
		Name:               "signup-admin-created-notify-normal-user",
		Email:              "signup-admin-created-notify-normal-user@example.com",
		Passwd:             "password",
		MustChangePassword: false,
	}
	require.NoError(t, user_model.CreateUser(t.Context(), user, &user_model.Meta{}))
	require.NoError(t, user_model.SetUserSetting(t.Context(), user.ID, user_model.SettingsKeyAdminInviteCreatedBy, "1"))

	form := &forms.RegisterForm{
		UserName: user.Name,
		Email:    user.Email,
		Password: "password",
		Retype:   "password",
	}
	sessionStore := session.NewMockMemStore("signup-admin-created-notify-normal")
	ctx, resp := contexttest.MockContext(t, "POST /user/sign_up", contexttest.MockContextOption{SessionStore: sessionStore})
	web.SetForm(ctx.Data, form)

	SignUpPost(ctx)

	assert.Equal(t, http.StatusSeeOther, resp.Code)
	assert.Equal(t, "/", test.RedirectURL(resp))
	assert.Equal(t, user.ID, sessionStore.Get(session.KeyUID))
}

func TestSignUpPostExistingAdminCreatedNotifyAccountRedirectsToChangePassword(t *testing.T) {
	require.NoError(t, unittest.PrepareTestDatabase())
	defer test.MockVariableValue(&setting.Service.DisableRegistration, false)()
	defer test.MockVariableValue(&setting.Service.AllowOnlyExternalRegistration, false)()

	user := &user_model.User{
		Name:               "signup-admin-created-notify-change-password-user",
		Email:              "signup-admin-created-notify-change-password-user@example.com",
		Passwd:             "password",
		MustChangePassword: true,
	}
	require.NoError(t, user_model.CreateUser(t.Context(), user, &user_model.Meta{}))
	require.NoError(t, user_model.SetUserSetting(t.Context(), user.ID, user_model.SettingsKeyAdminInviteCreatedBy, "1"))

	form := &forms.RegisterForm{
		UserName: user.Name,
		Email:    user.Email,
		Password: "password",
		Retype:   "password",
	}
	sessionStore := session.NewMockMemStore("signup-admin-created-notify-change-password")
	ctx, resp := contexttest.MockContext(t, "POST /user/sign_up", contexttest.MockContextOption{SessionStore: sessionStore})
	web.SetForm(ctx.Data, form)

	SignUpPost(ctx)

	assert.Equal(t, http.StatusSeeOther, resp.Code)
	assert.Equal(t, "/user/settings/change_password", test.RedirectURL(resp))
	assert.Equal(t, user.ID, sessionStore.Get(session.KeyUID))
}

func TestSignUpPostExistingAdminCreatedNotifyAccountWrongPasswordRedirectsToRecover(t *testing.T) {
	require.NoError(t, unittest.PrepareTestDatabase())
	defer test.MockVariableValue(&setting.Service.DisableRegistration, false)()
	defer test.MockVariableValue(&setting.Service.AllowOnlyExternalRegistration, false)()

	user := &user_model.User{
		Name:               "signup-admin-created-notify-wrong-password-user",
		Email:              "signup-admin-created-notify-wrong-password-user@example.com",
		Passwd:             "password",
		MustChangePassword: true,
	}
	require.NoError(t, user_model.CreateUser(t.Context(), user, &user_model.Meta{}))
	require.NoError(t, user_model.SetUserSetting(t.Context(), user.ID, user_model.SettingsKeyAdminInviteCreatedBy, "1"))

	form := &forms.RegisterForm{
		UserName: user.Name,
		Email:    user.Email,
		Password: "wrong-password",
		Retype:   "wrong-password",
	}
	ctx, resp := contexttest.MockContext(t, "POST /user/sign_up")
	web.SetForm(ctx.Data, form)

	SignUpPost(ctx)

	assert.Equal(t, http.StatusSeeOther, resp.Code)
	assert.Equal(t, "/user/forgot_password?email="+url.QueryEscape(user.Email), test.RedirectURL(resp))
}

func TestSignInPostLegacyPendingAdminInviteRedirectsToInvitationPrompt(t *testing.T) {
	require.NoError(t, unittest.PrepareTestDatabase())
	defer test.MockVariableValue(&setting.Service.EnablePasswordSignInForm, true)()

	user := &user_model.User{
		Name:          "legacy-admin-invite-user",
		Email:         "legacy-admin-invite-user@example.com",
		Passwd:        "password",
		ProhibitLogin: true,
	}
	require.NoError(t, user_model.CreateUser(t.Context(), user, &user_model.Meta{}, &user_model.CreateUserOverwriteOptions{IsActive: optional.Some(false)}))
	require.NoError(t, user_model.UpdateUserCols(t.Context(), user, "prohibit_login"))
	require.NoError(t, user_model.SetUserSetting(t.Context(), user.ID, user_model.SettingsKeyAdminInviteCreatedBy, "1"))

	sessionStore := session.NewMockMemStore("legacy-admin-invite")
	ctx, resp := contexttest.MockContext(t, "POST /user/login", contexttest.MockContextOption{SessionStore: sessionStore})
	web.SetForm(ctx.Data, &forms.SignInForm{UserName: user.Name, Password: "password"})

	SignInPost(ctx)

	assert.Equal(t, http.StatusSeeOther, resp.Code)
	assert.Equal(t, "/user/invitation?email="+url.QueryEscape(user.Email), test.RedirectURL(resp))
	assert.Nil(t, ctx.Session.Get(session.KeyUID))
}

func TestInvitationRendersPendingInvitePrompt(t *testing.T) {
	require.NoError(t, unittest.PrepareTestDatabase())

	user := &user_model.User{
		Name:   "invitation-prompt-user",
		Email:  "invitation-prompt-user@example.com",
		Passwd: "password",
	}
	require.NoError(t, user_model.CreateUser(t.Context(), user, &user_model.Meta{}, &user_model.CreateUserOverwriteOptions{IsActive: optional.Some(false)}))
	require.NoError(t, user_model.SetAdminInvitePending(t.Context(), user.ID, true))
	require.NoError(t, user_model.RecordAdminInviteMailAttempt(t.Context(), user.ID, true))
	require.NoError(t, user_model.SetAdminInviteResendHits(t.Context(), user.ID, 0))

	ctx, resp := contexttest.MockContext(t, "/user/invitation?email="+url.QueryEscape(user.Email))

	Invitation(ctx)

	assert.Equal(t, http.StatusOK, resp.Code)
	assert.Equal(t, user.Email, ctx.Data["PendingAccountRequestEmail"])
	assert.Equal(t, true, ctx.Data["ShowResendAccountRequestButton"])
	assert.Contains(t, string(ctx.Data["ActivationPromptMessage"].(template.HTML)), user.Email)
}

func TestResendAdminInvitePost(t *testing.T) {
	require.NoError(t, unittest.PrepareTestDatabase())

	user := &user_model.User{
		Name:   "admin-invite-resend-user",
		Email:  "admin-invite-resend-user@example.com",
		Passwd: "password",
	}
	require.NoError(t, user_model.CreateUser(t.Context(), user, &user_model.Meta{}, &user_model.CreateUserOverwriteOptions{IsActive: optional.Some(false)}))
	require.NoError(t, user_model.SetAdminInvitePending(t.Context(), user.ID, true))
	require.NoError(t, user_model.SetAdminInviteResendHits(t.Context(), user.ID, 0))
	require.NoError(t, user_model.SetUserSetting(t.Context(), user.ID, user_model.SettingsKeyAdminInviteCreatedByName, "admin-user"))

	ctx, resp := contexttest.MockContext(t, "POST /user/invitation/resend")
	ctx.SetFormString("email", user.Email)

	ResendAdminInvitePost(ctx)

	assert.Equal(t, http.StatusOK, resp.Code)
	assert.Equal(t, "success", ctx.Data["AccountRequestResendButtonState"])
	assert.Equal(t, user.Email, ctx.Data["PendingAccountRequestEmail"])

	hits, err := user_model.GetAdminInviteResendHits(t.Context(), user)
	require.NoError(t, err)
	assert.Equal(t, 1, hits)
}

func TestResetPasswdPostRequiresMatchingPasswordConfirmation(t *testing.T) {
	require.NoError(t, unittest.PrepareTestDatabase())
	defer test.MockVariableValue(&setting.Service.ResetPwdCodeLives, 180)()

	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
	resetPasswordCodeOptions := &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeResetPassword}
	code := user_model.GenerateUserTimeLimitCode(resetPasswordCodeOptions, user)
	require.NotNil(t, user_model.VerifyUserTimeLimitCode(t.Context(), resetPasswordCodeOptions, code))

	ctx, resp := contexttest.MockContext(t, "POST /user/recover_account")
	ctx.SetFormString("code", code)
	ctx.SetFormString("password", "new-password")
	ctx.SetFormString("retype", "different-password")

	ResetPasswdPost(ctx)

	assert.Equal(t, http.StatusOK, resp.Code)
	assert.Equal(t, true, ctx.Data["IsResetForm"])
	assert.Equal(t, true, ctx.Data["Err_Password"])
	assert.Equal(t, true, ctx.Data["Err_Retype"])
	assert.Equal(t, "new-password", ctx.Data["password"])
	assert.Equal(t, "different-password", ctx.Data["retype"])

	updatedUser, err := user_model.GetUserByID(t.Context(), user.ID)
	require.NoError(t, err)
	assert.Equal(t, user.Passwd, updatedUser.Passwd)
}

func TestActivateAdminInviteClearsProhibitLogin(t *testing.T) {
	require.NoError(t, unittest.PrepareTestDatabase())

	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
	user.IsActive = false
	user.ProhibitLogin = true
	require.NoError(t, user_model.UpdateUserCols(t.Context(), user, "is_active", "prohibit_login"))
	require.NoError(t, user_model.SetAdminInvitePending(t.Context(), user.ID, true))
	require.NoError(t, user_model.SetUserSetting(t.Context(), user.ID, user_model.SettingsKeyAdminInviteCreatedBy, "1"))
	code := user_model.GenerateUserTimeLimitCode(&user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeAdminInvite}, user)

	sessionStore := session.NewMockMemStore("admin-invite-activation")
	ctx, resp := contexttest.MockContext(t, "/user/activate?code="+code, contexttest.MockContextOption{SessionStore: sessionStore})

	Activate(ctx)

	assert.Equal(t, http.StatusSeeOther, resp.Code)
	updatedUser, err := user_model.GetUserByID(t.Context(), user.ID)
	require.NoError(t, err)
	assert.True(t, updatedUser.IsActive)
	assert.False(t, updatedUser.ProhibitLogin)
	isPendingAdminInvite, err := user_model.IsPendingAdminInvite(t.Context(), updatedUser)
	require.NoError(t, err)
	assert.False(t, isPendingAdminInvite)
	assert.Equal(t, user.ID, sessionStore.Get(session.KeyUID))
	createdBy, err := user_model.GetUserSetting(t.Context(), user.ID, user_model.SettingsKeyAdminInviteCreatedBy)
	require.NoError(t, err)
	assert.Equal(t, "1", createdBy)
}

func TestActivateAdminAccessSignsInAndInvalidatesCode(t *testing.T) {
	require.NoError(t, unittest.PrepareTestDatabase())

	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
	user.IsActive = true
	user.ProhibitLogin = false
	user.MustChangePassword = true
	oldRands := user.Rands
	require.NoError(t, user_model.UpdateUserCols(t.Context(), user, "is_active", "prohibit_login", "must_change_password"))

	codeOptions := &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeAdminAccess}
	code := user_model.GenerateUserTimeLimitCode(codeOptions, user)
	require.NotNil(t, user_model.VerifyUserTimeLimitCode(t.Context(), codeOptions, code))

	sessionStore := session.NewMockMemStore("admin-access-activation")
	ctx, resp := contexttest.MockContext(t, "/user/activate?code="+code, contexttest.MockContextOption{SessionStore: sessionStore})

	Activate(ctx)

	assert.Equal(t, http.StatusSeeOther, resp.Code)
	assert.Equal(t, user.ID, sessionStore.Get(session.KeyUID))

	updatedUser, err := user_model.GetUserByID(t.Context(), user.ID)
	require.NoError(t, err)
	assert.NotEqual(t, oldRands, updatedUser.Rands)
	assert.Nil(t, user_model.VerifyUserTimeLimitCode(t.Context(), codeOptions, code))
}

func addOAuth2Source(t *testing.T, authName string, cfg oauth2.Source) {
	cfg.Provider = util.IfZero(cfg.Provider, "gitea")
	err := auth_model.CreateSource(t.Context(), &auth_model.Source{
		Type:     auth_model.OAuth2,
		Name:     authName,
		IsActive: true,
		Cfg:      &cfg,
	})
	require.NoError(t, err)
}

func TestWebAuthUserLogin(t *testing.T) {
	ctx, resp := contexttest.MockContext(t, "/user/login")
	SignIn(ctx)
	assert.Equal(t, http.StatusOK, resp.Code)

	ctx, resp = contexttest.MockContext(t, "/user/login")
	ctx.IsSigned = true
	SignIn(ctx)
	assert.Equal(t, http.StatusSeeOther, resp.Code)
	assert.Equal(t, "/", test.RedirectURL(resp))

	ctx, resp = contexttest.MockContext(t, "/user/login?redirect_to=/other")
	ctx.IsSigned = true
	SignIn(ctx)
	assert.Equal(t, "/other", test.RedirectURL(resp))

	ctx, resp = contexttest.MockContext(t, "/user/login")
	ctx.Req.AddCookie(&http.Cookie{Name: "redirect_to", Value: "/other-cookie"})
	ctx.IsSigned = true
	SignIn(ctx)
	assert.Equal(t, "/other-cookie", test.RedirectURL(resp))

	ctx, resp = contexttest.MockContext(t, "/user/login?redirect_to="+url.QueryEscape("https://example.com"))
	ctx.IsSigned = true
	SignIn(ctx)
	assert.Equal(t, "/", test.RedirectURL(resp))
}

func TestWebAuthOAuth2(t *testing.T) {
	defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)()

	_ = oauth2.Init(t.Context())
	addOAuth2Source(t, "dummy+auth's source", oauth2.Source{})

	t.Run("OAuth2MissingField", func(t *testing.T) {
		defer test.MockVariableValue(&gothic.CompleteUserAuth, func(res http.ResponseWriter, req *http.Request) (goth.User, error) {
			return goth.User{Provider: "dummy+auth's source", UserID: "dummy-user"}, nil
		})()
		mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockMemStore("dummy-sid")}
		ctx, resp := contexttest.MockContext(t, "/user/oauth2/..../callback?code=dummy-code", mockOpt)
		ctx.SetPathParamRaw("provider", "dummy+auth%27s%20source")
		SignInOAuthCallback(ctx)
		assert.Equal(t, http.StatusSeeOther, resp.Code)
		assert.Equal(t, "/user/link_account", test.RedirectURL(resp))

		// then the user will be redirected to the link account page, and see a message about the missing fields
		ctx, _ = contexttest.MockContext(t, "/user/link_account", mockOpt)
		LinkAccount(ctx)
		assert.Equal(t, template.HTML("auth.oauth_callback_unable_auto_reg:dummy+auth&#39;s source,email"), ctx.Data["AutoRegistrationFailedPrompt"])
	})

	t.Run("OAuth2CallbackError", func(t *testing.T) {
		mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockMemStore("dummy-sid")}
		ctx, resp := contexttest.MockContext(t, "/user/oauth2/...../callback", mockOpt)
		ctx.SetPathParamRaw("provider", "dummy+auth%27s%20source")
		SignInOAuthCallback(ctx)
		assert.Equal(t, http.StatusSeeOther, resp.Code)
		assert.Equal(t, "/user/login", test.RedirectURL(resp))
		assert.Contains(t, ctx.Flash.ErrorMsg, "auth.oauth.signin.error.general")
	})

	t.Run("RedirectSingleProvider", func(t *testing.T) {
		enablePassword := &setting.Service.EnablePasswordSignInForm
		enableOpenID := &setting.Service.EnableOpenIDSignIn
		enablePasskey := &setting.Service.EnablePasskeyAuth
		defer test.MockVariableValue(enablePassword, false)()
		defer test.MockVariableValue(enableOpenID, false)()
		defer test.MockVariableValue(enablePasskey, false)()

		testSignIn := func(t *testing.T, link string, expectedCode int, expectedRedirect string) {
			ctx, resp := contexttest.MockContext(t, link)
			SignIn(ctx)
			assert.Equal(t, expectedCode, resp.Code)
			if expectedCode == http.StatusSeeOther {
				assert.Equal(t, expectedRedirect, test.RedirectURL(resp))
			}
		}
		testSignIn(t, "/user/login", http.StatusSeeOther, "/user/oauth2/dummy+auth%27s%20source")
		testSignIn(t, "/user/login?redirect_to=/", http.StatusSeeOther, "/user/oauth2/dummy+auth%27s%20source?redirect_to=%2F")

		*enablePassword, *enableOpenID, *enablePasskey = true, false, false
		testSignIn(t, "/user/login", http.StatusOK, "")
		*enablePassword, *enableOpenID, *enablePasskey = false, true, false
		testSignIn(t, "/user/login", http.StatusOK, "")
		*enablePassword, *enableOpenID, *enablePasskey = false, false, true
		testSignIn(t, "/user/login", http.StatusOK, "")

		*enablePassword, *enableOpenID, *enablePasskey = false, false, false
		addOAuth2Source(t, "dummy-auth-source-2", oauth2.Source{})
		testSignIn(t, "/user/login", http.StatusOK, "")
	})

	t.Run("OIDCLogout", func(t *testing.T) {
		var mockServer *httptest.Server
		mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			switch r.URL.Path {
			case "/.well-known/openid-configuration":
				_, _ = w.Write([]byte(`{
				"issuer": "` + mockServer.URL + `",
				"authorization_endpoint": "` + mockServer.URL + `/authorize",
				"token_endpoint": "` + mockServer.URL + `/token",
				"userinfo_endpoint": "` + mockServer.URL + `/userinfo",
				"end_session_endpoint": "https://example.com/oidc-logout?oidc-key=oidc-val"
			}`))
			default:
				http.NotFound(w, r)
			}
		}))
		defer mockServer.Close()

		addOAuth2Source(t, "oidc-auth-source", oauth2.Source{
			Provider:                      "openidConnect",
			ClientID:                      "mock-client-id",
			OpenIDConnectAutoDiscoveryURL: mockServer.URL + "/.well-known/openid-configuration",
		})
		authSource, err := auth_model.GetActiveOAuth2SourceByAuthName(t.Context(), "oidc-auth-source")
		require.NoError(t, err)

		mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockMemStore("dummy-sid")}
		ctx, resp := contexttest.MockContext(t, "/user/logout", mockOpt)
		ctx.Doer = &user_model.User{ID: 1, LoginType: auth_model.OAuth2, LoginSource: authSource.ID}
		SignOut(ctx)
		assert.Equal(t, http.StatusSeeOther, resp.Code)
		u, err := url.Parse(test.RedirectURL(resp))
		require.NoError(t, err)
		expectedValues := url.Values{"oidc-key": []string{"oidc-val"}, "post_logout_redirect_uri": []string{setting.AppURL}, "client_id": []string{"mock-client-id"}}
		assert.Equal(t, expectedValues, u.Query())
		u.RawQuery = ""
		assert.Equal(t, "https://example.com/oidc-logout", u.String())
	})
}
