package session

import (
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/valyala/fasthttp"

	"github.com/authelia/authelia/v4/internal/authentication"
	"github.com/authelia/authelia/v4/internal/authorization"
	"github.com/authelia/authelia/v4/internal/configuration/schema"
)

func newTestSession() (*Session, error) {
	config := schema.Session{}
	config.Cookies = []schema.SessionCookie{
		{
			SessionCookieCommon: schema.SessionCookieCommon{
				Name:       testName,
				Expiration: testExpiration,
			},
			Domain: testDomain,
		},
	}

	provider := NewProvider(config, nil)

	return provider.Get(testDomain)
}

func TestShouldInitializerSession(t *testing.T) {
	ctx := &fasthttp.RequestCtx{}

	provider, err := newTestSession()
	assert.NoError(t, err)

	session, err := provider.GetSession(ctx)
	assert.NoError(t, err)

	assert.Equal(t, provider.NewDefaultUserSession(), session)
}

func TestShouldUpdateSession(t *testing.T) {
	ctx := &fasthttp.RequestCtx{}

	provider, err := newTestSession()
	assert.NoError(t, err)

	session, _ := provider.GetSession(ctx)

	session.Username = testUsername
	session.AuthenticationMethodRefs.UsernameAndPassword = true
	session.AuthenticationMethodRefs.WebAuthn = true

	err = provider.SaveSession(ctx, session)
	assert.NoError(t, err)

	session, err = provider.GetSession(ctx)
	assert.NoError(t, err)

	assert.Equal(t, UserSession{
		CookieDomain: testDomain,
		Username:     testUsername,
		AuthenticationMethodRefs: authorization.AuthenticationMethodsReferences{
			UsernameAndPassword: true,
			WebAuthn:            true,
		},
	}, session)

	assert.Equal(t, authentication.TwoFactor, session.AuthenticationLevel(false))
}

func TestShouldSetSessionAuthenticationLevels(t *testing.T) {
	ctx := &fasthttp.RequestCtx{}

	timeOneFactor := time.Unix(1625048140, 0).UTC()
	timeTwoFactor := time.Unix(1625048150, 0).UTC()
	timeZeroFactor := time.Unix(0, 0).UTC()

	provider, err := newTestSession()
	assert.NoError(t, err)

	session, _ := provider.GetSession(ctx)

	session.SetOneFactorPassword(timeOneFactor, &authentication.UserDetails{Username: testUsername}, false)

	err = provider.SaveSession(ctx, session)
	assert.NoError(t, err)

	session, err = provider.GetSession(ctx)
	assert.NoError(t, err)

	authAt, err := session.AuthenticatedTime(authorization.OneFactor)
	assert.NoError(t, err)
	assert.Equal(t, timeOneFactor, authAt)

	authAt, err = session.AuthenticatedTime(authorization.TwoFactor)
	assert.NoError(t, err)
	assert.Equal(t, timeZeroFactor, authAt)

	authAt, err = session.AuthenticatedTime(authorization.Denied)
	assert.EqualError(t, err, "invalid authorization level")
	assert.Equal(t, timeZeroFactor, authAt)

	assert.Equal(t, UserSession{
		CookieDomain:              testDomain,
		Username:                  testUsername,
		LastActivity:              timeOneFactor.Unix(),
		FirstFactorAuthnTimestamp: timeOneFactor.Unix(),
		AuthenticationMethodRefs:  authorization.AuthenticationMethodsReferences{UsernameAndPassword: true, KnowledgeBasedAuthentication: true},
	}, session)

	assert.Equal(t, authentication.OneFactor, session.AuthenticationLevel(false))

	session.SetTwoFactorDuo(timeTwoFactor)

	err = provider.SaveSession(ctx, session)
	assert.NoError(t, err)

	session, err = provider.GetSession(ctx)
	assert.NoError(t, err)

	assert.Equal(t, UserSession{
		CookieDomain:               testDomain,
		Username:                   testUsername,
		LastActivity:               timeTwoFactor.Unix(),
		FirstFactorAuthnTimestamp:  timeOneFactor.Unix(),
		SecondFactorAuthnTimestamp: timeTwoFactor.Unix(),
		AuthenticationMethodRefs:   authorization.AuthenticationMethodsReferences{UsernameAndPassword: true, Duo: true, KnowledgeBasedAuthentication: true},
	}, session)

	assert.Equal(t, authentication.TwoFactor, session.AuthenticationLevel(false))

	authAt, err = session.AuthenticatedTime(authorization.OneFactor)
	assert.NoError(t, err)
	assert.Equal(t, timeOneFactor, authAt)

	authAt, err = session.AuthenticatedTime(authorization.TwoFactor)
	assert.NoError(t, err)
	assert.Equal(t, timeTwoFactor, authAt)

	authAt, err = session.AuthenticatedTime(authorization.Denied)
	assert.EqualError(t, err, "invalid authorization level")
	assert.Equal(t, timeZeroFactor, authAt)
}

func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
	ctx := &fasthttp.RequestCtx{}

	timeOneFactor := time.Unix(1625048140, 0).UTC()
	timeTwoFactor := time.Unix(1625048150, 0).UTC()
	timeZeroFactor := time.Unix(0, 0).UTC()

	provider, err := newTestSession()
	assert.NoError(t, err)

	session, _ := provider.GetSession(ctx)

	session.SetOneFactorPassword(timeOneFactor, &authentication.UserDetails{Username: testUsername}, false)

	err = provider.SaveSession(ctx, session)
	assert.NoError(t, err)

	session, err = provider.GetSession(ctx)
	assert.NoError(t, err)

	authAt, err := session.AuthenticatedTime(authorization.OneFactor)
	assert.NoError(t, err)
	assert.Equal(t, timeOneFactor, authAt)

	authAt, err = session.AuthenticatedTime(authorization.TwoFactor)
	assert.NoError(t, err)
	assert.Equal(t, timeZeroFactor, authAt)

	authAt, err = session.AuthenticatedTime(authorization.Denied)
	assert.EqualError(t, err, "invalid authorization level")
	assert.Equal(t, timeZeroFactor, authAt)

	assert.Equal(t, UserSession{
		CookieDomain:              testDomain,
		Username:                  testUsername,
		LastActivity:              timeOneFactor.Unix(),
		FirstFactorAuthnTimestamp: timeOneFactor.Unix(),
		AuthenticationMethodRefs:  authorization.AuthenticationMethodsReferences{UsernameAndPassword: true, KnowledgeBasedAuthentication: true},
	}, session)

	assert.Equal(t, authentication.OneFactor, session.AuthenticationLevel(false))

	session.SetTwoFactorWebAuthn(timeTwoFactor, true, false, false)

	err = provider.SaveSession(ctx, session)
	assert.NoError(t, err)

	session, err = provider.GetSession(ctx)
	assert.NoError(t, err)

	assert.Equal(t, authorization.AuthenticationMethodsReferences{UsernameAndPassword: true, WebAuthn: true, WebAuthnHardware: true, KnowledgeBasedAuthentication: true}, session.AuthenticationMethodRefs)
	assert.True(t, session.AuthenticationMethodRefs.MultiFactorAuthentication())

	authAt, err = session.AuthenticatedTime(authorization.OneFactor)
	assert.NoError(t, err)
	assert.Equal(t, timeOneFactor, authAt)

	authAt, err = session.AuthenticatedTime(authorization.TwoFactor)
	assert.NoError(t, err)
	assert.Equal(t, timeTwoFactor, authAt)

	authAt, err = session.AuthenticatedTime(authorization.Denied)
	assert.EqualError(t, err, "invalid authorization level")
	assert.Equal(t, timeZeroFactor, authAt)

	session.SetTwoFactorWebAuthn(timeTwoFactor, true, false, false)

	err = provider.SaveSession(ctx, session)
	assert.NoError(t, err)

	session, err = provider.GetSession(ctx)
	assert.NoError(t, err)

	assert.Equal(t,
		authorization.AuthenticationMethodsReferences{UsernameAndPassword: true, WebAuthn: true, WebAuthnHardware: true, KnowledgeBasedAuthentication: true},
		session.AuthenticationMethodRefs)

	session.SetTwoFactorWebAuthn(timeTwoFactor, true, false, false)

	err = provider.SaveSession(ctx, session)
	assert.NoError(t, err)

	session, err = provider.GetSession(ctx)
	assert.NoError(t, err)

	assert.Equal(t,
		authorization.AuthenticationMethodsReferences{UsernameAndPassword: true, WebAuthn: true, WebAuthnHardware: true, KnowledgeBasedAuthentication: true},
		session.AuthenticationMethodRefs)

	session.SetTwoFactorWebAuthn(timeTwoFactor, true, true, false)

	err = provider.SaveSession(ctx, session)
	assert.NoError(t, err)

	session, err = provider.GetSession(ctx)
	assert.NoError(t, err)

	assert.Equal(t,
		authorization.AuthenticationMethodsReferences{UsernameAndPassword: true, WebAuthn: true, WebAuthnUserPresence: true, WebAuthnHardware: true, KnowledgeBasedAuthentication: true},
		session.AuthenticationMethodRefs)

	session.SetTwoFactorWebAuthn(timeTwoFactor, true, true, false)

	err = provider.SaveSession(ctx, session)
	assert.NoError(t, err)

	session, err = provider.GetSession(ctx)
	assert.NoError(t, err)

	assert.Equal(t,
		authorization.AuthenticationMethodsReferences{UsernameAndPassword: true, WebAuthn: true, WebAuthnUserPresence: true, WebAuthnHardware: true, KnowledgeBasedAuthentication: true},
		session.AuthenticationMethodRefs)

	session.SetTwoFactorWebAuthn(timeTwoFactor, true, false, true)

	err = provider.SaveSession(ctx, session)
	assert.NoError(t, err)

	session, err = provider.GetSession(ctx)
	assert.NoError(t, err)

	assert.Equal(t,
		authorization.AuthenticationMethodsReferences{UsernameAndPassword: true, WebAuthn: true, WebAuthnUserVerified: true, WebAuthnHardware: true, KnowledgeBasedAuthentication: true},
		session.AuthenticationMethodRefs)

	session.SetTwoFactorWebAuthn(timeTwoFactor, true, false, true)

	err = provider.SaveSession(ctx, session)
	assert.NoError(t, err)

	session, err = provider.GetSession(ctx)
	assert.NoError(t, err)

	assert.Equal(t,
		authorization.AuthenticationMethodsReferences{UsernameAndPassword: true, WebAuthn: true, WebAuthnUserVerified: true, WebAuthnHardware: true, KnowledgeBasedAuthentication: true},
		session.AuthenticationMethodRefs)

	session.SetTwoFactorTOTP(timeTwoFactor)

	err = provider.SaveSession(ctx, session)
	assert.NoError(t, err)

	session, err = provider.GetSession(ctx)
	assert.NoError(t, err)

	assert.Equal(t,
		authorization.AuthenticationMethodsReferences{UsernameAndPassword: true, TOTP: true, WebAuthn: true, WebAuthnUserVerified: true, WebAuthnHardware: true, KnowledgeBasedAuthentication: true},
		session.AuthenticationMethodRefs)

	session.SetTwoFactorTOTP(timeTwoFactor)

	err = provider.SaveSession(ctx, session)
	assert.NoError(t, err)

	session, err = provider.GetSession(ctx)
	assert.NoError(t, err)

	assert.Equal(t,
		authorization.AuthenticationMethodsReferences{UsernameAndPassword: true, TOTP: true, WebAuthn: true, WebAuthnUserVerified: true, WebAuthnHardware: true, KnowledgeBasedAuthentication: true},
		session.AuthenticationMethodRefs)
}

func TestShouldDestroySessionAndWipeSessionData(t *testing.T) {
	ctx := &fasthttp.RequestCtx{}
	domainSession, err := newTestSession()
	assert.NoError(t, err)

	session, err := domainSession.GetSession(ctx)
	assert.NoError(t, err)

	session.Username = testUsername
	session.AuthenticationMethodRefs.UsernameAndPassword = true
	session.AuthenticationMethodRefs.WebAuthn = true

	err = domainSession.SaveSession(ctx, session)
	assert.NoError(t, err)

	newUserSession, err := domainSession.GetSession(ctx)
	assert.NoError(t, err)
	assert.Equal(t, testUsername, newUserSession.Username)
	assert.Equal(t, authentication.TwoFactor, newUserSession.AuthenticationLevel(false))

	err = domainSession.DestroySession(ctx)
	assert.NoError(t, err)

	newUserSession, err = domainSession.GetSession(ctx)
	assert.NoError(t, err)
	assert.Equal(t, "", newUserSession.Username)
	assert.Equal(t, authentication.NotAuthenticated, newUserSession.AuthenticationLevel(false))
}
