package auth

import (
	"fmt"
	"testing"
	"time"

	"code.secondbit.org/uuid"
)

const (
	profileChangeName = 1 << iota
	profileChangePassphrase
	profileChangeIterations
	profileChangeSalt
	profileChangePassphraseScheme
	profileChangeCompromised
	profileChangeLockedUntil
	profileChangePassphraseReset
	profileChangePassphraseResetCreated
	profileChangeLastSeen
)

var profileStores = []ProfileStore{NewMemstore()}

func compareProfiles(profile1, profile2 Profile) (success bool, field string, val1, val2 interface{}) {
	if !profile1.ID.Equal(profile2.ID) {
		return false, "ID", profile1.ID, profile2.ID
	}
	if profile1.Name != profile2.Name {
		return false, "name", profile1.Name, profile2.Name
	}
	if profile1.Passphrase != profile2.Passphrase {
		return false, "passphrase", profile1.Passphrase, profile2.Passphrase
	}
	if profile1.Iterations != profile2.Iterations {
		return false, "iterations", profile1.Iterations, profile2.Iterations
	}
	if profile1.Salt != profile2.Salt {
		return false, "salt", profile1.Salt, profile2.Salt
	}
	if profile1.PassphraseScheme != profile2.PassphraseScheme {
		return false, "passphrase scheme", profile1.PassphraseScheme, profile2.PassphraseScheme
	}
	if profile1.Compromised != profile2.Compromised {
		return false, "compromised", profile1.Compromised, profile2.Compromised
	}
	if !profile1.LockedUntil.Equal(profile2.LockedUntil) {
		return false, "locked until", profile1.LockedUntil, profile2.LockedUntil
	}
	if profile1.PassphraseReset != profile2.PassphraseReset {
		return false, "passphrase reset", profile1.PassphraseReset, profile2.PassphraseReset
	}
	if !profile1.PassphraseResetCreated.Equal(profile2.PassphraseResetCreated) {
		return false, "passphrase reset created", profile1.PassphraseResetCreated, profile2.PassphraseResetCreated
	}
	if !profile1.Created.Equal(profile2.Created) {
		return false, "created", profile1.Created, profile2.Created
	}
	if !profile1.LastSeen.Equal(profile2.LastSeen) {
		return false, "last seen", profile1.LastSeen, profile2.LastSeen
	}
	return true, "", nil, nil
}

func TestProfileStoreSuccess(t *testing.T) {
	t.Parallel()
	profile := Profile{
		ID:                     uuid.NewID(),
		Name:                   "name",
		Passphrase:             "passphrase",
		Iterations:             10000,
		Salt:                   "salt",
		PassphraseScheme:       1,
		Compromised:            false,
		LockedUntil:            time.Now().Add(time.Hour),
		PassphraseReset:        "passphrase reset",
		PassphraseResetCreated: time.Now(),
		Created:                time.Now(),
		LastSeen:               time.Now(),
	}
	for _, store := range profileStores {
		err := store.SaveProfile(profile)
		if err != nil {
			t.Errorf("Error saving profile to %T: %s", store, err)
		}
		err = store.SaveProfile(profile)
		if err != ErrProfileAlreadyExists {
			t.Errorf("Expected ErrProfileAlreadyExists from %T, got %+v", store, err)
		}
		retrieved, err := store.GetProfileByID(profile.ID)
		if err != nil {
			t.Errorf("Error retrieving profile from %T: %s", store, err)
		}
		match, field, expectation, result := compareProfiles(profile, retrieved)
		if !match {
			t.Errorf("Expected `%v` in the `%s` field of profile retrieved from %T, got `%v`", expectation, field, store, result)
		}
		err = store.DeleteProfile(profile.ID)
		if err != nil {
			t.Errorf("Error removing profile from %T: %s", store, err)
		}
		retrieved, err = store.GetProfileByID(profile.ID)
		if err != ErrProfileNotFound {
			t.Errorf("Expected ErrProfileNotFound from %T, got %+v and %+v", store, retrieved, err)
		}
		err = store.DeleteProfile(profile.ID)
		if err != ErrProfileNotFound {
			t.Errorf("Expected ErrProfileNotFound from %T, got %+v", store, err)
		}
	}
}

func TestProfileUpdates(t *testing.T) {
	t.Parallel()
	variations := 1 << 10
	profile := Profile{
		ID:                     uuid.NewID(),
		Name:                   "name",
		Passphrase:             "passphrase",
		Iterations:             10000,
		Salt:                   "salt",
		PassphraseScheme:       1,
		Compromised:            false,
		LockedUntil:            time.Now().Add(time.Hour),
		PassphraseReset:        "passphrase reset",
		PassphraseResetCreated: time.Now(),
		Created:                time.Now(),
		LastSeen:               time.Now(),
	}
	for i := 0; i < variations; i++ {
		var name, passphrase, salt, passphraseReset string
		var iterations int64
		var lockedUntil, passphraseResetCreated, lastSeen time.Time
		var passphraseScheme int
		var compromised bool

		change := ProfileChange{}
		expectation := profile
		result := profile
		if i&profileChangeName != 0 {
			name = fmt.Sprintf("name-%d", i)
			change.Name = &name
			expectation.Name = name
		}
		if i&profileChangePassphrase != 0 {
			passphrase = fmt.Sprintf("passphrase-%d", i)
			change.Passphrase = &passphrase
			expectation.Passphrase = passphrase
		}
		if i&profileChangeIterations != 0 {
			iterations = int64(i)
			change.Iterations = &iterations
			expectation.Iterations = iterations
		}
		if i&profileChangeSalt != 0 {
			salt = fmt.Sprintf("salt-%d", i)
			change.Salt = &salt
			expectation.Salt = salt
		}
		if i&profileChangePassphraseScheme != 0 {
			passphraseScheme = i
			change.PassphraseScheme = &passphraseScheme
			expectation.PassphraseScheme = passphraseScheme
		}
		if i&profileChangeCompromised != 0 {
			compromised = i%2 != 0
			change.Compromised = &compromised
			expectation.Compromised = compromised
		}
		if i&profileChangeLockedUntil != 0 {
			lockedUntil = time.Now()
			change.LockedUntil = &lockedUntil
			expectation.LockedUntil = lockedUntil
		}
		if i&profileChangePassphraseReset != 0 {
			passphraseReset = fmt.Sprintf("passphraseReset-%d", i)
			change.PassphraseReset = &passphraseReset
			expectation.PassphraseReset = passphraseReset
		}
		if i&profileChangePassphraseResetCreated != 0 {
			passphraseResetCreated = time.Now()
			change.PassphraseResetCreated = &passphraseResetCreated
			expectation.PassphraseResetCreated = passphraseResetCreated
		}
		if i&profileChangeLastSeen != 0 {
			lastSeen = time.Now()
			change.LastSeen = &lastSeen
			expectation.LastSeen = lastSeen
		}
		result.ApplyChange(change)
		match, field, expected, got := compareProfiles(expectation, result)
		if !match {
			t.Errorf("Expected field `%s` to be `%v`, got `%v`", field, expected, got)
		}
		for _, store := range profileStores {
			err := store.SaveProfile(profile)
			if err != nil {
				t.Errorf("Error saving profile in %T: %s", store, err)
			}
			err = store.UpdateProfile(profile.ID, change)
			if err != nil {
				t.Errorf("Error updating profile in %T: %s", store, err)
			}
			retrieved, err := store.GetProfileByID(profile.ID)
			if err != nil {
				t.Errorf("Error getting profile from %T: %s", store, err)
			}
			match, field, expected, got = compareProfiles(expectation, retrieved)
			if !match {
				t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
			}
			err = store.DeleteProfile(profile.ID)
			if err != nil {
				t.Errorf("Error deleting profile from %T: %s", store, err)
			}
			err = store.UpdateProfile(profile.ID, change)
			if err != ErrProfileNotFound {
				t.Errorf("Expected ErrProfileNotFound, got %v from %T", err, store)
			}
		}
	}
}

func TestProfilesUpdates(t *testing.T) {
	profile1 := Profile{
		ID: uuid.NewID(),
	}
	profile2 := Profile{
		ID: uuid.NewID(),
	}
	profile3 := Profile{
		ID: uuid.NewID(),
	}
	truth := true
	change := BulkProfileChange{
		Compromised: &truth,
	}
	for _, store := range profileStores {
		err := store.SaveProfile(profile1)
		if err != nil {
			t.Errorf("Error saving profile in %T: %s", store, err)
		}
		err = store.SaveProfile(profile2)
		if err != nil {
			t.Errorf("Error saving profile in %T: %s", store, err)
		}
		err = store.SaveProfile(profile3)
		if err != nil {
			t.Errorf("Error saving profile in %T: %s", store, err)
		}
		err = store.UpdateProfiles([]uuid.ID{profile1.ID, profile3.ID}, change)
		if err != nil {
			t.Errorf("Error updating profile in %T: %s", store, err)
		}
		profile1.Compromised = truth
		profile3.Compromised = truth
		retrieved, err := store.GetProfileByID(profile1.ID)
		if err != nil {
			t.Errorf("Error getting profile from %T: %s", store, err)
		}
		match, field, expected, got := compareProfiles(profile1, retrieved)
		if !match {
			t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
		}
		retrieved, err = store.GetProfileByID(profile2.ID)
		if err != nil {
			t.Errorf("Error getting profile from %T: %s", store, err)
		}
		match, field, expected, got = compareProfiles(profile2, retrieved)
		if !match {
			t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
		}
		retrieved, err = store.GetProfileByID(profile3.ID)
		if err != nil {
			t.Errorf("Error getting profile from %T: %s", store, err)
		}
		match, field, expected, got = compareProfiles(profile3, retrieved)
		if !match {
			t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
		}
	}
}
