// Copyright 2023 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package integration

import (
	"encoding/hex"
	"net/http"
	"net/url"
	"strings"
	"testing"

	"forgejo.org/models/auth"
	"forgejo.org/models/db"
	"forgejo.org/models/unittest"
	user_model "forgejo.org/models/user"
	"forgejo.org/modules/setting"
	"forgejo.org/modules/timeutil"
	"forgejo.org/tests"

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

// GetSessionForLTACookie returns a new session with only the LTA cookie being set.
func GetSessionForLTACookie(t *testing.T, ltaCookie *http.Cookie) *TestSession {
	t.Helper()

	ch := http.Header{}
	ch.Add("Cookie", ltaCookie.String())
	cr := http.Request{Header: ch}

	session := emptyTestSession(t)
	baseURL, err := url.Parse(setting.AppURL)
	require.NoError(t, err)
	session.jar.SetCookies(baseURL, cr.Cookies())

	return session
}

// GetLTACookieValue returns the value of the LTA cookie.
func GetLTACookieValue(t *testing.T, sess *TestSession) string {
	t.Helper()

	rememberCookie := sess.GetCookie(setting.CookieRememberName)
	assert.NotNil(t, rememberCookie)

	cookieValue, err := url.QueryUnescape(rememberCookie.Value)
	require.NoError(t, err)

	return cookieValue
}

// TestSessionCookie checks if the session cookie provides authentication.
func TestSessionCookie(t *testing.T) {
	defer tests.PrepareTestEnv(t)()

	sess := loginUser(t, "user1")
	assert.NotNil(t, sess.GetCookie(setting.SessionConfig.CookieName))

	req := NewRequest(t, "GET", "/user/settings")
	sess.MakeRequest(t, req, http.StatusOK)
}

// TestLTACookie checks if the LTA cookie that's returned is valid, exists in the database
// and provides authentication of no session cookie is present.
func TestLTACookie(t *testing.T) {
	defer tests.PrepareTestEnv(t)()

	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
	sess := emptyTestSession(t)

	req := NewRequestWithValues(t, "POST", "/user/login", map[string]string{
		"user_name": user.Name,
		"password":  userPassword,
		"remember":  "true",
	})
	sess.MakeRequest(t, req, http.StatusSeeOther)

	// Checks if the database entry exist for the user.
	ltaCookieValue := GetLTACookieValue(t, sess)
	lookupKey, validator, found := strings.Cut(ltaCookieValue, ":")
	assert.True(t, found)
	rawValidator, err := hex.DecodeString(validator)
	require.NoError(t, err)
	unittest.AssertExistsAndLoadBean(t, &auth.AuthorizationToken{LookupKey: lookupKey, HashedValidator: auth.HashValidator(rawValidator), UID: user.ID, Purpose: auth.LongTermAuthorization})

	// Check if the LTA cookie it provides authentication.
	// If LTA cookie provides authentication /user/login shouldn't return status 200.
	session := GetSessionForLTACookie(t, sess.GetCookie(setting.CookieRememberName))
	req = NewRequest(t, "GET", "/user/login")
	session.MakeRequest(t, req, http.StatusSeeOther)
}

// TestLTAPasswordChange checks that LTA doesn't provide authentication when a
// password change has happened and that the new LTA does provide authentication.
func TestLTAPasswordChange(t *testing.T) {
	defer tests.PrepareTestEnv(t)()

	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})

	sess := loginUserWithPasswordRemember(t, user.Name, userPassword, true)
	oldRememberCookie := sess.GetCookie(setting.CookieRememberName)
	assert.NotNil(t, oldRememberCookie)

	// Make a simple password change.
	req := NewRequestWithValues(t, "POST", "/user/settings/account", map[string]string{
		"old_password": userPassword,
		"password":     "password2",
		"retype":       "password2",
	})
	sess.MakeRequest(t, req, http.StatusSeeOther)
	rememberCookie := sess.GetCookie(setting.CookieRememberName)
	assert.NotNil(t, rememberCookie)

	// Check if the password really changed.
	assert.NotEqual(t, unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).Passwd, user.Passwd)

	// /user/settings/account should provide with a new LTA cookie, so check for that.
	// If LTA cookie provides authentication /user/login shouldn't return status 200.
	session := GetSessionForLTACookie(t, rememberCookie)
	req = NewRequest(t, "GET", "/user/login")
	session.MakeRequest(t, req, http.StatusSeeOther)

	// Check if the old LTA token is invalidated.
	session = GetSessionForLTACookie(t, oldRememberCookie)
	req = NewRequest(t, "GET", "/user/login")
	session.MakeRequest(t, req, http.StatusOK)
}

// TestLTAExpiry tests that the LTA expiry works.
func TestLTAExpiry(t *testing.T) {
	defer tests.PrepareTestEnv(t)()

	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})

	sess := loginUserWithPasswordRemember(t, user.Name, userPassword, true)

	ltaCookieValue := GetLTACookieValue(t, sess)
	lookupKey, _, found := strings.Cut(ltaCookieValue, ":")
	assert.True(t, found)

	// Ensure it's not expired.
	lta := unittest.AssertExistsAndLoadBean(t, &auth.AuthorizationToken{UID: user.ID, LookupKey: lookupKey, Purpose: auth.LongTermAuthorization})
	assert.False(t, lta.IsExpired())

	// Manually stub LTA's expiry.
	_, err := db.GetEngine(db.DefaultContext).ID(lta.ID).Table("forgejo_auth_token").Cols("expiry").Update(&auth.AuthorizationToken{Expiry: timeutil.TimeStampNow()})
	require.NoError(t, err)

	// Ensure it's expired.
	lta = unittest.AssertExistsAndLoadBean(t, &auth.AuthorizationToken{UID: user.ID, LookupKey: lookupKey, Purpose: auth.LongTermAuthorization})
	assert.True(t, lta.IsExpired())

	// Should return 200 OK, because LTA doesn't provide authorization anymore.
	session := GetSessionForLTACookie(t, sess.GetCookie(setting.CookieRememberName))
	req := NewRequest(t, "GET", "/user/login")
	session.MakeRequest(t, req, http.StatusOK)

	// Ensure it's deleted.
	unittest.AssertNotExistsBean(t, &auth.AuthorizationToken{UID: user.ID, LookupKey: lookupKey, Purpose: auth.LongTermAuthorization})
}
