Ctrl K
ComunidadGuías…

coderd/users_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package coderd_test

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"net/http"
	"slices"
	"strings"
	"testing"
	"time"

	"github.com/golang-jwt/jwt/v4"
	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"golang.org/x/sync/errgroup"
	"golang.org/x/xerrors"

	"github.com/coder/coder/v2/coderd"
	"github.com/coder/coder/v2/coderd/audit"
	"github.com/coder/coder/v2/coderd/coderdtest"
	"github.com/coder/coder/v2/coderd/coderdtest/oidctest"
	"github.com/coder/coder/v2/coderd/database"
	"github.com/coder/coder/v2/coderd/database/dbauthz"
	"github.com/coder/coder/v2/coderd/database/dbgen"
	"github.com/coder/coder/v2/coderd/database/dbtime"
	"github.com/coder/coder/v2/coderd/notifications"
	"github.com/coder/coder/v2/coderd/notifications/notificationstest"
	"github.com/coder/coder/v2/coderd/rbac"
	"github.com/coder/coder/v2/coderd/rbac/policy"
	"github.com/coder/coder/v2/coderd/util/slice"
	"github.com/coder/coder/v2/codersdk"
	"github.com/coder/coder/v2/testutil"
	"github.com/coder/serpent"
)

func TestFirstUser(t *testing.T) {
	t.Parallel()
	t.Run("BadRequest", func(t *testing.T) {
		t.Parallel()
		client := coderdtest.New(t, nil)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		has, err := client.HasFirstUser(context.Background())
		require.NoError(t, err)
		require.False(t, has)

		_, err = client.CreateFirstUser(ctx, codersdk.CreateFirstUserRequest{})
		require.Error(t, err)
	})

	t.Run("AlreadyExists", func(t *testing.T) {
		t.Parallel()
		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		_, err := client.CreateFirstUser(ctx, codersdk.CreateFirstUserRequest{
			Email:    "some@email.com",
			Username: "exampleuser",
			Password: "SomeSecurePassword!",
		})
		var apiErr *codersdk.Error
		require.ErrorAs(t, err, &apiErr)
		require.Equal(t, http.StatusConflict, apiErr.StatusCode())
	})

	t.Run("Create", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitShort)
		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)
		u, err := client.User(ctx, codersdk.Me)
		require.NoError(t, err)
		assert.Equal(t, coderdtest.FirstUserParams.Name, u.Name)
		assert.Equal(t, coderdtest.FirstUserParams.Email, u.Email)
		assert.Equal(t, coderdtest.FirstUserParams.Username, u.Username)
	})

	t.Run("Trial", func(t *testing.T) {
		t.Parallel()
		trialGenerated := make(chan codersdk.LicensorTrialRequest, 1)
		entitlementsRefreshed := make(chan struct{})

		client := coderdtest.New(t, &coderdtest.Options{
			TrialGenerator: func(_ context.Context, req codersdk.LicensorTrialRequest) error {
				trialGenerated <- req
				return nil
			},
			RefreshEntitlements: func(context.Context) error {
				close(entitlementsRefreshed)
				return nil
			},
		})

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		req := codersdk.CreateFirstUserRequest{
			Email:    "testuser@coder.com",
			Username: "testuser",
			Name:     "Test User",
			Password: "SomeSecurePassword!",
			Trial:    true,
		}
		_, err := client.CreateFirstUser(ctx, req)
		require.NoError(t, err)

		trialReq := testutil.TryReceive(ctx, t, trialGenerated)

		require.Equal(t, codersdk.LicensorTrialSourceNewUser, trialReq.Source)
		_ = testutil.TryReceive(ctx, t, entitlementsRefreshed)
	})
}

func TestFirstUser_OnboardingTelemetry(t *testing.T) {
	t.Parallel()

	t.Run("OnboardingInfoFlowsToSnapshot", func(t *testing.T) {
		t.Parallel()

		ctx := testutil.Context(t, testutil.WaitMedium)
		fTelemetry := newFakeTelemetryReporter(ctx, t, 10)
		client := coderdtest.New(t, &coderdtest.Options{
			TelemetryReporter: fTelemetry,
		})

		_, err := client.CreateFirstUser(ctx, codersdk.CreateFirstUserRequest{
			Email:    "admin@coder.com",
			Username: "admin",
			Password: "SomeSecurePassword!",
			OnboardingInfo: &codersdk.CreateFirstUserOnboardingInfo{
				NewsletterMarketing: false,
				NewsletterReleases:  true,
			},
		})
		require.NoError(t, err)

		snapshot := testutil.TryReceive(ctx, t, fTelemetry.snapshots)
		require.NotNil(t, snapshot.FirstUserOnboarding)
		require.False(t, snapshot.FirstUserOnboarding.NewsletterMarketing)
		require.True(t, snapshot.FirstUserOnboarding.NewsletterReleases)
	})

	t.Run("NilWhenOnboardingInfoOmitted", func(t *testing.T) {
		t.Parallel()

		ctx := testutil.Context(t, testutil.WaitMedium)
		fTelemetry := newFakeTelemetryReporter(ctx, t, 10)
		client := coderdtest.New(t, &coderdtest.Options{
			TelemetryReporter: fTelemetry,
		})

		_, err := client.CreateFirstUser(ctx, codersdk.CreateFirstUserRequest{
			Email:    "admin@coder.com",
			Username: "admin",
			Password: "SomeSecurePassword!",
			// No OnboardingInfo — simulates old CLI or OIDC flow.
		})
		require.NoError(t, err)

		snapshot := testutil.TryReceive(ctx, t, fTelemetry.snapshots)
		require.Nil(t, snapshot.FirstUserOnboarding)
	})

	t.Run("EmptyOnboardingInfoIsNonNilWithZeroFields", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitMedium)
		fTelemetry := newFakeTelemetryReporter(ctx, t, 10)
		client := coderdtest.New(t, &coderdtest.Options{
			TelemetryReporter: fTelemetry,
		})
		_, err := client.CreateFirstUser(ctx, codersdk.CreateFirstUserRequest{
			Email: "admin@coder.com", Username: "admin",
			Password:       "SomeSecurePassword!",
			OnboardingInfo: &codersdk.CreateFirstUserOnboardingInfo{},
		})
		require.NoError(t, err)
		snapshot := testutil.TryReceive(ctx, t, fTelemetry.snapshots)
		require.NotNil(t, snapshot.FirstUserOnboarding,
			"non-nil OnboardingInfo must produce non-nil telemetry")
		require.False(t, snapshot.FirstUserOnboarding.NewsletterMarketing)
		require.False(t, snapshot.FirstUserOnboarding.NewsletterReleases)
	})
}

func TestPostLogin(t *testing.T) {
	t.Parallel()
	t.Run("InvalidUser", func(t *testing.T) {
		t.Parallel()
		client := coderdtest.New(t, nil)
		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		_, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
			Email:    "my@email.org",
			Password: "password",
		})
		var apiErr *codersdk.Error
		require.ErrorAs(t, err, &apiErr)
		require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
	})

	t.Run("BadPassword", func(t *testing.T) {
		t.Parallel()
		auditor := audit.NewMock()
		client := coderdtest.New(t, &coderdtest.Options{Auditor: auditor})
		numLogs := len(auditor.AuditLogs())

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		req := codersdk.CreateFirstUserRequest{
			Email:    "testuser@coder.com",
			Username: "testuser",
			Password: "SomeSecurePassword!",
		}
		_, err := client.CreateFirstUser(ctx, req)
		require.NoError(t, err)
		_, err = client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
			Email:    req.Email,
			Password: "badpass",
		})
		numLogs++ // add an audit log for login
		var apiErr *codersdk.Error
		require.ErrorAs(t, err, &apiErr)
		require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())

		require.Len(t, auditor.AuditLogs(), numLogs)
		require.Equal(t, database.AuditActionLogin, auditor.AuditLogs()[numLogs-1].Action)
	})

	// "hunter2" was the input of the previous hardcoded simulated hash, which
	// an empty stored hash wrongly matched; this is a regression test.
	t.Run("NonexistentUser401", func(t *testing.T) {
		t.Parallel()
		client := coderdtest.New(t, nil)
		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		_, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
			Email:    "does-not-exist@coder.com",
			Password: "hunter2",
		})
		var apiErr *codersdk.Error
		require.ErrorAs(t, err, &apiErr)
		require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
		require.Equal(t, "Incorrect email or password.", apiErr.Message)
	})

	// Attempting built-in login as an SSO user returns a 401 to avoid
	// divulging login type.
	t.Run("SSOReturns401", func(t *testing.T) {
		t.Parallel()
		client, db := coderdtest.NewWithDatabase(t, nil)
		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		// An SSO user has no password hash stored. Create one directly in the
		// database since the API requires OIDC to be configured. dbgen.User
		// substitutes a random hash for an empty one, so clear it explicitly.
		ssoUser := dbgen.User(t, db, database.User{
			Email:     "sso-user@coder.com",
			LoginType: database.LoginTypeOIDC,
		})
		//nolint:gocritic // Test setup requires a system context to clear the hash.
		err := db.UpdateUserHashedPassword(dbauthz.AsSystemRestricted(ctx), database.UpdateUserHashedPasswordParams{
			ID:             ssoUser.ID,
			HashedPassword: []byte{},
		})
		require.NoError(t, err)

		anonClient := codersdk.New(client.URL)
		_, err = anonClient.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
			Email:    ssoUser.Email,
			Password: "hunter2",
		})
		var apiErr *codersdk.Error
		require.ErrorAs(t, err, &apiErr)
		require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
		require.Equal(t, "Incorrect email or password.", apiErr.Message)
		// The login type must not be leaked.
		require.NotContains(t, apiErr.Message, string(codersdk.LoginTypeOIDC))
	})

	// Regression: the legacy `login_type = 'none'` migration converts these
	// accounts to password auth, but they have no password hash. Converting
	// login type must never let someone authenticate with an empty or guessed
	// password.
	t.Run("ConvertedNoneUserHasNoUsablePassword", func(t *testing.T) {
		t.Parallel()
		client, db := coderdtest.NewWithDatabase(t, nil)
		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		// A legacy machine user was created with login_type 'none' and no
		// password. dbgen.User substitutes a random hash for an empty one, so
		// clear it explicitly to match the real account.
		noneUser := dbgen.User(t, db, database.User{
			Email:     "legacy-machine-user@coder.com",
			LoginType: database.LoginTypeNone,
		})
		//nolint:gocritic // Test setup requires a system context to clear the hash.
		err := db.UpdateUserHashedPassword(dbauthz.AsSystemRestricted(ctx), database.UpdateUserHashedPasswordParams{
			ID:             noneUser.ID,
			HashedPassword: []byte{},
		})
		require.NoError(t, err)

		// Apply the migration's conversion: login_type 'none' -> 'password'.
		//nolint:gocritic // Test setup requires a system context to convert the login type.
		_, err = db.UpdateUserLoginType(dbauthz.AsSystemRestricted(ctx), database.UpdateUserLoginTypeParams{
			NewLoginType: database.LoginTypePassword,
			UserID:       noneUser.ID,
		})
		require.NoError(t, err)

		// Neither an empty password nor a guessed one may authenticate. An empty
		// password is rejected by request validation (400); a non-empty guess
		// fails the hash comparison against the empty stored hash (401). Both must
		// deny access.
		cases := []struct {
			name       string
			password   string
			wantStatus int
		}{
			{"EmptyPassword", "", http.StatusBadRequest},
			{"GuessedPassword", "hunter2", http.StatusUnauthorized},
		}
		for _, tc := range cases {
			anonClient := codersdk.New(client.URL)
			_, err := anonClient.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
				Email:    noneUser.Email,
				Password: tc.password,
			})
			var apiErr *codersdk.Error
			require.ErrorAs(t, err, &apiErr, "%s must not authenticate", tc.name)
			require.Equal(t, tc.wantStatus, apiErr.StatusCode(), "%s", tc.name)
		}
	})

	t.Run("Suspended", func(t *testing.T) {
		t.Parallel()
		auditor := audit.NewMock()
		client := coderdtest.New(t, &coderdtest.Options{Auditor: auditor})
		numLogs := len(auditor.AuditLogs())
		first := coderdtest.CreateFirstUser(t, client)
		numLogs++ // add an audit log for create user
		numLogs++ // add an audit log for login

		member, _ := coderdtest.CreateAnotherUser(t, client, first.OrganizationID)
		numLogs++ // add an audit log for create user

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		memberUser, err := member.User(ctx, codersdk.Me)
		require.NoError(t, err, "fetch member user")

		_, err = client.UpdateUserStatus(ctx, memberUser.Username, codersdk.UserStatusSuspended)
		require.NoError(t, err, "suspend member")
		numLogs++ // add an audit log for update user

		// Test an existing session
		_, err = member.User(ctx, codersdk.Me)
		var apiErr *codersdk.Error
		require.ErrorAs(t, err, &apiErr)
		require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
		require.Contains(t, apiErr.Message, "Contact an admin")

		// Test a new session
		_, err = client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
			Email:    memberUser.Email,
			Password: "SomeSecurePassword!",
		})
		numLogs++ // add an audit log for login
		require.ErrorAs(t, err, &apiErr)
		require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
		require.Contains(t, apiErr.Message, "suspended")

		require.Len(t, auditor.AuditLogs(), numLogs)
		require.Equal(t, database.AuditActionLogin, auditor.AuditLogs()[numLogs-1].Action)
	})

	t.Run("DisabledPasswordAuth", func(t *testing.T) {
		t.Parallel()

		dc := coderdtest.DeploymentValues(t)
		client := coderdtest.New(t, &coderdtest.Options{
			DeploymentValues: dc,
		})

		first := coderdtest.CreateFirstUser(t, client)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		// With a user account.
		const password = "SomeSecurePassword!"
		user, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
			Email:           "test+user-@coder.com",
			Username:        "user",
			Password:        password,
			OrganizationIDs: []uuid.UUID{first.OrganizationID},
		})
		require.NoError(t, err)

		dc.DisablePasswordAuth = serpent.Bool(true)

		userClient := codersdk.New(client.URL)
		_, err = userClient.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
			Email:    user.Email,
			Password: password,
		})
		require.Error(t, err)
		var apiErr *codersdk.Error
		require.ErrorAs(t, err, &apiErr)
		require.Equal(t, http.StatusForbidden, apiErr.StatusCode())
		require.Contains(t, apiErr.Message, "Password authentication is disabled")
	})

	t.Run("Success", func(t *testing.T) {
		t.Parallel()
		auditor := audit.NewMock()
		client := coderdtest.New(t, &coderdtest.Options{Auditor: auditor})
		numLogs := len(auditor.AuditLogs())

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		req := codersdk.CreateFirstUserRequest{
			Email:    "testuser@coder.com",
			Username: "testuser",
			Password: "SomeSecurePassword!",
		}
		_, err := client.CreateFirstUser(ctx, req)
		require.NoError(t, err)
		numLogs++ // add an audit log for create user
		numLogs++ // add an audit log for login

		_, err = client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
			Email:    req.Email,
			Password: req.Password,
		})
		require.NoError(t, err)

		// Login should be case insensitive
		_, err = client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
			Email:    strings.ToUpper(req.Email),
			Password: req.Password,
		})
		require.NoError(t, err)

		require.Len(t, auditor.AuditLogs(), numLogs)
		require.Equal(t, database.AuditActionLogin, auditor.AuditLogs()[numLogs-1].Action)
	})

	t.Run("Lifetime&Expire", func(t *testing.T) {
		t.Parallel()

		client := coderdtest.New(t, nil)
		owner := coderdtest.CreateFirstUser(t, client)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		split := strings.Split(client.SessionToken(), "-")
		key, err := client.APIKeyByID(ctx, owner.UserID.String(), split[0])
		require.NoError(t, err, "fetch login key")
		require.Equal(t, int64(86400), key.LifetimeSeconds, "default should be 86400")

		// tokens have a longer life
		token, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{})
		require.NoError(t, err, "make new token api key")
		split = strings.Split(token.Key, "-")
		apiKey, err := client.APIKeyByID(ctx, owner.UserID.String(), split[0])
		require.NoError(t, err, "fetch api key")

		require.True(t, apiKey.ExpiresAt.After(dbtime.Now().Add(time.Hour*24*6)), "default tokens lasts more than 6 days")
		require.True(t, apiKey.ExpiresAt.Before(dbtime.Now().Add(time.Hour*24*8)), "default tokens lasts less than 8 days")
		require.Greater(t, apiKey.LifetimeSeconds, key.LifetimeSeconds, "token should have longer lifetime")
	})
}

func TestDeleteUser(t *testing.T) {
	t.Parallel()
	t.Run("Works", func(t *testing.T) {
		t.Parallel()
		client, _, api := coderdtest.NewWithAPI(t, nil)
		user := coderdtest.CreateFirstUser(t, client)
		authz := coderdtest.AssertRBAC(t, api, client)

		anotherClient, another := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
		err := client.DeleteUser(context.Background(), another.ID)
		require.NoError(t, err)
		// Attempt to create a user with the same email and username, and delete them again.
		another, err = client.CreateUserWithOrgs(context.Background(), codersdk.CreateUserRequestWithOrgs{
			Email:           another.Email,
			Username:        another.Username,
			Password:        "SomeSecurePassword!",
			OrganizationIDs: []uuid.UUID{user.OrganizationID},
		})
		require.NoError(t, err)
		err = client.DeleteUser(context.Background(), another.ID)
		require.NoError(t, err)

		// IMPORTANT: assert that the deleted user's session is no longer valid.
		_, err = anotherClient.User(context.Background(), codersdk.Me)
		require.Error(t, err)
		var apiErr *codersdk.Error
		require.ErrorAs(t, err, &apiErr)
		require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())

		// RBAC checks
		authz.AssertChecked(t, policy.ActionCreate, rbac.ResourceUser)
		authz.AssertChecked(t, policy.ActionDelete, another)
	})
	t.Run("NoPermission", func(t *testing.T) {
		t.Parallel()
		api := coderdtest.New(t, nil)
		firstUser := coderdtest.CreateFirstUser(t, api)
		client, _ := coderdtest.CreateAnotherUser(t, api, firstUser.OrganizationID)
		err := client.DeleteUser(context.Background(), firstUser.UserID)
		var apiErr *codersdk.Error
		require.ErrorAs(t, err, &apiErr)
		require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
	})
	t.Run("HasWorkspaces", func(t *testing.T) {
		t.Parallel()
		client, _ := coderdtest.NewWithProvisionerCloser(t, nil)
		user := coderdtest.CreateFirstUser(t, client)
		anotherClient, another := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
		version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
		template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
		coderdtest.CreateWorkspace(t, anotherClient, template.ID)
		err := client.DeleteUser(context.Background(), another.ID)
		var apiErr *codersdk.Error
		require.ErrorAs(t, err, &apiErr)
		require.Equal(t, http.StatusExpectationFailed, apiErr.StatusCode())
	})
	t.Run("Self", func(t *testing.T) {
		t.Parallel()
		client := coderdtest.New(t, nil)
		user := coderdtest.CreateFirstUser(t, client)
		err := client.DeleteUser(context.Background(), user.UserID)
		var apiErr *codersdk.Error
		require.Error(t, err, "should not be able to delete self")
		require.ErrorAs(t, err, &apiErr, "should be a coderd error")
		require.Equal(t, http.StatusForbidden, apiErr.StatusCode(), "should be forbidden")
	})
	t.Run("CountCheckIncludesAllWorkspaces", func(t *testing.T) {
		t.Parallel()
		client, _ := coderdtest.NewWithProvisionerCloser(t, nil)
		firstUser := coderdtest.CreateFirstUser(t, client)

		// Create a target user who will own a workspace
		targetUserClient, targetUser := coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID)

		// Create a User Admin who should not have permission to see the target user's workspace
		userAdminClient, userAdmin := coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID)

		// Grant User Admin role to the userAdmin
		userAdmin, err := client.UpdateUserRoles(context.Background(), userAdmin.ID.String(), codersdk.UpdateRoles{
			Roles: []string{rbac.RoleUserAdmin().String()},
		})
		require.NoError(t, err)

		// Create a template and workspace owned by the target user
		version := coderdtest.CreateTemplateVersion(t, client, firstUser.OrganizationID, nil)
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
		template := coderdtest.CreateTemplate(t, client, firstUser.OrganizationID, version.ID)
		_ = coderdtest.CreateWorkspace(t, targetUserClient, template.ID)

		workspaces, err := userAdminClient.Workspaces(context.Background(), codersdk.WorkspaceFilter{
			Owner: targetUser.Username,
		})
		require.NoError(t, err)
		require.Len(t, workspaces.Workspaces, 0)

		// Attempt to delete the target user - this should fail because the
		// user has a workspace not visible to the deleting user.
		err = userAdminClient.DeleteUser(context.Background(), targetUser.ID)
		var apiErr *codersdk.Error
		require.ErrorAs(t, err, &apiErr)
		require.Equal(t, http.StatusExpectationFailed, apiErr.StatusCode())
		require.Contains(t, apiErr.Message, "has workspaces")
	})
}

func TestNotifyUserStatusChanged(t *testing.T) {
	t.Parallel()

	type expectedNotification struct {
		TemplateID uuid.UUID
		UserID     uuid.UUID
	}

	verifyNotificationDispatched := func(notifyEnq *notificationstest.FakeEnqueuer, expectedNotifications []expectedNotification, member codersdk.User, label string) {
		require.Equal(t, len(expectedNotifications), len(notifyEnq.Sent()))

		// Validate that each expected notification is present in notifyEnq.Sent()
		for _, expected := range expectedNotifications {
			found := false
			for _, sent := range notifyEnq.Sent(notificationstest.WithTemplateID(expected.TemplateID)) {
				if sent.TemplateID == expected.TemplateID &&
					sent.UserID == expected.UserID &&
					slices.Contains(sent.Targets, member.ID) &&
					sent.Labels[label] == member.Username {
					found = true

					require.IsType(t, map[string]any{}, sent.Data["user"])
					userData := sent.Data["user"].(map[string]any)
					require.Equal(t, member.ID, userData["id"])
					require.Equal(t, member.Name, userData["name"])
					require.Equal(t, member.Email, userData["email"])

					break
				}
			}
			require.True(t, found, "Expected notification not found: %+v", expected)
		}
	}

	t.Run("Account suspended", func(t *testing.T) {
		t.Parallel()

		notifyEnq := &notificationstest.FakeEnqueuer{}
		adminClient := coderdtest.New(t, &coderdtest.Options{
			NotificationsEnqueuer: notifyEnq,
		})
		firstUser := coderdtest.CreateFirstUser(t, adminClient)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		_, userAdmin := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID, rbac.RoleUserAdmin())

		member, err := adminClient.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
			OrganizationIDs: []uuid.UUID{firstUser.OrganizationID},
			Email:           "another@user.org",
			Username:        "someone-else",
			Password:        "SomeSecurePassword!",
		})
		require.NoError(t, err)

		notifyEnq.Clear()

		// when
		_, err = adminClient.UpdateUserStatus(context.Background(), member.Username, codersdk.UserStatusSuspended)
		require.NoError(t, err)

		// then
		verifyNotificationDispatched(notifyEnq, []expectedNotification{
			{TemplateID: notifications.TemplateUserAccountSuspended, UserID: firstUser.UserID},
			{TemplateID: notifications.TemplateUserAccountSuspended, UserID: userAdmin.ID},
			{TemplateID: notifications.TemplateYourAccountSuspended, UserID: member.ID},
		}, member, "suspended_account_name")
	})

	t.Run("Account reactivated", func(t *testing.T) {
		t.Parallel()

		// given
		notifyEnq := &notificationstest.FakeEnqueuer{}
		adminClient := coderdtest.New(t, &coderdtest.Options{
			NotificationsEnqueuer: notifyEnq,
		})
		firstUser := coderdtest.CreateFirstUser(t, adminClient)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		_, userAdmin := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID, rbac.RoleUserAdmin())

		member, err := adminClient.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
			OrganizationIDs: []uuid.UUID{firstUser.OrganizationID},
			Email:           "another@user.org",
			Username:        "someone-else",
			Password:        "SomeSecurePassword!",
		})
		require.NoError(t, err)

		_, err = adminClient.UpdateUserStatus(context.Background(), member.Username, codersdk.UserStatusSuspended)
		require.NoError(t, err)

		notifyEnq.Clear()

		// when
		_, err = adminClient.UpdateUserStatus(context.Background(), member.Username, codersdk.UserStatusActive)
		require.NoError(t, err)

		// then
		verifyNotificationDispatched(notifyEnq, []expectedNotification{
			{TemplateID: notifications.TemplateUserAccountActivated, UserID: firstUser.UserID},
			{TemplateID: notifications.TemplateUserAccountActivated, UserID: userAdmin.ID},
			{TemplateID: notifications.TemplateYourAccountActivated, UserID: member.ID},
		}, member, "activated_account_name")
	})
}

func TestNotifyDeletedUser(t *testing.T) {
	t.Parallel()

	t.Run("OwnerNotified", func(t *testing.T) {
		t.Parallel()

		// given
		notifyEnq := &notificationstest.FakeEnqueuer{}
		adminClient := coderdtest.New(t, &coderdtest.Options{
			NotificationsEnqueuer: notifyEnq,
		})
		firstUserResponse := coderdtest.CreateFirstUser(t, adminClient)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		firstUser, err := adminClient.User(ctx, firstUserResponse.UserID.String())
		require.NoError(t, err)

		user, err := adminClient.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
			OrganizationIDs: []uuid.UUID{firstUserResponse.OrganizationID},
			Email:           "another@user.org",
			Username:        "someone-else",
			Password:        "SomeSecurePassword!",
		})
		require.NoError(t, err)

		// when
		err = adminClient.DeleteUser(context.Background(), user.ID)
		require.NoError(t, err)

		// then
		require.Len(t, notifyEnq.Sent(), 2)
		// notifyEnq.Sent()[0] is create account event
		require.Equal(t, notifications.TemplateUserAccountDeleted, notifyEnq.Sent()[1].TemplateID)
		require.Equal(t, firstUser.ID, notifyEnq.Sent()[1].UserID)
		require.Contains(t, notifyEnq.Sent()[1].Targets, user.ID)
		require.Equal(t, user.Username, notifyEnq.Sent()[1].Labels["deleted_account_name"])
		require.Equal(t, user.Name, notifyEnq.Sent()[1].Labels["deleted_account_user_name"])
		require.Equal(t, firstUser.Name, notifyEnq.Sent()[1].Labels["initiator"])
		require.Equal(t, "user", notifyEnq.Sent()[1].Labels["account_type"])
	})

	t.Run("UserAdminNotified", func(t *testing.T) {
		t.Parallel()

		// given
		notifyEnq := &notificationstest.FakeEnqueuer{}
		adminClient := coderdtest.New(t, &coderdtest.Options{
			NotificationsEnqueuer: notifyEnq,
		})
		firstUser := coderdtest.CreateFirstUser(t, adminClient)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		_, userAdmin := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID, rbac.RoleUserAdmin())

		member, err := adminClient.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
			OrganizationIDs: []uuid.UUID{firstUser.OrganizationID},
			Email:           "another@user.org",
			Username:        "someone-else",
			Password:        "SomeSecurePassword!",
		})
		require.NoError(t, err)

		// when
		err = adminClient.DeleteUser(context.Background(), member.ID)
		require.NoError(t, err)

		// then
		sent := notifyEnq.Sent()
		require.Len(t, sent, 5)
		// Other notifications:
		// "User admin" account created, "owner" notified
		// "Member" account created, "owner" notified
		// "Member" account created, "user admin" notified

		// "Member" account deleted, "owner" notified
		ownerNotifications := notifyEnq.Sent(func(n *notificationstest.FakeNotification) bool {
			return n.TemplateID == notifications.TemplateUserAccountDeleted &&
				n.UserID == firstUser.UserID &&
				slices.Contains(n.Targets, member.ID) &&
				n.Labels["deleted_account_name"] == member.Username
		})
		require.Len(t, ownerNotifications, 1)

		// "Member" account deleted, "user admin" notified
		adminNotifications := notifyEnq.Sent(func(n *notificationstest.FakeNotification) bool {
			return n.TemplateID == notifications.TemplateUserAccountDeleted &&
				n.UserID == userAdmin.ID &&
				slices.Contains(n.Targets, member.ID) &&
				n.Labels["deleted_account_name"] == member.Username
		})
		require.Len(t, adminNotifications, 1)
	})
}

func TestPostLogout(t *testing.T) {
	t.Parallel()

	// Checks that the cookie is cleared and the API Key is deleted from the database.
	t.Run("Logout", func(t *testing.T) {
		t.Parallel()
		auditor := audit.NewMock()
		client := coderdtest.New(t, &coderdtest.Options{Auditor: auditor})
		numLogs := len(auditor.AuditLogs())

		owner := coderdtest.CreateFirstUser(t, client)
		numLogs++ // add an audit log for login

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		keyID := strings.Split(client.SessionToken(), "-")[0]
		apiKey, err := client.APIKeyByID(ctx, owner.UserID.String(), keyID)
		require.NoError(t, err)
		require.Equal(t, keyID, apiKey.ID, "API key should exist in the database")

		fullURL, err := client.URL.Parse("/api/v2/users/logout")
		require.NoError(t, err, "Server URL should parse successfully")

		res, err := client.Request(ctx, http.MethodPost, fullURL.String(), nil)
		numLogs++ // add an audit log for logout

		require.NoError(t, err, "/logout request should succeed")
		res.Body.Close()
		require.Equal(t, http.StatusOK, res.StatusCode)

		require.Len(t, auditor.AuditLogs(), numLogs)
		require.Equal(t, database.AuditActionLogout, auditor.AuditLogs()[numLogs-1].Action)

		cookies := res.Cookies()

		var found bool
		for _, cookie := range cookies {
			if cookie.Name == codersdk.SessionTokenCookie {
				require.Equal(t, codersdk.SessionTokenCookie, cookie.Name, "Cookie should be the auth cookie")
				require.Equal(t, -1, cookie.MaxAge, "Cookie should be set to delete")
				found = true
			}
		}
		require.True(t, found, "auth cookie should be returned")

		_, err = client.APIKeyByID(ctx, owner.UserID.String(), keyID)
		sdkErr := &codersdk.Error{}
		require.ErrorAs(t, err, &sdkErr)
		require.Equal(t, http.StatusUnauthorized, sdkErr.StatusCode(), "Expecting 401")
	})
}

// nolint:bodyclose
func TestPostUsers(t *testing.T) {
	t.Parallel()
	t.Run("NoAuth", func(t *testing.T) {
		t.Parallel()
		client := coderdtest.New(t, nil)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		_, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{})
		require.Error(t, err)
	})

	t.Run("Conflicting", func(t *testing.T) {
		t.Parallel()
		client := coderdtest.New(t, nil)
		coderdtest.CreateFirstUser(t, client)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		me, err := client.User(ctx, codersdk.Me)
		require.NoError(t, err)
		_, err = client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
			Email:           me.Email,
			Username:        me.Username,
			Password:        "MySecurePassword!",
			OrganizationIDs: []uuid.UUID{uuid.New()},
		})
		var apiErr *codersdk.Error
		require.ErrorAs(t, err, &apiErr)
		require.Equal(t, http.StatusConflict, apiErr.StatusCode())
	})

	t.Run("OrganizationNotFound", func(t *testing.T) {
		t.Parallel()
		client := coderdtest.New(t, nil)
		coderdtest.CreateFirstUser(t, client)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		_, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
			OrganizationIDs: []uuid.UUID{uuid.New()},
			Email:           "another@user.org",
			Username:        "someone-else",
			Password:        "SomeSecurePassword!",
		})
		var apiErr *codersdk.Error
		require.ErrorAs(t, err, &apiErr)
		require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
	})

	t.Run("Create", func(t *testing.T) {
		t.Parallel()
		auditor := audit.NewMock()
		client := coderdtest.New(t, &coderdtest.Options{Auditor: auditor})
		numLogs := len(auditor.AuditLogs())

		firstUser := coderdtest.CreateFirstUser(t, client)
		numLogs++ // add an audit log for user create
		numLogs++ // add an audit log for login

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		user, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
			OrganizationIDs: []uuid.UUID{firstUser.OrganizationID},
			Email:           "another@user.org",
			Username:        "someone-else",
			Password:        "SomeSecurePassword!",
		})
		require.NoError(t, err)

		// User should default to dormant.
		require.Equal(t, codersdk.UserStatusDormant, user.Status)

		require.Len(t, auditor.AuditLogs(), numLogs)
		require.Equal(t, database.AuditActionCreate, auditor.AuditLogs()[numLogs-1].Action)
		require.Equal(t, database.AuditActionLogin, auditor.AuditLogs()[numLogs-2].Action)

		require.Len(t, user.OrganizationIDs, 1)
		assert.Equal(t, firstUser.OrganizationID, user.OrganizationIDs[0])
	})

	t.Run("CreateWithStatus", func(t *testing.T) {
		t.Parallel()
		auditor := audit.NewMock()
		client := coderdtest.New(t, &coderdtest.Options{Auditor: auditor})
		numLogs := len(auditor.AuditLogs())

		firstUser := coderdtest.CreateFirstUser(t, client)
		numLogs++ // add an audit log for user create
		numLogs++ // add an audit log for login

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		user, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
			OrganizationIDs: []uuid.UUID{firstUser.OrganizationID},
			Email:           "another@user.org",
			Username:        "someone-else",
			Password:        "SomeSecurePassword!",
			UserStatus:      new(codersdk.UserStatusActive),
		})
		require.NoError(t, err)

		require.Equal(t, codersdk.UserStatusActive, user.Status)

		require.Len(t, auditor.AuditLogs(), numLogs)
		require.Equal(t, database.AuditActionCreate, auditor.AuditLogs()[numLogs-1].Action)
		require.Equal(t, database.AuditActionLogin, auditor.AuditLogs()[numLogs-2].Action)

		require.Len(t, user.OrganizationIDs, 1)
		assert.Equal(t, firstUser.OrganizationID, user.OrganizationIDs[0])
	})

	t.Run("LastSeenAt", func(t *testing.T) {
		t.Parallel()
		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		client := coderdtest.New(t, nil)
		firstUserResp := coderdtest.CreateFirstUser(t, client)

		firstUser, err := client.User(ctx, firstUserResp.UserID.String())
		require.NoError(t, err)

		_, _ = coderdtest.CreateAnotherUser(t, client, firstUserResp.OrganizationID)

		allUsersRes, err := client.Users(ctx, codersdk.UsersRequest{})
		require.NoError(t, err)

		require.Len(t, allUsersRes.Users, 2)

		// We sent the "GET Users" request with the first user, but the second user
		// should be Never since they haven't performed a request.
		for _, user := range allUsersRes.Users {
			if user.ID == firstUser.ID {
				require.WithinDuration(t, firstUser.LastSeenAt, dbtime.Now(), testutil.WaitShort)
			} else {
				require.Zero(t, user.LastSeenAt)
			}
		}
	})

	t.Run("CreateNoneLoginType", func(t *testing.T) {
		t.Parallel()
		client := coderdtest.New(t, nil)
		first := coderdtest.CreateFirstUser(t, client)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		_, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
			OrganizationIDs: []uuid.UUID{first.OrganizationID},
			Email:           "another@user.org",
			Username:        "someone-else",
			Password:        "",
			UserLoginType:   codersdk.LoginTypeNone,
		})
		var apiErr *codersdk.Error
		require.ErrorAs(t, err, &apiErr)
		require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
		require.Contains(t, apiErr.Message, "service account")
	})

	t.Run("CreateOIDCLoginType", func(t *testing.T) {
		t.Parallel()
		email := "another@user.org"
		fake := oidctest.NewFakeIDP(t,
			oidctest.WithServing(),
		)
		cfg := fake.OIDCConfig(t, nil, func(cfg *coderd.OIDCConfig) {
			cfg.AllowSignups = true
		})

		client := coderdtest.New(t, &coderdtest.Options{
			OIDCConfig: cfg,
		})
		first := coderdtest.CreateFirstUser(t, client)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		_, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
			OrganizationIDs: []uuid.UUID{first.OrganizationID},
			Email:           email,
			Username:        "someone-else",
			Password:        "",
			UserLoginType:   codersdk.LoginTypeOIDC,
		})
		require.NoError(t, err)

		// Try to log in with OIDC.
		userClient, _ := fake.Login(t, client, jwt.MapClaims{
			"email":          email,
			"email_verified": true,
			"sub":            uuid.NewString(),
		})

		found, err := userClient.User(ctx, "me")
		require.NoError(t, err)
		require.Equal(t, found.LoginType, codersdk.LoginTypeOIDC)
	})

	t.Run("ServiceAccount/Unlicensed", func(t *testing.T) {
		t.Parallel()
		client := coderdtest.New(t, nil)
		first := coderdtest.CreateFirstUser(t, client)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		_, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
			OrganizationIDs: []uuid.UUID{first.OrganizationID},
			Username:        "service-acct-ok",
			UserLoginType:   codersdk.LoginTypeNone,
			ServiceAccount:  true,
		})
		var apiErr *codersdk.Error
		require.ErrorAs(t, err, &apiErr)
		require.Equal(t, http.StatusForbidden, apiErr.StatusCode())
		require.Contains(t, apiErr.Message, "Premium feature")
	})

	t.Run("NonServiceAccount/WithoutEmail", func(t *testing.T) {
		t.Parallel()
		client := coderdtest.New(t, nil)
		first := coderdtest.CreateFirstUser(t, client)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		_, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
			OrganizationIDs: []uuid.UUID{first.OrganizationID},
			Username:        "regular-no-email",
			UserLoginType:   codersdk.LoginTypePassword,
		})
		var apiErr *codersdk.Error
		require.ErrorAs(t, err, &apiErr)
		require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
	})
}

func TestNotifyCreatedUser(t *testing.T) {
	t.Parallel()

	t.Run("OwnerNotified", func(t *testing.T) {
		t.Parallel()

		// given
		notifyEnq := &notificationstest.FakeEnqueuer{}
		adminClient := coderdtest.New(t, &coderdtest.Options{
			NotificationsEnqueuer: notifyEnq,
		})
		firstUser := coderdtest.CreateFirstUser(t, adminClient)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		// when
		user, err := adminClient.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
			OrganizationIDs: []uuid.UUID{firstUser.OrganizationID},
			Email:           "another@user.org",
			Username:        "someone-else",
			Password:        "SomeSecurePassword!",
		})
		require.NoError(t, err)

		// then
		sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateUserAccountCreated))
		require.Len(t, sent, 1)
		require.Equal(t, notifications.TemplateUserAccountCreated, sent[0].TemplateID)
		require.Equal(t, firstUser.UserID, sent[0].UserID)
		require.Contains(t, sent[0].Targets, user.ID)
		require.Equal(t, user.Username, sent[0].Labels["created_account_name"])
		require.Equal(t, "user", sent[0].Labels["account_type"])

		require.IsType(t, map[string]any{}, sent[0].Data["user"])
		userData := sent[0].Data["user"].(map[string]any)
		require.Equal(t, user.ID, userData["id"])
		require.Equal(t, user.Name, userData["name"])
		require.Equal(t, user.Email, userData["email"])
	})

	t.Run("UserAdminNotified", func(t *testing.T) {
		t.Parallel()

		// given
		notifyEnq := &notificationstest.FakeEnqueuer{}
		adminClient := coderdtest.New(t, &coderdtest.Options{
			NotificationsEnqueuer: notifyEnq,
		})
		firstUser := coderdtest.CreateFirstUser(t, adminClient)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		userAdmin, err := adminClient.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
			OrganizationIDs: []uuid.UUID{firstUser.OrganizationID},
			Email:           "user-admin@user.org",
			Username:        "mr-user-admin",
			Password:        "SomeSecurePassword!",
		})
		require.NoError(t, err)

		_, err = adminClient.UpdateUserRoles(ctx, userAdmin.Username, codersdk.UpdateRoles{
			Roles: []string{
				rbac.RoleUserAdmin().String(),
			},
		})
		require.NoError(t, err)

		// when
		member, err := adminClient.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
			OrganizationIDs: []uuid.UUID{firstUser.OrganizationID},
			Email:           "another@user.org",
			Username:        "someone-else",
			Password:        "SomeSecurePassword!",
		})
		require.NoError(t, err)

		// then
		sent := notifyEnq.Sent()
		require.Len(t, sent, 3)

		// "User admin" account created, "owner" notified
		ownerNotifiedAboutUserAdmin := notifyEnq.Sent(func(n *notificationstest.FakeNotification) bool {
			return n.TemplateID == notifications.TemplateUserAccountCreated &&
				n.UserID == firstUser.UserID &&
				slices.Contains(n.Targets, userAdmin.ID) &&
				n.Labels["created_account_name"] == userAdmin.Username
		})
		require.Len(t, ownerNotifiedAboutUserAdmin, 1)

		// "Member" account created, "owner" notified
		ownerNotifiedAboutMember := notifyEnq.Sent(func(n *notificationstest.FakeNotification) bool {
			return n.TemplateID == notifications.TemplateUserAccountCreated &&
				n.UserID == firstUser.UserID &&
				slices.Contains(n.Targets, member.ID) &&
				n.Labels["created_account_name"] == member.Username
		})
		require.Len(t, ownerNotifiedAboutMember, 1)

		// "Member" account created, "user admin" notified
		userAdminNotifiedAboutMember := notifyEnq.Sent(func(n *notificationstest.FakeNotification) bool {
			return n.TemplateID == notifications.TemplateUserAccountCreated &&
				n.UserID == userAdmin.ID &&
				slices.Contains(n.Targets, member.ID) &&
				n.Labels["created_account_name"] == member.Username
		})
		require.Len(t, userAdminNotifiedAboutMember, 1)
	})
}

func TestUpdateUserEmailExperimental(t *testing.T) {
	t.Parallel()

	// Share one server because these assertions do not require isolated state.
	auditor := audit.NewMock()
	client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{Auditor: auditor})
	owner := coderdtest.CreateFirstUser(t, client)
	ctx := testutil.Context(t, testutil.WaitLong)

	// Only built-in deployment owners may use the endpoint.
	userAdminClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleUserAdmin())
	_, authTarget := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
	auditor.ResetLogs()

	err := userAdminClient.UpdateUserEmail(ctx, codersdk.UpdateUserEmailRequest{
		OldEmail: authTarget.Email,
		NewEmail: fmt.Sprintf("new-%s@example.com", uuid.NewString()),
	})
	var apiErr *codersdk.Error
	require.ErrorAs(t, err, &apiErr)
	require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
	unchanged, err := db.GetUserByID(dbauthz.AsSystemRestricted(ctx), authTarget.ID)
	require.NoError(t, err)
	require.Equal(t, authTarget.Email, unchanged.Email)
	logs := auditor.AuditLogs()
	require.Len(t, logs, 1)
	require.Equal(t, int32(http.StatusNotFound), logs[0].StatusCode)
	require.JSONEq(t, `{}`, string(logs[0].AdditionalFields))

	// Owners cannot update their own email.
	ownerUser, err := client.User(ctx, owner.UserID.String())
	require.NoError(t, err)
	err = client.UpdateUserEmail(ctx, codersdk.UpdateUserEmailRequest{
		OldEmail: ownerUser.Email,
		NewEmail: fmt.Sprintf("self-%s@example.com", uuid.NewString()),
	})
	require.ErrorAs(t, err, &apiErr)
	require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())

	// Invalid inputs do not change user state.
	_, validTarget := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
	for _, req := range []codersdk.UpdateUserEmailRequest{
		{OldEmail: " " + validTarget.Email, NewEmail: "new@example.com"},
		{OldEmail: validTarget.Email, NewEmail: validTarget.Email},
		{OldEmail: strings.ToUpper(validTarget.Email), NewEmail: validTarget.Email},
	} {
		err := client.UpdateUserEmail(ctx, req)
		require.ErrorAs(t, err, &apiErr)
		require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
	}

	// The old email must exist and the new email must be available.
	_, conflictTarget := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
	_, existingUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)

	err = client.UpdateUserEmail(ctx, codersdk.UpdateUserEmailRequest{
		OldEmail: fmt.Sprintf("missing-%s@example.com", uuid.NewString()),
		NewEmail: fmt.Sprintf("new-%s@example.com", uuid.NewString()),
	})
	require.ErrorAs(t, err, &apiErr)
	require.Equal(t, http.StatusNotFound, apiErr.StatusCode())

	err = client.UpdateUserEmail(ctx, codersdk.UpdateUserEmailRequest{
		OldEmail: conflictTarget.Email,
		NewEmail: strings.ToUpper(existingUser.Email),
	})
	require.ErrorAs(t, err, &apiErr)
	require.Equal(t, http.StatusConflict, apiErr.StatusCode())

	// Login type, status, and the target's owner role do not prevent updates.
	for _, tc := range []struct {
		name      string
		loginType database.LoginType
		role      rbac.RoleIdentifier
		status    database.UserStatus
	}{
		{name: "OIDC", loginType: database.LoginTypeOIDC},
		{name: "GitHub", loginType: database.LoginTypeGithub},
		{name: "Dormant", status: database.UserStatusDormant},
		{name: "Suspended", status: database.UserStatusSuspended},
		{name: "Owner", role: rbac.RoleOwner()},
	} {
		roles := []rbac.RoleIdentifier{}
		if tc.role.Name != "" {
			roles = append(roles, tc.role)
		}
		_, variant := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, roles...)
		if tc.loginType != "" {
			variantUpdated, err := db.UpdateUserLoginType(dbauthz.AsSystemRestricted(ctx), database.UpdateUserLoginTypeParams{
				UserID:       variant.ID,
				NewLoginType: tc.loginType,
			})
			require.NoError(t, err)
			variant.LoginType = codersdk.LoginType(variantUpdated.LoginType)
		}
		if tc.status != "" {
			_, err := db.UpdateUserStatus(dbauthz.AsSystemRestricted(ctx), database.UpdateUserStatusParams{
				ID:         variant.ID,
				Status:     tc.status,
				UpdatedAt:  dbtime.Now(),
				UserIsSeen: false,
			})
			require.NoError(t, err)
		}
		newVariantEmail := fmt.Sprintf("variant-%s@example.com", uuid.NewString())
		err := client.UpdateUserEmail(ctx, codersdk.UpdateUserEmailRequest{
			OldEmail: variant.Email,
			NewEmail: newVariantEmail,
		})
		require.NoError(t, err, "variant %s should succeed", tc.name)
	}

	// The API clears OTP state, revokes tokens, and records the audit event.
	memberClient, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
	oldEmail := member.Email
	newEmail := fmt.Sprintf("Updated-%s@example.com", uuid.NewString())
	oldToken := memberClient.SessionToken()
	extraToken, err := client.CreateToken(ctx, member.ID.String(), codersdk.CreateTokenRequest{})
	require.NoError(t, err)
	err = db.UpdateUserHashedOneTimePasscode(dbauthz.AsSystemRestricted(ctx), database.UpdateUserHashedOneTimePasscodeParams{
		ID:                       member.ID,
		HashedOneTimePasscode:    []byte("hashed-passcode"),
		OneTimePasscodeExpiresAt: sql.NullTime{Time: time.Now().Add(time.Hour), Valid: true},
	})
	require.NoError(t, err)
	auditor.ResetLogs()

	err = client.UpdateUserEmail(ctx, codersdk.UpdateUserEmailRequest{
		OldEmail: oldEmail,
		NewEmail: newEmail,
	})
	require.NoError(t, err)

	sideEffectsUser, err := db.GetUserByID(dbauthz.AsSystemRestricted(ctx), member.ID)
	require.NoError(t, err)
	require.Nil(t, sideEffectsUser.HashedOneTimePasscode)
	require.False(t, sideEffectsUser.OneTimePasscodeExpiresAt.Valid)

	for _, token := range []string{oldToken, extraToken.Key} {
		revokedClient := codersdk.New(client.URL, codersdk.WithSessionToken(token))
		_, err := revokedClient.User(ctx, codersdk.Me)
		require.Error(t, err)
	}
	_, err = client.User(ctx, codersdk.Me)
	require.NoError(t, err)

	logs = auditor.AuditLogs()
	require.Len(t, logs, 1)
	require.Equal(t, owner.UserID, logs[0].UserID)
	require.Equal(t, member.ID, logs[0].ResourceID)
	require.Equal(t, database.AuditActionWrite, logs[0].Action)
	require.Equal(t, int32(http.StatusNoContent), logs[0].StatusCode)
	var fields map[string]string
	require.NoError(t, json.Unmarshal(logs[0].AdditionalFields, &fields))
	require.Equal(t, oldEmail, fields["old_email"])
	require.Equal(t, newEmail, fields["new_email"])
}

func TestUpdateUserProfile(t *testing.T) {
	t.Parallel()
	t.Run("UserNotFound", func(t *testing.T) {
		t.Parallel()
		client := coderdtest.New(t, nil)
		coderdtest.CreateFirstUser(t, client)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		_, err := client.UpdateUserProfile(ctx, uuid.New().String(), codersdk.UpdateUserProfileRequest{
			Username: "newusername",
		})
		var apiErr *codersdk.Error
		require.ErrorAs(t, err, &apiErr)
		// Right now, we are raising a BAD request error because we don't support a
		// user accessing other users info
		require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
	})

	t.Run("ConflictingUsername", func(t *testing.T) {
		t.Parallel()
		client := coderdtest.New(t, nil)
		user := coderdtest.CreateFirstUser(t, client)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		existentUser, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
			Email:           "bruno@coder.com",
			Username:        "bruno",
			Password:        "SomeSecurePassword!",
			OrganizationIDs: []uuid.UUID{user.OrganizationID},
		})
		require.NoError(t, err)
		_, err = client.UpdateUserProfile(ctx, codersdk.Me, codersdk.UpdateUserProfileRequest{
			Username: existentUser.Username,
		})
		var apiErr *codersdk.Error
		require.ErrorAs(t, err, &apiErr)
		require.Equal(t, http.StatusConflict, apiErr.StatusCode())
	})

	t.Run("UpdateSelf", func(t *testing.T) {
		t.Parallel()
		auditor := audit.NewMock()
		client := coderdtest.New(t, &coderdtest.Options{Auditor: auditor})
		numLogs := len(auditor.AuditLogs())

		coderdtest.CreateFirstUser(t, client)
		numLogs++ // add an audit log for login

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		me, err := client.User(ctx, codersdk.Me)
		require.NoError(t, err)

		userProfile, err := client.UpdateUserProfile(ctx, codersdk.Me, codersdk.UpdateUserProfileRequest{
			Username: me.Username + "1",
			Name:     me.Name + "1",
		})
		numLogs++ // add an audit log for user update

		require.NoError(t, err)
		require.Equal(t, me.Username+"1", userProfile.Username)
		require.Equal(t, me.Name+"1", userProfile.Name)

		require.Len(t, auditor.AuditLogs(), numLogs)
		require.Equal(t, database.AuditActionWrite, auditor.AuditLogs()[numLogs-1].Action)
	})

	t.Run("UpdateSelfAsMember_Name", func(t *testing.T) {
		t.Parallel()
		auditor := audit.NewMock()
		client := coderdtest.New(t, &coderdtest.Options{Auditor: auditor})
		numLogs := len(auditor.AuditLogs())

		firstUser := coderdtest.CreateFirstUser(t, client)
		numLogs++ // add an audit log for login

		memberClient, memberUser := coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID)
		numLogs++ // add an audit log for user creation

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		newName := coderdtest.RandomName(t)
		userProfile, err := memberClient.UpdateUserProfile(ctx, codersdk.Me, codersdk.UpdateUserProfileRequest{
			Name:     newName,
			Username: memberUser.Username,
		})
		numLogs++ // add an audit log for user update
		numLogs++ // add an audit log for API key creation

		require.NoError(t, err)
		require.Equal(t, memberUser.Username, userProfile.Username)
		require.Equal(t, newName, userProfile.Name)

		require.Len(t, auditor.AuditLogs(), numLogs)
		require.Equal(t, database.AuditActionWrite, auditor.AuditLogs()[numLogs-1].Action)
	})

	t.Run("UpdateSelfAsMember_Username", func(t *testing.T) {
		t.Parallel()
		auditor := audit.NewMock()
		client := coderdtest.New(t, &coderdtest.Options{Auditor: auditor})

		firstUser := coderdtest.CreateFirstUser(t, client)
		memberClient, memberUser := coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		newUsername := coderdtest.RandomUsername(t)
		_, err := memberClient.UpdateUserProfile(ctx, codersdk.Me, codersdk.UpdateUserProfileRequest{
			Name:     memberUser.Name,
			Username: newUsername,
		})

		var apiErr *codersdk.Error
		require.ErrorAs(t, err, &apiErr)
		require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
	})

	t.Run("UpdateMemberAsAdmin_Username", func(t *testing.T) {
		t.Parallel()
		auditor := audit.NewMock()
		adminClient := coderdtest.New(t, &coderdtest.Options{Auditor: auditor})
		numLogs := len(auditor.AuditLogs())

		adminUser := coderdtest.CreateFirstUser(t, adminClient)
		numLogs++ // add an audit log for login

		_, memberUser := coderdtest.CreateAnotherUser(t, adminClient, adminUser.OrganizationID)
		numLogs++ // add an audit log for user creation

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		newUsername := coderdtest.RandomUsername(t)
		userProfile, err := adminClient.UpdateUserProfile(ctx, codersdk.Me, codersdk.UpdateUserProfileRequest{
			Name:     memberUser.Name,
			Username: newUsername,
		})

		numLogs++ // add an audit log for user update
		numLogs++ // add an audit log for API key creation

		require.NoError(t, err)
		require.Equal(t, newUsername, userProfile.Username)
		require.Equal(t, memberUser.Name, userProfile.Name)

		require.Len(t, auditor.AuditLogs(), numLogs)
		require.Equal(t, database.AuditActionWrite, auditor.AuditLogs()[numLogs-1].Action)
	})

	t.Run("InvalidRealUserName", func(t *testing.T) {
		t.Parallel()
		client := coderdtest.New(t, nil)
		user := coderdtest.CreateFirstUser(t, client)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		_, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
			Email:           "john@coder.com",
			Username:        "john",
			Password:        "SomeSecurePassword!",
			OrganizationIDs: []uuid.UUID{user.OrganizationID},
		})
		require.NoError(t, err)
		_, err = client.UpdateUserProfile(ctx, codersdk.Me, codersdk.UpdateUserProfileRequest{
			Name: " Mr Bean", // must not have leading space
		})
		var apiErr *codersdk.Error
		require.ErrorAs(t, err, &apiErr)
		require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
	})

	t.Run("UpdateAvatar", func(t *testing.T) {
		t.Parallel()
		client := coderdtest.New(t, nil)
		coderdtest.CreateFirstUser(t, client)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		me, err := client.User(ctx, codersdk.Me)
		require.NoError(t, err)

		// The first user is a password user, so the avatar is editable.
		const newAvatar = "/emojis/1f600.png"
		userProfile, err := client.UpdateUserProfile(ctx, codersdk.Me, codersdk.UpdateUserProfileRequest{
			Username:  me.Username,
			Name:      me.Name,
			AvatarURL: newAvatar,
		})
		require.NoError(t, err)
		require.Equal(t, newAvatar, userProfile.AvatarURL)
	})

	t.Run("IgnoresAvatarForSSOUser", func(t *testing.T) {
		t.Parallel()
		client, db := coderdtest.NewWithDatabase(t, nil)
		// The first user is an owner and can update other users' profiles.
		coderdtest.CreateFirstUser(t, client)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		// Avatars for SSO users are synced from the identity provider on login,
		// so a submitted avatar must be ignored and the existing one preserved.
		ssoUser := dbgen.User(t, db, database.User{
			Email:     "sso-avatar@coder.com",
			Username:  "sso-avatar",
			LoginType: database.LoginTypeOIDC,
		})

		// dbgen.User does not persist the avatar at creation, so set it directly
		// to emulate an avatar synced from the identity provider.
		const idpAvatar = "https://idp.example.com/avatar.png"
		//nolint:gocritic // Test setup requires a system context to set the avatar.
		ssoUser, err := db.UpdateUserProfile(dbauthz.AsSystemRestricted(ctx), database.UpdateUserProfileParams{
			ID:        ssoUser.ID,
			Email:     ssoUser.Email,
			Name:      ssoUser.Name,
			AvatarURL: idpAvatar,
			Username:  ssoUser.Username,
			UpdatedAt: dbtime.Now(),
		})
		require.NoError(t, err)

		userProfile, err := client.UpdateUserProfile(ctx, ssoUser.ID.String(), codersdk.UpdateUserProfileRequest{
			Username:  ssoUser.Username,
			Name:      ssoUser.Name,
			AvatarURL: "/emojis/1f600.png",
		})
		require.NoError(t, err)
		require.Equal(t, idpAvatar, userProfile.AvatarURL)
	})
}

func TestUpdateUserPassword(t *testing.T) {
	t.Parallel()

	t.Run("MemberCantUpdateAdminPassword", func(t *testing.T) {
		t.Parallel()
		client := coderdtest.New(t, nil)
		owner := coderdtest.CreateFirstUser(t, client)
		member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		err := member.UpdateUserPassword(ctx, owner.UserID.String(), codersdk.UpdateUserPasswordRequest{
			Password: "newpassword",
		})
		require.Error(t, err, "member should not be able to update admin password")
	})

	t.Run("AdminCanUpdateMemberPassword", func(t *testing.T) {
		t.Parallel()
		client := coderdtest.New(t, nil)
		owner := coderdtest.CreateFirstUser(t, client)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		member, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
			Email:           "coder@coder.com",
			Username:        "coder",
			Password:        "SomeStrongPassword!",
			OrganizationIDs: []uuid.UUID{owner.OrganizationID},
		})
		require.NoError(t, err, "create member")
		err = client.UpdateUserPassword(ctx, member.ID.String(), codersdk.UpdateUserPasswordRequest{
			Password: "SomeNewStrongPassword!",
		})
		require.NoError(t, err, "admin should be able to update member password")
		// Check if the member can login using the new password
		_, err = client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
			Email:    "coder@coder.com",
			Password: "SomeNewStrongPassword!",
		})
		require.NoError(t, err, "member should login successfully with the new password")
	})

	t.Run("UserAdminCanUpdateMemberPassword", func(t *testing.T) {
		t.Parallel()
		client := coderdtest.New(t, nil)
		owner := coderdtest.CreateFirstUser(t, client)

		userAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleUserAdmin())
		memberClient, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		apikey1, err := memberClient.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{})
		require.NoError(t, err)
		apikey1ID, _, ok := strings.Cut(apikey1.Key, "-")
		require.True(t, ok)

		apikey2, err := memberClient.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{})
		require.NoError(t, err)
		apikey2ID, _, ok := strings.Cut(apikey2.Key, "-")
		require.True(t, ok)

		// Confirm the token IDs resolve before the reset, so later
		// 404s prove password reset removed them.
		_, err = memberClient.APIKeyByID(ctx, member.ID.String(), apikey1ID)
		require.NoError(t, err)
		_, err = memberClient.APIKeyByID(ctx, member.ID.String(), apikey2ID)
		require.NoError(t, err)

		err = userAdmin.UpdateUserPassword(ctx, member.ID.String(), codersdk.UpdateUserPasswordRequest{
			Password: "SomeNewStrongPassword!",
		})
		require.NoError(t, err, "user-admin should be able to update member password")

		// The old session token was deleted with the password reset, so the
		// member client is unauthorized before it logs back in.
		_, err = memberClient.APIKeyByID(ctx, member.ID.String(), apikey1ID)
		require.Error(t, err)
		cerr := coderdtest.SDKError(t, err)
		require.Equal(t, http.StatusUnauthorized, cerr.StatusCode())

		resp, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
			Email:    member.Email,
			Password: "SomeNewStrongPassword!",
		})
		require.NoError(t, err, "member should login successfully with the new password")

		memberClient = codersdk.New(
			memberClient.URL,
			codersdk.WithSessionToken(resp.SessionToken),
			codersdk.WithHTTPClient(coderdtest.NewIsolatedHTTPClient(memberClient.URL)),
		)

		// After re-authentication, the old API keys should be gone from storage.
		_, err = memberClient.APIKeyByID(ctx, member.ID.String(), apikey1ID)
		require.Error(t, err)
		cerr = coderdtest.SDKError(t, err)
		require.Equal(t, http.StatusNotFound, cerr.StatusCode())

		_, err = memberClient.APIKeyByID(ctx, member.ID.String(), apikey2ID)
		require.Error(t, err)
		cerr = coderdtest.SDKError(t, err)
		require.Equal(t, http.StatusNotFound, cerr.StatusCode())
	})

	t.Run("AuditorCantUpdateOtherUserPassword", func(t *testing.T) {
		t.Parallel()
		client := coderdtest.New(t, nil)
		owner := coderdtest.CreateFirstUser(t, client)

		auditor, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleAuditor())

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		member, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
			Email:           "coder@coder.com",
			Username:        "coder",
			Password:        "SomeStrongPassword!",
			OrganizationIDs: []uuid.UUID{owner.OrganizationID},
		})
		require.NoError(t, err, "create member")

		err = auditor.UpdateUserPassword(ctx, member.ID.String(), codersdk.UpdateUserPasswordRequest{
			Password: "SomeNewStrongPassword!",
		})
		require.Error(t, err, "auditor should not be able to update member password")
		require.ErrorContains(t, err, "unexpected status code 404: Resource not found or you do not have access to this resource")
	})

	t.Run("MemberCanUpdateOwnPassword", func(t *testing.T) {
		t.Parallel()
		auditor := audit.NewMock()
		client := coderdtest.New(t, &coderdtest.Options{Auditor: auditor})
		numLogs := len(auditor.AuditLogs())

		owner := coderdtest.CreateFirstUser(t, client)
		numLogs++ // add an audit log for user create
		numLogs++ // add an audit log for login

		member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
		numLogs++ // add an audit log for user create

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		err := member.UpdateUserPassword(ctx, "me", codersdk.UpdateUserPasswordRequest{
			OldPassword: "SomeSecurePassword!",
			Password:    "MyNewSecurePassword!",
		})
		numLogs++ // add an audit log for user update

		require.NoError(t, err, "member should be able to update own password")

		require.Len(t, auditor.AuditLogs(), numLogs)
		require.Equal(t, database.AuditActionWrite, auditor.AuditLogs()[numLogs-1].Action)
	})

	t.Run("MemberCantUpdateOwnPasswordWithoutOldPassword", func(t *testing.T) {
		t.Parallel()
		client := coderdtest.New(t, nil)
		owner := coderdtest.CreateFirstUser(t, client)
		member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		err := member.UpdateUserPassword(ctx, "me", codersdk.UpdateUserPasswordRequest{
			Password: "newpassword",
		})
		require.Error(t, err, "member should not be able to update own password without providing old password")
		require.ErrorContains(t, err, "Old password is required.")
	})

	t.Run("AuditorCantTellIfPasswordIncorrect", func(t *testing.T) {
		t.Parallel()
		auditor := audit.NewMock()
		adminClient := coderdtest.New(t, &coderdtest.Options{Auditor: auditor})

		adminUser := coderdtest.CreateFirstUser(t, adminClient)

		auditorClient, _ := coderdtest.CreateAnotherUser(t, adminClient,
			adminUser.OrganizationID,
			rbac.RoleAuditor(),
		)

		_, memberUser := coderdtest.CreateAnotherUser(t, adminClient, adminUser.OrganizationID)
		numLogs := len(auditor.AuditLogs())

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		err := auditorClient.UpdateUserPassword(ctx, memberUser.ID.String(), codersdk.UpdateUserPasswordRequest{
			Password: "MySecurePassword!",
		})
		numLogs++ // add an audit log for user update

		require.Error(t, err, "auditors shouldn't be able to update passwords")
		var httpErr *codersdk.Error
		require.True(t, xerrors.As(err, &httpErr))
		// ensure that the error we get is "not found" and not "bad request"
		require.Equal(t, http.StatusNotFound, httpErr.StatusCode())

		require.Len(t, auditor.AuditLogs(), numLogs)
		require.Equal(t, database.AuditActionWrite, auditor.AuditLogs()[numLogs-1].Action)
		require.Equal(t, int32(http.StatusNotFound), auditor.AuditLogs()[numLogs-1].StatusCode)
	})

	t.Run("AdminCantUpdateOwnPasswordWithoutOldPassword", func(t *testing.T) {
		t.Parallel()
		auditor := audit.NewMock()
		client := coderdtest.New(t, &coderdtest.Options{Auditor: auditor})
		numLogs := len(auditor.AuditLogs())

		_ = coderdtest.CreateFirstUser(t, client)
		numLogs++ // add an audit log for login

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		err := client.UpdateUserPassword(ctx, "me", codersdk.UpdateUserPasswordRequest{
			Password: "MySecurePassword!",
		})
		numLogs++ // add an audit log for user update

		require.Error(t, err, "admin should not be able to update own password without providing old password")
		require.ErrorContains(t, err, "Old password is required.")

		require.Len(t, auditor.AuditLogs(), numLogs)
		require.Equal(t, database.AuditActionWrite, auditor.AuditLogs()[numLogs-1].Action)
	})

	t.Run("ValidateUserPassword", func(t *testing.T) {
		t.Parallel()
		auditor := audit.NewMock()
		client := coderdtest.New(t, &coderdtest.Options{Auditor: auditor})

		_ = coderdtest.CreateFirstUser(t, client)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		resp, err := client.ValidateUserPassword(ctx, codersdk.ValidateUserPasswordRequest{
			Password: "MySecurePassword!",
		})

		require.NoError(t, err, "users shoud be able to validate complexity of a potential new password")
		require.True(t, resp.Valid)
	})

	t.Run("ChangingPasswordDeletesKeys", func(t *testing.T) {
		t.Parallel()

		client := coderdtest.New(t, nil)
		user := coderdtest.CreateFirstUser(t, client)
		ctx := testutil.Context(t, testutil.WaitLong)

		apikey1, err := client.CreateToken(ctx, user.UserID.String(), codersdk.CreateTokenRequest{})
		require.NoError(t, err)

		apikey2, err := client.CreateToken(ctx, user.UserID.String(), codersdk.CreateTokenRequest{})
		require.NoError(t, err)

		err = client.UpdateUserPassword(ctx, "me", codersdk.UpdateUserPasswordRequest{
			OldPassword: "SomeSecurePassword!",
			Password:    "MyNewSecurePassword!",
		})
		require.NoError(t, err)

		// Trying to get an API key should fail since our client's token
		// has been deleted.
		_, err = client.APIKeyByID(ctx, user.UserID.String(), apikey1.Key)
		require.Error(t, err)
		cerr := coderdtest.SDKError(t, err)
		require.Equal(t, http.StatusUnauthorized, cerr.StatusCode())

		resp, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
			Email:    coderdtest.FirstUserParams.Email,
			Password: "MyNewSecurePassword!",
		})
		require.NoError(t, err)

		client.SetSessionToken(resp.SessionToken)

		// Trying to get an API key should fail since all keys are deleted
		// on password change.
		_, err = client.APIKeyByID(ctx, user.UserID.String(), apikey1.Key)
		require.Error(t, err)
		cerr = coderdtest.SDKError(t, err)
		require.Equal(t, http.StatusNotFound, cerr.StatusCode())

		_, err = client.APIKeyByID(ctx, user.UserID.String(), apikey2.Key)
		require.Error(t, err)
		cerr = coderdtest.SDKError(t, err)
		require.Equal(t, http.StatusNotFound, cerr.StatusCode())
	})

	t.Run("UserAdminCannotResetOwnerPassword", func(t *testing.T) {
		t.Parallel()
		client := coderdtest.New(t, nil)
		owner := coderdtest.CreateFirstUser(t, client)
		userAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleUserAdmin())

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		err := userAdmin.UpdateUserPassword(ctx, owner.UserID.String(), codersdk.UpdateUserPasswordRequest{
			Password: "SomeNewStrongPassword!",
		})
		require.Error(t, err, "user-admin should not be able to reset owner password")
		var apiErr *codersdk.Error
		require.ErrorAs(t, err, &apiErr)
		require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
		require.Contains(t, apiErr.Message, "Only owners can change the password of an owner")
	})

	t.Run("OwnerCanResetOwnerPassword", func(t *testing.T) {
		t.Parallel()
		client := coderdtest.New(t, nil)
		owner := coderdtest.CreateFirstUser(t, client)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		anotherOwner, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
			Email:           "another-owner@coder.com",
			Username:        "another-owner",
			Password:        "SomeStrongPassword!",
			OrganizationIDs: []uuid.UUID{owner.OrganizationID},
		})
		require.NoError(t, err)
		_, err = client.UpdateUserRoles(ctx, anotherOwner.ID.String(), codersdk.UpdateRoles{
			Roles: []string{rbac.RoleOwner().String()},
		})
		require.NoError(t, err)

		err = client.UpdateUserPassword(ctx, anotherOwner.ID.String(), codersdk.UpdateUserPasswordRequest{
			Password: "SomeNewStrongPassword!",
		})
		require.NoError(t, err, "owner should be able to reset another owner's password")

		_, err = client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
			Email:    "another-owner@coder.com",
			Password: "SomeNewStrongPassword!",
		})
		require.NoError(t, err, "other owner should login with the new password")
	})

	t.Run("PasswordsMustDiffer", func(t *testing.T) {
		t.Parallel()

		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)
		ctx := testutil.Context(t, testutil.WaitLong)

		err := client.UpdateUserPassword(ctx, "me", codersdk.UpdateUserPasswordRequest{
			Password: coderdtest.FirstUserParams.Password,
		})
		require.Error(t, err)
		cerr := coderdtest.SDKError(t, err)
		require.Equal(t, http.StatusBadRequest, cerr.StatusCode())
	})
}

// TestInitialRoles ensures the starting roles for the first user are correct.
func TestInitialRoles(t *testing.T) {
	t.Parallel()
	ctx := context.Background()
	client := coderdtest.New(t, nil)
	first := coderdtest.CreateFirstUser(t, client)

	roles, err := client.UserRoles(ctx, codersdk.Me)
	require.NoError(t, err)
	require.ElementsMatch(t, roles.Roles, []string{
		codersdk.RoleOwner,
	}, "should be a member and admin")

	require.ElementsMatch(t, roles.OrganizationRoles[first.OrganizationID], []string{}, "should be a member")
}

func TestPutUserSuspend(t *testing.T) {
	t.Parallel()

	t.Run("SuspendAnOwner", func(t *testing.T) {
		t.Parallel()
		client := coderdtest.New(t, nil)
		me := coderdtest.CreateFirstUser(t, client)
		_, user := coderdtest.CreateAnotherUser(t, client, me.OrganizationID, rbac.RoleOwner())

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		_, err := client.UpdateUserStatus(ctx, user.Username, codersdk.UserStatusSuspended)
		require.Error(t, err, "cannot suspend owners")
	})

	t.Run("SuspendAnotherUser", func(t *testing.T) {
		t.Parallel()
		auditor := audit.NewMock()
		client := coderdtest.New(t, &coderdtest.Options{Auditor: auditor})
		numLogs := len(auditor.AuditLogs())

		me := coderdtest.CreateFirstUser(t, client)
		numLogs++ // add an audit log for user create
		numLogs++ // add an audit log for login

		_, user := coderdtest.CreateAnotherUser(t, client, me.OrganizationID)
		numLogs++ // add an audit log for user create

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		user, err := client.UpdateUserStatus(ctx, user.Username, codersdk.UserStatusSuspended)
		require.NoError(t, err)
		require.Equal(t, user.Status, codersdk.UserStatusSuspended)
		numLogs++ // add an audit log for user update

		require.Len(t, auditor.AuditLogs(), numLogs)
		require.Equal(t, database.AuditActionWrite, auditor.AuditLogs()[numLogs-1].Action)
	})

	t.Run("SuspendItSelf", func(t *testing.T) {
		t.Parallel()
		client := coderdtest.New(t, nil)
		coderdtest.CreateFirstUser(t, client)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		client.User(ctx, codersdk.Me)
		_, err := client.UpdateUserStatus(ctx, codersdk.Me, codersdk.UserStatusSuspended)

		require.ErrorContains(t, err, "suspend yourself", "cannot suspend yourself")
	})
}

func TestActivateDormantUser(t *testing.T) {
	t.Parallel()
	client := coderdtest.New(t, nil)

	// Create users
	me := coderdtest.CreateFirstUser(t, client)
	ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
	defer cancel()
	anotherUser, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
		Email:           "coder@coder.com",
		Username:        "coder",
		Password:        "SomeStrongPassword!",
		OrganizationIDs: []uuid.UUID{me.OrganizationID},
	})
	require.NoError(t, err)

	// Ensure that new user has dormant account
	require.Equal(t, codersdk.UserStatusDormant, anotherUser.Status)

	// Activate user account
	_, err = client.UpdateUserStatus(ctx, anotherUser.Username, codersdk.UserStatusActive)
	require.NoError(t, err)

	// Verify if the account is active now
	anotherUser, err = client.User(ctx, anotherUser.Username)
	require.NoError(t, err)
	require.Equal(t, codersdk.UserStatusActive, anotherUser.Status)
}

func TestGetUser(t *testing.T) {
	t.Parallel()

	// Single instance shared across all sub-tests. All lookups
	// are read-only against the first user.
	client := coderdtest.New(t, nil)
	firstUser := coderdtest.CreateFirstUser(t, client)

	t.Run("ByMe", func(t *testing.T) {
		t.Parallel()

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		user, err := client.User(ctx, codersdk.Me)
		require.NoError(t, err)
		require.Equal(t, firstUser.UserID, user.ID)
		require.Equal(t, firstUser.OrganizationID, user.OrganizationIDs[0])
	})

	t.Run("ByID", func(t *testing.T) {
		t.Parallel()

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		user, err := client.User(ctx, firstUser.UserID.String())
		require.NoError(t, err)
		require.Equal(t, firstUser.UserID, user.ID)
		require.Equal(t, firstUser.OrganizationID, user.OrganizationIDs[0])
	})

	t.Run("ByUsername", func(t *testing.T) {
		t.Parallel()

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		exp, err := client.User(ctx, firstUser.UserID.String())
		require.NoError(t, err)

		user, err := client.User(ctx, exp.Username)
		require.NoError(t, err)
		require.Equal(t, exp.ID, user.ID)
	})
}

func TestGetUsersFilter(t *testing.T) {
	t.Parallel()

	client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{
		IncludeProvisionerDaemon: true,
		OIDCConfig: &coderd.OIDCConfig{
			AllowSignups: true,
		},
	})
	_ = coderdtest.CreateFirstUser(t, client)

	setupCtx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
	defer cancel()

	coderdtest.UsersFilter(setupCtx, t, client, api.Database, nil, nil, func(testCtx context.Context, req codersdk.UsersRequest) []codersdk.ReducedUser {
		res, err := client.Users(testCtx, req)
		require.NoError(t, err)
		reduced := make([]codersdk.ReducedUser, len(res.Users))
		for i, user := range res.Users {
			reduced[i] = user.ReducedUser
		}
		return reduced
	})
}

func TestGetUsersPagination(t *testing.T) {
	t.Parallel()
	client := coderdtest.New(t, nil)
	_ = coderdtest.CreateFirstUser(t, client)

	ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
	defer cancel()

	coderdtest.UsersPagination(ctx, t, client, nil, func(req codersdk.UsersRequest) ([]codersdk.ReducedUser, int) {
		res, err := client.Users(ctx, req)
		require.NoError(t, err)
		reduced := make([]codersdk.ReducedUser, len(res.Users))
		for i, user := range res.Users {
			reduced[i] = user.ReducedUser
		}
		return reduced, res.Count
	})
}

func TestPostTokens(t *testing.T) {
	t.Parallel()
	client := coderdtest.New(t, nil)
	_ = coderdtest.CreateFirstUser(t, client)

	ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
	defer cancel()

	apiKey, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{})
	require.NotNil(t, apiKey)
	require.GreaterOrEqual(t, len(apiKey.Key), 2)
	require.NoError(t, err)
}

func TestUserTerminalFont(t *testing.T) {
	t.Parallel()

	// Single instance shared across all sub-tests. Each sub-test
	// creates its own non-admin user for isolation.
	adminClient := coderdtest.New(t, nil)
	firstUser := coderdtest.CreateFirstUser(t, adminClient)

	t.Run("valid font", func(t *testing.T) {
		t.Parallel()

		client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
		defer cancel()

		// given
		initial, err := client.GetUserAppearanceSettings(ctx, codersdk.Me)
		require.NoError(t, err)
		require.Equal(t, codersdk.TerminalFontName(""), initial.TerminalFont)

		// when
		updated, err := client.UpdateUserAppearanceSettings(ctx, codersdk.Me, codersdk.UpdateUserAppearanceSettingsRequest{
			ThemePreference: "light",
			TerminalFont:    "fira-code",
		})
		require.NoError(t, err)

		// then
		require.Equal(t, codersdk.TerminalFontFiraCode, updated.TerminalFont)
	})

	t.Run("unsupported font", func(t *testing.T) {
		t.Parallel()

		client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
		defer cancel()

		// given
		initial, err := client.GetUserAppearanceSettings(ctx, codersdk.Me)
		require.NoError(t, err)
		require.Equal(t, codersdk.TerminalFontName(""), initial.TerminalFont)

		// when
		_, err = client.UpdateUserAppearanceSettings(ctx, codersdk.Me, codersdk.UpdateUserAppearanceSettingsRequest{
			ThemePreference: "light",
			TerminalFont:    "foobar",
		})

		// then
		require.Error(t, err)
	})

	t.Run("undefined font is not ok", func(t *testing.T) {
		t.Parallel()

		client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
		defer cancel()

		// given
		initial, err := client.GetUserAppearanceSettings(ctx, codersdk.Me)
		require.NoError(t, err)
		require.Equal(t, codersdk.TerminalFontName(""), initial.TerminalFont)

		// when
		_, err = client.UpdateUserAppearanceSettings(ctx, codersdk.Me, codersdk.UpdateUserAppearanceSettingsRequest{
			ThemePreference: "light",
			TerminalFont:    "",
		})

		// then
		require.Error(t, err)
	})
}

func TestUserThemeMode(t *testing.T) {
	t.Parallel()

	adminClient := coderdtest.New(t, nil)
	firstUser := coderdtest.CreateFirstUser(t, adminClient)

	t.Run("defaults to empty", func(t *testing.T) {
		t.Parallel()

		client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
		defer cancel()

		initial, err := client.GetUserAppearanceSettings(ctx, codersdk.Me)
		require.NoError(t, err)
		// A fresh user has never written any theme_* key. The GET handler
		// should return empty strings rather than error out.
		require.Equal(t, codersdk.ThemeModeUnset, initial.ThemeMode)
		require.Equal(t, "", initial.ThemeLight)
		require.Equal(t, "", initial.ThemeDark)
	})

	t.Run("sync mode roundtrip", func(t *testing.T) {
		t.Parallel()

		client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
		defer cancel()

		updated, err := client.UpdateUserAppearanceSettings(ctx, codersdk.Me, codersdk.UpdateUserAppearanceSettingsRequest{
			ThemePreference: "dark-tritan",
			ThemeMode:       codersdk.ThemeModeSync,
			ThemeLight:      "light-tritan",
			ThemeDark:       "dark-tritan",
			TerminalFont:    codersdk.TerminalFontGeistMono,
		})
		require.NoError(t, err)
		require.Equal(t, codersdk.ThemeModeSync, updated.ThemeMode)
		require.Equal(t, "light-tritan", updated.ThemeLight)
		require.Equal(t, "dark-tritan", updated.ThemeDark)

		// Fetched values should match.
		fetched, err := client.GetUserAppearanceSettings(ctx, codersdk.Me)
		require.NoError(t, err)
		require.Equal(t, codersdk.ThemeModeSync, fetched.ThemeMode)
		require.Equal(t, "light-tritan", fetched.ThemeLight)
		require.Equal(t, "dark-tritan", fetched.ThemeDark)
	})

	t.Run("sync mode accepts any concrete theme per slot", func(t *testing.T) {
		t.Parallel()

		client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
		defer cancel()

		updated, err := client.UpdateUserAppearanceSettings(ctx, codersdk.Me, codersdk.UpdateUserAppearanceSettingsRequest{
			ThemePreference: "dark-tritan",
			ThemeMode:       codersdk.ThemeModeSync,
			ThemeLight:      "dark-tritan",
			ThemeDark:       "light-tritan",
			TerminalFont:    codersdk.TerminalFontGeistMono,
		})
		require.NoError(t, err)
		require.Equal(t, codersdk.ThemeModeSync, updated.ThemeMode)
		require.Equal(t, "dark-tritan", updated.ThemeLight)
		require.Equal(t, "light-tritan", updated.ThemeDark)
	})

	t.Run("empty theme_mode is accepted for back-compat", func(t *testing.T) {
		t.Parallel()

		client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
		defer cancel()

		// A concrete legacy preference plus an unset mode is enough for
		// modern clients to treat the user as single mode. The server does
		// not write the new fields for old clients because doing so would
		// erase existing sync settings.
		updated, err := client.UpdateUserAppearanceSettings(ctx, codersdk.Me, codersdk.UpdateUserAppearanceSettingsRequest{
			ThemePreference: "dark",
			TerminalFont:    codersdk.TerminalFontGeistMono,
		})
		require.NoError(t, err)
		require.Equal(t, "dark", updated.ThemePreference)
		require.Equal(t, codersdk.ThemeModeUnset, updated.ThemeMode)
		require.Equal(t, "", updated.ThemeLight)
		require.Equal(t, "", updated.ThemeDark)
	})

	t.Run("omitted theme fields preserve sync settings", func(t *testing.T) {
		t.Parallel()

		client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
		defer cancel()

		_, err := client.UpdateUserAppearanceSettings(ctx, codersdk.Me, codersdk.UpdateUserAppearanceSettingsRequest{
			ThemePreference: "dark-tritan",
			ThemeMode:       codersdk.ThemeModeSync,
			ThemeLight:      "light-tritan",
			ThemeDark:       "dark-tritan",
			TerminalFont:    codersdk.TerminalFontGeistMono,
		})
		require.NoError(t, err)

		updated, err := client.UpdateUserAppearanceSettings(ctx, codersdk.Me, codersdk.UpdateUserAppearanceSettingsRequest{
			ThemePreference: "dark",
			TerminalFont:    codersdk.TerminalFontFiraCode,
		})
		require.NoError(t, err)
		require.Equal(t, "dark", updated.ThemePreference)
		require.Equal(t, codersdk.ThemeModeSync, updated.ThemeMode)
		require.Equal(t, "light-tritan", updated.ThemeLight)
		require.Equal(t, "dark-tritan", updated.ThemeDark)
		require.Equal(t, codersdk.TerminalFontFiraCode, updated.TerminalFont)

		fetched, err := client.GetUserAppearanceSettings(ctx, codersdk.Me)
		require.NoError(t, err)
		require.Equal(t, "dark", fetched.ThemePreference)
		require.Equal(t, codersdk.ThemeModeSync, fetched.ThemeMode)
		require.Equal(t, "light-tritan", fetched.ThemeLight)
		require.Equal(t, "dark-tritan", fetched.ThemeDark)
		require.Equal(t, codersdk.TerminalFontFiraCode, fetched.TerminalFont)
	})

	t.Run("single mode with omitted slots preserves sync settings", func(t *testing.T) {
		t.Parallel()

		client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
		defer cancel()

		_, err := client.UpdateUserAppearanceSettings(ctx, codersdk.Me, codersdk.UpdateUserAppearanceSettingsRequest{
			ThemePreference: "dark-tritan",
			ThemeMode:       codersdk.ThemeModeSync,
			ThemeLight:      "light-tritan",
			ThemeDark:       "dark-tritan",
			TerminalFont:    codersdk.TerminalFontGeistMono,
		})
		require.NoError(t, err)

		updated, err := client.UpdateUserAppearanceSettings(ctx, codersdk.Me, codersdk.UpdateUserAppearanceSettingsRequest{
			ThemePreference: "dark",
			ThemeMode:       codersdk.ThemeModeSingle,
			TerminalFont:    codersdk.TerminalFontFiraCode,
		})
		require.NoError(t, err)
		require.Equal(t, "dark", updated.ThemePreference)
		require.Equal(t, codersdk.ThemeModeSingle, updated.ThemeMode)
		require.Equal(t, "light-tritan", updated.ThemeLight)
		require.Equal(t, "dark-tritan", updated.ThemeDark)
		require.Equal(t, codersdk.TerminalFontFiraCode, updated.TerminalFont)

		fetched, err := client.GetUserAppearanceSettings(ctx, codersdk.Me)
		require.NoError(t, err)
		require.Equal(t, "dark", fetched.ThemePreference)
		require.Equal(t, codersdk.ThemeModeSingle, fetched.ThemeMode)
		require.Equal(t, "light-tritan", fetched.ThemeLight)
		require.Equal(t, "dark-tritan", fetched.ThemeDark)
		require.Equal(t, codersdk.TerminalFontFiraCode, fetched.TerminalFont)
	})

	t.Run("single mode with explicit slots updates slots", func(t *testing.T) {
		t.Parallel()

		client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
		defer cancel()

		updated, err := client.UpdateUserAppearanceSettings(ctx, codersdk.Me, codersdk.UpdateUserAppearanceSettingsRequest{
			ThemePreference: "light-tritan",
			ThemeMode:       codersdk.ThemeModeSingle,
			ThemeLight:      "dark-tritan",
			ThemeDark:       "light-protan-deuter",
			TerminalFont:    codersdk.TerminalFontFiraCode,
		})
		require.NoError(t, err)
		require.Equal(t, "light-tritan", updated.ThemePreference)
		require.Equal(t, codersdk.ThemeModeSingle, updated.ThemeMode)
		require.Equal(t, "dark-tritan", updated.ThemeLight)
		require.Equal(t, "light-protan-deuter", updated.ThemeDark)
		require.Equal(t, codersdk.TerminalFontFiraCode, updated.TerminalFont)

		fetched, err := client.GetUserAppearanceSettings(ctx, codersdk.Me)
		require.NoError(t, err)
		require.Equal(t, "light-tritan", fetched.ThemePreference)
		require.Equal(t, codersdk.ThemeModeSingle, fetched.ThemeMode)
		require.Equal(t, "dark-tritan", fetched.ThemeLight)
		require.Equal(t, "light-protan-deuter", fetched.ThemeDark)
		require.Equal(t, codersdk.TerminalFontFiraCode, fetched.TerminalFont)
	})

	t.Run("single mode with one explicit slot updates only that slot", func(t *testing.T) {
		t.Parallel()

		client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
		defer cancel()

		_, err := client.UpdateUserAppearanceSettings(ctx, codersdk.Me, codersdk.UpdateUserAppearanceSettingsRequest{
			ThemePreference: "dark-tritan",
			ThemeMode:       codersdk.ThemeModeSync,
			ThemeLight:      "light-tritan",
			ThemeDark:       "dark-tritan",
			TerminalFont:    codersdk.TerminalFontGeistMono,
		})
		require.NoError(t, err)

		updated, err := client.UpdateUserAppearanceSettings(ctx, codersdk.Me, codersdk.UpdateUserAppearanceSettingsRequest{
			ThemePreference: "light",
			ThemeMode:       codersdk.ThemeModeSingle,
			ThemeLight:      "light-protan-deuter",
			TerminalFont:    codersdk.TerminalFontFiraCode,
		})
		require.NoError(t, err)
		require.Equal(t, "light", updated.ThemePreference)
		require.Equal(t, codersdk.ThemeModeSingle, updated.ThemeMode)
		require.Equal(t, "light-protan-deuter", updated.ThemeLight)
		require.Equal(t, "dark-tritan", updated.ThemeDark)
		require.Equal(t, codersdk.TerminalFontFiraCode, updated.TerminalFont)

		fetched, err := client.GetUserAppearanceSettings(ctx, codersdk.Me)
		require.NoError(t, err)
		require.Equal(t, "light", fetched.ThemePreference)
		require.Equal(t, codersdk.ThemeModeSingle, fetched.ThemeMode)
		require.Equal(t, "light-protan-deuter", fetched.ThemeLight)
		require.Equal(t, "dark-tritan", fetched.ThemeDark)
		require.Equal(t, codersdk.TerminalFontFiraCode, fetched.TerminalFont)
	})

	t.Run("legacy auto with omitted theme_mode clears mode", func(t *testing.T) {
		t.Parallel()

		client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
		defer cancel()

		_, err := client.UpdateUserAppearanceSettings(ctx, codersdk.Me, codersdk.UpdateUserAppearanceSettingsRequest{
			ThemePreference: "dark-tritan",
			ThemeMode:       codersdk.ThemeModeSync,
			ThemeLight:      "light-tritan",
			ThemeDark:       "dark-tritan",
			TerminalFont:    codersdk.TerminalFontGeistMono,
		})
		require.NoError(t, err)

		updated, err := client.UpdateUserAppearanceSettings(ctx, codersdk.Me, codersdk.UpdateUserAppearanceSettingsRequest{
			ThemePreference: "auto",
			TerminalFont:    codersdk.TerminalFontFiraCode,
		})
		require.NoError(t, err)
		require.Equal(t, "auto", updated.ThemePreference)
		require.Equal(t, codersdk.ThemeModeUnset, updated.ThemeMode)
		require.Equal(t, "light-tritan", updated.ThemeLight)
		require.Equal(t, "dark-tritan", updated.ThemeDark)
		require.Equal(t, codersdk.TerminalFontFiraCode, updated.TerminalFont)

		fetched, err := client.GetUserAppearanceSettings(ctx, codersdk.Me)
		require.NoError(t, err)
		require.Equal(t, "auto", fetched.ThemePreference)
		require.Equal(t, codersdk.ThemeModeUnset, fetched.ThemeMode)
		require.Equal(t, "light-tritan", fetched.ThemeLight)
		require.Equal(t, "dark-tritan", fetched.ThemeDark)
		require.Equal(t, codersdk.TerminalFontFiraCode, fetched.TerminalFont)
	})

	t.Run("invalid theme_mode is rejected", func(t *testing.T) {
		t.Parallel()

		client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
		defer cancel()

		_, err := client.UpdateUserAppearanceSettings(ctx, codersdk.Me, codersdk.UpdateUserAppearanceSettingsRequest{
			ThemePreference: "dark",
			ThemeMode:       codersdk.ThemeMode("wizard"),
			TerminalFont:    codersdk.TerminalFontGeistMono,
		})
		var apiErr *codersdk.Error
		require.ErrorAs(t, err, &apiErr)
		require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
	})

	t.Run("invalid theme slots are rejected", func(t *testing.T) {
		t.Parallel()

		client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
		defer cancel()

		for _, tc := range []struct {
			name       string
			themeMode  codersdk.ThemeMode
			themeLight string
			themeDark  string
		}{
			{
				name:       "arbitrary light slot",
				themeMode:  codersdk.ThemeModeSync,
				themeLight: "../../etc/passwd",
				themeDark:  "dark",
			},
			{
				name:       "arbitrary dark slot",
				themeMode:  codersdk.ThemeModeSync,
				themeLight: "light",
				themeDark:  "xss-payload",
			},
			{
				name:       "empty light slot in sync mode",
				themeMode:  codersdk.ThemeModeSync,
				themeLight: "",
				themeDark:  "dark",
			},
			{
				name:       "empty dark slot in sync mode",
				themeMode:  codersdk.ThemeModeSync,
				themeLight: "light",
				themeDark:  "",
			},
			{
				name:       "arbitrary light slot in single mode",
				themeMode:  codersdk.ThemeModeSingle,
				themeLight: "../../etc/passwd",
			},
			{
				name:      "arbitrary dark slot with omitted mode",
				themeDark: "xss-payload",
			},
		} {
			t.Run(tc.name, func(t *testing.T) {
				_, err := client.UpdateUserAppearanceSettings(ctx, codersdk.Me, codersdk.UpdateUserAppearanceSettingsRequest{
					ThemePreference: "dark",
					ThemeMode:       tc.themeMode,
					ThemeLight:      tc.themeLight,
					ThemeDark:       tc.themeDark,
					TerminalFont:    codersdk.TerminalFontGeistMono,
				})
				var apiErr *codersdk.Error
				require.ErrorAs(t, err, &apiErr)
				require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
			})
		}
	})
}

func TestUserCollapseAssistantSteps(t *testing.T) {
	t.Parallel()

	client := coderdtest.New(t, nil)
	coderdtest.CreateFirstUser(t, client)
	ctx := testutil.Context(t, testutil.WaitShort)

	settings, err := client.GetUserPreferenceSettings(ctx, codersdk.Me)
	require.NoError(t, err)
	require.False(t, settings.CollapseAssistantSteps)

	for _, collapse := range []bool{true, false} {
		updated, err := client.UpdateUserPreferenceSettings(ctx, codersdk.Me, codersdk.UpdateUserPreferenceSettingsRequest{
			CollapseAssistantSteps: &collapse,
		})
		require.NoError(t, err)
		require.Equal(t, collapse, updated.CollapseAssistantSteps)

		settings, err := client.GetUserPreferenceSettings(ctx, codersdk.Me)
		require.NoError(t, err)
		require.Equal(t, collapse, settings.CollapseAssistantSteps)

		updated, err = client.UpdateUserPreferenceSettings(ctx, codersdk.Me, codersdk.UpdateUserPreferenceSettingsRequest{
			ThinkingDisplayMode: codersdk.ThinkingDisplayModePreview,
		})
		require.NoError(t, err)
		require.Equal(t, collapse, updated.CollapseAssistantSteps)
	}
}

func TestThinkingDisplayMode(t *testing.T) {
	t.Parallel()

	adminClient := coderdtest.New(t, nil)
	firstUser := coderdtest.CreateFirstUser(t, adminClient)

	t.Run("defaults to auto", func(t *testing.T) {
		t.Parallel()

		client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
		defer cancel()

		settings, err := client.GetUserPreferenceSettings(ctx, codersdk.Me)
		require.NoError(t, err)
		require.Equal(t, codersdk.ThinkingDisplayModeAuto, settings.ThinkingDisplayMode)
	})

	t.Run("round-trips a valid mode", func(t *testing.T) {
		t.Parallel()

		client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
		defer cancel()

		updated, err := client.UpdateUserPreferenceSettings(ctx, codersdk.Me, codersdk.UpdateUserPreferenceSettingsRequest{
			ThinkingDisplayMode: codersdk.ThinkingDisplayModeAlwaysCollapsed,
		})
		require.NoError(t, err)
		require.Equal(t, codersdk.ThinkingDisplayModeAlwaysCollapsed, updated.ThinkingDisplayMode)

		settings, err := client.GetUserPreferenceSettings(ctx, codersdk.Me)
		require.NoError(t, err)
		require.Equal(t, codersdk.ThinkingDisplayModeAlwaysCollapsed, settings.ThinkingDisplayMode)
	})

	t.Run("rejects invalid mode", func(t *testing.T) {
		t.Parallel()

		client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
		defer cancel()

		_, err := client.UpdateUserPreferenceSettings(ctx, codersdk.Me, codersdk.UpdateUserPreferenceSettingsRequest{
			ThinkingDisplayMode: "bogus",
		})
		var sdkErr *codersdk.Error
		require.ErrorAs(t, err, &sdkErr)
		require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
	})

	t.Run("empty mode preserves stored value", func(t *testing.T) {
		t.Parallel()

		client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
		defer cancel()

		// Set a non-default mode.
		_, err := client.UpdateUserPreferenceSettings(ctx, codersdk.Me, codersdk.UpdateUserPreferenceSettingsRequest{
			ThinkingDisplayMode: codersdk.ThinkingDisplayModePreview,
		})
		require.NoError(t, err)

		// Send an update that omits thinking_display_mode (zero value).
		updated, err := client.UpdateUserPreferenceSettings(ctx, codersdk.Me, codersdk.UpdateUserPreferenceSettingsRequest{})
		require.NoError(t, err)
		require.Equal(t, codersdk.ThinkingDisplayModePreview, updated.ThinkingDisplayMode)
	})
}

func TestAgentChatSendShortcutPreference(t *testing.T) {
	t.Parallel()

	adminClient := coderdtest.New(t, nil)
	firstUser := coderdtest.CreateFirstUser(t, adminClient)

	requireValidationField := func(t *testing.T, err error, field string) {
		t.Helper()

		var sdkErr *codersdk.Error
		require.ErrorAs(t, err, &sdkErr)
		require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
		require.Len(t, sdkErr.Validations, 1)
		require.Equal(t, field, sdkErr.Validations[0].Field)
	}

	t.Run("defaults to enter", func(t *testing.T) {
		t.Parallel()

		client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
		defer cancel()

		settings, err := client.GetUserPreferenceSettings(ctx, codersdk.Me)
		require.NoError(t, err)
		require.Equal(t, codersdk.AgentChatSendShortcutEnter, settings.AgentChatSendShortcut)
	})

	t.Run("round-trips shortcut", func(t *testing.T) {
		t.Parallel()

		client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
		defer cancel()

		updated, err := client.UpdateUserPreferenceSettings(ctx, codersdk.Me, codersdk.UpdateUserPreferenceSettingsRequest{
			AgentChatSendShortcut: codersdk.AgentChatSendShortcutModifierEnter,
		})
		require.NoError(t, err)
		require.Equal(t, codersdk.AgentChatSendShortcutModifierEnter, updated.AgentChatSendShortcut)

		settings, err := client.GetUserPreferenceSettings(ctx, codersdk.Me)
		require.NoError(t, err)
		require.Equal(t, codersdk.AgentChatSendShortcutModifierEnter, settings.AgentChatSendShortcut)
	})

	t.Run("rejects invalid shortcut", func(t *testing.T) {
		t.Parallel()

		client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
		defer cancel()

		_, err := client.UpdateUserPreferenceSettings(ctx, codersdk.Me, codersdk.UpdateUserPreferenceSettingsRequest{
			AgentChatSendShortcut: codersdk.AgentChatSendShortcut("bogus"),
		})
		requireValidationField(t, err, "agent_chat_send_shortcut")
	})

	t.Run("updates preserve stored shortcut", func(t *testing.T) {
		t.Parallel()

		client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
		defer cancel()

		_, err := client.UpdateUserPreferenceSettings(ctx, codersdk.Me, codersdk.UpdateUserPreferenceSettingsRequest{
			AgentChatSendShortcut: codersdk.AgentChatSendShortcutModifierEnter,
			ThinkingDisplayMode:   codersdk.ThinkingDisplayModePreview,
		})
		require.NoError(t, err)

		updated, err := client.UpdateUserPreferenceSettings(ctx, codersdk.Me, codersdk.UpdateUserPreferenceSettingsRequest{
			ThinkingDisplayMode: codersdk.ThinkingDisplayModeAlwaysExpanded,
		})
		require.NoError(t, err)
		require.Equal(t, codersdk.ThinkingDisplayModeAlwaysExpanded, updated.ThinkingDisplayMode)
		require.Equal(t, codersdk.AgentChatSendShortcutModifierEnter, updated.AgentChatSendShortcut)
	})
}

func TestAgentDisplayModePreferences(t *testing.T) {
	t.Parallel()

	adminClient := coderdtest.New(t, nil)
	firstUser := coderdtest.CreateFirstUser(t, adminClient)

	requireValidationField := func(t *testing.T, err error, field string) {
		t.Helper()

		var sdkErr *codersdk.Error
		require.ErrorAs(t, err, &sdkErr)
		require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
		require.Len(t, sdkErr.Validations, 1)
		require.Equal(t, field, sdkErr.Validations[0].Field)
	}

	t.Run("defaults shell tools to always collapsed", func(t *testing.T) {
		t.Parallel()

		client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
		defer cancel()

		settings, err := client.GetUserPreferenceSettings(ctx, codersdk.Me)
		require.NoError(t, err)
		require.Equal(t, codersdk.AgentDisplayModeAlwaysCollapsed, settings.ShellToolDisplayMode)
		require.Empty(t, settings.CodeDiffDisplayMode)
	})

	t.Run("round-trips shell tool display mode", func(t *testing.T) {
		t.Parallel()

		client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
		defer cancel()

		for _, mode := range []codersdk.AgentDisplayMode{
			codersdk.AgentDisplayModeAlwaysExpanded,
			codersdk.AgentDisplayModeAlwaysCollapsed,
		} {
			updated, err := client.UpdateUserPreferenceSettings(ctx, codersdk.Me, codersdk.UpdateUserPreferenceSettingsRequest{
				ShellToolDisplayMode: mode,
			})
			require.NoError(t, err)
			require.Equal(t, mode, updated.ShellToolDisplayMode)

			settings, err := client.GetUserPreferenceSettings(ctx, codersdk.Me)
			require.NoError(t, err)
			require.Equal(t, mode, settings.ShellToolDisplayMode)
		}
	})

	t.Run("round-trips code diff display mode", func(t *testing.T) {
		t.Parallel()

		client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
		defer cancel()

		for _, mode := range []codersdk.AgentDisplayMode{
			codersdk.AgentDisplayModeAlwaysExpanded,
			codersdk.AgentDisplayModeAlwaysCollapsed,
		} {
			updated, err := client.UpdateUserPreferenceSettings(ctx, codersdk.Me, codersdk.UpdateUserPreferenceSettingsRequest{
				CodeDiffDisplayMode: mode,
			})
			require.NoError(t, err)
			require.Equal(t, mode, updated.CodeDiffDisplayMode)

			settings, err := client.GetUserPreferenceSettings(ctx, codersdk.Me)
			require.NoError(t, err)
			require.Equal(t, mode, settings.CodeDiffDisplayMode)
		}
	})

	t.Run("updates preserve stored display modes", func(t *testing.T) {
		t.Parallel()

		client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
		defer cancel()

		_, err := client.UpdateUserPreferenceSettings(ctx, codersdk.Me, codersdk.UpdateUserPreferenceSettingsRequest{
			ThinkingDisplayMode:  codersdk.ThinkingDisplayModePreview,
			ShellToolDisplayMode: codersdk.AgentDisplayModeAlwaysCollapsed,
			CodeDiffDisplayMode:  codersdk.AgentDisplayModeAlwaysExpanded,
		})
		require.NoError(t, err)

		updated, err := client.UpdateUserPreferenceSettings(ctx, codersdk.Me, codersdk.UpdateUserPreferenceSettingsRequest{
			ShellToolDisplayMode: codersdk.AgentDisplayModeAlwaysExpanded,
		})
		require.NoError(t, err)
		require.Equal(t, codersdk.ThinkingDisplayModePreview, updated.ThinkingDisplayMode)
		require.Equal(t, codersdk.AgentDisplayModeAlwaysExpanded, updated.ShellToolDisplayMode)
		require.Equal(t, codersdk.AgentDisplayModeAlwaysExpanded, updated.CodeDiffDisplayMode)

		settings, err := client.GetUserPreferenceSettings(ctx, codersdk.Me)
		require.NoError(t, err)
		require.Equal(t, codersdk.ThinkingDisplayModePreview, settings.ThinkingDisplayMode)
		require.Equal(t, codersdk.AgentDisplayModeAlwaysExpanded, settings.ShellToolDisplayMode)
		require.Equal(t, codersdk.AgentDisplayModeAlwaysExpanded, settings.CodeDiffDisplayMode)
	})

	t.Run("rejects invalid shell tool display mode", func(t *testing.T) {
		t.Parallel()

		client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
		defer cancel()

		for _, tt := range []struct {
			name string
			mode codersdk.AgentDisplayMode
		}{
			{
				name: "bogus",
				mode: codersdk.AgentDisplayMode("bogus"),
			},
			{
				name: "thinking preview",
				mode: codersdk.AgentDisplayMode(codersdk.ThinkingDisplayModePreview),
			},
		} {
			t.Run(tt.name, func(t *testing.T) {
				_, err := client.UpdateUserPreferenceSettings(ctx, codersdk.Me, codersdk.UpdateUserPreferenceSettingsRequest{
					ShellToolDisplayMode: tt.mode,
				})
				requireValidationField(t, err, "shell_tool_display_mode")
			})
		}
	})

	t.Run("rejects invalid code diff display mode", func(t *testing.T) {
		t.Parallel()

		client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
		defer cancel()

		for _, tt := range []struct {
			name string
			mode codersdk.AgentDisplayMode
		}{
			{
				name: "bogus",
				mode: codersdk.AgentDisplayMode("bogus"),
			},
			{
				name: "thinking preview",
				mode: codersdk.AgentDisplayMode(codersdk.ThinkingDisplayModePreview),
			},
		} {
			t.Run(tt.name, func(t *testing.T) {
				_, err := client.UpdateUserPreferenceSettings(ctx, codersdk.Me, codersdk.UpdateUserPreferenceSettingsRequest{
					CodeDiffDisplayMode: tt.mode,
				})
				requireValidationField(t, err, "code_diff_display_mode")
			})
		}
	})
}

func TestWorkspacesByUser(t *testing.T) {
	t.Parallel()
	t.Run("Empty", func(t *testing.T) {
		t.Parallel()
		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
			Owner: codersdk.Me,
		})
		require.NoError(t, err)
		require.Len(t, res.Workspaces, 0)
	})
	t.Run("Access", func(t *testing.T) {
		t.Parallel()
		client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
		user := coderdtest.CreateFirstUser(t, client)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		newUser, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
			Email:           "test@coder.com",
			Username:        "someone",
			Password:        "MySecurePassword!",
			OrganizationIDs: []uuid.UUID{user.OrganizationID},
		})
		require.NoError(t, err)
		auth, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
			Email:    newUser.Email,
			Password: "MySecurePassword!",
		})
		require.NoError(t, err)

		newUserClient := codersdk.New(client.URL)
		newUserClient.SetSessionToken(auth.SessionToken)
		version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
		template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
		coderdtest.CreateWorkspace(t, client, template.ID)

		res, err := newUserClient.Workspaces(ctx, codersdk.WorkspaceFilter{Owner: codersdk.Me})
		require.NoError(t, err)
		require.Len(t, res.Workspaces, 0)

		res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{Owner: codersdk.Me})
		require.NoError(t, err)
		require.Len(t, res.Workspaces, 1)
	})
}

func TestDormantUser(t *testing.T) {
	t.Parallel()

	client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
	user := coderdtest.CreateFirstUser(t, client)

	ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
	defer cancel()

	// Create a new user
	newUser, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
		Email:           "test@coder.com",
		Username:        "someone",
		Password:        "MySecurePassword!",
		OrganizationIDs: []uuid.UUID{user.OrganizationID},
	})
	require.NoError(t, err)

	// User should be dormant as they haven't logged in yet
	users, err := client.Users(ctx, codersdk.UsersRequest{Search: newUser.Username})
	require.NoError(t, err)
	require.Len(t, users.Users, 1)
	require.Equal(t, codersdk.UserStatusDormant, users.Users[0].Status)

	// User logs in now
	_, err = client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
		Email:    newUser.Email,
		Password: "MySecurePassword!",
	})
	require.NoError(t, err)

	// User status should be active now
	users, err = client.Users(ctx, codersdk.UsersRequest{Search: newUser.Username})
	require.NoError(t, err)
	require.Len(t, users.Users, 1)
	require.Equal(t, codersdk.UserStatusActive, users.Users[0].Status)
}

// TestPaginatedUsers creates a list of users, then tries to paginate through
// them using different page sizes.
func TestPaginatedUsers(t *testing.T) {
	t.Parallel()
	client, db := coderdtest.NewWithDatabase(t, nil)
	coderdtest.CreateFirstUser(t, client)

	// This test takes longer than a long time.
	ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong*4)
	t.Cleanup(cancel)

	me, err := client.User(ctx, codersdk.Me)
	require.NoError(t, err)

	// When 50 users exist
	total := 50
	allUsers := make([]database.User, total+1)
	allUsers[0] = database.User{
		Email:    me.Email,
		Username: me.Username,
	}
	specialUsers := make([]database.User, total/2)

	eg, _ := errgroup.WithContext(ctx)
	// Create users
	for i := 0; i < total; i++ {
		eg.Go(func() error {
			email := fmt.Sprintf("%d@coder.com", i)
			username := fmt.Sprintf("user%d", i)
			if i%2 == 0 {
				email = fmt.Sprintf("%d@gmail.com", i)
				username = fmt.Sprintf("specialuser%d", i)
			}
			if i%3 == 0 {
				username = strings.ToUpper(username)
			}

			// We used to use the API to ceate users, but that is slow.
			// Instead, we create them directly in the database now
			// to prevent timeout flakes.
			newUser := dbgen.User(t, db, database.User{
				Email:    email,
				Username: username,
			})
			allUsers[i+1] = newUser
			if i%2 == 0 {
				specialUsers[i/2] = newUser
			}

			return nil
		})
	}
	err = eg.Wait()
	require.NoError(t, err, "create users failed")

	// Sorting the users will sort by username.
	sortDatabaseUsers(allUsers)
	sortDatabaseUsers(specialUsers)

	gmailSearch := func(request codersdk.UsersRequest) codersdk.UsersRequest {
		request.Search = "gmail"
		return request
	}
	usernameSearch := func(request codersdk.UsersRequest) codersdk.UsersRequest {
		request.Search = "specialuser"
		return request
	}

	tests := []struct {
		name     string
		limit    int
		allUsers []database.User
		opt      func(request codersdk.UsersRequest) codersdk.UsersRequest
	}{
		{name: "all users", limit: 10, allUsers: allUsers},
		{name: "all users", limit: 5, allUsers: allUsers},
		{name: "all users", limit: 3, allUsers: allUsers},
		{name: "gmail search", limit: 3, allUsers: specialUsers, opt: gmailSearch},
		{name: "gmail search", limit: 7, allUsers: specialUsers, opt: gmailSearch},
		{name: "username search", limit: 3, allUsers: specialUsers, opt: usernameSearch},
		{name: "username search", limit: 3, allUsers: specialUsers, opt: usernameSearch},
	}
	for _, tt := range tests {
		t.Run(fmt.Sprintf("%s %d", tt.name, tt.limit), func(t *testing.T) {
			t.Parallel()

			// This test takes longer than a long time.
			ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong*2)
			defer cancel()

			assertPagination(ctx, t, client, tt.limit, tt.allUsers, tt.opt)
		})
	}
}

// Assert pagination will page through the list of all users using the given
// limit for each page. The 'allUsers' is the expected full list to compare
// against.
func assertPagination(ctx context.Context, t *testing.T, client *codersdk.Client, limit int, allUsers []database.User,
	opt func(request codersdk.UsersRequest) codersdk.UsersRequest,
) {
	var count int
	if opt == nil {
		opt = func(request codersdk.UsersRequest) codersdk.UsersRequest {
			return request
		}
	}

	// Check the first page
	page, err := client.Users(ctx, opt(codersdk.UsersRequest{
		Pagination: codersdk.Pagination{
			Limit: limit,
		},
	}))
	require.NoError(t, err, "first page")
	require.Equalf(t, onlyUsernames(page.Users), onlyUsernames(allUsers[:limit]), "first page, limit=%d", limit)
	count += len(page.Users)

	for len(page.Users) != 0 {
		afterCursor := page.Users[len(page.Users)-1].ID
		// Assert each page is the next expected page
		// This is using a cursor, and only works if all users created_at
		// is unique.
		page, err = client.Users(ctx, opt(codersdk.UsersRequest{
			Pagination: codersdk.Pagination{
				Limit:   limit,
				AfterID: afterCursor,
			},
		}))
		require.NoError(t, err, "next cursor page")

		// Also check page by offset
		offsetPage, err := client.Users(ctx, opt(codersdk.UsersRequest{
			Pagination: codersdk.Pagination{
				Limit:  limit,
				Offset: count,
			},
		}))
		require.NoError(t, err, "next offset page")

		var expected []database.User
		if count+limit > len(allUsers) {
			expected = allUsers[count:]
		} else {
			expected = allUsers[count : count+limit]
		}
		require.Equalf(t, onlyUsernames(page.Users), onlyUsernames(expected), "next users, after=%s, limit=%d", afterCursor, limit)
		require.Equalf(t, onlyUsernames(offsetPage.Users), onlyUsernames(expected), "offset users, offset=%d, limit=%d", count, limit)

		// Also check the before
		prevPage, err := client.Users(ctx, opt(codersdk.UsersRequest{
			Pagination: codersdk.Pagination{
				Offset: count - limit,
				Limit:  limit,
			},
		}))
		require.NoError(t, err, "prev page")
		require.Equal(t, onlyUsernames(allUsers[count-limit:count]), onlyUsernames(prevPage.Users), "prev users")
		count += len(page.Users)
	}
}

func sortDatabaseUsers(users []database.User) {
	slices.SortFunc(users, func(a, b database.User) int {
		return slice.Ascending(strings.ToLower(a.Username), strings.ToLower(b.Username))
	})
}

func onlyUsernames[U codersdk.User | database.User](users []U) []string {
	var out []string
	for _, u := range users {
		switch u := (any(u)).(type) {
		case codersdk.User:
			out = append(out, u.Username)
		case database.User:
			out = append(out, u.Username)
		}
	}
	return out
}

func BenchmarkUsersMe(b *testing.B) {
	client := coderdtest.New(b, nil)
	_ = coderdtest.CreateFirstUser(b, client)

	ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
	defer cancel()

	b.ReportAllocs()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		_, err := client.User(ctx, codersdk.Me)
		require.NoError(b, err)
	}
}