// Copyright IBM Corp. 2020, 2026
// SPDX-License-Identifier: BUSL-1.1

package password

import (
	"context"
	"slices"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"github.com/hashicorp/boundary/globals"
	"github.com/hashicorp/boundary/internal/auth/password/store"
	"github.com/hashicorp/boundary/internal/db"
	dbassert "github.com/hashicorp/boundary/internal/db/assert"
	"github.com/hashicorp/boundary/internal/db/timestamp"
	"github.com/hashicorp/boundary/internal/errors"
	"github.com/hashicorp/boundary/internal/iam"
	"github.com/hashicorp/boundary/internal/kms"
	"github.com/hashicorp/boundary/internal/oplog"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"google.golang.org/protobuf/types/known/timestamppb"
)

func TestCheckLoginName(t *testing.T) {
	tests := []struct {
		in   string
		want bool
	}{
		{"", false},
		{" leading-spaces", false},
		{"trailing-spaces ", false},
		{"contains spaces", false},
		{"NotLowerCase", false},
		{"valid.loginname", true},
		{"valid-loginname", true},
		{"validloginname", true},
	}
	for _, tt := range tests {
		t.Run(tt.in, func(t *testing.T) {
			assert.Equal(t, tt.want, validLoginName(tt.in))
		})
	}
}

func TestRepository_CreateAccount(t *testing.T) {
	conn, _ := db.TestSetup(t, "postgres")
	rw := db.New(conn)
	wrapper := db.TestWrapper(t)

	kms := kms.TestKms(t, conn, wrapper)
	iamRepo := iam.TestRepo(t, conn, wrapper)
	org, _ := iam.TestScopes(t, iamRepo)

	authMethods := TestAuthMethods(t, conn, org.GetPublicId(), 1)
	authMethod := authMethods[0]

	tests := []struct {
		name       string
		in         *Account
		opts       []Option
		want       *Account
		wantIsErr  errors.Code
		wantErrMsg string
	}{
		{
			name:       "nil-Account",
			wantIsErr:  errors.InvalidParameter,
			wantErrMsg: "password.(Repository).CreateAccount: missing Account: parameter violation: error #100",
		},
		{
			name:       "nil-embedded-Account",
			in:         &Account{},
			wantIsErr:  errors.InvalidParameter,
			wantErrMsg: "password.(Repository).CreateAccount: missing embedded Account: parameter violation: error #100",
		},
		{
			name: "invalid-no-scope-id",
			in: &Account{
				Account: &store.Account{},
			},
			wantIsErr:  errors.InvalidParameter,
			wantErrMsg: "password.(Repository).CreateAccount: missing auth method id: parameter violation: error #100",
		},
		{
			name: "invalid-public-id-set",
			in: &Account{
				Account: &store.Account{
					AuthMethodId: authMethod.PublicId,
					PublicId:     "hcst_OOOOOOOOOO",
				},
			},
			wantIsErr:  errors.InvalidParameter,
			wantErrMsg: "password.(Repository).CreateAccount: public id must be empty: parameter violation: error #100",
		},
		{
			name: "invalid-loginname-uppercase",
			in: &Account{
				Account: &store.Account{
					AuthMethodId: authMethod.PublicId,
					LoginName:    "KaZmiErcZak11",
				},
			},
			wantIsErr:  errors.InvalidParameter,
			wantErrMsg: "password.(Repository).CreateAccount: login name must be all-lowercase alphanumeric, period or hyphen. got: KaZmiErcZak11: parameter violation: error #100",
		},
		{
			name: "invalid-loginname-leading-space",
			in: &Account{
				Account: &store.Account{
					AuthMethodId: authMethod.PublicId,
					LoginName:    " kazmierczak12",
				},
			},
			wantIsErr:  errors.InvalidParameter,
			wantErrMsg: "password.(Repository).CreateAccount: login name must be all-lowercase alphanumeric, period or hyphen. got:  kazmierczak12: parameter violation: error #100",
		},
		{
			name: "invalid-loginname-trailing-space",
			in: &Account{
				Account: &store.Account{
					AuthMethodId: authMethod.PublicId,
					LoginName:    "kazmierczak13 ",
				},
			},
			wantIsErr:  errors.InvalidParameter,
			wantErrMsg: "password.(Repository).CreateAccount: login name must be all-lowercase alphanumeric, period or hyphen. got: kazmierczak13 : parameter violation: error #100",
		},
		{
			name: "invalid-loginname-space-in-name",
			in: &Account{
				Account: &store.Account{
					AuthMethodId: authMethod.PublicId,
					LoginName:    "kazmier czak14",
				},
			},
			wantIsErr:  errors.InvalidParameter,
			wantErrMsg: "password.(Repository).CreateAccount: login name must be all-lowercase alphanumeric, period or hyphen. got: kazmier czak14: parameter violation: error #100",
		},
		{
			name: "invalid-loginname-too-short",
			in: &Account{
				Account: &store.Account{
					AuthMethodId: authMethod.PublicId,
					LoginName:    "ka",
				},
			},
			wantIsErr:  errors.TooShort,
			wantErrMsg: "password.(Repository).CreateAccount: username: ka, must be longer than 3: integrity violation: error #113",
		},
		{
			name: "invalid-password-too-short",
			in: &Account{
				Account: &store.Account{
					AuthMethodId: authMethod.PublicId,
					LoginName:    "kazmierczak123",
				},
			},
			opts: []Option{
				WithPassword("a"),
			},
			wantIsErr:  errors.PasswordTooShort,
			wantErrMsg: "password.(Repository).CreateAccount: must be longer than 8: password violation: error #200",
		},
		{
			name: "valid-no-options",
			in: &Account{
				Account: &store.Account{
					AuthMethodId: authMethod.PublicId,
					LoginName:    "kazmierczak",
				},
			},
			want: &Account{
				Account: &store.Account{
					AuthMethodId: authMethod.PublicId,
					LoginName:    "kazmierczak",
				},
			},
		},
		{
			name: "valid-with-name",
			in: &Account{
				Account: &store.Account{
					AuthMethodId: authMethod.PublicId,
					Name:         "test-name-repo",
					LoginName:    "kazmierczak1",
				},
			},
			want: &Account{
				Account: &store.Account{
					AuthMethodId: authMethod.PublicId,
					Name:         "test-name-repo",
					LoginName:    "kazmierczak1",
				},
			},
		},
		{
			name: "valid-with-description",
			in: &Account{
				Account: &store.Account{
					AuthMethodId: authMethod.PublicId,
					Description:  ("test-description-repo"),
					LoginName:    "kazmierczak2",
				},
			},
			want: &Account{
				Account: &store.Account{
					AuthMethodId: authMethod.PublicId,
					Description:  ("test-description-repo"),
					LoginName:    "kazmierczak2",
				},
			},
		},
		{
			name: "valid-with-password",
			in: &Account{
				Account: &store.Account{
					AuthMethodId: authMethod.PublicId,
					LoginName:    "kazmierczak3",
				},
			},
			opts: []Option{
				WithPassword("1234567890"),
			},
			want: &Account{
				Account: &store.Account{
					AuthMethodId: authMethod.PublicId,
					LoginName:    "kazmierczak3",
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert, require := assert.New(t), require.New(t)
			repo, err := NewRepository(context.Background(), rw, rw, kms)
			assert.NoError(err)
			require.NotNil(repo)
			got, err := repo.CreateAccount(context.Background(), org.GetPublicId(), tt.in, tt.opts...)
			if tt.wantIsErr != 0 {
				assert.Truef(errors.Match(errors.T(tt.wantIsErr), err), "Unexpected error %s", err)
				assert.Equal(tt.wantErrMsg, err.Error())
				return
			}
			require.NoError(err)
			assert.Empty(tt.in.PublicId)
			require.NotNil(got)
			assertPublicId(t, globals.PasswordAccountPrefix, got.PublicId)
			assert.NotSame(tt.in, got)
			assert.Equal(tt.want.Name, got.Name)
			assert.Equal(tt.want.Description, got.Description)
			assert.Equal(got.CreateTime, got.UpdateTime)

			assert.NoError(db.TestVerifyOplog(t, rw, got.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second)))

			opts := GetOpts(tt.opts...)
			if opts.withPassword {
				authAcct, err := repo.Authenticate(context.Background(), org.GetPublicId(), tt.in.AuthMethodId, tt.in.LoginName, opts.password)
				require.NoError(err)
				assert.NoError(db.TestVerifyOplog(t, rw, authAcct.CredentialId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second)))
			}
		})
	}
}

func TestRepository_CreateAccount_DuplicateNames(t *testing.T) {
	conn, _ := db.TestSetup(t, "postgres")
	rw := db.New(conn)
	wrapper := db.TestWrapper(t)

	kms := kms.TestKms(t, conn, wrapper)
	iamRepo := iam.TestRepo(t, conn, wrapper)

	t.Run("invalid-duplicate-names", func(t *testing.T) {
		assert, require := assert.New(t), require.New(t)
		repo, err := NewRepository(context.Background(), rw, rw, kms)
		assert.NoError(err)
		require.NotNil(repo)

		org, _ := iam.TestScopes(t, iamRepo)
		authMethods := TestAuthMethods(t, conn, org.GetPublicId(), 1)
		authMethod := authMethods[0]

		in := &Account{
			Account: &store.Account{
				AuthMethodId: authMethod.GetPublicId(),
				Name:         "test-name-repo",
				LoginName:    "kazmierczak3",
			},
		}

		got, err := repo.CreateAccount(context.Background(), org.GetPublicId(), in)
		require.NoError(err)
		require.NotNil(got)
		assertPublicId(t, globals.PasswordAccountPrefix, got.PublicId)
		assert.NotSame(in, got)
		assert.Equal(in.Name, got.Name)
		assert.Equal(in.Description, got.Description)
		assert.Equal(got.CreateTime, got.UpdateTime)

		got2, err := repo.CreateAccount(context.Background(), org.GetPublicId(), in)
		assert.Truef(errors.Match(errors.T(errors.NotUnique), err), "Unexpected error %s", err)
		assert.Nil(got2)
	})

	t.Run("valid-duplicate-names-diff-parents", func(t *testing.T) {
		assert, require := assert.New(t), require.New(t)
		repo, err := NewRepository(context.Background(), rw, rw, kms)
		assert.NoError(err)
		require.NotNil(repo)

		org, _ := iam.TestScopes(t, iamRepo)
		authMethods := TestAuthMethods(t, conn, org.GetPublicId(), 2)
		authMethoda, authMethodb := authMethods[0], authMethods[1]
		in := &Account{
			Account: &store.Account{
				Name:      "test-name-repo",
				LoginName: "kazmierczak4",
			},
		}
		in2 := in.clone()

		in.AuthMethodId = authMethoda.GetPublicId()
		got, err := repo.CreateAccount(context.Background(), org.GetPublicId(), in)
		require.NoError(err)
		require.NotNil(got)
		assertPublicId(t, globals.PasswordAccountPrefix, got.PublicId)
		assert.NotSame(in, got)
		assert.Equal(in.Name, got.Name)
		assert.Equal(in.Description, got.Description)
		assert.Equal(got.CreateTime, got.UpdateTime)

		in2.AuthMethodId = authMethodb.GetPublicId()
		got2, err := repo.CreateAccount(context.Background(), org.GetPublicId(), in2)
		assert.NoError(err)
		require.NotNil(got2)
		assertPublicId(t, globals.PasswordAccountPrefix, got2.PublicId)
		assert.NotSame(in2, got2)
		assert.Equal(in2.Name, got2.Name)
		assert.Equal(in2.Description, got2.Description)
		assert.Equal(got2.CreateTime, got2.UpdateTime)
	})
}

func TestRepository_LookupAccount(t *testing.T) {
	ctx := context.Background()
	conn, _ := db.TestSetup(t, "postgres")
	rw := db.New(conn)
	wrapper := db.TestWrapper(t)

	kms := kms.TestKms(t, conn, wrapper)
	iamRepo := iam.TestRepo(t, conn, wrapper)
	org, _ := iam.TestScopes(t, iamRepo)

	authMethod := TestAuthMethods(t, conn, org.GetPublicId(), 1)[0]
	account := TestAccount(t, conn, authMethod.GetPublicId(), "name1")

	newAcctId, err := newAccountId(ctx)
	require.NoError(t, err)
	tests := []struct {
		name       string
		in         string
		want       *Account
		wantIsErr  errors.Code
		wantErrMsg string
	}{
		{
			name:       "With no public id",
			wantIsErr:  errors.InvalidPublicId,
			wantErrMsg: "password.(Repository).LookupAccount: missing public id: parameter violation: error #102",
		},
		{
			name: "With non existing account id",
			in:   newAcctId,
		},
		{
			name: "With existing account id",
			in:   account.GetPublicId(),
			want: account,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert, require := assert.New(t), require.New(t)
			repo, err := NewRepository(ctx, rw, rw, kms)
			assert.NoError(err)
			require.NotNil(repo)
			got, err := repo.LookupAccount(ctx, tt.in)
			if tt.wantIsErr != 0 {
				assert.Truef(errors.Match(errors.T(tt.wantIsErr), err), "Unexpected error %s", err)
				assert.Equal(tt.wantErrMsg, err.Error())
				return
			}
			require.NoError(err)
			assert.EqualValues(tt.want, got)
		})
	}
}

func TestRepository_DeleteAccount(t *testing.T) {
	ctx := context.Background()
	conn, _ := db.TestSetup(t, "postgres")
	rw := db.New(conn)
	wrapper := db.TestWrapper(t)

	kms := kms.TestKms(t, conn, wrapper)
	iamRepo := iam.TestRepo(t, conn, wrapper)
	org, _ := iam.TestScopes(t, iamRepo)

	authMethod := TestAuthMethods(t, conn, org.GetPublicId(), 1)[0]
	account := TestAccount(t, conn, authMethod.GetPublicId(), "name1")

	newAcctId, err := newAccountId(ctx)
	require.NoError(t, err)
	tests := []struct {
		name       string
		in         string
		want       int
		wantIsErr  errors.Code
		wantErrMsg string
	}{
		{
			name:       "With no public id",
			wantIsErr:  errors.InvalidPublicId,
			wantErrMsg: "password.(Repository).DeleteAccount: missing public id: parameter violation: error #102",
		},
		{
			name: "With non existing account id",
			in:   newAcctId,
			want: 0,
		},
		{
			name: "With existing account id",
			in:   account.GetPublicId(),
			want: 1,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert, require := assert.New(t), require.New(t)
			repo, err := NewRepository(ctx, rw, rw, kms)
			assert.NoError(err)
			require.NotNil(repo)
			got, err := repo.DeleteAccount(ctx, org.GetPublicId(), tt.in)
			if tt.wantIsErr != 0 {
				assert.Truef(errors.Match(errors.T(tt.wantIsErr), err), "Unexpected error %s", err)
				assert.Equal(tt.wantErrMsg, err.Error())
				return
			}
			require.NoError(err)
			assert.EqualValues(tt.want, got)
		})
	}
}

func TestRepository_ListAccounts(t *testing.T) {
	conn, _ := db.TestSetup(t, "postgres")
	rw := db.New(conn)
	wrapper := db.TestWrapper(t)

	kms := kms.TestKms(t, conn, wrapper)
	iamRepo := iam.TestRepo(t, conn, wrapper)
	org, _ := iam.TestScopes(t, iamRepo)

	authMethods := TestAuthMethods(t, conn, org.GetPublicId(), 3)
	accounts1 := TestMultipleAccounts(t, conn, authMethods[0].GetPublicId(), 3)
	accounts2 := TestMultipleAccounts(t, conn, authMethods[1].GetPublicId(), 4)
	_ = accounts2
	slices.Reverse(accounts1)

	tests := []struct {
		name       string
		in         string
		opts       []Option
		want       []*Account
		wantIsErr  errors.Code
		wantErrMsg string
	}{
		{
			name:       "With no auth method id",
			wantIsErr:  errors.InvalidParameter,
			wantErrMsg: "password.(Repository).listAccounts: missing auth method id: parameter violation: error #100",
		},
		{
			name: "With no accounts id",
			in:   authMethods[2].GetPublicId(),
			want: []*Account{},
		},
		{
			name: "With first auth method id",
			in:   authMethods[0].GetPublicId(),
			want: accounts1,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert, require := assert.New(t), require.New(t)
			repo, err := NewRepository(context.Background(), rw, rw, kms)
			assert.NoError(err)
			require.NotNil(repo)
			got, ttime, err := repo.listAccounts(context.Background(), tt.in, tt.opts...)
			if tt.wantIsErr != 0 {
				assert.Truef(errors.Match(errors.T(tt.wantIsErr), err), "Unexpected error %s", err)
				assert.Equal(tt.wantErrMsg, err.Error())
				return
			}
			require.NoError(err)
			assert.Empty(cmp.Diff(tt.want, got, cmpopts.IgnoreUnexported(Account{}, store.Account{}, timestamp.Timestamp{}, timestamppb.Timestamp{})))
			// Transaction timestamp should be within ~10 seconds of now
			assert.True(time.Now().Before(ttime.Add(10 * time.Second)))
			assert.True(time.Now().After(ttime.Add(-10 * time.Second)))
		})
	}
}

func TestRepository_ListAccounts_Limits(t *testing.T) {
	conn, _ := db.TestSetup(t, "postgres")
	rw := db.New(conn)
	wrapper := db.TestWrapper(t)

	kms := kms.TestKms(t, conn, wrapper)
	iamRepo := iam.TestRepo(t, conn, wrapper)
	org, _ := iam.TestScopes(t, iamRepo)

	am := TestAuthMethods(t, conn, org.GetPublicId(), 1)[0]

	accountCount := 10
	_ = TestMultipleAccounts(t, conn, am.GetPublicId(), accountCount)

	tests := []struct {
		name     string
		repoOpts []Option
		listOpts []Option
		wantLen  int
	}{
		{
			name:    "With no limits",
			wantLen: accountCount,
		},
		{
			name:     "With repo limit",
			repoOpts: []Option{WithLimit(3)},
			wantLen:  3,
		},
		{
			name:     "With negative repo limit",
			repoOpts: []Option{WithLimit(-1)},
			wantLen:  accountCount,
		},
		{
			name:     "With List limit",
			listOpts: []Option{WithLimit(3)},
			wantLen:  3,
		},
		{
			name:     "With negative List limit",
			listOpts: []Option{WithLimit(-1)},
			wantLen:  accountCount,
		},
		{
			name:     "With repo smaller than list limit",
			repoOpts: []Option{WithLimit(2)},
			listOpts: []Option{WithLimit(6)},
			wantLen:  6,
		},
		{
			name:     "With repo larger than list limit",
			repoOpts: []Option{WithLimit(6)},
			listOpts: []Option{WithLimit(2)},
			wantLen:  2,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert, require := assert.New(t), require.New(t)
			repo, err := NewRepository(context.Background(), rw, rw, kms, tt.repoOpts...)
			assert.NoError(err)
			require.NotNil(repo)
			got, ttime, err := repo.listAccounts(context.Background(), am.GetPublicId(), tt.listOpts...)
			require.NoError(err)
			assert.Len(got, tt.wantLen)
			// Transaction timestamp should be within ~10 seconds of now
			assert.True(time.Now().Before(ttime.Add(10 * time.Second)))
			assert.True(time.Now().After(ttime.Add(-10 * time.Second)))
		})
	}
}

func TestRepository_UpdateAccount(t *testing.T) {
	conn, _ := db.TestSetup(t, "postgres")
	rw := db.New(conn)
	wrapper := db.TestWrapper(t)
	kms := kms.TestKms(t, conn, wrapper)
	iamRepo := iam.TestRepo(t, conn, wrapper)
	ctx := context.Background()

	changeLoginName := func(s string) func(*Account) *Account {
		return func(a *Account) *Account {
			a.LoginName = s
			return a
		}
	}

	changeName := func(s string) func(*Account) *Account {
		return func(a *Account) *Account {
			a.Name = s
			return a
		}
	}

	changeDescription := func(s string) func(*Account) *Account {
		return func(a *Account) *Account {
			a.Description = s
			return a
		}
	}

	makeNil := func() func(*Account) *Account {
		return func(a *Account) *Account {
			return nil
		}
	}

	makeEmbeddedNil := func() func(*Account) *Account {
		return func(a *Account) *Account {
			return &Account{}
		}
	}

	deletePublicId := func() func(*Account) *Account {
		return func(a *Account) *Account {
			a.PublicId = ""
			return a
		}
	}

	nonExistentPublicId := func() func(*Account) *Account {
		return func(a *Account) *Account {
			a.PublicId = "abcd_OOOOOOOOOO"
			return a
		}
	}

	combine := func(fns ...func(a *Account) *Account) func(*Account) *Account {
		return func(a *Account) *Account {
			for _, fn := range fns {
				a = fn(a)
			}
			return a
		}
	}

	tests := []struct {
		name       string
		orig       *Account
		chgFn      func(*Account) *Account
		masks      []string
		want       *Account
		wantCount  int
		wantIsErr  errors.Code
		wantErrMsg string
	}{
		{
			name: "nil-Account",
			orig: &Account{
				Account: &store.Account{},
			},
			chgFn:      makeNil(),
			masks:      []string{"Name", "Description"},
			wantIsErr:  errors.InvalidParameter,
			wantErrMsg: "password.(Repository).UpdateAccount: missing Account: parameter violation: error #100",
		},
		{
			name: "nil-embedded-Account",
			orig: &Account{
				Account: &store.Account{},
			},
			chgFn:      makeEmbeddedNil(),
			masks:      []string{"Name", "Description"},
			wantIsErr:  errors.InvalidParameter,
			wantErrMsg: "password.(Repository).UpdateAccount: missing embedded Account: parameter violation: error #100",
		},
		{
			name: "no-public-id",
			orig: &Account{
				Account: &store.Account{},
			},
			chgFn:      deletePublicId(),
			masks:      []string{"Name", "Description"},
			wantIsErr:  errors.InvalidPublicId,
			wantErrMsg: "password.(Repository).UpdateAccount: missing public id: parameter violation: error #102",
		},
		{
			name: "updating-non-existent-Account",
			orig: &Account{
				Account: &store.Account{
					Name: "test-name-repo",
				},
			},
			chgFn:      combine(nonExistentPublicId(), changeName("test-update-name-repo")),
			masks:      []string{"Name"},
			wantIsErr:  errors.RecordNotFound,
			wantErrMsg: "password.(Repository).UpdateAccount: abcd_OOOOOOOOOO: db.DoTx: password.(Repository).UpdateAccount: db.Update: record not found, search issue: error #1100: dbw.Update: dbw.lookupAfterWrite: dbw.LookupById: record not found",
		},
		{
			name: "empty-field-mask",
			orig: &Account{
				Account: &store.Account{
					Name: "test-name-repo",
				},
			},
			chgFn:      changeName("test-update-name-repo"),
			wantIsErr:  errors.EmptyFieldMask,
			wantErrMsg: "password.(Repository).UpdateAccount: missing field mask: parameter violation: error #104",
		},
		{
			name: "read-only-fields-in-field-mask",
			orig: &Account{
				Account: &store.Account{
					Name: "test-name-repo",
				},
			},
			chgFn:      changeName("test-update-name-repo"),
			masks:      []string{"PublicId", "CreateTime", "UpdateTime", "AuthMethodId"},
			wantIsErr:  errors.InvalidFieldMask,
			wantErrMsg: "password.(Repository).UpdateAccount: PublicId: parameter violation: error #103",
		},
		{
			name: "unknown-field-in-field-mask",
			orig: &Account{
				Account: &store.Account{
					Name: "test-name-repo",
				},
			},
			chgFn:      changeName("test-update-name-repo"),
			masks:      []string{"Bilbo"},
			wantIsErr:  errors.InvalidFieldMask,
			wantErrMsg: "password.(Repository).UpdateAccount: Bilbo: parameter violation: error #103",
		},
		{
			name: "change-name",
			orig: &Account{
				Account: &store.Account{
					Name: "test-name-repo",
				},
			},
			chgFn: changeName("test-update-name-repo"),
			masks: []string{"Name"},
			want: &Account{
				Account: &store.Account{
					Name: "test-update-name-repo",
				},
			},
			wantCount: 1,
		},
		{
			name: "change-description",
			orig: &Account{
				Account: &store.Account{
					Description: "test-description-repo",
				},
			},
			chgFn: changeDescription("test-update-description-repo"),
			masks: []string{"Description"},
			want: &Account{
				Account: &store.Account{
					Description: "test-update-description-repo",
				},
			},
			wantCount: 1,
		},
		{
			name: "change-name-and-description",
			orig: &Account{
				Account: &store.Account{
					Name:        "test-name-repo",
					Description: "test-description-repo",
				},
			},
			chgFn: combine(changeDescription("test-update-description-repo"), changeName("test-update-name-repo")),
			masks: []string{"Name", "Description"},
			want: &Account{
				Account: &store.Account{
					Name:        "test-update-name-repo",
					Description: "test-update-description-repo",
				},
			},
			wantCount: 1,
		},
		{
			name: "delete-name",
			orig: &Account{
				Account: &store.Account{
					Name:        "test-name-repo",
					Description: "test-description-repo",
				},
			},
			masks: []string{"Name"},
			chgFn: combine(changeDescription("test-update-description-repo"), changeName("")),
			want: &Account{
				Account: &store.Account{
					Description: "test-description-repo",
				},
			},
			wantCount: 1,
		},
		{
			name: "delete-description",
			orig: &Account{
				Account: &store.Account{
					Name:        "test-name-repo",
					Description: "test-description-repo",
				},
			},
			masks: []string{"Description"},
			chgFn: combine(changeDescription(""), changeName("test-update-name-repo")),
			want: &Account{
				Account: &store.Account{
					Name: "test-name-repo",
				},
			},
			wantCount: 1,
		},
		{
			name: "do-not-delete-name",
			orig: &Account{
				Account: &store.Account{
					Name:        "test-name-repo",
					Description: "test-description-repo",
				},
			},
			masks: []string{"Description"},
			chgFn: combine(changeDescription("test-update-description-repo"), changeName("")),
			want: &Account{
				Account: &store.Account{
					Name:        "test-name-repo",
					Description: "test-update-description-repo",
				},
			},
			wantCount: 1,
		},
		{
			name: "do-not-delete-description",
			orig: &Account{
				Account: &store.Account{
					Name:        "test-name-repo",
					Description: "test-description-repo",
				},
			},
			masks: []string{"Name"},
			chgFn: combine(changeDescription(""), changeName("test-update-name-repo")),
			want: &Account{
				Account: &store.Account{
					Name:        "test-update-name-repo",
					Description: "test-description-repo",
				},
			},
			wantCount: 1,
		},
		{
			name: "change-login-name",
			orig: &Account{
				Account: &store.Account{
					LoginName: "kazmierczak",
				},
			},
			chgFn: changeLoginName("mothball"),
			masks: []string{"LoginName"},
			want: &Account{
				Account: &store.Account{
					LoginName: "mothball",
				},
			},
			wantCount: 1,
		},
		{
			name: "change-login-name-mixed-caps",
			orig: &Account{
				Account: &store.Account{
					LoginName: "kazmierczak",
				},
			},
			chgFn:      changeLoginName("KaZmIeRcZaK"),
			masks:      []string{"LoginName"},
			wantIsErr:  errors.InvalidParameter,
			wantErrMsg: "password.(Repository).UpdateAccount: invalid username: must be all-lowercase alphanumeric, period or hyphen, got KaZmIeRcZaK: parameter violation: error #100",
		},
		{
			name: "change-login-name-to-short",
			orig: &Account{
				Account: &store.Account{
					LoginName: "kazmierczak",
				},
			},
			chgFn:      changeLoginName("ka"),
			masks:      []string{"LoginName"},
			wantIsErr:  errors.TooShort,
			wantErrMsg: "password.(Repository).UpdateAccount: username: ka, must be longer than 3: integrity violation: error #113",
		},
		{
			name: "delete-login-name",
			orig: &Account{
				Account: &store.Account{
					LoginName: "kazmierczak",
				},
			},
			chgFn:      changeLoginName(""),
			masks:      []string{"LoginName"},
			wantIsErr:  errors.InvalidParameter,
			wantErrMsg: "password.(Repository).UpdateAccount: invalid username: must be all-lowercase alphanumeric, period or hyphen, got : parameter violation: error #100",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert, require := assert.New(t), require.New(t)
			repo, err := NewRepository(context.Background(), rw, rw, kms)
			assert.NoError(err)
			require.NotNil(repo)

			org, _ := iam.TestScopes(t, iamRepo)
			am := TestAuthMethods(t, conn, org.PublicId, 1)[0]

			tt.orig.AuthMethodId = am.PublicId
			if tt.orig.LoginName == "" {
				tt.orig.LoginName = "kazmierczak"
			}
			orig, err := repo.CreateAccount(context.Background(), org.GetPublicId(), tt.orig)
			assert.NoError(err)
			require.NotNil(orig)

			if tt.chgFn != nil {
				orig = tt.chgFn(orig)
			}
			got, gotCount, err := repo.UpdateAccount(context.Background(), org.GetPublicId(), orig, 1, tt.masks)
			if tt.wantIsErr != 0 {
				assert.Truef(errors.Match(errors.T(tt.wantIsErr), err), "want err: %q got: %q", tt.wantIsErr, err)
				assert.Equal(tt.wantErrMsg, err.Error())
				assert.Equal(tt.wantCount, gotCount, "row count")
				assert.Nil(got)
				return
			}
			assert.NoError(err)
			assert.Empty(tt.orig.PublicId)
			if tt.wantCount == 0 {
				assert.Equal(tt.wantCount, gotCount, "row count")
				assert.Nil(got)
				return
			}
			require.NotNil(got)
			assertPublicId(t, globals.PasswordAccountPrefix, got.PublicId)
			assert.Equal(tt.wantCount, gotCount, "row count")
			assert.NotSame(tt.orig, got)
			assert.Equal(tt.orig.AuthMethodId, got.AuthMethodId)
			underlyingDB, err := conn.SqlDB(ctx)
			require.NoError(err)
			dbassert := dbassert.New(t, underlyingDB)
			if tt.want.Name == "" {
				dbassert.IsNull(got, "name")
				return
			}
			assert.Equal(tt.want.Name, got.Name)
			if tt.want.Description == "" {
				dbassert.IsNull(got, "description")
				return
			}
			assert.Equal(tt.want.Description, got.Description)
			if tt.wantCount > 0 {
				assert.NoError(db.TestVerifyOplog(t, rw, got.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)))
			}
		})
	}
}

func TestRepository_UpdateAccount_DupeNames(t *testing.T) {
	ctx := context.Background()
	conn, _ := db.TestSetup(t, "postgres")
	rw := db.New(conn)
	wrapper := db.TestWrapper(t)
	kms := kms.TestKms(t, conn, wrapper)
	iamRepo := iam.TestRepo(t, conn, wrapper)

	t.Run("invalid-duplicate-names", func(t *testing.T) {
		assert, require := assert.New(t), require.New(t)
		repo, err := NewRepository(ctx, rw, rw, kms)
		assert.NoError(err)
		require.NotNil(repo)

		name := "test-dup-name"
		org, _ := iam.TestScopes(t, iamRepo)
		am := TestAuthMethods(t, conn, org.PublicId, 1)[0]
		acts := TestMultipleAccounts(t, conn, am.PublicId, 2)

		aa := acts[0]
		ab := acts[1]

		aa.Name = name
		got1, gotCount1, err := repo.UpdateAccount(context.Background(), org.GetPublicId(), aa, 1, []string{"name"})
		assert.NoError(err)
		require.NotNil(got1)
		assert.Equal(name, got1.Name)
		assert.Equal(1, gotCount1, "row count")
		assert.NoError(db.TestVerifyOplog(t, rw, aa.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)))

		ab.Name = name
		got2, gotCount2, err := repo.UpdateAccount(context.Background(), org.GetPublicId(), ab, 1, []string{"name"})
		assert.Truef(errors.Match(errors.T(errors.NotUnique), err), "Unexpected error %s", err)
		assert.Nil(got2)
		assert.Equal(db.NoRowsAffected, gotCount2, "row count")
		err = db.TestVerifyOplog(t, rw, ab.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))
		assert.Error(err)
		assert.True(errors.IsNotFoundError(err))
	})

	t.Run("valid-duplicate-names-diff-AuthMethods", func(t *testing.T) {
		assert, require := assert.New(t), require.New(t)
		repo, err := NewRepository(ctx, rw, rw, kms)
		assert.NoError(err)
		require.NotNil(repo)

		org, _ := iam.TestScopes(t, iamRepo)
		ams := TestAuthMethods(t, conn, org.PublicId, 2)

		ama := ams[0]
		amb := ams[1]

		in := &Account{
			Account: &store.Account{
				Name: "test-name-repo",
			},
		}
		in2 := in.clone()

		in.AuthMethodId = ama.PublicId
		in.LoginName = "kazmierczak"
		got, err := repo.CreateAccount(context.Background(), org.GetPublicId(), in)
		assert.NoError(err)
		require.NotNil(got)
		assertPublicId(t, globals.PasswordAccountPrefix, got.PublicId)
		assert.NotSame(in, got)
		assert.Equal(in.Name, got.Name)
		assert.Equal(in.Description, got.Description)

		in2.AuthMethodId = amb.PublicId
		in2.LoginName = "kazmierczak2"
		in2.Name = "first-name"
		got2, err := repo.CreateAccount(context.Background(), org.GetPublicId(), in2)
		assert.NoError(err)
		require.NotNil(got2)
		got2.Name = got.Name
		got3, gotCount3, err := repo.UpdateAccount(context.Background(), org.GetPublicId(), got2, 1, []string{"name"})
		assert.NoError(err)
		require.NotNil(got3)
		assert.NotSame(got2, got3)
		assert.Equal(got.Name, got3.Name)
		assert.Equal(got2.Description, got3.Description)
		assert.Equal(1, gotCount3, "row count")
		assert.NoError(db.TestVerifyOplog(t, rw, got2.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)))
	})

	t.Run("invalid-duplicate-loginnames", func(t *testing.T) {
		assert, require := assert.New(t), require.New(t)
		repo, err := NewRepository(ctx, rw, rw, kms)
		assert.NoError(err)
		require.NotNil(repo)

		loginName := "kazmierczak12"
		org, _ := iam.TestScopes(t, iamRepo)
		am := TestAuthMethods(t, conn, org.PublicId, 1)[0]
		acts := TestMultipleAccounts(t, conn, am.PublicId, 2)

		aa := acts[0]
		ab := acts[1]

		aa.LoginName = loginName
		got1, gotCount1, err := repo.UpdateAccount(context.Background(), org.GetPublicId(), aa, 1, []string{"LoginName"})
		assert.NoError(err)
		require.NotNil(got1)
		assert.Equal(loginName, got1.LoginName)
		assert.Equal(1, gotCount1, "row count")
		assert.NoError(db.TestVerifyOplog(t, rw, aa.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)))

		ab.LoginName = loginName
		got2, gotCount2, err := repo.UpdateAccount(context.Background(), org.GetPublicId(), ab, 1, []string{"LoginName"})
		assert.Truef(errors.Match(errors.T(errors.NotUnique), err), "Unexpected error %s", err)
		assert.Nil(got2)
		assert.Equal(db.NoRowsAffected, gotCount2, "row count")
		err = db.TestVerifyOplog(t, rw, ab.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))
		assert.Error(err)
		assert.True(errors.IsNotFoundError(err))
	})

	t.Run("valid-duplicate-loginnames-diff-AuthMethods", func(t *testing.T) {
		assert, require := assert.New(t), require.New(t)
		repo, err := NewRepository(ctx, rw, rw, kms)
		assert.NoError(err)
		require.NotNil(repo)

		org, _ := iam.TestScopes(t, iamRepo)
		ams := TestAuthMethods(t, conn, org.PublicId, 2)

		ama := ams[0]
		amb := ams[1]

		in := &Account{
			Account: &store.Account{
				LoginName: "kazmierczak",
			},
		}
		in2 := in.clone()

		in.AuthMethodId = ama.PublicId
		got, err := repo.CreateAccount(context.Background(), org.GetPublicId(), in)
		assert.NoError(err)
		require.NotNil(got)
		assertPublicId(t, globals.PasswordAccountPrefix, got.PublicId)
		assert.NotSame(in, got)
		assert.Equal(in.LoginName, got.LoginName)

		in2.AuthMethodId = amb.PublicId
		in2.LoginName = "kazmierczak2"
		got2, err := repo.CreateAccount(context.Background(), org.GetPublicId(), in2)
		assert.NoError(err)
		require.NotNil(got2)
		got2.LoginName = got.LoginName
		got3, gotCount3, err := repo.UpdateAccount(context.Background(), org.GetPublicId(), got2, 1, []string{"LoginName"})
		assert.NoError(err)
		require.NotNil(got3)
		assert.NotSame(got2, got3)
		assert.Equal(got.LoginName, got3.LoginName)
		assert.Equal(1, gotCount3, "row count")
		assert.NoError(db.TestVerifyOplog(t, rw, got2.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)))
	})

	t.Run("change-authmethod-id", func(t *testing.T) {
		assert, require := assert.New(t), require.New(t)
		repo, err := NewRepository(ctx, rw, rw, kms)
		assert.NoError(err)
		require.NotNil(repo)

		org, _ := iam.TestScopes(t, iamRepo)
		ams := TestAuthMethods(t, conn, org.PublicId, 2)

		ama := ams[0]
		amb := ams[1]

		aa := TestMultipleAccounts(t, conn, ama.PublicId, 1)[0]
		ab := TestMultipleAccounts(t, conn, amb.PublicId, 1)[0]

		assert.NotEqual(aa.AuthMethodId, ab.AuthMethodId)
		orig := aa.clone()

		aa.AuthMethodId = ab.AuthMethodId
		assert.Equal(aa.AuthMethodId, ab.AuthMethodId)

		got1, gotCount1, err := repo.UpdateAccount(context.Background(), org.GetPublicId(), aa, 1, []string{"name"})

		assert.NoError(err)
		require.NotNil(got1)
		assert.Equal(orig.AuthMethodId, got1.AuthMethodId)
		assert.Equal(1, gotCount1, "row count")
		assert.NoError(db.TestVerifyOplog(t, rw, aa.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)))
	})
}
