package user_auth

import (
	"encoding/json"
	"fmt"
	"os"
	"testing"
	"time"

	"geeks-accelerator/oss/saas-starter-kit/internal/account"
	"geeks-accelerator/oss/saas-starter-kit/internal/account/account_preference"
	"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
	"geeks-accelerator/oss/saas-starter-kit/internal/platform/tests"
	"geeks-accelerator/oss/saas-starter-kit/internal/user"
	"geeks-accelerator/oss/saas-starter-kit/internal/user_account"
	"github.com/google/go-cmp/cmp"
	"github.com/pborman/uuid"
	"github.com/pkg/errors"
)

var (
	test *tests.Test
	repo *Repository
)

// TestMain is the entry point for testing.
func TestMain(m *testing.M) {
	os.Exit(testMain(m))
}

func testMain(m *testing.M) int {
	test = tests.New()
	defer test.TearDown()

	tknGen := &auth.MockTokenGenerator{}

	userRepo := user.MockRepository(test.MasterDB)
	userAccRepo := user_account.NewRepository(test.MasterDB)
	accPrefRepo := account_preference.NewRepository(test.MasterDB)

	repo = NewRepository(test.MasterDB, tknGen, userRepo, userAccRepo, accPrefRepo)

	return m.Run()
}

// TestAuthenticate validates the behavior around authenticating users.
func TestAuthenticate(t *testing.T) {
	defer tests.Recover(t)

	t.Log("Given the need to authenticate users")
	{
		t.Log("\tWhen handling a single User.")
		{
			ctx := tests.Context()

			// Auth tokens are valid for an our and is verified against current time.
			// Issue the token one hour ago.
			now := time.Now().Add(time.Hour * -1)

			// Try to authenticate an invalid user.
			_, err := repo.Authenticate(ctx,
				AuthenticateRequest{
					Email:    "doesnotexist@gmail.com",
					Password: "xy7",
				}, time.Hour, now)
			if errors.Cause(err) != ErrAuthenticationFailure {
				t.Logf("\t\tGot : %+v", err)
				t.Logf("\t\tWant: %+v", ErrAuthenticationFailure)
				t.Fatalf("\t%s\tAuthenticate non existant user failed.", tests.Failed)
			}
			t.Logf("\t%s\tAuthenticate non existant user ok.", tests.Success)

			// Create a new user for testing.
			usrAcc, err := user_account.MockUserAccount(ctx, test.MasterDB, now, user_account.UserAccountRole_User)
			if err != nil {
				t.Log("\t\tGot :", err)
				t.Fatalf("\t%s\tCreate user account failed.", tests.Failed)
			}
			t.Logf("\t%s\tCreate user account ok.", tests.Success)

			// Add 30 minutes to now to simulate time passing.
			now = now.Add(time.Minute * 30)

			acc2, err := account.MockAccount(ctx, test.MasterDB, now)
			if err != nil {
				t.Log("\t\tGot :", err)
				t.Fatalf("\t%s\tCreate second account failed.", tests.Failed)
			}
			t.Logf("\t%s\tCreate second account ok.", tests.Success)

			// Associate second new account with user user. Need to ensure that now
			// is always greater than the first user_account entry created so it will
			// be returned consistently back in the same order, last.
			account2Role := auth.RoleUser
			_, err = repo.UserAccount.Create(ctx, auth.Claims{}, user_account.UserAccountCreateRequest{
				UserID:    usrAcc.UserID,
				AccountID: acc2.ID,
				Roles:     []user_account.UserAccountRole{user_account.UserAccountRole(account2Role)},
			}, now)

			// Add 30 minutes to now to simulate time passing.
			now = now.Add(time.Minute * 5)

			// Try to authenticate valid user with invalid password.
			_, err = repo.Authenticate(ctx,
				AuthenticateRequest{
					Email:    usrAcc.User.Email,
					Password: "xy7",
				},
				time.Hour, now)
			if errors.Cause(err) != ErrAuthenticationFailure {
				t.Logf("\t\tGot : %+v", err)
				t.Logf("\t\tWant: %+v", ErrAuthenticationFailure)
				t.Fatalf("\t%s\tAuthenticate user w/invalid password failed.", tests.Failed)
			}
			t.Logf("\t%s\tAuthenticate user w/invalid password ok.", tests.Success)

			// Verify that the user can be authenticated with the created user.
			tkn1, err := repo.Authenticate(ctx,
				AuthenticateRequest{
					Email:    usrAcc.User.Email,
					Password: usrAcc.User.Password,
				}, time.Hour, now)
			if err != nil {
				t.Log("\t\tGot :", err)
				t.Fatalf("\t%s\tAuthenticate user failed.", tests.Failed)
			}
			t.Logf("\t%s\tAuthenticate user ok.", tests.Success)

			// Ensure the token string was correctly generated.
			claims1, err := repo.TknGen.ParseClaims(tkn1.AccessToken)
			if err != nil {
				t.Log("\t\tGot :", err)
				t.Fatalf("\t%s\tParse claims from token failed.", tests.Failed)
			}
			expectClaims := tkn1.claims
			expectClaims.RootUserID = ""
			expectClaims.RootAccountID = ""
			expectClaims.Subject = usrAcc.UserID
			expectClaims.Audience = usrAcc.AccountID

			if diff := cmpClaims(claims1, expectClaims); diff != "" {
				t.Fatalf("\t%s\tExpected parsed claims to match from token. Diff:\n%s", tests.Failed, diff)
			}
			t.Logf("\t%s\tAuthenticate parse claims from token ok.", tests.Success)

			// Try switching to a second account using the first set of claims.
			tkn2, err := repo.SwitchAccount(ctx, claims1,
				SwitchAccountRequest{AccountID: acc2.ID}, time.Hour, now)
			if err != nil {
				t.Log("\t\tGot :", err)
				t.Fatalf("\t%s\tSwitchAccount user failed.", tests.Failed)
			}
			t.Logf("\t%s\tSwitchAccount user ok.", tests.Success)

			// Ensure the token string was correctly generated.
			claims2, err := repo.TknGen.ParseClaims(tkn2.AccessToken)
			if err != nil {
				t.Log("\t\tGot :", err)
				t.Fatalf("\t%s\tParse claims from token failed.", tests.Failed)
			}
			expectClaims = tkn2.claims
			expectClaims.RootUserID = usrAcc.UserID
			expectClaims.RootAccountID = acc2.ID
			expectClaims.Subject = usrAcc.UserID
			expectClaims.Audience = acc2.ID

			if diff := cmpClaims(claims2, expectClaims); diff != "" {
				t.Fatalf("\t%s\tExpected parsed claims to match from token. Diff:\n%s", tests.Failed, diff)
			}
			t.Logf("\t%s\tSwitchAccount parse claims from token ok.", tests.Success)
		}
	}
}

// TestUserUpdatePassword validates update user password works.
func TestUserUpdatePassword(t *testing.T) {

	t.Log("Given the need ensure a user password can be updated.")
	{
		ctx := tests.Context()

		now := time.Date(2018, time.October, 1, 0, 0, 0, 0, time.UTC)

		// Create a new user for testing.
		usrAcc, err := user_account.MockUserAccount(ctx, test.MasterDB, now, user_account.UserAccountRole_User)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tCreate user account failed.", tests.Failed)
		}
		t.Logf("\t%s\tCreate user account ok.", tests.Success)

		// Verify that the user can be authenticated with the created user.
		_, err = repo.Authenticate(ctx,
			AuthenticateRequest{
				Email:    usrAcc.User.Email,
				Password: usrAcc.User.Password,
			}, time.Hour, now)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tAuthenticate failed.", tests.Failed)
		}

		// Update the users password.
		newPass := uuid.NewRandom().String()
		err = repo.User.UpdatePassword(ctx, auth.Claims{}, user.UserUpdatePasswordRequest{
			ID:              usrAcc.UserID,
			Password:        newPass,
			PasswordConfirm: newPass,
		}, now)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tUpdate password failed.", tests.Failed)
		}
		t.Logf("\t%s\tUpdatePassword ok.", tests.Success)

		// Verify that the user can be authenticated with the updated password.
		_, err = repo.Authenticate(ctx,
			AuthenticateRequest{
				Email:    usrAcc.User.Email,
				Password: newPass,
			}, time.Hour, now)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tAuthenticate failed.", tests.Failed)
		}
		t.Logf("\t%s\tAuthenticate ok.", tests.Success)
	}
}

// TestUserResetPassword validates that reset password for a user works.
func TestUserResetPassword(t *testing.T) {

	t.Log("Given the need ensure a user can reset their password.")
	{
		ctx := tests.Context()

		now := time.Date(2018, time.October, 1, 0, 0, 0, 0, time.UTC)

		// Create a new user for testing.
		usrAcc, err := user_account.MockUserAccount(ctx, test.MasterDB, now, user_account.UserAccountRole_User)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tCreate user account failed.", tests.Failed)
		}
		t.Logf("\t%s\tCreate user account ok.", tests.Success)

		ttl := time.Hour

		// Make the reset password request.
		resetHash, err := repo.User.ResetPassword(ctx, user.UserResetPasswordRequest{
			Email: usrAcc.User.Email,
			TTL:   ttl,
		}, now)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tResetPassword failed.", tests.Failed)
		}
		t.Logf("\t%s\tResetPassword ok.", tests.Success)

		// Assuming we have received the email and clicked the link, we now can ensure confirm works.
		newPass := uuid.NewRandom().String()
		reset, err := repo.User.ResetConfirm(ctx, user.UserResetConfirmRequest{
			ResetHash:       resetHash,
			Password:        newPass,
			PasswordConfirm: newPass,
		}, now)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tResetConfirm failed.", tests.Failed)
		} else if reset.ID != usrAcc.User.ID {
			t.Logf("\t\tGot : %+v", reset.ID)
			t.Logf("\t\tWant: %+v", usrAcc.User.ID)
			t.Fatalf("\t%s\tResetConfirm failed.", tests.Failed)
		}
		t.Logf("\t%s\tResetConfirm ok.", tests.Success)

		// Verify that the user can be authenticated with the updated password.
		_, err = repo.Authenticate(ctx,
			AuthenticateRequest{
				Email:    usrAcc.User.Email,
				Password: newPass,
			}, time.Hour, now)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tAuthenticate failed.", tests.Failed)
		}
		t.Logf("\t%s\tAuthenticate ok.", tests.Success)
	}
}

// TestSwitchAccount validates the behavior around allowing users to switch between their accounts.
func TestSwitchAccount(t *testing.T) {
	defer tests.Recover(t)

	// Auth tokens are valid for an our and is verified against current time.
	// Issue the token one hour ago.
	now := time.Now().Add(time.Hour * -1)

	ctx := tests.Context()

	type authTest struct {
		name          string
		root          *user_account.MockUserAccountResponse
		switch1Req    SwitchAccountRequest
		switch1Roles  []user_account.UserAccountRole
		switch1Scopes []string
		switch1Err    error
		switch2Req    SwitchAccountRequest
		switch2Roles  []user_account.UserAccountRole
		switch2Scopes []string
		switch2Err    error
	}

	var authTests []authTest

	// Test all the combinations there the user has access to all three accounts.
	if true {
		for _, roles := range [][]user_account.UserAccountRole{
			[]user_account.UserAccountRole{user_account.UserAccountRole_Admin, user_account.UserAccountRole_Admin, user_account.UserAccountRole_Admin},
			[]user_account.UserAccountRole{user_account.UserAccountRole_User, user_account.UserAccountRole_User, user_account.UserAccountRole_User},
			[]user_account.UserAccountRole{user_account.UserAccountRole_Admin, user_account.UserAccountRole_User, user_account.UserAccountRole_Admin},
			[]user_account.UserAccountRole{user_account.UserAccountRole_User, user_account.UserAccountRole_Admin, user_account.UserAccountRole_User},
		} {
			// Create a new user for testing.
			usrAcc, err := user_account.MockUserAccount(ctx, test.MasterDB, now, roles[0])
			if err != nil {
				t.Log("\t\tGot :", err)
				t.Fatalf("\t%s\tCreate user account failed.", tests.Failed)
			}

			// Create the second account.
			now = now.Add(time.Minute)
			acc2, err := account.MockAccount(ctx, test.MasterDB, now)
			if err != nil {
				t.Log("\t\tGot :", err)
				t.Fatalf("\t%s\tCreate second account failed.", tests.Failed)
			}

			// Associate the second account with root user.
			usrAcc2, err := repo.UserAccount.Create(ctx, auth.Claims{}, user_account.UserAccountCreateRequest{
				UserID:    usrAcc.UserID,
				AccountID: acc2.ID,
				Roles:     []user_account.UserAccountRole{user_account.UserAccountRole(roles[1])},
			}, now)
			if err != nil {
				t.Log("\t\tGot :", err)
				t.Fatalf("\t%s\tLinking second account to user failed.", tests.Failed)
			}

			// Create the third account.
			now = now.Add(time.Minute)
			acc3, err := account.MockAccount(ctx, test.MasterDB, now)
			if err != nil {
				t.Log("\t\tGot :", err)
				t.Fatalf("\t%s\tCreate third account failed.", tests.Failed)
			}

			// Associate the third account with root user.
			usrAcc3, err := repo.UserAccount.Create(ctx, auth.Claims{}, user_account.UserAccountCreateRequest{
				UserID:    usrAcc.UserID,
				AccountID: acc3.ID,
				Roles:     []user_account.UserAccountRole{user_account.UserAccountRole(roles[2])},
			}, now)
			if err != nil {
				t.Log("\t\tGot :", err)
				t.Fatalf("\t%s\tLinking third account to user failed.", tests.Failed)
			}

			authTests = append(authTests, authTest{
				name: fmt.Sprintf("Root account role %s -> role %s account 2 -> role %s account 3.",
					roles[0], roles[1], roles[2]),
				root:         usrAcc,
				switch1Req:   SwitchAccountRequest{AccountID: acc2.ID},
				switch1Roles: usrAcc2.Roles,
				switch1Err:   nil,
				switch2Req:   SwitchAccountRequest{AccountID: acc3.ID},
				switch2Err:   nil,
				switch2Roles: usrAcc3.Roles,
			})
		}
	}

	// Root account 1 -> invalid account 2
	if true {
		// Create a new user for testing.
		usrAcc, err := user_account.MockUserAccount(ctx, test.MasterDB, now, user_account.UserAccountRole_Admin)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tCreate user account failed.", tests.Failed)
		}

		// Create the second account and don't associate it with the root user.
		now = now.Add(time.Minute)
		acc2, err := account.MockAccount(ctx, test.MasterDB, now)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tCreate second account failed.", tests.Failed)
		}

		authTests = append(authTests, authTest{
			name:       "Root account 1 -> invalid account 2.",
			root:       usrAcc,
			switch1Req: SwitchAccountRequest{AccountID: acc2.ID},
			switch1Err: ErrAuthenticationFailure,
		})
	}

	// Root account 1 -> valid account 2 with scopes -> valid account 3 with invalid scope.
	if true {
		// Create a new user for testing.
		usrAcc, err := user_account.MockUserAccount(ctx, test.MasterDB, now, user_account.UserAccountRole_Admin)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tCreate user account failed.", tests.Failed)
		}

		// Create the second account.
		now = now.Add(time.Minute)
		acc2, err := account.MockAccount(ctx, test.MasterDB, now)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tCreate second account failed.", tests.Failed)
		}

		// Associate the second account with root user.
		usrAcc2, err := repo.UserAccount.Create(ctx, auth.Claims{}, user_account.UserAccountCreateRequest{
			UserID:    usrAcc.UserID,
			AccountID: acc2.ID,
			Roles:     []user_account.UserAccountRole{user_account.UserAccountRole_Admin},
		}, now)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tLinking second account to user failed.", tests.Failed)
		}

		// Create the third account.
		now = now.Add(time.Minute)
		acc3, err := account.MockAccount(ctx, test.MasterDB, now)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tCreate third account failed.", tests.Failed)
		}

		// Associate the third account with root user.
		usrAcc3, err := repo.UserAccount.Create(ctx, auth.Claims{}, user_account.UserAccountCreateRequest{
			UserID:    usrAcc.UserID,
			AccountID: acc3.ID,
			Roles:     []user_account.UserAccountRole{user_account.UserAccountRole_User},
		}, now)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tLinking third account to user failed.", tests.Failed)
		}

		authTests = append(authTests, authTest{
			name:          "Root account 1 -> valid account 2 with scopes -> valid account 3 with invalid scope.",
			root:          usrAcc,
			switch1Req:    SwitchAccountRequest{AccountID: acc2.ID},
			switch1Roles:  usrAcc2.Roles,
			switch1Scopes: []string{user_account.UserAccountRole_User.String()},
			switch1Err:    nil,
			switch2Req:    SwitchAccountRequest{AccountID: acc3.ID},
			switch2Roles:  usrAcc3.Roles,
			switch2Scopes: []string{user_account.UserAccountRole_Admin.String()},
			switch2Err:    ErrForbidden,
		})
	}

	// Add 30 minutes to now to simulate time passing.
	now = now.Add(time.Minute * 5)

	t.Log("Given the need to switch accounts.")
	{
		for i, authTest := range authTests {
			t.Logf("\tTest: %d\tWhen running test: %s", i, authTest.name)
			{
				// Verify that the user can be authenticated with the created user.
				var claims1 auth.Claims
				tkn1, err := repo.Authenticate(ctx,
					AuthenticateRequest{
						Email:    authTest.root.User.Email,
						Password: authTest.root.User.Password,
					}, time.Hour, now)
				if err != nil {
					t.Log("\t\tGot :", err)
					t.Fatalf("\t%s\tAuthenticate user failed.", tests.Failed)
				} else {
					// Ensure the token string was correctly generated.
					claims1, err = repo.TknGen.ParseClaims(tkn1.AccessToken)
					if err != nil {
						t.Log("\t\tGot :", err)
						t.Fatalf("\t%s\tParse claims from token failed.", tests.Failed)
					}
					expectClaims := tkn1.claims
					expectClaims.RootUserID = ""
					expectClaims.RootAccountID = ""
					expectClaims.Subject = authTest.root.UserID
					expectClaims.Audience = authTest.root.AccountID
					expectClaims.Roles = rolesStringSlice(authTest.root.Roles)

					if diff := cmpClaims(claims1, expectClaims); diff != "" {
						t.Fatalf("\t%s\tExpected parsed claims to match from token. Diff:\n%s", tests.Failed, diff)
					}
				}
				t.Logf("\t%s\tAuthenticate root user with role %v ok.", tests.Success, authTest.root.Roles)

				// Try to switch to account 2.
				var claims2 auth.Claims
				tkn2, err := repo.SwitchAccount(ctx, claims1, authTest.switch1Req, time.Hour, now, authTest.switch1Scopes...)
				if err != authTest.switch1Err {
					if errors.Cause(err) != authTest.switch1Err {
						t.Log("\t\tExpected :", authTest.switch1Err)
						t.Log("\t\tGot :", err)
						t.Fatalf("\t%s\tSwitchAccount account 1 with role %v failed.", tests.Failed, authTest.switch1Roles)
					}
				} else {
					// Ensure the token string was correctly generated.
					claims2, err = repo.TknGen.ParseClaims(tkn2.AccessToken)
					if err != nil {
						t.Log("\t\tGot :", err)
						t.Fatalf("\t%s\tParse claims from token failed.", tests.Failed)
					}
					expectClaims := tkn2.claims
					expectClaims.RootUserID = authTest.root.UserID
					expectClaims.RootAccountID = authTest.switch1Req.AccountID
					expectClaims.Subject = authTest.root.UserID
					expectClaims.Audience = authTest.switch1Req.AccountID

					if len(authTest.switch1Scopes) > 0 {
						expectClaims.Roles = authTest.switch1Scopes
					} else {
						expectClaims.Roles = rolesStringSlice(authTest.switch1Roles)
					}

					if diff := cmpClaims(claims2, expectClaims); diff != "" {
						t.Fatalf("\t%s\tExpected parsed claims to match from token. Diff:\n%s", tests.Failed, diff)
					}
				}
				t.Logf("\t%s\tSwitchAccount account 1 with role %v ok.", tests.Success, authTest.switch1Roles)

				// If the user can't login, don't need to test any further.
				if authTest.switch1Err != nil || authTest.switch2Req.AccountID == "" {
					continue
				}

				// Try to switch to account 3.
				tkn3, err := repo.SwitchAccount(ctx, claims2, authTest.switch2Req, time.Hour, now, authTest.switch2Scopes...)
				if err != authTest.switch2Err {
					if errors.Cause(err) != authTest.switch2Err {
						t.Log("\t\tExpected :", authTest.switch2Err)
						t.Log("\t\tGot :", err)
						t.Fatalf("\t%s\tSwitchAccount account 2 with role %v failed.", tests.Failed, authTest.switch2Roles)
					}
				} else {
					// Ensure the token string was correctly generated.
					claims3, err := repo.TknGen.ParseClaims(tkn3.AccessToken)
					if err != nil {
						t.Log("\t\tGot :", err)
						t.Fatalf("\t%s\tParse claims from token failed.", tests.Failed)
					}
					expectClaims := tkn3.claims
					expectClaims.RootUserID = authTest.root.UserID
					expectClaims.RootAccountID = authTest.switch2Req.AccountID
					expectClaims.Subject = authTest.root.UserID
					expectClaims.Audience = authTest.switch2Req.AccountID

					if len(authTest.switch2Scopes) > 0 {
						expectClaims.Roles = authTest.switch2Scopes
					} else {
						expectClaims.Roles = rolesStringSlice(authTest.switch2Roles)
					}

					if diff := cmpClaims(claims3, expectClaims); diff != "" {
						t.Fatalf("\t%s\tExpected parsed claims to match from token. Diff:\n%s", tests.Failed, diff)
					}
				}
				t.Logf("\t%s\tSwitchAccount account 2 with role %v ok.", tests.Success, authTest.switch2Roles)
			}
		}
	}
}

// TestVirtualLogin validates the behavior around allowing users to virtual login users.
func TestVirtualLogin(t *testing.T) {
	defer tests.Recover(t)

	// Auth tokens are valid for an our and is verified against current time.
	// Issue the token one hour ago.
	now := time.Now().Add(time.Hour * -1)

	ctx := tests.Context()

	type authTest struct {
		name         string
		root         *user_account.MockUserAccountResponse
		login1Req    VirtualLoginRequest
		login1Err    error
		login1Role   user_account.UserAccountRole
		login2Req    VirtualLoginRequest
		login2Err    error
		login2Role   user_account.UserAccountRole
		login2Logout bool
	}

	var authTests []authTest

	// Root admin -> role admin -> role admin
	{
		// Create a new user for testing.
		usrAcc, err := user_account.MockUserAccount(ctx, test.MasterDB, now, user_account.UserAccountRole_Admin)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tCreate user account failed.", tests.Failed)
		}

		usr2, err := user.MockUser(ctx, test.MasterDB, now)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tCreate second account failed.", tests.Failed)
		}

		// Associate second user with basic role associated with the same account.
		usrAcc2, err := repo.UserAccount.Create(ctx, auth.Claims{}, user_account.UserAccountCreateRequest{
			UserID:    usr2.ID,
			AccountID: usrAcc.AccountID,
			Roles:     []user_account.UserAccountRole{user_account.UserAccountRole(user_account.UserAccountRole_Admin)},
		}, now)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tLinking second user to account failed.", tests.Failed)
		}

		usr3, err := user.MockUser(ctx, test.MasterDB, now)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tCreate second account failed.", tests.Failed)
		}

		// Associate second user with basic role associated with the same account.
		usrAcc3, err := repo.UserAccount.Create(ctx, auth.Claims{}, user_account.UserAccountCreateRequest{
			UserID:    usr3.ID,
			AccountID: usrAcc.AccountID,
			Roles:     []user_account.UserAccountRole{user_account.UserAccountRole(user_account.UserAccountRole_Admin)},
		}, now)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tLinking third user to account failed.", tests.Failed)
		}

		authTests = append(authTests, authTest{
			name: "Root admin -> role admin -> role admin",
			root: usrAcc,
			login1Req: VirtualLoginRequest{
				UserID:    usr2.ID,
				AccountID: usrAcc.AccountID,
			},
			login1Role: usrAcc2.Roles[0],
			login1Err:  nil,
			login2Req: VirtualLoginRequest{
				UserID:    usr3.ID,
				AccountID: usrAcc.AccountID,
			},
			login2Err:    nil,
			login2Role:   usrAcc3.Roles[0],
			login2Logout: true,
		})
	}

	// Root admin -> role admin -> role user
	if true {
		// Create a new user for testing.
		usrAcc, err := user_account.MockUserAccount(ctx, test.MasterDB, now, user_account.UserAccountRole_Admin)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tCreate user account failed.", tests.Failed)
		}

		usr2, err := user.MockUser(ctx, test.MasterDB, now)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tCreate second account failed.", tests.Failed)
		}

		// Associate second user with basic role associated with the same account.
		usrAcc2, err := repo.UserAccount.Create(ctx, auth.Claims{}, user_account.UserAccountCreateRequest{
			UserID:    usr2.ID,
			AccountID: usrAcc.AccountID,
			Roles:     []user_account.UserAccountRole{user_account.UserAccountRole(user_account.UserAccountRole_Admin)},
		}, now)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tLinking second user to account failed.", tests.Failed)
		}

		usr3, err := user.MockUser(ctx, test.MasterDB, now)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tCreate second account failed.", tests.Failed)
		}

		// Associate second user with basic role associated with the same account.
		usrAcc3, err := repo.UserAccount.Create(ctx, auth.Claims{}, user_account.UserAccountCreateRequest{
			UserID:    usr3.ID,
			AccountID: usrAcc.AccountID,
			Roles:     []user_account.UserAccountRole{user_account.UserAccountRole(user_account.UserAccountRole_User)},
		}, now)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tLinking third user to account failed.", tests.Failed)
		}

		authTests = append(authTests, authTest{
			name: "Root admin -> role admin -> role user",
			root: usrAcc,
			login1Req: VirtualLoginRequest{
				UserID:    usr2.ID,
				AccountID: usrAcc.AccountID,
			},
			login1Err:  nil,
			login1Role: usrAcc2.Roles[0],
			login2Req: VirtualLoginRequest{
				UserID:    usr3.ID,
				AccountID: usrAcc.AccountID,
			},
			login2Err:    nil,
			login2Role:   usrAcc3.Roles[0],
			login2Logout: true,
		})
	}

	// Root admin -> role user -> role admin
	if true {
		// Create a new user for testing.
		usrAcc, err := user_account.MockUserAccount(ctx, test.MasterDB, now, user_account.UserAccountRole_Admin)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tCreate user account failed.", tests.Failed)
		}

		usr2, err := user.MockUser(ctx, test.MasterDB, now)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tCreate second account failed.", tests.Failed)
		}

		// Associate second user with basic role associated with the same account.
		usrAcc2, err := repo.UserAccount.Create(ctx, auth.Claims{}, user_account.UserAccountCreateRequest{
			UserID:    usr2.ID,
			AccountID: usrAcc.AccountID,
			Roles:     []user_account.UserAccountRole{user_account.UserAccountRole(user_account.UserAccountRole_User)},
		}, now)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tLinking second user to account failed.", tests.Failed)
		}

		usr3, err := user.MockUser(ctx, test.MasterDB, now)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tCreate second account failed.", tests.Failed)
		}

		// Associate second user with basic role associated with the same account.
		usrAcc3, err := repo.UserAccount.Create(ctx, auth.Claims{}, user_account.UserAccountCreateRequest{
			UserID:    usr3.ID,
			AccountID: usrAcc.AccountID,
			Roles:     []user_account.UserAccountRole{user_account.UserAccountRole(user_account.UserAccountRole_Admin)},
		}, now)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tLinking third user to account failed.", tests.Failed)
		}

		authTests = append(authTests, authTest{
			name: "Root admin -> role user -> role admin",
			root: usrAcc,
			login1Req: VirtualLoginRequest{
				UserID:    usr2.ID,
				AccountID: usrAcc.AccountID,
			},
			login1Err:  nil,
			login1Role: usrAcc2.Roles[0],
			login2Req: VirtualLoginRequest{
				UserID:    usr3.ID,
				AccountID: usrAcc.AccountID,
			},
			login2Err:    ErrForbidden,
			login2Role:   usrAcc3.Roles[0],
			login2Logout: true,
		})
	}

	// Root user -> role admin
	if true {
		// Create a new user for testing.
		usrAcc, err := user_account.MockUserAccount(ctx, test.MasterDB, now, user_account.UserAccountRole_User)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tCreate user account failed.", tests.Failed)
		}

		usr2, err := user.MockUser(ctx, test.MasterDB, now)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tCreate second account failed.", tests.Failed)
		}

		// Associate second user with basic role associated with the same account.
		usrAcc2, err := repo.UserAccount.Create(ctx, auth.Claims{}, user_account.UserAccountCreateRequest{
			UserID:    usr2.ID,
			AccountID: usrAcc.AccountID,
			Roles:     []user_account.UserAccountRole{user_account.UserAccountRole(user_account.UserAccountRole_Admin)},
		}, now)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tLinking second user to account failed.", tests.Failed)
		}

		authTests = append(authTests, authTest{
			name: "Root user -> role admin",
			root: usrAcc,
			login1Req: VirtualLoginRequest{
				UserID:    usr2.ID,
				AccountID: usrAcc.AccountID,
			},
			login1Err:    ErrForbidden,
			login1Role:   usrAcc2.Roles[0],
			login2Logout: true,
		})
	}

	// Root user -> role user
	if true {
		// Create a new user for testing.
		usrAcc, err := user_account.MockUserAccount(ctx, test.MasterDB, now, user_account.UserAccountRole_User)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tCreate user account failed.", tests.Failed)
		}

		usr2, err := user.MockUser(ctx, test.MasterDB, now)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tCreate second account failed.", tests.Failed)
		}

		// Associate second user with basic role associated with the same account.
		usrAcc2, err := repo.UserAccount.Create(ctx, auth.Claims{}, user_account.UserAccountCreateRequest{
			UserID:    usr2.ID,
			AccountID: usrAcc.AccountID,
			Roles:     []user_account.UserAccountRole{user_account.UserAccountRole(user_account.UserAccountRole_User)},
		}, now)
		if err != nil {
			t.Log("\t\tGot :", err)
			t.Fatalf("\t%s\tLinking second user to account failed.", tests.Failed)
		}

		authTests = append(authTests, authTest{
			name: "Root user -> role admin",
			root: usrAcc,
			login1Req: VirtualLoginRequest{
				UserID:    usr2.ID,
				AccountID: usrAcc.AccountID,
			},
			login1Err:    ErrForbidden,
			login1Role:   usrAcc2.Roles[0],
			login2Logout: true,
		})
	}

	// Add 30 minutes to now to simulate time passing.
	now = now.Add(time.Minute * 5)

	t.Log("Given the need to virtual login.")
	{
		for i, authTest := range authTests {
			t.Logf("\tTest: %d\tWhen running test: %s", i, authTest.name)
			{
				// Verify that the user can be authenticated with the created user.
				var claims1 auth.Claims
				tkn1, err := repo.Authenticate(ctx,
					AuthenticateRequest{
						Email:    authTest.root.User.Email,
						Password: authTest.root.User.Password,
					}, time.Hour, now)
				if err != nil {
					t.Log("\t\tGot :", err)
					t.Fatalf("\t%s\tAuthenticate user failed.", tests.Failed)
				} else {
					// Ensure the token string was correctly generated.
					claims1, err = repo.TknGen.ParseClaims(tkn1.AccessToken)
					if err != nil {
						t.Log("\t\tGot :", err)
						t.Fatalf("\t%s\tParse claims from token failed.", tests.Failed)
					}
					expectClaims := tkn1.claims
					expectClaims.RootUserID = ""
					expectClaims.RootAccountID = ""
					expectClaims.Subject = authTest.root.UserID
					expectClaims.Audience = authTest.root.AccountID

					// Hack for Unhandled Exception in go-cmp@v0.3.0/cmp/options.go:229
					if diff := cmpClaims(claims1, expectClaims); diff != "" {
						t.Fatalf("\t%s\tExpected parsed claims to match from token. Diff:\n%s", tests.Failed, diff)
					}
				}
				t.Logf("\t%s\tAuthenticate root user with role %s ok.", tests.Success, authTest.root.Roles[0])

				// Try virtual login to user 2.
				var claims2 auth.Claims
				tkn2, err := repo.VirtualLogin(ctx, claims1, authTest.login1Req, time.Hour, now)
				if err != authTest.login1Err {
					if errors.Cause(err) != authTest.login1Err {
						t.Log("\t\tExpected :", authTest.login1Err)
						t.Log("\t\tGot :", err)
						t.Fatalf("\t%s\tVirtualLogin user 1 with role %s failed.", tests.Failed, authTest.login1Role)
					}
				} else {
					// Ensure the token string was correctly generated.
					claims2, err = repo.TknGen.ParseClaims(tkn2.AccessToken)
					if err != nil {
						t.Log("\t\tGot :", err)
						t.Fatalf("\t%s\tParse claims from token failed.", tests.Failed)
					}
					expectClaims := tkn2.claims
					expectClaims.RootUserID = authTest.root.UserID
					expectClaims.RootAccountID = authTest.root.AccountID
					expectClaims.Subject = authTest.login1Req.UserID
					expectClaims.Audience = authTest.login1Req.AccountID

					// Hack for Unhandled Exception in go-cmp@v0.3.0/cmp/options.go:229
					if diff := cmpClaims(claims2, expectClaims); diff != "" {
						t.Fatalf("\t%s\tExpected parsed claims to match from token. Diff:\n%s", tests.Failed, diff)
					}
				}
				t.Logf("\t%s\tVirtualLogin user 1 with role %s ok.", tests.Success, authTest.login1Role)

				// If the user can't login, don't need to test any further.
				if authTest.login1Err != nil {
					continue
				}

				// Try virtual login to user 3.
				tkn3, err := repo.VirtualLogin(ctx, claims2, authTest.login2Req, time.Hour, now)
				if err != authTest.login2Err {
					if errors.Cause(err) != authTest.login2Err {
						t.Log("\t\tExpected :", authTest.login2Err)
						t.Log("\t\tGot :", err)
						t.Fatalf("\t%s\tVirtualLogin user 2 with role %s failed.", tests.Failed, authTest.login2Role)
					}
				} else {
					// Ensure the token string was correctly generated.
					claims3, err := repo.TknGen.ParseClaims(tkn3.AccessToken)
					if err != nil {
						t.Log("\t\tGot :", err)
						t.Fatalf("\t%s\tParse claims from token failed.", tests.Failed)
					}
					expectClaims := tkn3.claims
					expectClaims.RootUserID = authTest.root.UserID
					expectClaims.RootAccountID = authTest.root.AccountID
					expectClaims.Subject = authTest.login2Req.UserID
					expectClaims.Audience = authTest.login2Req.AccountID

					// Hack for Unhandled Exception in go-cmp@v0.3.0/cmp/options.go:229
					if diff := cmpClaims(claims3, expectClaims); diff != "" {
						t.Fatalf("\t%s\tExpected parsed claims to match from token. Diff:\n%s", tests.Failed, diff)
					}
				}
				t.Logf("\t%s\tVirtualLogin user 2 with role %s ok.", tests.Success, authTest.login2Role)

				if authTest.login2Logout {
					tknOut, err := repo.VirtualLogout(ctx, claims2, time.Hour, now)
					if err != nil {
						t.Log("\t\tGot :", err)
						t.Fatalf("\t%s\tVirtualLogout user 2 failed.", tests.Failed)
					}

					// Ensure the token string was correctly generated.
					claimsOut, err := repo.TknGen.ParseClaims(tknOut.AccessToken)
					if err != nil {
						t.Log("\t\tGot :", err)
						t.Fatalf("\t%s\tParse claims from token failed.", tests.Failed)
					}
					expectClaims := tknOut.claims
					expectClaims.RootUserID = authTest.root.UserID
					expectClaims.RootAccountID = authTest.root.AccountID
					expectClaims.Subject = authTest.root.UserID
					expectClaims.Audience = authTest.root.AccountID

					if diff := cmpClaims(claimsOut, expectClaims); diff != "" {
						t.Fatalf("\t%s\tExpected parsed claims to match from token. Diff:\n%s", tests.Failed, diff)
					}
					t.Logf("\t%s\tVirtualLogout user 2 with role %s ok.", tests.Success, authTest.login2Role)
				}
			}
		}
	}
}

// rolesStringSlice converts a list of roles to a string slice.
func rolesStringSlice(roles []user_account.UserAccountRole) []string {
	var l []string
	for _, r := range roles {
		l = append(l, string(r))
	}
	return l
}

// cmpClaims is a hack for Unhandled Exception in go-cmp@v0.3.0/cmp/options.go:229
func cmpClaims(actualClaims, expectedclaims auth.Claims) string {
	dat1, _ := json.Marshal(actualClaims)
	dat2, _ := json.Marshal(expectedclaims)
	return cmp.Diff(string(dat1), string(dat2))
}
