package integration

import (
	"testing"

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

	"gitlab.com/verygoodsoftwarenotvirus/todo/internal/authorization"
	"gitlab.com/verygoodsoftwarenotvirus/todo/internal/observability/tracing"
	"gitlab.com/verygoodsoftwarenotvirus/todo/pkg/client/httpclient"
	"gitlab.com/verygoodsoftwarenotvirus/todo/pkg/types"
	"gitlab.com/verygoodsoftwarenotvirus/todo/pkg/types/fakes"
)

func checkAccountEquality(t *testing.T, expected, actual *types.Account) {
	t.Helper()

	assert.NotZero(t, actual.ID)
	assert.Equal(t, expected.Name, actual.Name, "expected BucketName for account %s to be %v, but it was %v ", expected.ID, expected.Name, actual.Name)
	assert.NotZero(t, actual.CreatedOn)
}

func (s *TestSuite) TestAccounts_Creating() {
	s.runForEachClientExcept("should be possible to create accounts", func(testClients *testClientWrapper) func() {
		return func() {
			t := s.T()

			ctx, span := tracing.StartCustomSpan(s.ctx, t.Name())
			defer span.End()

			exampleAccount := fakes.BuildFakeAccount()
			exampleAccountInput := fakes.BuildFakeAccountCreationInputFromAccount(exampleAccount)
			createdAccount, err := testClients.main.CreateAccount(ctx, exampleAccountInput)
			requireNotNilAndNoProblems(t, createdAccount, err)

			checkAccountEquality(t, exampleAccount, createdAccount)

			assert.NoError(t, testClients.main.ArchiveAccount(ctx, createdAccount.ID))
		}
	})
}

func (s *TestSuite) TestAccounts_Listing() {
	s.runForEachClientExcept("should be possible to list accounts", func(testClients *testClientWrapper) func() {
		return func() {
			t := s.T()

			ctx, span := tracing.StartCustomSpan(s.ctx, t.Name())
			defer span.End()

			var expected []*types.Account
			for i := 0; i < 5; i++ {

				exampleAccount := fakes.BuildFakeAccount()
				exampleAccountInput := fakes.BuildFakeAccountCreationInputFromAccount(exampleAccount)
				createdAccount, accountCreationErr := testClients.main.CreateAccount(ctx, exampleAccountInput)
				requireNotNilAndNoProblems(t, createdAccount, accountCreationErr)

				expected = append(expected, createdAccount)
			}

			actual, err := testClients.main.GetAccounts(ctx, nil)
			requireNotNilAndNoProblems(t, actual, err)
			assert.True(
				t,
				len(expected) <= len(actual.Accounts),
				"expected %d to be <= %d",
				len(expected),
				len(actual.Accounts),
			)

			for _, createdAccount := range actual.Accounts {
				assert.NoError(t, testClients.main.ArchiveAccount(ctx, createdAccount.ID))
			}
		}
	})
}

func (s *TestSuite) TestAccounts_Reading_Returns404ForNonexistentAccount() {
	s.runForEachClientExcept("should not be possible to read a non-existent account", func(testClients *testClientWrapper) func() {
		return func() {
			t := s.T()

			ctx, span := tracing.StartCustomSpan(s.ctx, t.Name())
			defer span.End()

			_, err := testClients.main.GetAccount(ctx, nonexistentID)
			assert.Error(t, err)
		}
	})
}

func (s *TestSuite) TestAccounts_Reading() {
	s.runForEachClientExcept("should be possible to read an account", func(testClients *testClientWrapper) func() {
		return func() {
			t := s.T()

			ctx, span := tracing.StartCustomSpan(s.ctx, t.Name())
			defer span.End()

			exampleAccount := fakes.BuildFakeAccount()
			exampleAccountInput := fakes.BuildFakeAccountCreationInputFromAccount(exampleAccount)
			createdAccount, err := testClients.main.CreateAccount(ctx, exampleAccountInput)
			requireNotNilAndNoProblems(t, createdAccount, err)

			actual, err := testClients.main.GetAccount(ctx, createdAccount.ID)
			requireNotNilAndNoProblems(t, actual, err)

			checkAccountEquality(t, exampleAccount, actual)

			assert.NoError(t, testClients.main.ArchiveAccount(ctx, createdAccount.ID))
		}
	})
}

func (s *TestSuite) TestAccounts_Updating_Returns404ForNonexistentAccount() {
	s.runForEachClientExcept("should not be possible to update a non-existent account", func(testClients *testClientWrapper) func() {
		return func() {
			t := s.T()

			ctx, span := tracing.StartCustomSpan(s.ctx, t.Name())
			defer span.End()

			exampleAccount := fakes.BuildFakeAccount()
			exampleAccount.ID = nonexistentID

			assert.Error(t, testClients.main.UpdateAccount(ctx, exampleAccount))
		}
	})
}

func convertAccountToAccountUpdateInput(x *types.Account) *types.AccountUpdateInput {
	return &types.AccountUpdateInput{
		Name:          x.Name,
		BelongsToUser: x.BelongsToUser,
	}
}

func (s *TestSuite) TestAccounts_Updating() {
	s.runForEachClientExcept("should be possible to update an account", func(testClients *testClientWrapper) func() {
		return func() {
			t := s.T()

			ctx, span := tracing.StartCustomSpan(s.ctx, t.Name())
			defer span.End()

			exampleAccount := fakes.BuildFakeAccount()
			exampleAccountInput := fakes.BuildFakeAccountCreationInputFromAccount(exampleAccount)
			createdAccount, err := testClients.main.CreateAccount(ctx, exampleAccountInput)
			requireNotNilAndNoProblems(t, createdAccount, err)

			createdAccount.Update(convertAccountToAccountUpdateInput(exampleAccount))
			assert.NoError(t, testClients.main.UpdateAccount(ctx, createdAccount))

			actual, err := testClients.main.GetAccount(ctx, createdAccount.ID)
			requireNotNilAndNoProblems(t, actual, err)

			checkAccountEquality(t, exampleAccount, actual)
			assert.NotNil(t, actual.LastUpdatedOn)

			assert.NoError(t, testClients.main.ArchiveAccount(ctx, createdAccount.ID))
		}
	})
}

func (s *TestSuite) TestAccounts_Archiving_Returns404ForNonexistentAccount() {
	s.runForEachClientExcept("should not be possible to archive a non-existent account", func(testClients *testClientWrapper) func() {
		return func() {
			t := s.T()

			ctx, span := tracing.StartCustomSpan(s.ctx, t.Name())
			defer span.End()

			assert.Error(t, testClients.main.ArchiveAccount(ctx, nonexistentID))
		}
	})
}

func (s *TestSuite) TestAccounts_Archiving() {
	s.runForEachClientExcept("should be possible to archive an account", func(testClients *testClientWrapper) func() {
		return func() {
			t := s.T()

			ctx, span := tracing.StartCustomSpan(s.ctx, t.Name())
			defer span.End()

			exampleAccount := fakes.BuildFakeAccount()
			exampleAccountInput := fakes.BuildFakeAccountCreationInputFromAccount(exampleAccount)
			createdAccount, err := testClients.main.CreateAccount(ctx, exampleAccountInput)
			requireNotNilAndNoProblems(t, createdAccount, err)

			assert.NoError(t, testClients.main.ArchiveAccount(ctx, createdAccount.ID))
		}
	})
}

func (s *TestSuite) TestAccounts_ChangingMemberships() {
	s.runForCookieClient("should be possible to change members of an account", func(testClients *testClientWrapper) func() {
		return func() {
			t := s.T()

			ctx, span := tracing.StartCustomSpan(s.ctx, t.Name())
			defer span.End()

			const userCount = 1

			currentStatus, statusErr := testClients.main.UserStatus(s.ctx)
			requireNotNilAndNoProblems(t, currentStatus, statusErr)
			t.Logf("initial account is %s; initial user ID is %s", currentStatus.ActiveAccount, s.user.ID)

			accountCreationInput := &types.AccountCreationInput{
				Name: fakes.BuildFakeAccount().Name,
			}
			account, accountCreationErr := testClients.main.CreateAccount(ctx, accountCreationInput)
			require.NoError(t, accountCreationErr)
			require.NotNil(t, account)

			t.Logf("created account %s", account.ID)

			require.NoError(t, testClients.main.SwitchActiveAccount(ctx, account.ID))

			t.Logf("switched main test client active account to %s, creating webhook", account.ID)

			stopChan := make(chan bool, 1)
			notificationsChan, err := testClients.main.SubscribeToDataChangeNotifications(ctx, stopChan)
			require.NotNil(t, notificationsChan)
			require.NoError(t, err)

			exampleWebhook := fakes.BuildFakeWebhook()
			exampleWebhookInput := fakes.BuildFakeWebhookCreationInputFromWebhook(exampleWebhook)
			createdWebhookID, err := testClients.main.CreateWebhook(ctx, exampleWebhookInput)
			require.NoError(t, err)

			n := <-notificationsChan
			assert.Equal(t, n.DataType, types.WebhookDataType)
			require.NotNil(t, n.Webhook)
			checkWebhookEquality(t, exampleWebhook, n.Webhook)

			createdWebhook, err := testClients.main.GetWebhook(ctx, createdWebhookID)
			requireNotNilAndNoProblems(t, createdWebhook, err)
			require.Equal(t, account.ID, createdWebhook.BelongsToAccount)

			t.Logf("created webhook %s for account %s", createdWebhook.ID, createdWebhook.BelongsToAccount)

			users := []*types.User{}
			clients := []*httpclient.Client{}

			for i := 0; i < userCount; i++ {
				u, _, c, _ := createUserAndClientForTest(ctx, t)
				users = append(users, u)
				clients = append(clients, c)

				currentStatus, statusErr = c.UserStatus(s.ctx)
				requireNotNilAndNoProblems(t, currentStatus, statusErr)
				t.Logf("created user user %q with account %s", u.ID, currentStatus.ActiveAccount)
			}

			for i := 0; i < userCount; i++ {
				t.Logf("checking that user %q CANNOT see webhook %s belonging to account %s", users[i].ID, createdWebhook.ID, createdWebhook.BelongsToAccount)
				webhook, err := clients[i].GetWebhook(ctx, createdWebhook.ID)
				require.Nil(t, webhook)
				require.Error(t, err)
			}

			for i := 0; i < userCount; i++ {
				t.Logf("adding user %q to account %s", users[i].ID, account.ID)
				require.NoError(t, testClients.main.AddUserToAccount(ctx, &types.AddUserToAccountInput{
					UserID:       users[i].ID,
					AccountID:    account.ID,
					Reason:       t.Name(),
					AccountRoles: []string{authorization.AccountAdminRole.String()},
				}))
				t.Logf("added user %q to account %s", users[i].ID, account.ID)

				n := <-notificationsChan
				assert.Equal(t, n.DataType, types.UserMembershipDataType)

				t.Logf("setting user %q's client to account %s", users[i].ID, account.ID)
				require.NoError(t, clients[i].SwitchActiveAccount(ctx, account.ID))

				currentStatus, statusErr = clients[i].UserStatus(s.ctx)
				requireNotNilAndNoProblems(t, currentStatus, statusErr)
				require.Equal(t, currentStatus.ActiveAccount, account.ID)
				t.Logf("set user %q's current active account to %s", users[i].ID, account.ID)
			}

			for i := 0; i < userCount; i++ {
				input := &types.ModifyUserPermissionsInput{
					Reason:   t.Name(),
					NewRoles: []string{authorization.AccountAdminRole.String()},
				}
				require.NoError(t, testClients.main.ModifyMemberPermissions(ctx, account.ID, users[i].ID, input))
			}

			for i := 0; i < userCount; i++ {
				t.Logf("checking if user %q CAN now see webhook %s belonging to account %s", users[i].ID, createdWebhook.ID, createdWebhook.BelongsToAccount)
				webhook, err := clients[i].GetWebhook(ctx, createdWebhook.ID)
				requireNotNilAndNoProblems(t, webhook, err)
			}

			for i := 0; i < userCount; i++ {
				require.NoError(t, testClients.main.RemoveUserFromAccount(ctx, account.ID, users[i].ID))
			}

			for i := 0; i < userCount; i++ {
				webhook, err := clients[i].GetWebhook(ctx, createdWebhook.ID)
				require.Nil(t, webhook)
				require.Error(t, err)
			}

			require.NoError(t, testClients.main.ArchiveWebhook(ctx, createdWebhook.ID))

			for i := 0; i < userCount; i++ {
				require.NoError(t, testClients.admin.ArchiveUser(ctx, users[i].ID))
			}
		}
	})

	s.runForPASETOClient("should be possible to change members of an account", func(testClients *testClientWrapper) func() {
		return func() {
			t := s.T()

			ctx, span := tracing.StartCustomSpan(s.ctx, t.Name())
			defer span.End()

			const userCount = 1

			currentStatus, statusErr := testClients.main.UserStatus(s.ctx)
			requireNotNilAndNoProblems(t, currentStatus, statusErr)
			t.Logf("initial account is %s; initial user ID is %s", currentStatus.ActiveAccount, s.user.ID)

			accountCreationInput := &types.AccountCreationInput{
				Name: fakes.BuildFakeAccount().Name,
			}
			account, accountCreationErr := testClients.main.CreateAccount(ctx, accountCreationInput)
			require.NoError(t, accountCreationErr)
			require.NotNil(t, account)

			t.Logf("created account %s", account.ID)

			require.NoError(t, testClients.main.SwitchActiveAccount(ctx, account.ID))

			t.Logf("switched main test client active account to %s, creating webhook", account.ID)

			exampleWebhook := fakes.BuildFakeWebhook()
			exampleWebhookInput := fakes.BuildFakeWebhookCreationInputFromWebhook(exampleWebhook)
			createdWebhookID, err := testClients.main.CreateWebhook(ctx, exampleWebhookInput)
			require.NoError(t, err)

			var createdWebhook *types.Webhook
			checkFunc := func() bool {
				createdWebhook, err = testClients.main.GetWebhook(ctx, createdWebhookID)
				return assert.NotNil(t, createdWebhook) && assert.NoError(t, err)
			}
			assert.Eventually(t, checkFunc, creationTimeout, waitPeriod)

			requireNotNilAndNoProblems(t, createdWebhook, err)
			require.Equal(t, account.ID, createdWebhook.BelongsToAccount)

			t.Logf("created webhook %s for account %s", createdWebhook.ID, createdWebhook.BelongsToAccount)

			users := []*types.User{}
			clients := []*httpclient.Client{}

			for i := 0; i < userCount; i++ {
				u, _, c, _ := createUserAndClientForTest(ctx, t)
				users = append(users, u)
				clients = append(clients, c)

				currentStatus, statusErr = c.UserStatus(s.ctx)
				requireNotNilAndNoProblems(t, currentStatus, statusErr)
				t.Logf("created user user %q with account %s", u.ID, currentStatus.ActiveAccount)
			}

			for i := 0; i < userCount; i++ {
				t.Logf("checking that user %q CANNOT see webhook %s belonging to account %s", users[i].ID, createdWebhook.ID, createdWebhook.BelongsToAccount)
				webhook, err := clients[i].GetWebhook(ctx, createdWebhook.ID)
				require.Nil(t, webhook)
				require.Error(t, err)
			}

			for i := 0; i < userCount; i++ {
				t.Logf("adding user %q to account %s", users[i].ID, account.ID)
				require.NoError(t, testClients.main.AddUserToAccount(ctx, &types.AddUserToAccountInput{
					UserID:       users[i].ID,
					AccountID:    account.ID,
					Reason:       t.Name(),
					AccountRoles: []string{authorization.AccountAdminRole.String()},
				}))
				t.Logf("added user %q to account %s", users[i].ID, account.ID)

				t.Logf("setting user %q's client to account %s", users[i].ID, account.ID)
				checkFunc = func() bool {
					return assert.NoError(t, clients[i].SwitchActiveAccount(ctx, account.ID))
				}
				assert.Eventually(t, checkFunc, creationTimeout, waitPeriod)

				currentStatus, statusErr = clients[i].UserStatus(s.ctx)
				requireNotNilAndNoProblems(t, currentStatus, statusErr)
				require.Equal(t, currentStatus.ActiveAccount, account.ID)
				t.Logf("set user %q's current active account to %s", users[i].ID, account.ID)
			}

			for i := 0; i < userCount; i++ {
				input := &types.ModifyUserPermissionsInput{
					Reason:   t.Name(),
					NewRoles: []string{authorization.AccountAdminRole.String()},
				}
				require.NoError(t, testClients.main.ModifyMemberPermissions(ctx, account.ID, users[i].ID, input))
			}

			for i := 0; i < userCount; i++ {
				t.Logf("checking if user %q CAN now see webhook %s belonging to account %s", users[i].ID, createdWebhook.ID, createdWebhook.BelongsToAccount)
				webhook, err := clients[i].GetWebhook(ctx, createdWebhook.ID)
				requireNotNilAndNoProblems(t, webhook, err)
			}

			for i := 0; i < userCount; i++ {
				require.NoError(t, testClients.main.RemoveUserFromAccount(ctx, account.ID, users[i].ID))
			}

			for i := 0; i < userCount; i++ {
				webhook, err := clients[i].GetWebhook(ctx, createdWebhook.ID)
				require.Nil(t, webhook)
				require.Error(t, err)
			}

			require.NoError(t, testClients.main.ArchiveWebhook(ctx, createdWebhook.ID))

			for i := 0; i < userCount; i++ {
				require.NoError(t, testClients.admin.ArchiveUser(ctx, users[i].ID))
			}
		}
	})
}

func (s *TestSuite) TestAccounts_OwnershipTransfer() {
	s.runForCookieClient("should be possible to transfer ownership of an account", func(testClients *testClientWrapper) func() {
		return func() {
			t := s.T()

			ctx, span := tracing.StartCustomSpan(s.ctx, t.Name())
			defer span.End()

			futureOwner, _, _, futureOwnerClient := createUserAndClientForTest(ctx, t)

			accountCreationInput := &types.AccountCreationInput{
				Name: fakes.BuildFakeAccount().Name,
			}
			account, accountCreationErr := testClients.main.CreateAccount(ctx, accountCreationInput)
			require.NoError(t, accountCreationErr)
			require.NotNil(t, account)

			t.Logf("created account %s", account.ID)

			require.NoError(t, testClients.main.SwitchActiveAccount(ctx, account.ID))

			t.Logf("switched to active account: %s", account.ID)

			stopChan := make(chan bool, 1)
			notificationsChan, err := testClients.main.SubscribeToDataChangeNotifications(ctx, stopChan)
			require.NotNil(t, notificationsChan)
			require.NoError(t, err)

			exampleWebhook := fakes.BuildFakeWebhook()
			exampleWebhookInput := fakes.BuildFakeWebhookCreationInputFromWebhook(exampleWebhook)
			createdWebhookID, err := testClients.main.CreateWebhook(ctx, exampleWebhookInput)
			require.NoError(t, err)

			n := <-notificationsChan
			assert.Equal(t, n.DataType, types.WebhookDataType)
			require.NotNil(t, n.Webhook)
			checkWebhookEquality(t, exampleWebhook, n.Webhook)

			createdWebhook, err := testClients.main.GetWebhook(ctx, createdWebhookID)
			requireNotNilAndNoProblems(t, createdWebhook, err)

			t.Logf("created webhook %s belonging to account %s", createdWebhook.ID, createdWebhook.BelongsToAccount)
			require.Equal(t, account.ID, createdWebhook.BelongsToAccount)

			webhook, err := futureOwnerClient.GetWebhook(ctx, createdWebhook.ID)
			require.Nil(t, webhook)
			require.Error(t, err)

			require.NoError(t, testClients.main.TransferAccountOwnership(ctx, account.ID, &types.AccountOwnershipTransferInput{
				Reason:       t.Name(),
				CurrentOwner: account.BelongsToUser,
				NewOwner:     futureOwner.ID,
			}))

			t.Logf("transferred account %s from user %s to user %s", account.ID, account.BelongsToUser, futureOwner.ID)

			require.NoError(t, futureOwnerClient.SwitchActiveAccount(ctx, account.ID))

			webhook, err = futureOwnerClient.GetWebhook(ctx, createdWebhook.ID)
			requireNotNilAndNoProblems(t, webhook, err)

			webhook, err = testClients.main.GetWebhook(ctx, createdWebhook.ID)
			require.Nil(t, webhook)
			require.Error(t, err)

			require.NoError(t, futureOwnerClient.ArchiveWebhook(ctx, createdWebhook.ID))

			require.Error(t, testClients.main.ArchiveWebhook(ctx, createdWebhook.ID))
			require.NoError(t, testClients.admin.ArchiveUser(ctx, futureOwner.ID))
		}
	})

	s.runForPASETOClient("should be possible to transfer ownership of an account", func(testClients *testClientWrapper) func() {
		return func() {
			t := s.T()

			ctx, span := tracing.StartCustomSpan(s.ctx, t.Name())
			defer span.End()

			futureOwner, _, _, futureOwnerClient := createUserAndClientForTest(ctx, t)

			accountCreationInput := &types.AccountCreationInput{
				Name: fakes.BuildFakeAccount().Name,
			}
			account, accountCreationErr := testClients.main.CreateAccount(ctx, accountCreationInput)
			require.NoError(t, accountCreationErr)
			require.NotNil(t, account)

			t.Logf("created account %s", account.ID)

			require.NoError(t, testClients.main.SwitchActiveAccount(ctx, account.ID))

			t.Logf("switched to active account: %s", account.ID)

			exampleWebhook := fakes.BuildFakeWebhook()
			exampleWebhookInput := fakes.BuildFakeWebhookCreationInputFromWebhook(exampleWebhook)
			createdWebhookID, err := testClients.main.CreateWebhook(ctx, exampleWebhookInput)
			require.NoError(t, err)

			var createdWebhook *types.Webhook
			checkFunc := func() bool {
				createdWebhook, err = testClients.main.GetWebhook(ctx, createdWebhookID)
				return assert.NotNil(t, createdWebhook) && assert.NoError(t, err)
			}
			assert.Eventually(t, checkFunc, creationTimeout, waitPeriod)

			requireNotNilAndNoProblems(t, createdWebhook, err)

			t.Logf("created webhook %s belonging to account %s", createdWebhook.ID, createdWebhook.BelongsToAccount)
			require.Equal(t, account.ID, createdWebhook.BelongsToAccount)

			webhook, err := futureOwnerClient.GetWebhook(ctx, createdWebhook.ID)
			require.Nil(t, webhook)
			require.Error(t, err)

			require.NoError(t, testClients.main.TransferAccountOwnership(ctx, account.ID, &types.AccountOwnershipTransferInput{
				Reason:       t.Name(),
				CurrentOwner: account.BelongsToUser,
				NewOwner:     futureOwner.ID,
			}))

			t.Logf("transferred account %s from user %s to user %s", account.ID, account.BelongsToUser, futureOwner.ID)

			require.NoError(t, futureOwnerClient.SwitchActiveAccount(ctx, account.ID))

			webhook, err = futureOwnerClient.GetWebhook(ctx, createdWebhook.ID)
			requireNotNilAndNoProblems(t, webhook, err)

			webhook, err = testClients.main.GetWebhook(ctx, createdWebhook.ID)
			require.Nil(t, webhook)
			require.Error(t, err)

			require.NoError(t, futureOwnerClient.ArchiveWebhook(ctx, createdWebhook.ID))

			require.Error(t, testClients.main.ArchiveWebhook(ctx, createdWebhook.ID))
			require.NoError(t, testClients.admin.ArchiveUser(ctx, futureOwner.ID))
		}
	})
}
package integration

import (
	"testing"

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

	"gitlab.com/verygoodsoftwarenotvirus/todo/internal/authorization"
	"gitlab.com/verygoodsoftwarenotvirus/todo/internal/observability/tracing"
	"gitlab.com/verygoodsoftwarenotvirus/todo/pkg/client/httpclient"
	"gitlab.com/verygoodsoftwarenotvirus/todo/pkg/types"
	"gitlab.com/verygoodsoftwarenotvirus/todo/pkg/types/fakes"
)

func checkAccountEquality(t *testing.T, expected, actual *types.Account) {
	t.Helper()

	assert.NotZero(t, actual.ID)
	assert.Equal(t, expected.Name, actual.Name, "expected BucketName for account %s to be %v, but it was %v ", expected.ID, expected.Name, actual.Name)
	assert.NotZero(t, actual.CreatedOn)
}

func (s *TestSuite) TestAccounts_Creating() {
	s.runForEachClientExcept("should be possible to create accounts", func(testClients *testClientWrapper) func() {
		return func() {
			t := s.T()

			ctx, span := tracing.StartCustomSpan(s.ctx, t.Name())
			defer span.End()

			// Create account.
			exampleAccount := fakes.BuildFakeAccount()
			exampleAccountInput := fakes.BuildFakeAccountCreationInputFromAccount(exampleAccount)
			createdAccount, err := testClients.main.CreateAccount(ctx, exampleAccountInput)
			requireNotNilAndNoProblems(t, createdAccount, err)

			// Assert account equality.
			checkAccountEquality(t, exampleAccount, createdAccount)

			// Clean up.
			assert.NoError(t, testClients.main.ArchiveAccount(ctx, createdAccount.ID))
		}
	})
}

func (s *TestSuite) TestAccounts_Listing() {
	s.runForEachClientExcept("should be possible to list accounts", func(testClients *testClientWrapper) func() {
		return func() {
			t := s.T()

			ctx, span := tracing.StartCustomSpan(s.ctx, t.Name())
			defer span.End()

			// Create accounts.
			var expected []*types.Account
			for i := 0; i < 5; i++ {
				// Create account.
				exampleAccount := fakes.BuildFakeAccount()
				exampleAccountInput := fakes.BuildFakeAccountCreationInputFromAccount(exampleAccount)
				createdAccount, accountCreationErr := testClients.main.CreateAccount(ctx, exampleAccountInput)
				requireNotNilAndNoProblems(t, createdAccount, accountCreationErr)

				expected = append(expected, createdAccount)
			}

			// Assert account list equality.
			actual, err := testClients.main.GetAccounts(ctx, nil)
			requireNotNilAndNoProblems(t, actual, err)
			assert.True(
				t,
				len(expected) <= len(actual.Accounts),
				"expected %d to be <= %d",
				len(expected),
				len(actual.Accounts),
			)

			// Clean up.
			for _, createdAccount := range actual.Accounts {
				assert.NoError(t, testClients.main.ArchiveAccount(ctx, createdAccount.ID))
			}
		}
	})
}

func (s *TestSuite) TestAccounts_Reading_Returns404ForNonexistentAccount() {
	s.runForEachClientExcept("should not be possible to read a non-existent account", func(testClients *testClientWrapper) func() {
		return func() {
			t := s.T()

			ctx, span := tracing.StartCustomSpan(s.ctx, t.Name())
			defer span.End()

			// Attempt to fetch nonexistent account.
			_, err := testClients.main.GetAccount(ctx, nonexistentID)
			assert.Error(t, err)
		}
	})
}

func (s *TestSuite) TestAccounts_Reading() {
	s.runForEachClientExcept("should be possible to read an account", func(testClients *testClientWrapper) func() {
		return func() {
			t := s.T()

			ctx, span := tracing.StartCustomSpan(s.ctx, t.Name())
			defer span.End()

			// Create account.
			exampleAccount := fakes.BuildFakeAccount()
			exampleAccountInput := fakes.BuildFakeAccountCreationInputFromAccount(exampleAccount)
			createdAccount, err := testClients.main.CreateAccount(ctx, exampleAccountInput)
			requireNotNilAndNoProblems(t, createdAccount, err)

			// Fetch account.
			actual, err := testClients.main.GetAccount(ctx, createdAccount.ID)
			requireNotNilAndNoProblems(t, actual, err)

			// Assert account equality.
			checkAccountEquality(t, exampleAccount, actual)

			// Clean up account.
			assert.NoError(t, testClients.main.ArchiveAccount(ctx, createdAccount.ID))
		}
	})
}

func (s *TestSuite) TestAccounts_Updating_Returns404ForNonexistentAccount() {
	s.runForEachClientExcept("should not be possible to update a non-existent account", func(testClients *testClientWrapper) func() {
		return func() {
			t := s.T()

			ctx, span := tracing.StartCustomSpan(s.ctx, t.Name())
			defer span.End()

			exampleAccount := fakes.BuildFakeAccount()
			exampleAccount.ID = nonexistentID

			assert.Error(t, testClients.main.UpdateAccount(ctx, exampleAccount))
		}
	})
}

// convertAccountToAccountUpdateInput creates an AccountUpdateInput struct from an account.
func convertAccountToAccountUpdateInput(x *types.Account) *types.AccountUpdateInput {
	return &types.AccountUpdateInput{
		Name:          x.Name,
		BelongsToUser: x.BelongsToUser,
	}
}

func (s *TestSuite) TestAccounts_Updating() {
	s.runForEachClientExcept("should be possible to update an account", func(testClients *testClientWrapper) func() {
		return func() {
			t := s.T()

			ctx, span := tracing.StartCustomSpan(s.ctx, t.Name())
			defer span.End()

			// Create account.
			exampleAccount := fakes.BuildFakeAccount()
			exampleAccountInput := fakes.BuildFakeAccountCreationInputFromAccount(exampleAccount)
			createdAccount, err := testClients.main.CreateAccount(ctx, exampleAccountInput)
			requireNotNilAndNoProblems(t, createdAccount, err)

			// Change account.
			createdAccount.Update(convertAccountToAccountUpdateInput(exampleAccount))
			assert.NoError(t, testClients.main.UpdateAccount(ctx, createdAccount))

			// Fetch account.
			actual, err := testClients.main.GetAccount(ctx, createdAccount.ID)
			requireNotNilAndNoProblems(t, actual, err)

			// Assert account equality.
			checkAccountEquality(t, exampleAccount, actual)
			assert.NotNil(t, actual.LastUpdatedOn)

			// Clean up account.
			assert.NoError(t, testClients.main.ArchiveAccount(ctx, createdAccount.ID))
		}
	})
}

func (s *TestSuite) TestAccounts_Archiving_Returns404ForNonexistentAccount() {
	s.runForEachClientExcept("should not be possible to archive a non-existent account", func(testClients *testClientWrapper) func() {
		return func() {
			t := s.T()

			ctx, span := tracing.StartCustomSpan(s.ctx, t.Name())
			defer span.End()

			assert.Error(t, testClients.main.ArchiveAccount(ctx, nonexistentID))
		}
	})
}

func (s *TestSuite) TestAccounts_Archiving() {
	s.runForEachClientExcept("should be possible to archive an account", func(testClients *testClientWrapper) func() {
		return func() {
			t := s.T()

			ctx, span := tracing.StartCustomSpan(s.ctx, t.Name())
			defer span.End()

			// Create account.
			exampleAccount := fakes.BuildFakeAccount()
			exampleAccountInput := fakes.BuildFakeAccountCreationInputFromAccount(exampleAccount)
			createdAccount, err := testClients.main.CreateAccount(ctx, exampleAccountInput)
			requireNotNilAndNoProblems(t, createdAccount, err)

			// Clean up account.
			assert.NoError(t, testClients.main.ArchiveAccount(ctx, createdAccount.ID))
		}
	})
}

func (s *TestSuite) TestAccounts_ChangingMemberships() {
	s.runForCookieClient("should be possible to change members of an account", func(testClients *testClientWrapper) func() {
		return func() {
			t := s.T()

			ctx, span := tracing.StartCustomSpan(s.ctx, t.Name())
			defer span.End()

			const userCount = 1

			currentStatus, statusErr := testClients.main.UserStatus(s.ctx)
			requireNotNilAndNoProblems(t, currentStatus, statusErr)
			t.Logf("initial account is %s; initial user ID is %s", currentStatus.ActiveAccount, s.user.ID)

			// fetch account data
			accountCreationInput := &types.AccountCreationInput{
				Name: fakes.BuildFakeAccount().Name,
			}
			account, accountCreationErr := testClients.main.CreateAccount(ctx, accountCreationInput)
			require.NoError(t, accountCreationErr)
			require.NotNil(t, account)

			t.Logf("created account %s", account.ID)

			require.NoError(t, testClients.main.SwitchActiveAccount(ctx, account.ID))

			t.Logf("switched main test client active account to %s, creating webhook", account.ID)

			stopChan := make(chan bool, 1)
			notificationsChan, err := testClients.main.SubscribeToDataChangeNotifications(ctx, stopChan)
			require.NotNil(t, notificationsChan)
			require.NoError(t, err)

			// Create webhook.
			exampleWebhook := fakes.BuildFakeWebhook()
			exampleWebhookInput := fakes.BuildFakeWebhookCreationInputFromWebhook(exampleWebhook)
			createdWebhookID, err := testClients.main.CreateWebhook(ctx, exampleWebhookInput)
			require.NoError(t, err)

			n := <-notificationsChan
			assert.Equal(t, n.DataType, types.WebhookDataType)
			require.NotNil(t, n.Webhook)
			checkWebhookEquality(t, exampleWebhook, n.Webhook)

			createdWebhook, err := testClients.main.GetWebhook(ctx, createdWebhookID)
			requireNotNilAndNoProblems(t, createdWebhook, err)
			require.Equal(t, account.ID, createdWebhook.BelongsToAccount)

			t.Logf("created webhook %s for account %s", createdWebhook.ID, createdWebhook.BelongsToAccount)

			// create dummy users
			users := []*types.User{}
			clients := []*httpclient.Client{}

			// create users
			for i := 0; i < userCount; i++ {
				u, _, c, _ := createUserAndClientForTest(ctx, t)
				users = append(users, u)
				clients = append(clients, c)

				currentStatus, statusErr = c.UserStatus(s.ctx)
				requireNotNilAndNoProblems(t, currentStatus, statusErr)
				t.Logf("created user user %q with account %s", u.ID, currentStatus.ActiveAccount)
			}

			// check that each user cannot see the unreachable webhook
			for i := 0; i < userCount; i++ {
				t.Logf("checking that user %q CANNOT see webhook %s belonging to account %s", users[i].ID, createdWebhook.ID, createdWebhook.BelongsToAccount)
				webhook, err := clients[i].GetWebhook(ctx, createdWebhook.ID)
				require.Nil(t, webhook)
				require.Error(t, err)
			}

			// add them to the account
			for i := 0; i < userCount; i++ {
				t.Logf("adding user %q to account %s", users[i].ID, account.ID)
				require.NoError(t, testClients.main.AddUserToAccount(ctx, &types.AddUserToAccountInput{
					UserID:       users[i].ID,
					AccountID:    account.ID,
					Reason:       t.Name(),
					AccountRoles: []string{authorization.AccountAdminRole.String()},
				}))
				t.Logf("added user %q to account %s", users[i].ID, account.ID)

				n := <-notificationsChan
				assert.Equal(t, n.DataType, types.UserMembershipDataType)

				t.Logf("setting user %q's client to account %s", users[i].ID, account.ID)
				require.NoError(t, clients[i].SwitchActiveAccount(ctx, account.ID))

				currentStatus, statusErr = clients[i].UserStatus(s.ctx)
				requireNotNilAndNoProblems(t, currentStatus, statusErr)
				require.Equal(t, currentStatus.ActiveAccount, account.ID)
				t.Logf("set user %q's current active account to %s", users[i].ID, account.ID)
			}

			// grant all permissions
			for i := 0; i < userCount; i++ {
				input := &types.ModifyUserPermissionsInput{
					Reason:   t.Name(),
					NewRoles: []string{authorization.AccountAdminRole.String()},
				}
				require.NoError(t, testClients.main.ModifyMemberPermissions(ctx, account.ID, users[i].ID, input))
			}

			// check that each user can see the webhook
			for i := 0; i < userCount; i++ {
				t.Logf("checking if user %q CAN now see webhook %s belonging to account %s", users[i].ID, createdWebhook.ID, createdWebhook.BelongsToAccount)
				webhook, err := clients[i].GetWebhook(ctx, createdWebhook.ID)
				requireNotNilAndNoProblems(t, webhook, err)
			}

			// remove users from account
			for i := 0; i < userCount; i++ {
				require.NoError(t, testClients.main.RemoveUserFromAccount(ctx, account.ID, users[i].ID))
			}

			// check that each user cannot see the webhook
			for i := 0; i < userCount; i++ {
				webhook, err := clients[i].GetWebhook(ctx, createdWebhook.ID)
				require.Nil(t, webhook)
				require.Error(t, err)
			}

			// Clean up.
			require.NoError(t, testClients.main.ArchiveWebhook(ctx, createdWebhook.ID))

			for i := 0; i < userCount; i++ {
				require.NoError(t, testClients.admin.ArchiveUser(ctx, users[i].ID))
			}
		}
	})

	s.runForPASETOClient("should be possible to change members of an account", func(testClients *testClientWrapper) func() {
		return func() {
			t := s.T()

			ctx, span := tracing.StartCustomSpan(s.ctx, t.Name())
			defer span.End()

			const userCount = 1

			currentStatus, statusErr := testClients.main.UserStatus(s.ctx)
			requireNotNilAndNoProblems(t, currentStatus, statusErr)
			t.Logf("initial account is %s; initial user ID is %s", currentStatus.ActiveAccount, s.user.ID)

			// fetch account data
			accountCreationInput := &types.AccountCreationInput{
				Name: fakes.BuildFakeAccount().Name,
			}
			account, accountCreationErr := testClients.main.CreateAccount(ctx, accountCreationInput)
			require.NoError(t, accountCreationErr)
			require.NotNil(t, account)

			t.Logf("created account %s", account.ID)

			require.NoError(t, testClients.main.SwitchActiveAccount(ctx, account.ID))

			t.Logf("switched main test client active account to %s, creating webhook", account.ID)

			// Create webhook.
			exampleWebhook := fakes.BuildFakeWebhook()
			exampleWebhookInput := fakes.BuildFakeWebhookCreationInputFromWebhook(exampleWebhook)
			createdWebhookID, err := testClients.main.CreateWebhook(ctx, exampleWebhookInput)
			require.NoError(t, err)

			var createdWebhook *types.Webhook
			checkFunc := func() bool {
				createdWebhook, err = testClients.main.GetWebhook(ctx, createdWebhookID)
				return assert.NotNil(t, createdWebhook) && assert.NoError(t, err)
			}
			assert.Eventually(t, checkFunc, creationTimeout, waitPeriod)

			// assert webhook equality
			requireNotNilAndNoProblems(t, createdWebhook, err)
			require.Equal(t, account.ID, createdWebhook.BelongsToAccount)

			t.Logf("created webhook %s for account %s", createdWebhook.ID, createdWebhook.BelongsToAccount)

			// create dummy users
			users := []*types.User{}
			clients := []*httpclient.Client{}

			// create users
			for i := 0; i < userCount; i++ {
				u, _, c, _ := createUserAndClientForTest(ctx, t)
				users = append(users, u)
				clients = append(clients, c)

				currentStatus, statusErr = c.UserStatus(s.ctx)
				requireNotNilAndNoProblems(t, currentStatus, statusErr)
				t.Logf("created user user %q with account %s", u.ID, currentStatus.ActiveAccount)
			}

			// check that each user cannot see the unreachable webhook
			for i := 0; i < userCount; i++ {
				t.Logf("checking that user %q CANNOT see webhook %s belonging to account %s", users[i].ID, createdWebhook.ID, createdWebhook.BelongsToAccount)
				webhook, err := clients[i].GetWebhook(ctx, createdWebhook.ID)
				require.Nil(t, webhook)
				require.Error(t, err)
			}

			// add them to the account
			for i := 0; i < userCount; i++ {
				t.Logf("adding user %q to account %s", users[i].ID, account.ID)
				require.NoError(t, testClients.main.AddUserToAccount(ctx, &types.AddUserToAccountInput{
					UserID:       users[i].ID,
					AccountID:    account.ID,
					Reason:       t.Name(),
					AccountRoles: []string{authorization.AccountAdminRole.String()},
				}))
				t.Logf("added user %q to account %s", users[i].ID, account.ID)

				t.Logf("setting user %q's client to account %s", users[i].ID, account.ID)
				checkFunc = func() bool {
					return assert.NoError(t, clients[i].SwitchActiveAccount(ctx, account.ID))
				}
				assert.Eventually(t, checkFunc, creationTimeout, waitPeriod)

				currentStatus, statusErr = clients[i].UserStatus(s.ctx)
				requireNotNilAndNoProblems(t, currentStatus, statusErr)
				require.Equal(t, currentStatus.ActiveAccount, account.ID)
				t.Logf("set user %q's current active account to %s", users[i].ID, account.ID)
			}

			// grant all permissions
			for i := 0; i < userCount; i++ {
				input := &types.ModifyUserPermissionsInput{
					Reason:   t.Name(),
					NewRoles: []string{authorization.AccountAdminRole.String()},
				}
				require.NoError(t, testClients.main.ModifyMemberPermissions(ctx, account.ID, users[i].ID, input))
			}

			// check that each user can see the webhook
			for i := 0; i < userCount; i++ {
				t.Logf("checking if user %q CAN now see webhook %s belonging to account %s", users[i].ID, createdWebhook.ID, createdWebhook.BelongsToAccount)
				webhook, err := clients[i].GetWebhook(ctx, createdWebhook.ID)
				requireNotNilAndNoProblems(t, webhook, err)
			}

			// remove users from account
			for i := 0; i < userCount; i++ {
				require.NoError(t, testClients.main.RemoveUserFromAccount(ctx, account.ID, users[i].ID))
			}

			// check that each user cannot see the webhook
			for i := 0; i < userCount; i++ {
				webhook, err := clients[i].GetWebhook(ctx, createdWebhook.ID)
				require.Nil(t, webhook)
				require.Error(t, err)
			}

			// Clean up.
			require.NoError(t, testClients.main.ArchiveWebhook(ctx, createdWebhook.ID))

			for i := 0; i < userCount; i++ {
				require.NoError(t, testClients.admin.ArchiveUser(ctx, users[i].ID))
			}
		}
	})
}

func (s *TestSuite) TestAccounts_OwnershipTransfer() {
	s.runForCookieClient("should be possible to transfer ownership of an account", func(testClients *testClientWrapper) func() {
		return func() {
			t := s.T()

			ctx, span := tracing.StartCustomSpan(s.ctx, t.Name())
			defer span.End()

			// create users
			futureOwner, _, _, futureOwnerClient := createUserAndClientForTest(ctx, t)

			// fetch account data
			accountCreationInput := &types.AccountCreationInput{
				Name: fakes.BuildFakeAccount().Name,
			}
			account, accountCreationErr := testClients.main.CreateAccount(ctx, accountCreationInput)
			require.NoError(t, accountCreationErr)
			require.NotNil(t, account)

			t.Logf("created account %s", account.ID)

			require.NoError(t, testClients.main.SwitchActiveAccount(ctx, account.ID))

			t.Logf("switched to active account: %s", account.ID)

			// create a webhook
			stopChan := make(chan bool, 1)
			notificationsChan, err := testClients.main.SubscribeToDataChangeNotifications(ctx, stopChan)
			require.NotNil(t, notificationsChan)
			require.NoError(t, err)

			// Create webhook.
			exampleWebhook := fakes.BuildFakeWebhook()
			exampleWebhookInput := fakes.BuildFakeWebhookCreationInputFromWebhook(exampleWebhook)
			createdWebhookID, err := testClients.main.CreateWebhook(ctx, exampleWebhookInput)
			require.NoError(t, err)

			n := <-notificationsChan
			assert.Equal(t, n.DataType, types.WebhookDataType)
			require.NotNil(t, n.Webhook)
			checkWebhookEquality(t, exampleWebhook, n.Webhook)

			createdWebhook, err := testClients.main.GetWebhook(ctx, createdWebhookID)
			requireNotNilAndNoProblems(t, createdWebhook, err)

			t.Logf("created webhook %s belonging to account %s", createdWebhook.ID, createdWebhook.BelongsToAccount)
			require.Equal(t, account.ID, createdWebhook.BelongsToAccount)

			// check that user cannot see the webhook
			webhook, err := futureOwnerClient.GetWebhook(ctx, createdWebhook.ID)
			require.Nil(t, webhook)
			require.Error(t, err)

			// add them to the account
			require.NoError(t, testClients.main.TransferAccountOwnership(ctx, account.ID, &types.AccountOwnershipTransferInput{
				Reason:       t.Name(),
				CurrentOwner: account.BelongsToUser,
				NewOwner:     futureOwner.ID,
			}))

			t.Logf("transferred account %s from user %s to user %s", account.ID, account.BelongsToUser, futureOwner.ID)

			require.NoError(t, futureOwnerClient.SwitchActiveAccount(ctx, account.ID))

			// check that user can see the webhook
			webhook, err = futureOwnerClient.GetWebhook(ctx, createdWebhook.ID)
			requireNotNilAndNoProblems(t, webhook, err)

			// check that old user cannot see the webhook
			webhook, err = testClients.main.GetWebhook(ctx, createdWebhook.ID)
			require.Nil(t, webhook)
			require.Error(t, err)

			// check that new owner can delete the webhook
			require.NoError(t, futureOwnerClient.ArchiveWebhook(ctx, createdWebhook.ID))

			// Clean up.
			require.Error(t, testClients.main.ArchiveWebhook(ctx, createdWebhook.ID))
			require.NoError(t, testClients.admin.ArchiveUser(ctx, futureOwner.ID))
		}
	})

	s.runForPASETOClient("should be possible to transfer ownership of an account", func(testClients *testClientWrapper) func() {
		return func() {
			t := s.T()

			ctx, span := tracing.StartCustomSpan(s.ctx, t.Name())
			defer span.End()

			// create users
			futureOwner, _, _, futureOwnerClient := createUserAndClientForTest(ctx, t)

			// fetch account data
			accountCreationInput := &types.AccountCreationInput{
				Name: fakes.BuildFakeAccount().Name,
			}
			account, accountCreationErr := testClients.main.CreateAccount(ctx, accountCreationInput)
			require.NoError(t, accountCreationErr)
			require.NotNil(t, account)

			t.Logf("created account %s", account.ID)

			require.NoError(t, testClients.main.SwitchActiveAccount(ctx, account.ID))

			t.Logf("switched to active account: %s", account.ID)

			// create a webhook
			exampleWebhook := fakes.BuildFakeWebhook()
			exampleWebhookInput := fakes.BuildFakeWebhookCreationInputFromWebhook(exampleWebhook)
			createdWebhookID, err := testClients.main.CreateWebhook(ctx, exampleWebhookInput)
			require.NoError(t, err)

			var createdWebhook *types.Webhook
			checkFunc := func() bool {
				createdWebhook, err = testClients.main.GetWebhook(ctx, createdWebhookID)
				return assert.NotNil(t, createdWebhook) && assert.NoError(t, err)
			}
			assert.Eventually(t, checkFunc, creationTimeout, waitPeriod)

			requireNotNilAndNoProblems(t, createdWebhook, err)

			t.Logf("created webhook %s belonging to account %s", createdWebhook.ID, createdWebhook.BelongsToAccount)
			require.Equal(t, account.ID, createdWebhook.BelongsToAccount)

			// check that user cannot see the webhook
			webhook, err := futureOwnerClient.GetWebhook(ctx, createdWebhook.ID)
			require.Nil(t, webhook)
			require.Error(t, err)

			// add them to the account
			require.NoError(t, testClients.main.TransferAccountOwnership(ctx, account.ID, &types.AccountOwnershipTransferInput{
				Reason:       t.Name(),
				CurrentOwner: account.BelongsToUser,
				NewOwner:     futureOwner.ID,
			}))

			t.Logf("transferred account %s from user %s to user %s", account.ID, account.BelongsToUser, futureOwner.ID)

			require.NoError(t, futureOwnerClient.SwitchActiveAccount(ctx, account.ID))

			// check that user can see the webhook
			webhook, err = futureOwnerClient.GetWebhook(ctx, createdWebhook.ID)
			requireNotNilAndNoProblems(t, webhook, err)

			// check that old user cannot see the webhook
			webhook, err = testClients.main.GetWebhook(ctx, createdWebhook.ID)
			require.Nil(t, webhook)
			require.Error(t, err)

			// check that new owner can delete the webhook
			require.NoError(t, futureOwnerClient.ArchiveWebhook(ctx, createdWebhook.ID))

			// Clean up.
			require.Error(t, testClients.main.ArchiveWebhook(ctx, createdWebhook.ID))
			require.NoError(t, testClients.admin.ArchiveUser(ctx, futureOwner.ID))
		}
	})
}
