Ctrl K
ComunidadGuías…

coderd/exp_chats_test.go

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

Archivo original ↗Preguntar a KiBot
package coderd_test

import (
	"bytes"
	"context"
	"crypto/sha256"
	"database/sql"
	"encoding/base64"
	"encoding/json"
	stderrors "errors"
	"fmt"
	"io"
	"mime"
	"net/http"
	"net/http/httptest"
	"net/url"
	"regexp"
	"slices"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"github.com/go-jose/go-jose/v4/jwt"
	"github.com/google/uuid"
	"github.com/modelcontextprotocol/go-sdk/mcp"
	"github.com/sqlc-dev/pqtype"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"golang.org/x/sync/errgroup"
	"golang.org/x/sync/singleflight"
	"golang.org/x/xerrors"

	"cdr.dev/slog/v3/sloggers/slogtest"
	aibclient "github.com/coder/coder/v2/aibridge/client"
	"github.com/coder/coder/v2/coderd"
	"github.com/coder/coder/v2/coderd/aibridge"
	"github.com/coder/coder/v2/coderd/aibridgedtest"
	"github.com/coder/coder/v2/coderd/audit"
	"github.com/coder/coder/v2/coderd/coderdtest"
	"github.com/coder/coder/v2/coderd/database"
	"github.com/coder/coder/v2/coderd/database/db2sdk"
	"github.com/coder/coder/v2/coderd/database/dbauthz"
	"github.com/coder/coder/v2/coderd/database/dbfake"
	"github.com/coder/coder/v2/coderd/database/dbgen"
	"github.com/coder/coder/v2/coderd/database/dbtestutil"
	"github.com/coder/coder/v2/coderd/database/dbtime"
	"github.com/coder/coder/v2/coderd/externalauth"
	"github.com/coder/coder/v2/coderd/httpapi/httperror"
	"github.com/coder/coder/v2/coderd/jwtutils"
	"github.com/coder/coder/v2/coderd/rbac"
	"github.com/coder/coder/v2/coderd/rbac/policy"
	"github.com/coder/coder/v2/coderd/util/ptr"
	"github.com/coder/coder/v2/coderd/x/chatd"
	"github.com/coder/coder/v2/coderd/x/chatd/chathooks"
	"github.com/coder/coder/v2/coderd/x/chatd/chatprompt"
	"github.com/coder/coder/v2/coderd/x/chatd/chatprovider"
	"github.com/coder/coder/v2/coderd/x/chatd/chatstate"
	"github.com/coder/coder/v2/coderd/x/chatd/chattest"
	"github.com/coder/coder/v2/codersdk"
	"github.com/coder/coder/v2/codersdk/x/agenthooks"
	"github.com/coder/coder/v2/testutil"
	"github.com/coder/serpent"
	"github.com/coder/websocket"
	"github.com/coder/websocket/wsjson"
)

func newChatTestOptions(
	t testing.TB,
	values *codersdk.DeploymentValues,
	overrides ...func(*coderdtest.Options),
) *coderdtest.Options {
	t.Helper()

	// Enable experiment-gated chat endpoints in tests.
	if len(values.Experiments) == 0 {
		values.Experiments = serpent.StringArray{
			string(codersdk.ExperimentChatVirtualDesktop),
			string(codersdk.ExperimentAgentLifecycleHooks),
			string(codersdk.ExperimentChatInlineMCPServers),
		}
	}

	opts := &coderdtest.Options{
		DeploymentValues: values,
	}
	for _, override := range overrides {
		override(opts)
	}
	return opts
}

func newChatClient(t testing.TB, overrides ...func(*coderdtest.Options)) *codersdk.ExperimentalClient {
	t.Helper()

	opts := newChatTestOptions(t, coderdtest.DeploymentValues(t), overrides...)
	client, _, api := coderdtest.NewWithAPI(t, opts)
	aibridgedtest.StartTestAIBridgeDaemon(t.Context(), t, api, nil)
	return codersdk.NewExperimentalClient(client)
}

func newChatClientWithAPI(t testing.TB, overrides ...func(*coderdtest.Options)) (*codersdk.ExperimentalClient, *coderd.API) {
	t.Helper()

	opts := newChatTestOptions(t, coderdtest.DeploymentValues(t), overrides...)
	client, _, api := coderdtest.NewWithAPI(t, opts)
	aibridgedtest.StartTestAIBridgeDaemon(t.Context(), t, api, nil)
	return codersdk.NewExperimentalClient(client), api
}

func newChatClientWithDeploymentValues(
	t testing.TB,
	values *codersdk.DeploymentValues,
) *codersdk.ExperimentalClient {
	t.Helper()

	opts := newChatTestOptions(t, values)
	client, _, api := coderdtest.NewWithAPI(t, opts)
	aibridgedtest.StartTestAIBridgeDaemon(t.Context(), t, api, nil)
	return codersdk.NewExperimentalClient(client)
}

func newChatClientWithDatabase(t testing.TB, overrides ...func(*coderdtest.Options)) (*codersdk.ExperimentalClient, database.Store) {
	t.Helper()

	opts := newChatTestOptions(t, coderdtest.DeploymentValues(t), overrides...)
	client, _, api := coderdtest.NewWithAPI(t, opts)
	aibridgedtest.StartTestAIBridgeDaemon(t.Context(), t, api, nil)
	return codersdk.NewExperimentalClient(client), api.Database
}

func newChatClientWithAPIAndDatabase(t testing.TB, overrides ...func(*coderdtest.Options)) (*codersdk.ExperimentalClient, database.Store, *coderd.API) {
	t.Helper()

	opts := newChatTestOptions(t, coderdtest.DeploymentValues(t), overrides...)
	client, _, api := coderdtest.NewWithAPI(t, opts)
	aibridgedtest.StartTestAIBridgeDaemon(t.Context(), t, api, nil)
	return codersdk.NewExperimentalClient(client), api.Database, api
}

func newChatClientWithoutAIBridge(t testing.TB, overrides ...func(*coderdtest.Options)) (*codersdk.ExperimentalClient, database.Store, *coderd.API) {
	t.Helper()

	opts := newChatTestOptions(t, coderdtest.DeploymentValues(t), overrides...)
	client, _, api := coderdtest.NewWithAPI(t, opts)
	return codersdk.NewExperimentalClient(client), api.Database, api
}

func insertTestChatQueuedMessage(
	ctx context.Context,
	t testing.TB,
	db database.Store,
	chatID uuid.UUID,
	content json.RawMessage,
	modelConfigID uuid.UUID,
) database.ChatQueuedMessage {
	t.Helper()
	return insertTestChatQueuedMessageWithReasoningEffort(ctx, t, db, chatID, content, modelConfigID, "")
}

func insertTestChatQueuedMessageWithReasoningEffort(
	ctx context.Context,
	t testing.TB,
	db database.Store,
	chatID uuid.UUID,
	content json.RawMessage,
	modelConfigID uuid.UUID,
	reasoningEffort string,
) database.ChatQueuedMessage {
	t.Helper()

	queued, err := db.InsertChatQueuedMessage(
		dbauthz.AsSystemRestricted(ctx),
		database.InsertChatQueuedMessageParams{
			ChatID:          chatID,
			Content:         content,
			ModelConfigID:   uuid.NullUUID{UUID: modelConfigID, Valid: modelConfigID != uuid.Nil},
			ReasoningEffort: database.NullChatReasoningEffort{ChatReasoningEffort: database.ChatReasoningEffort(reasoningEffort), Valid: reasoningEffort != ""},
		},
	)
	require.NoError(t, err)
	return queued
}

// findUserMessage returns the first user-role message from a slice of chat
// messages, failing the test if none is found.
func findUserMessage(t testing.TB, messages []database.ChatMessage) database.ChatMessage {
	t.Helper()
	idx := slices.IndexFunc(messages, func(m database.ChatMessage) bool {
		return m.Role == database.ChatMessageRoleUser
	})
	require.NotEqual(t, -1, idx, "expected to find a user message")
	return messages[idx]
}

// failNextChatSystemPromptStore lets a test force single chat system prompt
// queries to fail. One-shot flags disarm when the forced failure fires, and
// armed counters fail every matching read until the armed count reaches zero.
// Flags and counters are shared across InTx wrappers.
type failNextChatSystemPromptStore struct {
	database.Store

	failNextGetChatIncludeDefaultSystemPrompt    *atomic.Bool
	failNextGetChatSystemPromptConfig            *atomic.Bool
	armedGetChatSystemPromptConfigFailures       *atomic.Int64
	getChatSystemPromptConfigCallsBeforeFailure  *atomic.Int64
	failNextUpsertChatIncludeDefaultSystemPrompt *atomic.Bool
	failNextGetChatOrganizationSystemPrompt      *atomic.Bool
}

func newFailNextChatSystemPromptStore(store database.Store) *failNextChatSystemPromptStore {
	return &failNextChatSystemPromptStore{
		Store: store,
		failNextGetChatIncludeDefaultSystemPrompt:    &atomic.Bool{},
		failNextGetChatSystemPromptConfig:            &atomic.Bool{},
		armedGetChatSystemPromptConfigFailures:       &atomic.Int64{},
		getChatSystemPromptConfigCallsBeforeFailure:  &atomic.Int64{},
		failNextUpsertChatIncludeDefaultSystemPrompt: &atomic.Bool{},
		failNextGetChatOrganizationSystemPrompt:      &atomic.Bool{},
	}
}

func (s *failNextChatSystemPromptStore) InTx(function func(database.Store) error, txOpts *database.TxOptions) error {
	return s.Store.InTx(func(tx database.Store) error {
		return function(&failNextChatSystemPromptStore{
			Store:                                        tx,
			failNextGetChatSystemPromptConfig:            s.failNextGetChatSystemPromptConfig,
			armedGetChatSystemPromptConfigFailures:       s.armedGetChatSystemPromptConfigFailures,
			getChatSystemPromptConfigCallsBeforeFailure:  s.getChatSystemPromptConfigCallsBeforeFailure,
			failNextUpsertChatIncludeDefaultSystemPrompt: s.failNextUpsertChatIncludeDefaultSystemPrompt,
			failNextGetChatOrganizationSystemPrompt:      s.failNextGetChatOrganizationSystemPrompt,
		})
	}, txOpts)
}

func (s *failNextChatSystemPromptStore) GetChatOrganizationSystemPrompt(ctx context.Context, organizationID uuid.UUID) (database.ChatOrganizationSystemPrompt, error) {
	if s.failNextGetChatOrganizationSystemPrompt.CompareAndSwap(true, false) {
		return database.ChatOrganizationSystemPrompt{}, stderrors.New("forced organization system prompt read failure")
	}
	return s.Store.GetChatOrganizationSystemPrompt(ctx, organizationID)
}

func (s *failNextChatSystemPromptStore) GetChatIncludeDefaultSystemPrompt(ctx context.Context) (bool, error) {
	if s.failNextGetChatIncludeDefaultSystemPrompt.CompareAndSwap(true, false) {
		return false, stderrors.New("forced include-default read failure")
	}
	return s.Store.GetChatIncludeDefaultSystemPrompt(ctx)
}

func (s *failNextChatSystemPromptStore) UpsertChatIncludeDefaultSystemPrompt(ctx context.Context, includeDefault bool) error {
	if s.failNextUpsertChatIncludeDefaultSystemPrompt.CompareAndSwap(true, false) {
		return stderrors.New("forced include-default upsert failure")
	}
	return s.Store.UpsertChatIncludeDefaultSystemPrompt(ctx, includeDefault)
}

func (s *failNextChatSystemPromptStore) GetChatSystemPromptConfig(ctx context.Context) (database.GetChatSystemPromptConfigRow, error) {
	if s.failNextGetChatSystemPromptConfig.CompareAndSwap(true, false) {
		return database.GetChatSystemPromptConfigRow{}, stderrors.New("forced chat system prompt configuration read failure")
	}
	// Armed failures fire on every read until the armed count is spent.
	// Best-effort audit captures swallow these errors without retrying,
	// which is why a one-shot flag is not enough: the failure would stay
	// latent and fire on a later, unrelated read.
	if s.armedGetChatSystemPromptConfigFailures != nil && s.armedGetChatSystemPromptConfigFailures.Load() > 0 {
		// When a skip count is set, the first N reads succeed so failures
		// can be aimed at a specific read, e.g. the New re-read after the
		// Old capture.
		if s.getChatSystemPromptConfigCallsBeforeFailure != nil && s.getChatSystemPromptConfigCallsBeforeFailure.Load() > 0 {
			s.getChatSystemPromptConfigCallsBeforeFailure.Add(-1)
			return s.Store.GetChatSystemPromptConfig(ctx)
		}
		s.armedGetChatSystemPromptConfigFailures.Add(-1)
		return database.GetChatSystemPromptConfigRow{}, stderrors.New("forced chat system prompt configuration read failure")
	}
	return s.Store.GetChatSystemPromptConfig(ctx)
}

// failNextUpsertChatSystemPromptStore lets a test force the system-prompt
// upsert to fail once, sharing its failure state across InTx wrappers.
type failNextUpsertChatSystemPromptStore struct {
	database.Store

	failNextUpsertChatSystemPrompt *atomic.Bool
}

func newFailNextUpsertChatSystemPromptStore(store database.Store) *failNextUpsertChatSystemPromptStore {
	return &failNextUpsertChatSystemPromptStore{
		Store:                          store,
		failNextUpsertChatSystemPrompt: &atomic.Bool{},
	}
}

func (s *failNextUpsertChatSystemPromptStore) InTx(function func(database.Store) error, txOpts *database.TxOptions) error {
	return s.Store.InTx(func(tx database.Store) error {
		return function(&failNextUpsertChatSystemPromptStore{
			Store:                          tx,
			failNextUpsertChatSystemPrompt: s.failNextUpsertChatSystemPrompt,
		})
	}, txOpts)
}

func (s *failNextUpsertChatSystemPromptStore) UpsertChatSystemPrompt(ctx context.Context, prompt string) error {
	if s.failNextUpsertChatSystemPrompt.CompareAndSwap(true, false) {
		return stderrors.New("forced system prompt upsert failure")
	}
	return s.Store.UpsertChatSystemPrompt(ctx, prompt)
}

// failNextAcquireLockStore lets a test force the next advisory lock
// acquisition for one lock ID to fail with a deadline error, simulating a
// lock wait that timed out. The failure state is shared across InTx
// wrappers.
type failNextAcquireLockStore struct {
	database.Store

	lockID              int64
	failNextAcquireLock *atomic.Bool
}

func newFailNextAcquireLockStore(store database.Store, lockID int64) *failNextAcquireLockStore {
	return &failNextAcquireLockStore{
		Store:               store,
		lockID:              lockID,
		failNextAcquireLock: &atomic.Bool{},
	}
}

func (s *failNextAcquireLockStore) InTx(function func(database.Store) error, txOpts *database.TxOptions) error {
	return s.Store.InTx(func(tx database.Store) error {
		return function(&failNextAcquireLockStore{
			Store:               tx,
			lockID:              s.lockID,
			failNextAcquireLock: s.failNextAcquireLock,
		})
	}, txOpts)
}

func (s *failNextAcquireLockStore) AcquireLock(ctx context.Context, id int64) error {
	if id == s.lockID && s.failNextAcquireLock.CompareAndSwap(true, false) {
		return context.DeadlineExceeded
	}
	return s.Store.AcquireLock(ctx, id)
}

type failNextTxStore struct {
	database.Store

	txID                string
	failNextTransaction *atomic.Bool
}

func newFailNextTxStore(store database.Store, txID string) *failNextTxStore {
	return &failNextTxStore{
		Store:               store,
		txID:                txID,
		failNextTransaction: &atomic.Bool{},
	}
}

func (s *failNextTxStore) InTx(function func(database.Store) error, txOpts *database.TxOptions) error {
	if txOpts != nil && txOpts.TxIdentifier == s.txID && s.failNextTransaction.CompareAndSwap(true, false) {
		return stderrors.New("forced transaction failure for " + s.txID)
	}
	return s.Store.InTx(function, txOpts)
}

// failNextGetChatPlanModeInstructionsStore lets a test force the plan-mode
// instructions baseline read to fail once. Failure state is shared across InTx
// wrappers.
type failNextGetChatPlanModeInstructionsStore struct {
	database.Store

	failNextGetChatPlanModeInstructions *atomic.Bool
}

func newFailNextGetChatPlanModeInstructionsStore(store database.Store) *failNextGetChatPlanModeInstructionsStore {
	return &failNextGetChatPlanModeInstructionsStore{
		Store:                               store,
		failNextGetChatPlanModeInstructions: &atomic.Bool{},
	}
}

func (s *failNextGetChatPlanModeInstructionsStore) InTx(function func(database.Store) error, txOpts *database.TxOptions) error {
	return s.Store.InTx(func(tx database.Store) error {
		return function(&failNextGetChatPlanModeInstructionsStore{
			Store:                               tx,
			failNextGetChatPlanModeInstructions: s.failNextGetChatPlanModeInstructions,
		})
	}, txOpts)
}

func (s *failNextGetChatPlanModeInstructionsStore) GetChatPlanModeInstructions(ctx context.Context) (string, error) {
	if s.failNextGetChatPlanModeInstructions.CompareAndSwap(true, false) {
		return "", stderrors.New("forced plan mode instructions read failure")
	}
	return s.Store.GetChatPlanModeInstructions(ctx)
}

// failNextUpsertChatPlanModeInstructionsStore lets a test force the plan-mode
// instructions upsert to fail once, sharing its failure state across InTx
// wrappers.
type failNextUpsertChatPlanModeInstructionsStore struct {
	database.Store

	failNextUpsertChatPlanModeInstructions *atomic.Bool
}

func newFailNextUpsertChatPlanModeInstructionsStore(store database.Store) *failNextUpsertChatPlanModeInstructionsStore {
	return &failNextUpsertChatPlanModeInstructionsStore{
		Store:                                  store,
		failNextUpsertChatPlanModeInstructions: &atomic.Bool{},
	}
}

func (s *failNextUpsertChatPlanModeInstructionsStore) InTx(function func(database.Store) error, txOpts *database.TxOptions) error {
	return s.Store.InTx(func(tx database.Store) error {
		return function(&failNextUpsertChatPlanModeInstructionsStore{
			Store:                                  tx,
			failNextUpsertChatPlanModeInstructions: s.failNextUpsertChatPlanModeInstructions,
		})
	}, txOpts)
}

func (s *failNextUpsertChatPlanModeInstructionsStore) UpsertChatPlanModeInstructions(ctx context.Context, instructions string) error {
	if s.failNextUpsertChatPlanModeInstructions.CompareAndSwap(true, false) {
		return stderrors.New("forced plan mode instructions upsert failure")
	}
	return s.Store.UpsertChatPlanModeInstructions(ctx, instructions)
}

type chatModelConfigLockedReadMutation struct {
	id    uuid.UUID
	model string
}

// chatModelConfigHookStore forces a specific in-transaction chat model config
// operation to behave abnormally. Each hook targets one ID and consumes
// itself on first match. The single-consume gate uses atomic.Pointer so a
// concurrent InTx wrapper cannot race with the arming goroutine.
type chatModelConfigHookStore struct {
	database.Store

	inTx bool

	failNextUpdate            *atomic.Pointer[uuid.UUID]
	failNextDelete            *atomic.Pointer[uuid.UUID]
	failProviderReferenceLock *atomic.Pointer[uuid.UUID]
	vanishAtLockedRead        *atomic.Pointer[uuid.UUID]
	mutateAtLockedRead        *atomic.Pointer[chatModelConfigLockedReadMutation]
}

func newChatModelConfigHookStore(store database.Store) *chatModelConfigHookStore {
	return &chatModelConfigHookStore{
		Store:                     store,
		failNextUpdate:            &atomic.Pointer[uuid.UUID]{},
		failNextDelete:            &atomic.Pointer[uuid.UUID]{},
		failProviderReferenceLock: &atomic.Pointer[uuid.UUID]{},
		vanishAtLockedRead:        &atomic.Pointer[uuid.UUID]{},
		mutateAtLockedRead:        &atomic.Pointer[chatModelConfigLockedReadMutation]{},
	}
}

func newChatClientWithModelConfigHookStore(t testing.TB) (*codersdk.ExperimentalClient, *chatModelConfigHookStore) {
	t.Helper()

	rawDB, pubsub := dbtestutil.NewDB(t)
	store := newChatModelConfigHookStore(rawDB)
	client := newChatClient(t, func(opts *coderdtest.Options) {
		opts.Database = store
		opts.Pubsub = pubsub
	})
	return client, store
}

func (s *chatModelConfigHookStore) armFailNextUpdate(id uuid.UUID) {
	s.failNextUpdate.Store(&id)
}

func (s *chatModelConfigHookStore) armFailNextDelete(id uuid.UUID) {
	s.failNextDelete.Store(&id)
}

func (s *chatModelConfigHookStore) armFailProviderReferenceLock(id uuid.UUID) {
	s.failProviderReferenceLock.Store(&id)
}

func (s *chatModelConfigHookStore) armVanishAtLockedRead(id uuid.UUID) {
	s.vanishAtLockedRead.Store(&id)
}

func (s *chatModelConfigHookStore) armMutateAtLockedRead(id uuid.UUID, model string) {
	s.mutateAtLockedRead.Store(&chatModelConfigLockedReadMutation{id: id, model: model})
}

func (s *chatModelConfigHookStore) InTx(function func(database.Store) error, txOpts *database.TxOptions) error {
	return s.Store.InTx(func(tx database.Store) error {
		return function(&chatModelConfigHookStore{
			Store:                     tx,
			inTx:                      true,
			failNextUpdate:            s.failNextUpdate,
			failNextDelete:            s.failNextDelete,
			failProviderReferenceLock: s.failProviderReferenceLock,
			vanishAtLockedRead:        s.vanishAtLockedRead,
			mutateAtLockedRead:        s.mutateAtLockedRead,
		})
	}, txOpts)
}

func consumeChatModelConfigHook(hook *atomic.Pointer[uuid.UUID], id uuid.UUID) bool {
	target := hook.Load()
	if target == nil || *target != id {
		return false
	}
	return hook.CompareAndSwap(target, nil)
}

func (s *chatModelConfigHookStore) GetAIProviderByIDForReferenceLock(
	ctx context.Context,
	id uuid.UUID,
) (database.AIProvider, error) {
	if consumeChatModelConfigHook(s.failProviderReferenceLock, id) {
		return database.AIProvider{}, stderrors.New("forced provider reference lock failure")
	}
	return s.Store.GetAIProviderByIDForReferenceLock(ctx, id)
}

func (s *chatModelConfigHookStore) UpdateChatModelConfig(
	ctx context.Context,
	arg database.UpdateChatModelConfigParams,
) (database.ChatModelConfig, error) {
	if consumeChatModelConfigHook(s.failNextUpdate, arg.ID) {
		return database.ChatModelConfig{}, sql.ErrNoRows
	}
	return s.Store.UpdateChatModelConfig(ctx, arg)
}

func (s *chatModelConfigHookStore) DeleteChatModelConfigByID(
	ctx context.Context,
	id uuid.UUID,
) (uuid.UUID, error) {
	if consumeChatModelConfigHook(s.failNextDelete, id) {
		return uuid.Nil, sql.ErrNoRows
	}
	return s.Store.DeleteChatModelConfigByID(ctx, id)
}

func (s *chatModelConfigHookStore) GetChatModelConfigByID(
	ctx context.Context,
	id uuid.UUID,
) (database.ChatModelConfig, error) {
	if s.inTx && consumeChatModelConfigHook(s.vanishAtLockedRead, id) {
		return database.ChatModelConfig{}, sql.ErrNoRows
	}
	if s.inTx {
		mutation := s.mutateAtLockedRead.Load()
		if mutation != nil && mutation.id == id && s.mutateAtLockedRead.CompareAndSwap(mutation, nil) {
			row, err := s.Store.GetChatModelConfigByID(ctx, id)
			if err != nil {
				return row, err
			}
			row.Model = mutation.model
			return row, nil
		}
	}
	return s.Store.GetChatModelConfigByID(ctx, id)
}

type failNextGetChatSiteConfigValueStore struct {
	database.Store

	failNextGetChatSiteConfigValue *atomic.Bool
}

type failNextUpsertChatRetentionDaysStore struct {
	database.Store

	failNextUpsert *atomic.Bool
}

func newFailNextUpsertChatRetentionDaysStore(store database.Store) *failNextUpsertChatRetentionDaysStore {
	return &failNextUpsertChatRetentionDaysStore{
		Store:          store,
		failNextUpsert: &atomic.Bool{},
	}
}

func (s *failNextUpsertChatRetentionDaysStore) InTx(function func(database.Store) error, txOpts *database.TxOptions) error {
	return s.Store.InTx(func(tx database.Store) error {
		return function(&failNextUpsertChatRetentionDaysStore{
			Store:          tx,
			failNextUpsert: s.failNextUpsert,
		})
	}, txOpts)
}

func (s *failNextUpsertChatRetentionDaysStore) UpsertChatRetentionDays(ctx context.Context, retentionDays int32) error {
	if s.failNextUpsert.CompareAndSwap(true, false) {
		return stderrors.New("forced chat retention days upsert failure")
	}
	return s.Store.UpsertChatRetentionDays(ctx, retentionDays)
}

func newFailNextGetChatSiteConfigValueStore(store database.Store) *failNextGetChatSiteConfigValueStore {
	return &failNextGetChatSiteConfigValueStore{
		Store:                          store,
		failNextGetChatSiteConfigValue: &atomic.Bool{},
	}
}

func (s *failNextGetChatSiteConfigValueStore) InTx(function func(database.Store) error, txOpts *database.TxOptions) error {
	return s.Store.InTx(func(tx database.Store) error {
		return function(&failNextGetChatSiteConfigValueStore{
			Store:                          tx,
			failNextGetChatSiteConfigValue: s.failNextGetChatSiteConfigValue,
		})
	}, txOpts)
}

func (s *failNextGetChatSiteConfigValueStore) GetChatSiteConfigValue(
	ctx context.Context,
	configKey string,
) (database.GetChatSiteConfigValueRow, error) {
	if s.failNextGetChatSiteConfigValue.CompareAndSwap(true, false) {
		return database.GetChatSiteConfigValueRow{}, stderrors.New("forced chat site configuration read failure")
	}
	return s.Store.GetChatSiteConfigValue(ctx, configKey)
}

func insertAssistantMessage(
	t *testing.T,
	db database.Store,
	chatID uuid.UUID,
	modelConfigID uuid.UUID,
) {
	t.Helper()

	assistantContent, err := chatprompt.MarshalParts([]codersdk.ChatMessagePart{
		codersdk.ChatMessageText("assistant"),
	})
	require.NoError(t, err)

	_ = dbgen.ChatMessage(t, db, database.ChatMessage{
		ChatID:        chatID,
		ModelConfigID: uuid.NullUUID{UUID: modelConfigID, Valid: true},
		Role:          database.ChatMessageRoleAssistant,
		Content:       assistantContent,
	})
}

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

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		_ = coderdtest.CreateFirstUser(t, client.Client)
		aiProvider := createAIProviderForTest(t, client, "openai-compat", "test-api-key")

		org := dbgen.Organization(t, db, database.Organization{IsDefault: false})
		// The org's Everyone group shares the org ID (migration 000058).
		dbgen.Group(t, db, database.Group{
			ID:             org.ID,
			Name:           database.EveryoneGroup,
			OrganizationID: org.ID,
		})
		memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, org.ID)
		memberClient := codersdk.NewExperimentalClient(memberClientRaw)

		contextLimit := int64(4096)
		modelConfig, err := client.CreateChatModel(ctx, org.ID, codersdk.CreateChatModelRequest{
			AIProviderID: &aiProvider.ID,
			Model:        "gpt-4o-mini",
			ContextLimit: &contextLimit,
		})
		require.NoError(t, err)
		require.True(t, modelConfig.IsDefault, "first config in the org must self-elect as default")

		chat, err := memberClient.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: org.ID,
			Content: []codersdk.ChatInputPart{{
				Type: codersdk.ChatInputPartTypeText,
				Text: "hello from a non-default org",
			}},
		})
		require.NoError(t, err)
		require.Equal(t, modelConfig.ID, chat.LastModelConfigID)
	})

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

		client, db := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		defaultConfig := createChatModel(t, client)
		memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
		memberClient := codersdk.NewExperimentalClient(memberClientRaw)
		newOrgMember := func(t *testing.T) (uuid.UUID, *codersdk.ExperimentalClient) {
			t.Helper()
			org := dbgen.Organization(t, db, database.Organization{})
			orgMemberRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, org.ID)
			return org.ID, codersdk.NewExperimentalClient(orgMemberRaw)
		}
		crossOrgMCPServerID, thirdOrgMCPServerID := uuid.New(), uuid.New()

		// Parallel cases share a deployment, so prepare must not mutate
		// shared fixtures.
		tests := []struct {
			name        string
			prepare     func(t *testing.T, req *codersdk.CreateChatRequest) *codersdk.ExperimentalClient
			wantStatus  int
			wantMessage string
			wantDetail  string
		}{
			{
				name: "NonDefaultOrgDoesNotUseDeploymentDefault",
				prepare: func(t *testing.T, req *codersdk.CreateChatRequest) *codersdk.ExperimentalClient {
					orgID, orgMember := newOrgMember(t)
					req.OrganizationID = orgID
					return orgMember
				},
				wantStatus:  http.StatusBadRequest,
				wantMessage: "No chat model is available in this organization.",
				wantDetail:  "Ask an organization administrator to configure and enable a chat model.",
			},
			{
				name: "DisabledLocalDefaultDoesNotUseDefaultOrgDefault",
				prepare: func(t *testing.T, req *codersdk.CreateChatRequest) *codersdk.ExperimentalClient {
					orgID, orgMember := newOrgMember(t)
					dbgen.ChatModelConfig(t, db, database.ChatModelConfig{
						AIProviderID:   uuid.NullUUID{UUID: defaultConfig.AIProviderID, Valid: true},
						Model:          "disabled-local-default-" + uuid.NewString(),
						OrganizationID: orgID,
						IsDefault:      true,
					}, func(params *database.InsertChatModelConfigParams) {
						params.Enabled = false
					})
					req.OrganizationID = orgID
					return orgMember
				},
				wantStatus:  http.StatusBadRequest,
				wantMessage: "No chat model is available in this organization.",
				wantDetail:  "Ask an organization administrator to configure and enable a chat model.",
			},
			{
				name: "DefaultOrgExplicitModelRejectedForNonDefaultOrg",
				prepare: func(t *testing.T, req *codersdk.CreateChatRequest) *codersdk.ExperimentalClient {
					orgID, orgMember := newOrgMember(t)
					req.OrganizationID = orgID
					req.ModelConfigID = ptr.Ref(defaultConfig.ID)
					return orgMember
				},
				wantStatus:  http.StatusBadRequest,
				wantMessage: "Invalid model_config_id: model config not found or disabled.",
			},
			{
				name: "ThirdOrgExplicitModelRejected",
				prepare: func(t *testing.T, req *codersdk.CreateChatRequest) *codersdk.ExperimentalClient {
					orgID, orgMember := newOrgMember(t)
					thirdOrg := dbgen.Organization(t, db, database.Organization{})
					thirdConfig := dbgen.ChatModelConfig(t, db, database.ChatModelConfig{
						AIProviderID:   uuid.NullUUID{UUID: defaultConfig.AIProviderID, Valid: true},
						Model:          "third-org-" + uuid.NewString(),
						Enabled:        true,
						IsDefault:      true,
						OrganizationID: thirdOrg.ID,
					})
					req.OrganizationID = orgID
					req.ModelConfigID = ptr.Ref(thirdConfig.ID)
					return orgMember
				},
				wantStatus:  http.StatusBadRequest,
				wantMessage: "Invalid model_config_id: model config not found or disabled.",
			},
			{
				name: "MCPServerIDsCrossOrgRejected",
				prepare: func(t *testing.T, req *codersdk.CreateChatRequest) *codersdk.ExperimentalClient {
					dbgen.MCPServerConfig(t, db, database.MCPServerConfig{
						ID:             crossOrgMCPServerID,
						OrganizationID: firstUser.OrganizationID,
						Enabled:        true,
					})
					orgID, orgMember := newOrgMember(t)
					dbgen.ChatModelConfig(t, db, database.ChatModelConfig{
						AIProviderID:   uuid.NullUUID{UUID: defaultConfig.AIProviderID, Valid: true},
						OrganizationID: orgID,
						IsDefault:      true,
					})
					req.OrganizationID = orgID
					req.MCPServerIDs = []uuid.UUID{crossOrgMCPServerID}
					return orgMember
				},
				wantStatus:  http.StatusBadRequest,
				wantMessage: "One or more MCP server IDs are invalid or disabled.",
				wantDetail:  "Invalid IDs: " + crossOrgMCPServerID.String(),
			},
			{
				name: "MCPServerIDsThirdOrgRejected",
				prepare: func(t *testing.T, req *codersdk.CreateChatRequest) *codersdk.ExperimentalClient {
					thirdOrg := dbgen.Organization(t, db, database.Organization{})
					dbgen.MCPServerConfig(t, db, database.MCPServerConfig{
						ID:             thirdOrgMCPServerID,
						OrganizationID: thirdOrg.ID,
						Enabled:        true,
					})
					orgID, orgMember := newOrgMember(t)
					dbgen.ChatModelConfig(t, db, database.ChatModelConfig{
						AIProviderID:   uuid.NullUUID{UUID: defaultConfig.AIProviderID, Valid: true},
						OrganizationID: orgID,
						IsDefault:      true,
					})
					req.OrganizationID = orgID
					req.MCPServerIDs = []uuid.UUID{thirdOrgMCPServerID}
					return orgMember
				},
				wantStatus:  http.StatusBadRequest,
				wantMessage: "One or more MCP server IDs are invalid or disabled.",
				wantDetail:  "Invalid IDs: " + thirdOrgMCPServerID.String(),
			},
			{
				name: "RejectsInvalidReasoningEffort",
				prepare: func(_ *testing.T, req *codersdk.CreateChatRequest) *codersdk.ExperimentalClient {
					req.ReasoningEffort = ptr.Ref(" HIGH ")
					return client
				},
				wantStatus:  http.StatusBadRequest,
				wantMessage: "Invalid reasoning_effort value.",
				wantDetail:  `Invalid value " HIGH ", must be one of none, minimal, low, medium, high, xhigh, max`,
			},
			{
				name: "DisabledModelConfigRejected",
				prepare: func(t *testing.T, req *codersdk.CreateChatRequest) *codersdk.ExperimentalClient {
					disabledConfig := createDisabledChatModel(
						t,
						client,
						coderdtest.TestChatProviderOpenAICompat,
						"gpt-4o-create-disabled-"+uuid.NewString(),
					)
					req.ModelConfigID = ptr.Ref(disabledConfig.ID)
					return client
				},
				wantStatus:  http.StatusBadRequest,
				wantMessage: "Invalid model_config_id: model config not found or disabled.",
			},
			{
				name: "ProviderDisabledModelConfigRejected",
				prepare: func(t *testing.T, req *codersdk.CreateChatRequest) *codersdk.ExperimentalClient {
					providerDisabledConfig := createProviderDisabledChatModel(
						t,
						client,
						"openai",
						"gpt-4o-create-provider-disabled-"+uuid.NewString(),
					)
					req.ModelConfigID = ptr.Ref(providerDisabledConfig.ID)
					return client
				},
				wantStatus:  http.StatusBadRequest,
				wantMessage: "Invalid model_config_id: provider is not enabled for this model.",
			},
			{
				name: "DeniedSameOrganizationExplicitModelIsRejected",
				prepare: func(t *testing.T, req *codersdk.CreateChatRequest) *codersdk.ExperimentalClient {
					provider := createAIProviderForTest(t, client, "openai-compat", "test-api-key")
					privateConfig := dbgen.ChatModelConfig(t, db, database.ChatModelConfig{
						AIProviderID:   uuid.NullUUID{UUID: provider.ID, Valid: true},
						Model:          "private-explicit-" + uuid.NewString(),
						Enabled:        true,
						OrganizationID: firstUser.OrganizationID,
						GroupACL:       database.ChatACL{},
					})
					req.ModelConfigID = ptr.Ref(privateConfig.ID)
					return memberClient
				},
				wantStatus:  http.StatusBadRequest,
				wantMessage: "Invalid model_config_id: model config not found or disabled.",
			},
			{
				name: "WorkspaceNotAccessible",
				prepare: func(t *testing.T, req *codersdk.CreateChatRequest) *codersdk.ExperimentalClient {
					workspaceBuild := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
						OrganizationID: firstUser.OrganizationID,
						OwnerID:        firstUser.UserID,
					}).WithAgent().Do()
					req.WorkspaceID = &workspaceBuild.Workspace.ID
					return memberClient
				},
				wantStatus:  http.StatusBadRequest,
				wantMessage: "Workspace not found or you do not have access to this resource",
			},
			{
				name: "WorkspaceAccessibleButNoSSH",
				prepare: func(t *testing.T, req *codersdk.CreateChatRequest) *codersdk.ExperimentalClient {
					orgAdminClientRaw, _ := coderdtest.CreateAnotherUser(
						t,
						client.Client,
						firstUser.OrganizationID,
						rbac.ScopedRoleOrgAdmin(firstUser.OrganizationID),
					)
					workspaceBuild := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
						OrganizationID: firstUser.OrganizationID,
						OwnerID:        firstUser.UserID,
					}).WithAgent().Do()
					req.WorkspaceID = &workspaceBuild.Workspace.ID
					return codersdk.NewExperimentalClient(orgAdminClientRaw)
				},
				wantStatus:  http.StatusBadRequest,
				wantMessage: "Workspace not found or you do not have access to this resource",
			},
			{
				name: "WorkspaceNotFound",
				prepare: func(_ *testing.T, req *codersdk.CreateChatRequest) *codersdk.ExperimentalClient {
					req.WorkspaceID = ptr.Ref(uuid.New())
					return client
				},
				wantStatus:  http.StatusBadRequest,
				wantMessage: "Workspace not found or you do not have access to this resource",
			},
			{
				name: "EmptyText",
				// No model exists in the new org, so this also checks that
				// input is rejected before model resolution.
				prepare: func(t *testing.T, req *codersdk.CreateChatRequest) *codersdk.ExperimentalClient {
					orgID, orgMember := newOrgMember(t)
					req.OrganizationID = orgID
					req.Content[0].Text = "   "
					return orgMember
				},
				wantStatus:  http.StatusBadRequest,
				wantMessage: "Invalid input part.",
				wantDetail:  "content[0].text cannot be empty.",
			},
			{
				name: "UnsupportedPartType",
				prepare: func(t *testing.T, req *codersdk.CreateChatRequest) *codersdk.ExperimentalClient {
					orgID, orgMember := newOrgMember(t)
					req.OrganizationID = orgID
					req.Content[0].Type = codersdk.ChatInputPartType("image")
					return orgMember
				},
				wantStatus:  http.StatusBadRequest,
				wantMessage: "Invalid input part.",
				wantDetail:  `content[0].type "image" is not supported.`,
			},
			{
				name: "NilOrganizationID",
				prepare: func(_ *testing.T, req *codersdk.CreateChatRequest) *codersdk.ExperimentalClient {
					req.OrganizationID = uuid.Nil
					return memberClient
				},
				wantStatus:  http.StatusBadRequest,
				wantMessage: "organization_id is required.",
			},
			{
				name: "NonMemberOrganization",
				prepare: func(t *testing.T, req *codersdk.CreateChatRequest) *codersdk.ExperimentalClient {
					req.OrganizationID = dbgen.Organization(t, db, database.Organization{}).ID
					return memberClient
				},
				wantStatus:  http.StatusForbidden,
				wantMessage: "You are not a member of the specified organization.",
			},
			{
				name: "CrossOrgWorkspaceMismatch",
				prepare: func(t *testing.T, req *codersdk.CreateChatRequest) *codersdk.ExperimentalClient {
					workspaceBuild := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
						OrganizationID: firstUser.OrganizationID,
						OwnerID:        firstUser.UserID,
					}).WithAgent().Do()
					// The admin joins the second organization so the request
					// passes the membership check and fails on the workspace.
					secondOrg := dbgen.Organization(t, db, database.Organization{})
					dbgen.OrganizationMember(t, db, database.OrganizationMember{
						OrganizationID: secondOrg.ID,
						UserID:         firstUser.UserID,
					})
					req.OrganizationID = secondOrg.ID
					req.WorkspaceID = &workspaceBuild.Workspace.ID
					return client
				},
				wantStatus:  http.StatusBadRequest,
				wantMessage: "Workspace does not belong to the specified organization.",
			},
		}
		for _, tt := range tests {
			t.Run(tt.name, func(t *testing.T) {
				t.Parallel()

				ctx := testutil.Context(t, testutil.WaitLong)
				req := codersdk.CreateChatRequest{
					OrganizationID: firstUser.OrganizationID,
					Content: []codersdk.ChatInputPart{{
						Type: codersdk.ChatInputPartTypeText,
						Text: "hello",
					}},
				}
				chatClient := tt.prepare(t, &req)
				_, err := chatClient.CreateChat(ctx, req)
				sdkErr := requireSDKError(t, err, tt.wantStatus)
				require.Equal(t, tt.wantMessage, sdkErr.Message)
				require.Equal(t, tt.wantDetail, sdkErr.Detail)
			})
		}
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		mAudit := audit.NewMock()
		client := newChatClient(t, func(opts *coderdtest.Options) {
			opts.Auditor = mAudit
		})
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
		memberClient := codersdk.NewExperimentalClient(memberClientRaw)

		chat, err := memberClient.CreateChat(ctx, codersdk.CreateChatRequest{
			PlanMode:       codersdk.ChatPlanModePlan,
			OrganizationID: firstUser.OrganizationID, Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "hello from chats route tests",
				},
			},
		})
		require.NoError(t, err)

		require.NotEqual(t, uuid.Nil, chat.ID)
		require.Equal(t, member.ID, chat.OwnerID)
		require.Equal(t, modelConfig.ID, chat.LastModelConfigID)
		require.Equal(t, "hello from chats route tests", chat.Title)
		require.NotZero(t, chat.CreatedAt)
		require.NotZero(t, chat.UpdatedAt)
		require.Nil(t, chat.WorkspaceID)
		require.NotNil(t, chat.RootChatID)
		require.Equal(t, chat.ID, *chat.RootChatID)

		chatResult, err := memberClient.GetChat(ctx, chat.ID)
		require.NoError(t, err)
		messagesResult, err := memberClient.GetChatMessages(ctx, chat.ID, nil)
		require.NoError(t, err)
		require.Equal(t, chat.ID, chatResult.ID)
		require.Equal(t, codersdk.ChatPlanModePlan, chatResult.PlanMode)

		foundUserMessage := false
		for _, message := range messagesResult.Messages {
			if message.Role != codersdk.ChatMessageRoleUser {
				continue
			}
			for _, part := range message.Content {
				if part.Type == codersdk.ChatMessagePartTypeText &&
					part.Text == "hello from chats route tests" {
					foundUserMessage = true
					break
				}
			}
		}
		require.True(t, foundUserMessage)
		require.True(t, mAudit.Contains(t, database.AuditLog{
			Action:         database.AuditActionCreate,
			ResourceType:   database.ResourceTypeChat,
			ResourceID:     chat.ID,
			ResourceTarget: chat.ID.String()[:8],
			UserID:         member.ID,
		}))
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		coderdtest.CreateFirstUser(t, client.Client)
		deploymentDefault := createChatModel(t, client)

		secondOrg := dbgen.Organization(t, db, database.Organization{})
		_ = dbgen.ChatModelConfig(t, db, database.ChatModelConfig{
			AIProviderID:   uuid.NullUUID{UUID: deploymentDefault.AIProviderID, Valid: true},
			OrganizationID: secondOrg.ID,
			IsDefault:      true,
		})
		orgConfig := dbgen.MCPServerConfig(t, db, database.MCPServerConfig{
			OrganizationID: secondOrg.ID,
			Enabled:        true,
		})
		memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, secondOrg.ID)
		memberClient := codersdk.NewExperimentalClient(memberClientRaw)

		chat, err := memberClient.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: secondOrg.ID,
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "chat with a duplicated MCP server ID",
				},
			},
			MCPServerIDs: []uuid.UUID{orgConfig.ID, orgConfig.ID},
		})
		require.NoError(t, err)
		require.Equal(t, []uuid.UUID{orgConfig.ID}, chat.MCPServerIDs)

		_, err = memberClient.CreateChatMessage(ctx, chat.ID, codersdk.CreateChatMessageRequest{
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "update to a duplicated MCP server ID",
				},
			},
			MCPServerIDs: &[]uuid.UUID{orgConfig.ID, orgConfig.ID},
		})
		require.NoError(t, err)

		storedChat, err := db.GetChatByID(dbauthz.AsSystemRestricted(ctx), chat.ID)
		require.NoError(t, err)
		require.Equal(t, []uuid.UUID{orgConfig.ID}, storedChat.MCPServerIDs)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)

		enabledCfg := dbgen.MCPServerConfig(t, db, database.MCPServerConfig{
			OrganizationID: firstUser.OrganizationID,
			Enabled:        true,
		})
		disabledCfg, err := client.UpdateMCPServerConfig(ctx, enabledCfg.OrganizationID, enabledCfg.ID, codersdk.UpdateMCPServerConfigRequest{
			Enabled: ptr.Ref(false),
		})
		require.NoError(t, err)

		memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
		memberClient := codersdk.NewExperimentalClient(memberClientRaw)

		_, err = memberClient.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: firstUser.OrganizationID,
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "chat with a disabled MCP server",
				},
			},
			MCPServerIDs: []uuid.UUID{disabledCfg.ID},
		})
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, "One or more MCP server IDs are invalid or disabled.", sdkErr.Message)
		require.Equal(t, "Invalid IDs: "+disabledCfg.ID.String(), sdkErr.Detail)

		chat, err := memberClient.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: firstUser.OrganizationID,
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "chat without MCP servers",
				},
			},
		})
		require.NoError(t, err)

		_, err = memberClient.CreateChatMessage(ctx, chat.ID, codersdk.CreateChatMessageRequest{
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "selecting the disabled config",
				},
			},
			MCPServerIDs: &[]uuid.UUID{disabledCfg.ID},
		})
		sdkErr = requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, "One or more MCP server IDs are invalid or disabled.", sdkErr.Message)
		require.Equal(t, "Invalid IDs: "+disabledCfg.ID.String(), sdkErr.Detail)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)

		cfg := dbgen.MCPServerConfig(t, db, database.MCPServerConfig{
			OrganizationID: firstUser.OrganizationID,
			Enabled:        true,
		})

		memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
		memberClient := codersdk.NewExperimentalClient(memberClientRaw)

		chat, err := memberClient.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: firstUser.OrganizationID,
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "chat with a server that is disabled later",
				},
			},
			MCPServerIDs: []uuid.UUID{cfg.ID},
		})
		require.NoError(t, err)

		_, err = client.UpdateMCPServerConfig(ctx, cfg.OrganizationID, cfg.ID, codersdk.UpdateMCPServerConfigRequest{
			Enabled: ptr.Ref(false),
		})
		require.NoError(t, err)

		// The frontend resubmits the persisted selection on every
		// send, so a server disabled after selection must not block
		// the send.
		_, err = memberClient.CreateChatMessage(ctx, chat.ID, codersdk.CreateChatMessageRequest{
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "still sendable after the server was disabled",
				},
			},
			MCPServerIDs: &[]uuid.UUID{cfg.ID},
		})
		require.NoError(t, err)

		storedChat, err := db.GetChatByID(dbauthz.AsSystemRestricted(ctx), chat.ID)
		require.NoError(t, err)
		require.Equal(t, []uuid.UUID{cfg.ID}, storedChat.MCPServerIDs)

		secondCfg := dbgen.MCPServerConfig(t, db, database.MCPServerConfig{
			OrganizationID: firstUser.OrganizationID,
			Enabled:        true,
		})
		_, err = client.UpdateMCPServerConfig(ctx, secondCfg.OrganizationID, secondCfg.ID, codersdk.UpdateMCPServerConfigRequest{
			Enabled: ptr.Ref(false),
		})
		require.NoError(t, err)

		_, err = memberClient.CreateChatMessage(ctx, chat.ID, codersdk.CreateChatMessageRequest{
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "adding another disabled server is rejected",
				},
			},
			MCPServerIDs: &[]uuid.UUID{cfg.ID, secondCfg.ID},
		})
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, "One or more MCP server IDs are invalid or disabled.", sdkErr.Message)
		require.Equal(t, "Invalid IDs: "+secondCfg.ID.String(), sdkErr.Detail)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)

		memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
		memberClient := codersdk.NewExperimentalClient(memberClientRaw)

		chat, err := memberClient.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: firstUser.OrganizationID,
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "hello from an organization member",
				},
			},
		})
		require.NoError(t, err)
		require.Equal(t, member.ID, chat.OwnerID)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		rawDB, pubsub := dbtestutil.NewDB(t)
		client := newChatClient(t, func(opts *coderdtest.Options) {
			opts.Database = rawDB
			opts.Pubsub = pubsub
		})
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)

		memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
		memberClient := codersdk.NewExperimentalClient(memberClientRaw)
		createChat := func() error {
			_, err := memberClient.CreateChat(ctx, codersdk.CreateChatRequest{
				OrganizationID: firstUser.OrganizationID,
				Content: []codersdk.ChatInputPart{
					{Type: codersdk.ChatInputPartTypeText, Text: "hello"},
				},
			})
			return err
		}
		setDefaults := func(roles []string) {
			t.Helper()
			org, err := rawDB.GetOrganizationByID(ctx, firstUser.OrganizationID)
			require.NoError(t, err)
			_, err = rawDB.UpdateOrganization(ctx, database.UpdateOrganizationParams{
				ID:                    org.ID,
				UpdatedAt:             dbtime.Now(),
				Name:                  org.Name,
				DisplayName:           org.DisplayName,
				Description:           org.Description,
				Icon:                  org.Icon,
				DefaultOrgMemberRoles: roles,
			})
			require.NoError(t, err)
		}
		setMemberRoles := func(roles []string) {
			t.Helper()
			_, err := client.UpdateOrganizationMemberRoles(ctx, firstUser.OrganizationID, member.ID.String(), codersdk.UpdateRoles{Roles: roles})
			require.NoError(t, err)
		}

		setDefaults([]string{codersdk.RoleOrganizationWorkspaceAccess})
		requireSDKError(t, createChat(), http.StatusForbidden)

		setMemberRoles([]string{codersdk.RoleAgentsAccess})
		require.NoError(t, createChat())

		setMemberRoles([]string{})
		requireSDKError(t, createChat(), http.StatusForbidden)

		setDefaults(rbac.DefaultOrgMemberRoles())
		require.NoError(t, createChat())
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		user := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)

		chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: user.OrganizationID,
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "think hard from the start",
				},
			},
			ReasoningEffort: ptr.Ref("high"),
		})
		require.NoError(t, err)

		storedChat, err := db.GetChatByID(dbauthz.AsSystemRestricted(ctx), chat.ID)
		require.NoError(t, err)
		require.True(t, storedChat.LastReasoningEffort.Valid)
		require.Equal(t, database.ChatReasoningEffortHigh, storedChat.LastReasoningEffort.ChatReasoningEffort)

		messages, err := db.GetChatMessagesByChatID(dbauthz.AsSystemRestricted(ctx), database.GetChatMessagesByChatIDParams{
			ChatID:  chat.ID,
			AfterID: 0,
		})
		require.NoError(t, err)
		userMsg := findUserMessage(t, messages)
		require.True(t, userMsg.ReasoningEffort.Valid)
		require.Equal(t, database.ChatReasoningEffortHigh, userMsg.ReasoningEffort.ChatReasoningEffort)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)

		chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: firstUser.OrganizationID,
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "verify hidden system prompt",
				},
			},
		})
		require.NoError(t, err)

		messagesResult, err := client.GetChatMessages(ctx, chat.ID, nil)
		require.NoError(t, err)
		for _, message := range messagesResult.Messages {
			require.NotEqual(t, codersdk.ChatMessageRoleSystem, message.Role)
		}
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		defaultConfig := createChatModel(t, client)
		_, err := client.UpdateAIProvider(ctx, defaultConfig.AIProviderID.String(), codersdk.UpdateAIProviderRequest{
			Enabled: ptr.Ref(false),
		})
		require.NoError(t, err)

		// Omitting model_config_id resolves the default model, whose
		// provider is now disabled.
		_, err = client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: firstUser.OrganizationID,
			Content: []codersdk.ChatInputPart{{
				Type: codersdk.ChatInputPartTypeText,
				Text: "hello",
			}},
		})
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, "No chat model is available in this organization.", sdkErr.Message)
		require.Equal(t, "Ask an organization administrator to configure and enable a chat model.", sdkErr.Detail)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		provider := createAIProviderForTest(t, client, "openai-compat", "test-api-key")
		_ = dbgen.ChatModelConfig(t, db, database.ChatModelConfig{
			AIProviderID:   uuid.NullUUID{UUID: provider.ID, Valid: true},
			Model:          "private-default-" + uuid.NewString(),
			Enabled:        true,
			IsDefault:      true,
			OrganizationID: firstUser.OrganizationID,
			GroupACL:       database.ChatACL{},
		})
		memberClientRaw, _ := coderdtest.CreateAnotherUser(
			t,
			client.Client,
			firstUser.OrganizationID,
		)
		memberClient := codersdk.NewExperimentalClient(memberClientRaw)

		_, err := memberClient.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: firstUser.OrganizationID,
			Content: []codersdk.ChatInputPart{{
				Type: codersdk.ChatInputPartTypeText,
				Text: "do not select the private default",
			}},
		})
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, "No chat model is available in this organization.", sdkErr.Message)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		user := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)

		chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: user.OrganizationID,
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "hello with system prompt",
				},
			},
			SystemPrompt: "You are a Go expert.",
		})
		require.NoError(t, err)
		require.NotEqual(t, uuid.Nil, chat.ID)

		// Use the DB directly to see system messages, which are
		// hidden from the public API.
		dbMessages, err := db.GetChatMessagesForPromptByChatID(dbauthz.AsSystemRestricted(ctx), chat.ID)
		require.NoError(t, err)

		// Expect: deployment system prompt, per-chat system prompt,
		// workspace awareness, user message.
		var systemMessages []database.ChatMessage
		for _, msg := range dbMessages {
			if msg.Role == database.ChatMessageRoleSystem {
				systemMessages = append(systemMessages, msg)
			}
		}
		require.GreaterOrEqual(t, len(systemMessages), 2,
			"expected at least deployment + per-chat system messages")

		// The per-chat system prompt should be the second system
		// message and contain the user-specified text.
		foundPerChat := false
		for _, msg := range systemMessages {
			if msg.Content.Valid {
				raw := string(msg.Content.RawMessage)
				if strings.Contains(raw, "You are a Go expert.") {
					foundPerChat = true
					break
				}
			}
		}
		require.True(t, foundPerChat,
			"per-chat system prompt not found in system messages")
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		user := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)

		chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: user.OrganizationID,
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "hello without system prompt",
				},
			},
			SystemPrompt: "",
		})
		require.NoError(t, err)

		dbMessages, err := db.GetChatMessagesForPromptByChatID(dbauthz.AsSystemRestricted(ctx), chat.ID)
		require.NoError(t, err)

		// No per-chat system prompt should be present.
		for _, msg := range dbMessages {
			if msg.Role == database.ChatMessageRoleSystem && msg.Content.Valid {
				raw := string(msg.Content.RawMessage)
				require.NotContains(t, raw, "You are a Go expert.",
					"unexpected per-chat system prompt in messages")
			}
		}
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		user := coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		workspaceBuild := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
			OrganizationID: user.OrganizationID,
			OwnerID:        user.UserID,
		}).WithAgent().Do()

		chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: user.OrganizationID,
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "hello",
				},
			},
			WorkspaceID: &workspaceBuild.Workspace.ID,
		})
		require.NoError(t, err)
		require.NotNil(t, chat.WorkspaceID)
		require.Equal(t, workspaceBuild.Workspace.ID, *chat.WorkspaceID)
		require.Equal(t, modelConfig.ID, chat.LastModelConfigID)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)

		_, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: firstUser.OrganizationID,
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "hello",
				},
			},
		})
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, "No chat model is available in this organization.", sdkErr.Message)
		require.Equal(t, "Ask an organization administrator to configure and enable a chat model.", sdkErr.Detail)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)

		// Empty content creates an idle chat; see
		// TestPostChats_EmptyContent for the full behavior.
		chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: firstUser.OrganizationID,
			Content:        nil,
		})
		require.NoError(t, err)
		require.Equal(t, codersdk.ChatStatusWaiting, chat.Status)
	})
}

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

	mAudit := audit.NewMock()
	client, api := newChatClientWithAPI(t, func(opts *coderdtest.Options) {
		opts.Auditor = mAudit
	})
	firstUser := coderdtest.CreateFirstUser(t, client.Client)
	createChatModel(t, client)

	helloRequest := func(organizationID uuid.UUID) codersdk.CreateChatRequest {
		return codersdk.CreateChatRequest{
			OrganizationID: organizationID,
			Content: []codersdk.ChatInputPart{{
				Type: codersdk.ChatInputPartTypeText,
				Text: "hello from CreateChatAsUser",
			}},
		}
	}

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

		ctx := testutil.Context(t, testutil.WaitLong)
		_, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)

		chat, err := api.CreateChatAsUser(ctx, member.ID, helloRequest(firstUser.OrganizationID))
		require.NoError(t, err)
		require.Equal(t, member.ID, chat.OwnerID)
		require.Equal(t, codersdk.ChatClientTypeAPI, chat.ClientType)
		require.Equal(t, "hello from CreateChatAsUser", chat.Title)

		require.True(t, mAudit.Contains(t, database.AuditLog{
			Action:         database.AuditActionCreate,
			ResourceType:   database.ResourceTypeChat,
			ResourceID:     chat.ID,
			UserID:         member.ID,
			OrganizationID: firstUser.OrganizationID,
			StatusCode:     http.StatusCreated,
		}))
	})

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

		cases := []struct {
			name        string
			refuse      func(ctx context.Context, t *testing.T, userID uuid.UUID) uuid.UUID
			wantMessage string
		}{
			{
				name: "SuspendedUser",
				refuse: func(ctx context.Context, t *testing.T, userID uuid.UUID) uuid.UUID {
					_, err := client.UpdateUserStatus(ctx, userID.String(), codersdk.UserStatusSuspended)
					require.NoError(t, err)
					return userID
				},
				wantMessage: "not active",
			},
			{
				name: "DeletedUser",
				refuse: func(ctx context.Context, t *testing.T, userID uuid.UUID) uuid.UUID {
					require.NoError(t, client.DeleteUser(ctx, userID))
					return userID
				},
				wantMessage: "User is deleted.",
			},
			{
				name: "MissingUser",
				refuse: func(context.Context, *testing.T, uuid.UUID) uuid.UUID {
					return uuid.New()
				},
				wantMessage: "User does not exist.",
			},
		}
		for _, tc := range cases {
			t.Run(tc.name, func(t *testing.T) {
				t.Parallel()

				ctx := testutil.Context(t, testutil.WaitLong)
				_, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
				userID := tc.refuse(ctx, t, member.ID)

				_, err := api.CreateChatAsUser(ctx, userID, helloRequest(firstUser.OrganizationID))
				responder, ok := httperror.IsResponder(err)
				require.True(t, ok, "want a responder error, got %v", err)
				status, resp := responder.Response()
				require.Equal(t, http.StatusForbidden, status)
				require.Contains(t, resp.Message, tc.wantMessage)

				chats, err := api.Database.GetChats(dbauthz.AsSystemRestricted(ctx), database.GetChatsParams{
					OwnedOnly: true,
					ViewerID:  userID,
				})
				require.NoError(t, err)
				require.Empty(t, chats)
				for _, log := range mAudit.AuditLogs() {
					require.False(t, log.ResourceType == database.ResourceTypeChat && log.UserID == userID, "refused user got a chat audit entry")
				}
			})
		}
	})

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

		activationAudits := func(userID uuid.UUID) int {
			count := 0
			for _, log := range mAudit.AuditLogs() {
				if log.ResourceType == database.ResourceTypeUser && log.ResourceID == userID && log.Action == database.AuditActionWrite {
					count++
				}
			}
			return count
		}
		cases := []struct {
			name        string
			status      database.UserStatus
			lastSeenAgo time.Duration
			wantSeen    bool
		}{
			{name: "DormantIsActivated", status: database.UserStatusDormant, lastSeenAgo: 100 * 24 * time.Hour, wantSeen: true},
			{name: "SeenRecentlyKeepsLastSeen", status: database.UserStatusActive, lastSeenAgo: 30 * time.Minute},
			{name: "SeenOverAnHourAgoUpdatesLastSeen", status: database.UserStatusActive, lastSeenAgo: 2 * time.Hour, wantSeen: true},
		}
		for _, tc := range cases {
			t.Run(tc.name, func(t *testing.T) {
				t.Parallel()

				ctx := testutil.Context(t, testutil.WaitLong)
				_, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
				systemCtx := dbauthz.AsSystemRestricted(ctx)
				now := dbtime.Now()
				lastSeen := now.Add(-tc.lastSeenAgo)
				_, err := api.Database.UpdateUserLastSeenAt(systemCtx, database.UpdateUserLastSeenAtParams{
					ID:         member.ID,
					LastSeenAt: lastSeen,
					UpdatedAt:  now,
				})
				require.NoError(t, err)
				_, err = api.Database.UpdateUserStatus(systemCtx, database.UpdateUserStatusParams{
					ID:        member.ID,
					Status:    tc.status,
					UpdatedAt: now,
				})
				require.NoError(t, err)
				auditsBefore := activationAudits(member.ID)

				_, err = api.CreateChatAsUser(ctx, member.ID, helloRequest(firstUser.OrganizationID))
				require.NoError(t, err)

				user, err := api.Database.GetUserByID(systemCtx, member.ID)
				require.NoError(t, err)
				require.Equal(t, database.UserStatusActive, user.Status)
				if tc.wantSeen {
					require.True(t, user.LastSeenAt.After(lastSeen), "last_seen_at stayed at %s", user.LastSeenAt)
				} else {
					require.True(t, lastSeen.Equal(user.LastSeenAt), "last_seen_at changed from %s to %s", lastSeen, user.LastSeenAt)
				}
				wantAudits := auditsBefore
				if tc.status == database.UserStatusDormant {
					wantAudits++
				}
				require.Equal(t, wantAudits, activationAudits(member.ID))
			})
		}
	})

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

		const hookSecret = "test-hook-secret-32-bytes-minimum!!"
		consumer := newHookConsumer(t, hookSecret, agenthooks.Hooks{
			UserPromptSubmit: func(context.Context, agenthooks.Meta, agenthooks.UserPromptSubmitData) (agenthooks.Response, error) {
				return agenthooks.Response{
					Permission:  &agenthooks.Permission{Decision: agenthooks.PermissionDeny},
					UserMessage: "blocked by policy",
				}, nil
			},
		})
		t.Cleanup(consumer.Close)
		hookClient, hookAPI := newChatClientWithAPI(t, func(opts *coderdtest.Options) {
			opts.ChatWorkerDisabled = true
			require.NoError(t, opts.DeploymentValues.AI.Chat.HookURL.Set(consumer.URL))
			opts.DeploymentValues.AI.Chat.HookSecret = serpent.String(hookSecret)
			opts.DeploymentValues.AI.Chat.HookTimeout = serpent.Duration(testutil.WaitShort)
			opts.DeploymentValues.AI.Chat.HookEnabled = serpent.Bool(true)
		})
		hookUser := coderdtest.CreateFirstUser(t, hookClient.Client)
		createChatModel(t, hookClient)
		ctx := testutil.Context(t, testutil.WaitLong)

		_, err := hookAPI.CreateChatAsUser(ctx, hookUser.UserID, helloRequest(hookUser.OrganizationID))
		responder, ok := httperror.IsResponder(err)
		require.True(t, ok, "want a responder error, got %v", err)
		status, resp := responder.Response()
		require.Equal(t, http.StatusForbidden, status)
		require.Equal(t, "blocked by policy", resp.Message)
		denied, ok := stderrors.AsType[*chathooks.UserPromptDeniedError](err)
		require.True(t, ok, "want a hook denial, got %v", err)
		require.Equal(t, "blocked by policy", denied.UserMessage)
	})
}

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

	mAudit := audit.NewMockWithDiffFn(func(old, newVal any) audit.Map {
		diff := audit.Map{}
		oldChat, oldOK := old.(database.Chat)
		newChat, newOK := newVal.(database.Chat)
		if !oldOK || !newOK {
			return diff
		}
		if oldChat.ID != newChat.ID {
			diff["id"] = audit.OldNew{Old: oldChat.ID, New: newChat.ID}
		}
		if oldChat.Archived != newChat.Archived {
			diff["archived"] = audit.OldNew{Old: oldChat.Archived, New: newChat.Archived}
		}
		return diff
	})
	client, api := newChatClientWithAPI(t, withChatWorkerDisabled, func(opts *coderdtest.Options) {
		opts.Auditor = mAudit
	})
	firstUser := coderdtest.CreateFirstUser(t, client.Client)
	createChatModel(t, client)

	newChat := func(ctx context.Context, t *testing.T) (*codersdk.ExperimentalClient, codersdk.Chat) {
		t.Helper()
		memberClient, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
		chat, err := api.CreateChatAsUser(ctx, member.ID, codersdk.CreateChatRequest{
			OrganizationID: firstUser.OrganizationID,
			Content:        []codersdk.ChatInputPart{{Type: codersdk.ChatInputPartTypeText, Text: "hello"}},
		})
		require.NoError(t, err)
		return codersdk.NewExperimentalClient(memberClient), chat
	}
	acquireChat := func(ctx context.Context, t *testing.T, chatID uuid.UUID) {
		t.Helper()
		chatdCtx := dbauthz.AsChatd(ctx) //nolint:gocritic // Test fixture mirrors chatd background transitions.
		machine := chatstate.NewChatMachine(api.Database, api.Pubsub, chatID)
		require.NoError(t, machine.Update(chatdCtx, func(tx *chatstate.Tx, _ database.Store) error {
			_, err := tx.Acquire(chatstate.AcquireInput{WorkerID: uuid.New(), RunnerID: uuid.New()})
			return err
		}))
	}
	message := codersdk.CreateChatMessageRequest{
		Content: []codersdk.ChatInputPart{{Type: codersdk.ChatInputPartTypeText, Text: "hello again"}},
	}
	unarchiveRequest := codersdk.UpdateChatRequest{Archived: ptr.Ref(false)}
	chatWrites := func(chatID uuid.UUID) []database.AuditLog {
		var logs []database.AuditLog
		for _, log := range mAudit.AuditLogs() {
			if log.ResourceID == chatID && log.Action == database.AuditActionWrite {
				logs = append(logs, log)
			}
		}
		return logs
	}
	requireSameAudit := func(t *testing.T, overHTTP, inProcess database.AuditLog) {
		t.Helper()
		require.Equal(t, overHTTP.ResourceType, inProcess.ResourceType)
		require.Equal(t, overHTTP.ResourceTarget, inProcess.ResourceTarget)
		require.Equal(t, overHTTP.UserID, inProcess.UserID)
		require.Equal(t, overHTTP.OrganizationID, inProcess.OrganizationID)
		require.Equal(t, overHTTP.StatusCode, inProcess.StatusCode)
	}

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

		ctx := testutil.Context(t, testutil.WaitLong)
		_, chat := newChat(ctx, t)
		driveChatToWaiting(ctx, t, api, chat.ID)

		resp, err := api.SendChatMessageAsUser(ctx, chat.OwnerID, chat.ID, message)
		require.NoError(t, err)
		require.False(t, resp.Queued)
		require.NotNil(t, resp.Message)
		require.Equal(t, codersdk.ChatMessageRoleUser, resp.Message.Role)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		_, chat := newChat(ctx, t)
		acquireChat(ctx, t, chat.ID)

		interrupted, err := api.InterruptChatAsUser(ctx, chat.OwnerID, chat.ID)
		require.NoError(t, err)
		require.Equal(t, chat.ID, interrupted.ID)
		require.Equal(t, codersdk.ChatStatusInterrupting, interrupted.Status)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		memberClient, chat := newChat(ctx, t)
		driveChatToWaiting(ctx, t, api, chat.ID)
		require.NoError(t, memberClient.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{Archived: ptr.Ref(true)}))

		require.NoError(t, api.UpdateChatAsUser(ctx, chat.OwnerID, chat.ID, unarchiveRequest))
		got, err := memberClient.GetChat(ctx, chat.ID)
		require.NoError(t, err)
		require.False(t, got.Archived)

		logs := chatWrites(chat.ID)
		require.Len(t, logs, 2)
		archive, unarchive := logs[0], logs[1]
		requireSameAudit(t, archive, unarchive)
		require.Equal(t, chat.OwnerID, unarchive.UserID)
		require.EqualValues(t, http.StatusNoContent, unarchive.StatusCode)
		var diff map[string]codersdk.AuditDiffField
		require.NoError(t, json.Unmarshal(unarchive.Diff, &diff))
		require.Equal(t, map[string]codersdk.AuditDiffField{"archived": {Old: true, New: false}}, diff)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		memberClient, chat := newChat(ctx, t)

		require.Error(t, memberClient.UpdateChat(ctx, chat.ID, unarchiveRequest))
		require.Error(t, api.UpdateChatAsUser(ctx, chat.OwnerID, chat.ID, unarchiveRequest))

		logs := chatWrites(chat.ID)
		require.Len(t, logs, 2)
		overHTTP, inProcess := logs[0], logs[1]
		requireSameAudit(t, overHTTP, inProcess)
		require.EqualValues(t, http.StatusBadRequest, inProcess.StatusCode)
		require.JSONEq(t, "{}", string(overHTTP.Diff))
		require.JSONEq(t, "{}", string(inProcess.Diff))
	})

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

		send := func(ctx context.Context, userID, chatID uuid.UUID) error {
			_, err := api.SendChatMessageAsUser(ctx, userID, chatID, message)
			return err
		}
		interrupt := func(ctx context.Context, userID, chatID uuid.UUID) error {
			_, err := api.InterruptChatAsUser(ctx, userID, chatID)
			return err
		}
		unarchive := func(ctx context.Context, userID, chatID uuid.UUID) error {
			return api.UpdateChatAsUser(ctx, userID, chatID, unarchiveRequest)
		}
		otherUser := func(_ context.Context, t *testing.T, _ *codersdk.ExperimentalClient, _ codersdk.Chat) uuid.UUID {
			_, other := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
			return other.ID
		}
		cases := []struct {
			name        string
			act         func(ctx context.Context, userID, chatID uuid.UUID) error
			prepare     func(ctx context.Context, t *testing.T, memberClient *codersdk.ExperimentalClient, chat codersdk.Chat) uuid.UUID
			wantStatus  int
			wantMessage string
		}{
			{
				name:        "SendOtherUsersChat",
				act:         send,
				prepare:     otherUser,
				wantStatus:  http.StatusNotFound,
				wantMessage: "Resource not found",
			},
			{
				name: "SendSiteOwnerNotChatOwner",
				act:  send,
				prepare: func(context.Context, *testing.T, *codersdk.ExperimentalClient, codersdk.Chat) uuid.UUID {
					return firstUser.UserID
				},
				wantStatus:  http.StatusForbidden,
				wantMessage: "Only the chat owner may send messages.",
			},
			{
				name:        "InterruptOtherUsersChat",
				act:         interrupt,
				prepare:     otherUser,
				wantStatus:  http.StatusNotFound,
				wantMessage: "Resource not found",
			},
			{
				name: "UnarchiveReadOnlyMember",
				act:  unarchive,
				prepare: func(ctx context.Context, t *testing.T, memberClient *codersdk.ExperimentalClient, chat codersdk.Chat) uuid.UUID {
					driveChatToWaiting(ctx, t, api, chat.ID)
					require.NoError(t, memberClient.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{Archived: ptr.Ref(true)}))
					_, reader := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
					require.NoError(t, memberClient.UpdateChatACL(ctx, chat.ID, codersdk.UpdateChatACL{
						UserRoles: map[string]codersdk.ChatRole{reader.ID.String(): codersdk.ChatRoleRead},
					}))
					return reader.ID
				},
				wantStatus:  http.StatusNotFound,
				wantMessage: "Resource not found",
			},
		}
		for _, tc := range cases {
			t.Run(tc.name, func(t *testing.T) {
				t.Parallel()

				ctx := testutil.Context(t, testutil.WaitLong)
				memberClient, chat := newChat(ctx, t)
				userID := tc.prepare(ctx, t, memberClient, chat)

				err := tc.act(ctx, userID, chat.ID)
				responder, ok := httperror.IsResponder(err)
				require.True(t, ok, "want a responder error, got %v", err)
				status, resp := responder.Response()
				require.Equal(t, tc.wantStatus, status)
				require.Contains(t, resp.Message, tc.wantMessage)
			})
		}
	})
}

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

	helloRequest := func(ownerID, organizationID uuid.UUID) codersdk.CreateChatRequest {
		return codersdk.CreateChatRequest{
			OrganizationID: organizationID,
			OwnerID:        &ownerID,
			Content: []codersdk.ChatInputPart{{
				Type: codersdk.ChatInputPartTypeText,
				Text: "hello on behalf of another user",
			}},
		}
	}

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)
		memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
		memberClient := codersdk.NewExperimentalClient(memberClientRaw)

		chat, err := client.CreateChat(ctx, helloRequest(member.ID, firstUser.OrganizationID))
		require.NoError(t, err)
		require.Equal(t, member.ID, chat.OwnerID)

		// The member can read the chat; the prompt stays attributed to the admin.
		_, err = memberClient.GetChat(ctx, chat.ID)
		require.NoError(t, err)
		messages, err := db.GetChatMessagesByChatID(dbauthz.AsSystemRestricted(ctx), database.GetChatMessagesByChatIDParams{
			ChatID: chat.ID,
		})
		require.NoError(t, err)
		userMsg := findUserMessage(t, messages)
		require.Equal(t, uuid.NullUUID{UUID: firstUser.UserID, Valid: true}, userMsg.CreatedBy)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)
		_, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)

		// Service accounts are a licensed feature, so seed one directly. It
		// is not an organization member: site-wide authority is sufficient.
		serviceAccount := dbgen.User(t, db, database.User{
			IsServiceAccount: true,
			RBACRoles:        []string{rbac.RoleOwner().String()},
		})
		_, token := dbgen.APIKey(t, db, database.APIKey{
			UserID:    serviceAccount.ID,
			LoginType: database.LoginTypeToken,
		})
		serviceAccountClient := codersdk.New(
			client.URL,
			codersdk.WithSessionToken(token),
			codersdk.WithHTTPClient(coderdtest.NewIsolatedHTTPClient(client.URL)),
		)
		t.Cleanup(serviceAccountClient.HTTPClient.CloseIdleConnections)

		chat, err := serviceAccountClient.CreateChat(ctx, helloRequest(member.ID, firstUser.OrganizationID))
		require.NoError(t, err)
		require.Equal(t, member.ID, chat.OwnerID)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)

		_, err := client.CreateChat(ctx, helloRequest(uuid.Nil, firstUser.OrganizationID))
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, "Invalid owner_id: must be a user ID or omitted.", sdkErr.Message)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)
		memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
		memberClient := codersdk.NewExperimentalClient(memberClientRaw)

		chat, err := memberClient.CreateChat(ctx, helloRequest(member.ID, firstUser.OrganizationID))
		require.NoError(t, err)
		require.Equal(t, member.ID, chat.OwnerID)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)
		_, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
		workspaceBuild := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
			OrganizationID: firstUser.OrganizationID,
			OwnerID:        member.ID,
		}).WithAgent().Do()

		req := helloRequest(member.ID, firstUser.OrganizationID)
		req.WorkspaceID = &workspaceBuild.Workspace.ID
		chat, err := client.CreateChat(ctx, req)
		require.NoError(t, err)
		require.NotNil(t, chat.WorkspaceID)
		require.Equal(t, workspaceBuild.Workspace.ID, *chat.WorkspaceID)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)
		_, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
		workspaceBuild := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
			OrganizationID: firstUser.OrganizationID,
			OwnerID:        firstUser.UserID,
		}).WithAgent().Do()

		// The admin can reach their own workspace, but the chat connects
		// as the member, who cannot.
		req := helloRequest(member.ID, firstUser.OrganizationID)
		req.WorkspaceID = &workspaceBuild.Workspace.ID
		_, err := client.CreateChat(ctx, req)
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, "Workspace not found or you do not have access to this resource", sdkErr.Message)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)
		provider := createAIProviderForTest(t, client, "openai-compat", "test-api-key")
		privateConfig := dbgen.ChatModelConfig(t, db, database.ChatModelConfig{
			AIProviderID: uuid.NullUUID{UUID: provider.ID, Valid: true}, OrganizationID: firstUser.OrganizationID,
			Model: "private-" + uuid.NewString(), Enabled: true, GroupACL: database.ChatACL{},
		})
		_, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)

		// The admin can read the private model; the member cannot, and
		// chatd would silently fall back to the default at run time.
		req := helloRequest(member.ID, firstUser.OrganizationID)
		req.ModelConfigID = &privateConfig.ID
		_, err := client.CreateChat(ctx, req)
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, "Invalid model_config_id: model config not found or disabled.", sdkErr.Message)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)
		orgAdminClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.ScopedRoleOrgAdmin(firstUser.OrganizationID))
		orgAdminClient := codersdk.NewExperimentalClient(orgAdminClientRaw)
		_, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)

		// Org admins hold org-scoped chat:create, but a chat runs with its
		// owner's credentials, so acting as another user needs site-wide
		// authority.
		_, err := orgAdminClient.CreateChat(ctx, helloRequest(member.ID, firstUser.OrganizationID))
		requireSDKError(t, err, http.StatusForbidden)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)
		memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
		memberClient := codersdk.NewExperimentalClient(memberClientRaw)
		_, other := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)

		_, err := memberClient.CreateChat(ctx, helloRequest(other.ID, firstUser.OrganizationID))
		requireSDKError(t, err, http.StatusForbidden)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)
		otherOrg := dbgen.Organization(t, db, database.Organization{})
		_, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)

		_, err := client.CreateChat(ctx, helloRequest(member.ID, otherOrg.ID))
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, "Chat owner must be an active member of the organization.", sdkErr.Message)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)

		_, err := client.CreateChat(ctx, helloRequest(uuid.New(), firstUser.OrganizationID))
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, "Chat owner must be an active member of the organization.", sdkErr.Message)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)
		_, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
		_, err := client.UpdateUserStatus(ctx, member.ID.String(), codersdk.UserStatusSuspended)
		require.NoError(t, err)

		_, err = client.CreateChat(ctx, helloRequest(member.ID, firstUser.OrganizationID))
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, "Chat owner must be an active member of the organization.", sdkErr.Message)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)
		// Organization service accounts deliberately hold no chat permissions.
		serviceAccount := dbgen.User(t, db, database.User{IsServiceAccount: true})
		dbgen.OrganizationMember(t, db, database.OrganizationMember{
			OrganizationID: firstUser.OrganizationID,
			UserID:         serviceAccount.ID,
		})

		_, err := client.CreateChat(ctx, helloRequest(serviceAccount.ID, firstUser.OrganizationID))
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, "Chat owner does not have permission to use chats.", sdkErr.Message)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		rawDB, pubsub := dbtestutil.NewDB(t)
		client, _ := newChatClientWithDatabase(t, func(opts *coderdtest.Options) {
			opts.Database = rawDB
			opts.Pubsub = pubsub
		})
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)
		// A custom role can grant chat:create without read or update.
		role, err := rawDB.InsertCustomRole(ctx, database.InsertCustomRoleParams{
			Name:           testutil.GetRandomName(t),
			DisplayName:    "Chat Creator",
			OrganizationID: uuid.NullUUID{UUID: firstUser.OrganizationID, Valid: true},
			OrgPermissions: database.CustomRolePermissions{{
				ResourceType: rbac.ResourceChat.Type,
				Action:       policy.ActionCreate,
			}},
		})
		require.NoError(t, err)
		serviceAccount := dbgen.User(t, rawDB, database.User{IsServiceAccount: true})
		dbgen.OrganizationMember(t, rawDB, database.OrganizationMember{
			OrganizationID: firstUser.OrganizationID,
			UserID:         serviceAccount.ID,
			Roles:          []string{role.Name},
		})

		_, err = client.CreateChat(ctx, helloRequest(serviceAccount.ID, firstUser.OrganizationID))
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, "Chat owner does not have permission to use chats.", sdkErr.Message)
	})
}

// TestChats_ForceOnMCPServerEnforced is the endpoint-level regression
// test for Cure53 CDM-02-010: a regular user who strips force_on MCP
// server IDs from mcp_server_ids when creating a chat or sending a
// message must not be able to exclude those servers.
func TestChats_ForceOnMCPServerEnforced(t *testing.T) {
	t.Parallel()

	ctx := testutil.Context(t, testutil.WaitLong)
	client := newChatClient(t)
	firstUser := coderdtest.CreateFirstUser(t, client.Client)
	_ = createChatModel(t, client)

	// An admin marks an MCP server as Force On.
	forced, err := client.CreateMCPServerConfig(ctx, firstUser.OrganizationID, codersdk.CreateMCPServerConfigRequest{
		DisplayName:   "Forced Server",
		Slug:          "forced-server",
		Transport:     "streamable_http",
		URL:           "https://mcp.example.com/forced",
		AuthType:      "none",
		Availability:  "force_on",
		Enabled:       true,
		ToolAllowList: []string{},
		ToolDenyList:  []string{},
	})
	require.NoError(t, err)

	// A regular member tampers with the request by clearing
	// mcp_server_ids (Cure53 CDM-02-010 reproduction).
	memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
	memberClient := codersdk.NewExperimentalClient(memberClientRaw)

	chat, err := memberClient.CreateChat(ctx, codersdk.CreateChatRequest{
		OrganizationID: firstUser.OrganizationID,
		Content: []codersdk.ChatInputPart{{
			Type: codersdk.ChatInputPartTypeText,
			Text: "test message",
		}},
		MCPServerIDs: []uuid.UUID{},
	})
	require.NoError(t, err)
	require.Contains(t, chat.MCPServerIDs, forced.ID,
		"force_on MCP server must be enforced on chat creation")

	// Sending a message with an emptied list must not remove the
	// forced server either.
	_, err = memberClient.CreateChatMessage(ctx, chat.ID, codersdk.CreateChatMessageRequest{
		Content: []codersdk.ChatInputPart{{
			Type: codersdk.ChatInputPartTypeText,
			Text: "second message",
		}},
		MCPServerIDs: &[]uuid.UUID{},
	})
	require.NoError(t, err)

	chatResult, err := memberClient.GetChat(ctx, chat.ID)
	require.NoError(t, err)
	require.Contains(t, chatResult.MCPServerIDs, forced.ID,
		"force_on MCP server must survive a tampered mcp_server_ids update")
}

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

	client := newChatClient(t)
	firstUser := coderdtest.CreateFirstUser(t, client.Client)
	_ = createChatModel(t, client)

	memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
	memberClient := codersdk.NewExperimentalClient(memberClientRaw)

	newChat := func(t *testing.T, clientType codersdk.ChatClientType) codersdk.Chat {
		t.Helper()
		ctx := testutil.Context(t, testutil.WaitLong)
		chat, err := memberClient.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: firstUser.OrganizationID,
			Content: []codersdk.ChatInputPart{{
				Type: codersdk.ChatInputPartTypeText,
				Text: "client type test",
			}},
			ClientType: clientType,
		})
		require.NoError(t, err)
		return chat
	}

	t.Run("DefaultIsAPI", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitLong)

		// Omit ClientType entirely — should default to "api".
		chat := newChat(t, "")
		require.Equal(t, codersdk.ChatClientTypeAPI, chat.ClientType)

		got, err := memberClient.GetChat(ctx, chat.ID)
		require.NoError(t, err)
		require.Equal(t, codersdk.ChatClientTypeAPI, got.ClientType)
	})

	t.Run("ExplicitAPI", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitLong)

		chat := newChat(t, codersdk.ChatClientTypeAPI)
		require.Equal(t, codersdk.ChatClientTypeAPI, chat.ClientType)

		got, err := memberClient.GetChat(ctx, chat.ID)
		require.NoError(t, err)
		require.Equal(t, codersdk.ChatClientTypeAPI, got.ClientType)
	})

	t.Run("ExplicitUI", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitLong)

		chat := newChat(t, codersdk.ChatClientTypeUI)
		require.Equal(t, codersdk.ChatClientTypeUI, chat.ClientType)

		got, err := memberClient.GetChat(ctx, chat.ID)
		require.NoError(t, err)
		require.Equal(t, codersdk.ChatClientTypeUI, got.ClientType)
	})

	t.Run("InvalidClientType", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitLong)

		_, err := memberClient.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: firstUser.OrganizationID,
			Content: []codersdk.ChatInputPart{{
				Type: codersdk.ChatInputPartTypeText,
				Text: "bad client type",
			}},
			ClientType: "bogus",
		})
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Contains(t, sdkErr.Message, "Invalid client_type")
	})
}

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

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		firstChatA, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: firstUser.OrganizationID,
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "first owner chat",
				},
			},
		})
		require.NoError(t, err)

		firstChatB, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: firstUser.OrganizationID,
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "second owner chat",
				},
			},
		})
		require.NoError(t, err)

		memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
		memberClient := codersdk.NewExperimentalClient(memberClientRaw)
		memberDBChat := dbgen.Chat(t, db, database.Chat{
			OrganizationID:    firstUser.OrganizationID,
			OwnerID:           member.ID,
			LastModelConfigID: modelConfig.ID,
			Title:             "member chat only",
		})

		chats, err := client.ListChats(ctx, nil)
		require.NoError(t, err)
		require.Len(t, chats, 2)

		chatIndexes := make(map[uuid.UUID]int, len(chats))
		chatsByID := make(map[uuid.UUID]codersdk.Chat, len(chats))
		for i, chat := range chats {
			chatIndexes[chat.ID] = i
			chatsByID[chat.ID] = chat

			require.Equal(t, firstUser.UserID, chat.OwnerID)
			require.Equal(t, modelConfig.ID, chat.LastModelConfigID)
			// The chat may have been picked up by the chat worker
			// before we list, so accept any status it may have
			// reached by now.
			require.Contains(t, []codersdk.ChatStatus{
				codersdk.ChatStatusRunning,
				codersdk.ChatStatusError,
				codersdk.ChatStatusWaiting,
			}, chat.Status, "unexpected chat status: %s", chat.Status)
			require.NotZero(t, chat.CreatedAt)
			require.NotZero(t, chat.UpdatedAt)
			require.Nil(t, chat.ParentChatID)
			require.Nil(t, chat.WorkspaceID)
			require.NotNil(t, chat.RootChatID)
			require.Equal(t, chat.ID, *chat.RootChatID)
			require.NotNil(t, chat.DiffStatus)
			require.Equal(t, chat.ID, chat.DiffStatus.ChatID)
		}
		require.Contains(t, chatsByID, firstChatA.ID)
		require.Contains(t, chatsByID, firstChatB.ID)
		require.NotContains(t, chatsByID, memberDBChat.ID)
		require.Equal(t, "first owner chat", chatsByID[firstChatA.ID].Title)
		require.Equal(t, "second owner chat", chatsByID[firstChatB.ID].Title)

		for i := 1; i < len(chats); i++ {
			require.False(t, chats[i-1].UpdatedAt.Before(chats[i].UpdatedAt))
		}
		// The list is already verified as sorted by UpdatedAt
		// descending (loop above). We intentionally do NOT
		// compare positions using the creation-time UpdatedAt
		// values because the chat worker may pick up a chat and
		// mutate UpdatedAt between CreateChat and ListChats.

		memberChats, err := memberClient.ListChats(ctx, nil)
		require.NoError(t, err)
		require.Len(t, memberChats, 1)
		require.Equal(t, memberDBChat.ID, memberChats[0].ID)
		require.Equal(t, member.ID, memberChats[0].OwnerID)
		require.Equal(t, "member chat only", memberChats[0].Title)
		require.NotNil(t, memberChats[0].RootChatID)
		require.Equal(t, memberChats[0].ID, *memberChats[0].RootChatID)
		require.NotNil(t, memberChats[0].DiffStatus)
		require.Equal(t, memberChats[0].ID, memberChats[0].DiffStatus.ChatID)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)
		ownerClientRaw, owner := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.RoleOwner())
		ownerClient := codersdk.NewExperimentalClient(ownerClientRaw)
		memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
		memberClient := codersdk.NewExperimentalClient(memberClientRaw)

		ownedChat := dbgen.Chat(t, db, database.Chat{
			OrganizationID:    firstUser.OrganizationID,
			OwnerID:           owner.ID,
			LastModelConfigID: modelConfig.ID,
			Title:             "owner created chat",
			Status:            database.ChatStatusWaiting,
		})
		sharedChat := dbgen.Chat(t, db, database.Chat{
			OrganizationID:    firstUser.OrganizationID,
			OwnerID:           member.ID,
			LastModelConfigID: modelConfig.ID,
			Title:             "member shared chat",
			Status:            database.ChatStatusWaiting,
		})
		unsharedReadableChat := dbgen.Chat(t, db, database.Chat{
			OrganizationID:    firstUser.OrganizationID,
			OwnerID:           firstUser.UserID,
			LastModelConfigID: modelConfig.ID,
			Title:             "unshared readable chat",
			Status:            database.ChatStatusWaiting,
		})

		err := db.UpdateChatACLByID(dbauthz.As(ctx, rbac.Subject{
			ID:    member.ID.String(),
			Roles: rbac.RoleIdentifiers{rbac.RoleOwner()},
			Scope: rbac.ScopeAll,
		}), database.UpdateChatACLByIDParams{
			ID: sharedChat.ID,
			UserACL: database.ChatACL{
				owner.ID.String(): database.ChatACLEntry{Permissions: []policy.Action{policy.ActionRead}},
			},
			GroupACL: database.ChatACL{},
		})
		require.NoError(t, err)

		ownerChats, err := ownerClient.ListChats(ctx, &codersdk.ListChatsOptions{
			Query: "source:created_by_me,shared_with_me",
		})
		require.NoError(t, err)

		ownerChatIDs := make(map[uuid.UUID]struct{}, len(ownerChats))
		for _, chat := range ownerChats {
			ownerChatIDs[chat.ID] = struct{}{}
		}
		require.Contains(t, ownerChatIDs, ownedChat.ID)
		require.Contains(t, ownerChatIDs, sharedChat.ID)
		require.NotContains(t, ownerChatIDs, unsharedReadableChat.ID)

		sharedOnlyChats, err := ownerClient.ListChats(ctx, &codersdk.ListChatsOptions{
			Source: codersdk.ChatListSourceSharedWithMe,
		})
		require.NoError(t, err)
		sharedOnlyChatIDs := make(map[uuid.UUID]struct{}, len(sharedOnlyChats))
		for _, chat := range sharedOnlyChats {
			sharedOnlyChatIDs[chat.ID] = struct{}{}
		}
		require.Contains(t, sharedOnlyChatIDs, sharedChat.ID)
		require.NotContains(t, sharedOnlyChatIDs, ownedChat.ID)
		require.NotContains(t, sharedOnlyChatIDs, unsharedReadableChat.ID)

		memberChats, err := memberClient.ListChats(ctx, &codersdk.ListChatsOptions{
			Query: "source:created_by_me,shared_with_me",
		})
		require.NoError(t, err)
		memberChatIDs := make(map[uuid.UUID]struct{}, len(memberChats))
		for _, chat := range memberChats {
			memberChatIDs[chat.ID] = struct{}{}
		}
		require.Contains(t, memberChatIDs, sharedChat.ID)
		require.NotContains(t, memberChatIDs, ownedChat.ID)
		require.NotContains(t, memberChatIDs, unsharedReadableChat.ID)
	})

	t.Run("OrgMemberCanAccessOwnChats", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
		memberClient := codersdk.NewExperimentalClient(memberClientRaw)
		chat := dbgen.Chat(t, db, database.Chat{
			OrganizationID:    firstUser.OrganizationID,
			OwnerID:           member.ID,
			LastModelConfigID: modelConfig.ID,
			Title:             "member chat",
		})

		chats, err := memberClient.ListChats(ctx, nil)
		require.NoError(t, err)
		require.Len(t, chats, 1)
		require.Equal(t, chat.ID, chats[0].ID)

		err = memberClient.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{
			Title: ptr.Ref("new title"),
		})
		require.NoError(t, err)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		_ = coderdtest.CreateFirstUser(t, client.Client)

		unauthenticatedClient := codersdk.NewExperimentalClient(codersdk.New(client.URL))
		_, err := unauthenticatedClient.ListChats(ctx, nil)
		requireSDKError(t, err, http.StatusUnauthorized)
	})
	t.Run("Pagination", func(t *testing.T) {
		t.Parallel()

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		// Insert chats with a terminal status so the chatd
		// processor never acquires them and never bumps
		// updated_at. The GetChats cursor subquery re-reads the
		// cursor row's updated_at, so a concurrent bump would
		// shift the cursor position between page requests.
		const totalChats = 5
		createdChatIDs := make([]uuid.UUID, 0, totalChats)
		for i := range totalChats {
			dbChat := dbgen.Chat(t, db, database.Chat{
				OrganizationID:    firstUser.OrganizationID,
				OwnerID:           firstUser.UserID,
				LastModelConfigID: modelConfig.ID,
				Title:             fmt.Sprintf("chat-%d", i),
				Status:            database.ChatStatusWaiting,
			})
			createdChatIDs = append(createdChatIDs, dbChat.ID)
		}

		// Fetch first page with limit=2.
		page1, err := client.ListChats(ctx, &codersdk.ListChatsOptions{
			Pagination: codersdk.Pagination{Limit: 2},
		})
		require.NoError(t, err)
		require.Len(t, page1, 2)

		// Fetch second page using after_id from last item of page 1.
		page2, err := client.ListChats(ctx, &codersdk.ListChatsOptions{
			Pagination: codersdk.Pagination{
				AfterID: uuid.MustParse(page1[len(page1)-1].ID.String()),
				Limit:   2,
			},
		})
		require.NoError(t, err)
		require.Len(t, page2, 2)

		// Ensure page1 and page2 have no overlap.
		page1IDs := make(map[uuid.UUID]struct{})
		for _, c := range page1 {
			page1IDs[c.ID] = struct{}{}
		}
		for _, c := range page2 {
			_, overlap := page1IDs[c.ID]
			require.False(t, overlap, "page2 should not contain items from page1")
		}

		// Fetch third page — should have 1 remaining chat.
		page3, err := client.ListChats(ctx, &codersdk.ListChatsOptions{
			Pagination: codersdk.Pagination{
				AfterID: uuid.MustParse(page2[len(page2)-1].ID.String()),
				Limit:   2,
			},
		})
		require.NoError(t, err)
		require.Len(t, page3, 1)

		// All 5 chats should be accounted for.
		allIDs := make(map[uuid.UUID]struct{})
		for _, c := range append(append(page1, page2...), page3...) {
			allIDs[c.ID] = struct{}{}
		}
		for _, id := range createdChatIDs {
			_, found := allIDs[id]
			require.True(t, found, "chat %s should appear in paginated results", id)
		}

		// Fetch with offset=3, limit=2 — should return 2 chats.
		offsetPage, err := client.ListChats(ctx, &codersdk.ListChatsOptions{
			Pagination: codersdk.Pagination{Offset: 3, Limit: 2},
		})
		require.NoError(t, err)
		require.Len(t, offsetPage, 2)

		// No limit should return all chats.
		allChats, err := client.ListChats(ctx, nil)
		require.NoError(t, err)
		require.Len(t, allChats, totalChats)
	})

	// Test that a pinned chat with an old updated_at appears on page 1.
	t.Run("PinnedOnFirstPage", func(t *testing.T) {
		t.Parallel()

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		// Insert chats directly with a terminal status: see
		// the Pagination subtest for the cursor-race rationale.
		// Direct insertion also avoids spawning 51 background
		// chat processors, which causes timeouts under -race.
		pinnedDBChat := dbgen.Chat(t, db, database.Chat{
			OrganizationID:    firstUser.OrganizationID,
			OwnerID:           firstUser.UserID,
			LastModelConfigID: modelConfig.ID,
			Title:             "pinned-chat",
			Status:            database.ChatStatusWaiting,
		})

		// Fill page 1 with newer chats so the pinned chat
		// would normally be pushed off the first page
		// (default limit 50).
		const fillerCount = 51
		for i := range fillerCount {
			_ = dbgen.Chat(t, db, database.Chat{
				OrganizationID:    firstUser.OrganizationID,
				OwnerID:           firstUser.UserID,
				LastModelConfigID: modelConfig.ID,
				Title:             fmt.Sprintf("filler-%d", i),
				Status:            database.ChatStatusWaiting,
			})
		}

		// Pin the earliest chat.
		err := client.UpdateChat(ctx, pinnedDBChat.ID, codersdk.UpdateChatRequest{
			PinOrder: ptr.Ref(int32(1)),
		})
		require.NoError(t, err)

		// Fetch page 1 with default limit (50).
		page1, err := client.ListChats(ctx, &codersdk.ListChatsOptions{
			Pagination: codersdk.Pagination{Limit: 50},
		})
		require.NoError(t, err)

		// The pinned chat must appear on page 1.
		page1IDs := make(map[uuid.UUID]struct{}, len(page1))
		for _, c := range page1 {
			page1IDs[c.ID] = struct{}{}
		}
		_, found := page1IDs[pinnedDBChat.ID]
		require.True(t, found, "pinned chat should appear on page 1")

		// The pinned chat should be the first item in the list.
		require.Equal(t, pinnedDBChat.ID, page1[0].ID, "pinned chat should be first")
	})

	// Test cursor pagination with a mix of pinned and unpinned chats.
	t.Run("CursorWithPins", func(t *testing.T) {
		t.Parallel()

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		// Insert chats directly with a terminal status: see
		// the Pagination subtest for the cursor-race rationale.
		const totalChats = 5
		createdChatIDs := make([]uuid.UUID, 0, totalChats)
		for i := range totalChats {
			dbChat := dbgen.Chat(t, db, database.Chat{
				OrganizationID:    firstUser.OrganizationID,
				OwnerID:           firstUser.UserID,
				LastModelConfigID: modelConfig.ID,
				Title:             fmt.Sprintf("cursor-pin-chat-%d", i),
				Status:            database.ChatStatusWaiting,
			})
			createdChatIDs = append(createdChatIDs, dbChat.ID)
		}

		// Pin the first two chats (oldest updated_at).
		// PinChatByID and UpdateChatPinOrder do not touch
		// updated_at, so the cursor ordering stays stable.
		err := client.UpdateChat(ctx, createdChatIDs[0], codersdk.UpdateChatRequest{
			PinOrder: ptr.Ref(int32(1)),
		})
		require.NoError(t, err)
		err = client.UpdateChat(ctx, createdChatIDs[1], codersdk.UpdateChatRequest{
			PinOrder: ptr.Ref(int32(1)),
		})
		require.NoError(t, err)

		// Paginate with limit=2 using cursor (after_id).
		const pageSize = 2
		maxPages := totalChats/pageSize + 2
		var allPaginated []codersdk.Chat
		var afterID uuid.UUID
		for range maxPages {
			opts := &codersdk.ListChatsOptions{
				Pagination: codersdk.Pagination{Limit: pageSize},
			}
			if afterID != uuid.Nil {
				opts.AfterID = afterID
			}
			page, listErr := client.ListChats(ctx, opts)
			require.NoError(t, listErr)
			if len(page) == 0 {
				break
			}
			allPaginated = append(allPaginated, page...)
			afterID = page[len(page)-1].ID
		}

		// All chats should appear exactly once.
		seenIDs := make(map[uuid.UUID]struct{}, len(allPaginated))
		for _, c := range allPaginated {
			_, dup := seenIDs[c.ID]
			require.False(t, dup, "chat %s appeared more than once", c.ID)
			seenIDs[c.ID] = struct{}{}
		}
		require.Len(t, seenIDs, totalChats, "all chats should appear in paginated results")

		// Pinned chats should come before unpinned ones, and
		// within the pinned group, lower pin_order sorts first.
		pinnedSeen := false
		unpinnedSeen := false
		for _, c := range allPaginated {
			if c.PinOrder > 0 {
				require.False(t, unpinnedSeen, "pinned chat %s appeared after unpinned chat", c.ID)
				pinnedSeen = true
			} else {
				unpinnedSeen = true
			}
		}
		require.True(t, pinnedSeen, "at least one pinned chat should exist")

		// Verify within-pinned ordering: pin_order=1 before
		// pin_order=2 (the -pin_order DESC column).
		require.Equal(t, createdChatIDs[0], allPaginated[0].ID,
			"pin_order=1 chat should be first")
		require.Equal(t, createdChatIDs[1], allPaginated[1].ID,
			"pin_order=2 chat should be second")
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		user := coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		// Create a parent chat via the API.
		parentChat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: user.OrganizationID,
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "root chat with children",
				},
			},
		})
		require.NoError(t, err)

		// Insert child chats directly via the database.
		child1 := dbgen.Chat(t, db, database.Chat{
			OrganizationID:    user.OrganizationID,
			OwnerID:           user.UserID,
			LastModelConfigID: modelConfig.ID,
			Title:             "child one",
			ParentChatID:      uuid.NullUUID{UUID: parentChat.ID, Valid: true},
			RootChatID:        uuid.NullUUID{UUID: parentChat.ID, Valid: true},
		})

		child2 := dbgen.Chat(t, db, database.Chat{
			OrganizationID:    user.OrganizationID,
			OwnerID:           user.UserID,
			LastModelConfigID: modelConfig.ID,
			Title:             "child two",
			ParentChatID:      uuid.NullUUID{UUID: parentChat.ID, Valid: true},
			RootChatID:        uuid.NullUUID{UUID: parentChat.ID, Valid: true},
		})

		// Also create a standalone root chat to verify it still appears.
		standalone, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: user.OrganizationID,
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "standalone root chat",
				},
			},
		})
		require.NoError(t, err)

		chats, err := client.ListChats(ctx, nil)
		require.NoError(t, err)

		// Only root chats should appear at the top level.
		rootIDs := make(map[uuid.UUID]struct{}, len(chats))
		for _, c := range chats {
			rootIDs[c.ID] = struct{}{}
			require.Nil(t, c.ParentChatID, "top-level entry should have no parent")
		}
		require.Contains(t, rootIDs, parentChat.ID)
		require.Contains(t, rootIDs, standalone.ID)
		require.NotContains(t, rootIDs, child1.ID, "child1 should not appear at top level")
		require.NotContains(t, rootIDs, child2.ID, "child2 should not appear at top level")

		// Find the parent in the list and verify children are embedded.
		var parent codersdk.Chat
		for _, c := range chats {
			if c.ID == parentChat.ID {
				parent = c
				break
			}
		}
		require.Len(t, parent.Children, 2, "parent should embed 2 children")

		// Children are ordered by created_at DESC (newest first).
		childIDs := []uuid.UUID{parent.Children[0].ID, parent.Children[1].ID}
		require.Equal(t, child2.ID, childIDs[0])
		require.Equal(t, child1.ID, childIDs[1])

		// Verify each child has correct parent/root references.
		for _, child := range parent.Children {
			require.NotNil(t, child.ParentChatID)
			require.Equal(t, parentChat.ID, *child.ParentChatID)
			require.NotNil(t, child.RootChatID)
			require.Equal(t, parentChat.ID, *child.RootChatID)
		}

		// Standalone root chat should have an empty children slice.
		for _, c := range chats {
			if c.ID == standalone.ID {
				require.NotNil(t, c.Children)
				require.Empty(t, c.Children)
				break
			}
		}
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		user := coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		// Create 3 root chats, each with 2 children.
		for i := range 3 {
			parent, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
				OrganizationID: user.OrganizationID,
				Content: []codersdk.ChatInputPart{
					{
						Type: codersdk.ChatInputPartTypeText,
						Text: fmt.Sprintf("parent %d", i),
					},
				},
			})
			require.NoError(t, err)
			for j := range 2 {
				_ = dbgen.Chat(t, db, database.Chat{
					OrganizationID:    user.OrganizationID,
					OwnerID:           user.UserID,
					LastModelConfigID: modelConfig.ID,
					Title:             fmt.Sprintf("child %d-%d", i, j),
					ParentChatID:      uuid.NullUUID{UUID: parent.ID, Valid: true},
					RootChatID:        uuid.NullUUID{UUID: parent.ID, Valid: true},
				})
			}
		}

		// Request with limit=2: should get 2 root chats (not 2 of
		// the 9 total chats). Each root should have its children.
		chats, err := client.ListChats(ctx, &codersdk.ListChatsOptions{
			Pagination: codersdk.Pagination{Limit: 2},
		})
		require.NoError(t, err)
		require.Len(t, chats, 2, "limit should apply to root chats only")
		for _, c := range chats {
			require.Nil(t, c.ParentChatID)
			require.Len(t, c.Children, 2, "each root should embed its 2 children")
		}
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		user := coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		// Helper that creates a chat (root or child) with a diff status URL.
		create := func(title, url string, parentID uuid.NullUUID) database.Chat {
			rootID := parentID
			chat := dbgen.Chat(t, db, database.Chat{
				OrganizationID:    user.OrganizationID,
				OwnerID:           user.UserID,
				LastModelConfigID: modelConfig.ID,
				Title:             title,
				ParentChatID:      parentID,
				RootChatID:        rootID,
			})
			if url != "" {
				staleAt := time.Now().UTC().Add(time.Hour).Truncate(time.Second)
				_, err := db.UpsertChatDiffStatusReference(
					dbauthz.AsSystemRestricted(ctx),
					database.UpsertChatDiffStatusReferenceParams{
						ChatID:          chat.ID,
						Url:             sql.NullString{String: url, Valid: true},
						GitBranch:       "feature/test",
						GitRemoteOrigin: "git@github.com:coder/coder.git",
						StaleAt:         staleAt,
					},
				)
				require.NoError(t, err)
			}
			return chat
		}

		// Root chat directly linked to the target PR.
		rootWithPR := create("root with pr", "https://github.com/coder/coder/pull/1", uuid.NullUUID{})

		// Root chat whose sub-agent owns the PR. The filter should still
		// surface the parent because the URL lives on a descendant.
		rootWithChildPR := create("root with child pr", "", uuid.NullUUID{})
		_ = create(
			"sub-agent with pr",
			"https://github.com/coder/coder/pull/2",
			uuid.NullUUID{UUID: rootWithChildPR.ID, Valid: true},
		)

		// Root chat with an unrelated PR; should not match either filter.
		_ = create("unrelated pr", "https://github.com/coder/coder/pull/999", uuid.NullUUID{})

		// Root chat with no diff status at all.
		_ = create("no diff", "", uuid.NullUUID{})

		// Archived root chat that points at the same URL as `rootWithPR`.
		// Used to verify the archived filter and the diff_url filter
		// compose at the SQL layer rather than ignoring each other.
		archivedWithPR := create(
			"archived with pr",
			"https://github.com/coder/coder/pull/3",
			uuid.NullUUID{},
		)
		require.NoError(t, client.UpdateChat(ctx, archivedWithPR.ID, codersdk.UpdateChatRequest{
			Archived: ptr.Ref(true),
		}))

		t.Run("MatchesRoot", func(t *testing.T) {
			chats, err := client.ListChats(ctx, &codersdk.ListChatsOptions{
				Query: `diff_url:"https://github.com/coder/coder/pull/1"`,
			})
			require.NoError(t, err)
			require.Len(t, chats, 1)
			require.Equal(t, rootWithPR.ID, chats[0].ID)
		})

		t.Run("MatchesViaSubAgent", func(t *testing.T) {
			chats, err := client.ListChats(ctx, &codersdk.ListChatsOptions{
				Query: `diff_url:"https://github.com/coder/coder/pull/2"`,
			})
			require.NoError(t, err)
			require.Len(t, chats, 1, "root chat should surface even when only a child has the PR")
			require.Equal(t, rootWithChildPR.ID, chats[0].ID)
		})

		t.Run("CaseInsensitive", func(t *testing.T) {
			chats, err := client.ListChats(ctx, &codersdk.ListChatsOptions{
				Query: `diff_url:"HTTPS://GITHUB.COM/CODER/CODER/PULL/1"`,
			})
			require.NoError(t, err)
			require.Len(t, chats, 1)
			require.Equal(t, rootWithPR.ID, chats[0].ID)
		})

		t.Run("NoMatch", func(t *testing.T) {
			chats, err := client.ListChats(ctx, &codersdk.ListChatsOptions{
				Query: `diff_url:"https://github.com/coder/coder/pull/424242"`,
			})
			require.NoError(t, err)
			require.Empty(t, chats)
		})

		t.Run("InvalidURL", func(t *testing.T) {
			_, err := client.ListChats(ctx, &codersdk.ListChatsOptions{
				Query: `diff_url:"ftp://example.com/x"`,
			})
			sdkErr := requireSDKError(t, err, http.StatusBadRequest)
			require.NotEmpty(t, sdkErr.Validations, "expected validation error")
			require.Equal(t, "diff_url", sdkErr.Validations[0].Field)
		})

		t.Run("ArchivedFilteredOut", func(t *testing.T) {
			// Default archived filter is false, so an archived chat with
			// a matching diff URL must not surface.
			chats, err := client.ListChats(ctx, &codersdk.ListChatsOptions{
				Query: `diff_url:"https://github.com/coder/coder/pull/3"`,
			})
			require.NoError(t, err)
			require.Empty(t, chats, "archived chat must not match the default filter")
		})

		t.Run("ArchivedTrueComposes", func(t *testing.T) {
			chats, err := client.ListChats(ctx, &codersdk.ListChatsOptions{
				Query: `archived:true diff_url:"https://github.com/coder/coder/pull/3"`,
			})
			require.NoError(t, err)
			require.Len(t, chats, 1)
			require.Equal(t, archivedWithPR.ID, chats[0].ID)
		})
	})
}

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

	providerByID := func(t *testing.T, response codersdk.OrganizationChatModelsResponse, id uuid.UUID) codersdk.ChatModelProviderDescriptor {
		t.Helper()
		for _, provider := range response.Providers {
			if provider.ID == id {
				return provider
			}
		}
		t.Fatalf("provider %s not found", id)
		return codersdk.ChatModelProviderDescriptor{}
	}
	containsModel := func(response codersdk.OrganizationChatModelsResponse, id uuid.UUID) bool {
		return slices.ContainsFunc(response.Models, func(model codersdk.ChatModel) bool { return model.ID == id })
	}

	t.Run("Success", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		model := createChatModel(t, client)
		response, err := client.ChatModels(ctx, firstUser.OrganizationID)
		require.NoError(t, err)
		require.True(t, containsModel(response, model.ID))
		require.True(t, providerByID(t, response, model.AIProviderID).Available)
	})

	t.Run("DeniedSameOrganizationEnabledModelIsHidden", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		provider := createAIProviderForTest(t, client, "openai-compat", "test-api-key")
		privateConfig := dbgen.ChatModelConfig(t, db, database.ChatModelConfig{
			AIProviderID: uuid.NullUUID{UUID: provider.ID, Valid: true}, OrganizationID: firstUser.OrganizationID,
			Model: "private-" + uuid.NewString(), Enabled: true, GroupACL: database.ChatACL{},
		})
		memberRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
		response, err := codersdk.NewExperimentalClient(memberRaw).ChatModels(ctx, firstUser.OrganizationID)
		require.NoError(t, err)
		require.False(t, containsModel(response, privateConfig.ID))
	})

	t.Run("NonDefaultOrgIncludesOnlyLocalModels", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		_ = coderdtest.CreateFirstUser(t, client.Client)
		provider := createAIProviderForTest(t, client, "openai-compat", "test-api-key")
		defaultConfig := createChatModel(t, client)
		chatOrg := dbgen.Organization(t, db, database.Organization{IsDefault: false})
		thirdOrg := dbgen.Organization(t, db, database.Organization{IsDefault: false})
		contextLimit := int64(4096)
		local, err := client.CreateChatModel(ctx, chatOrg.ID, codersdk.CreateChatModelRequest{AIProviderID: &provider.ID, Model: "local-" + uuid.NewString(), ContextLimit: &contextLimit})
		require.NoError(t, err)
		third, err := client.CreateChatModel(ctx, thirdOrg.ID, codersdk.CreateChatModelRequest{AIProviderID: &provider.ID, Model: "third-" + uuid.NewString(), ContextLimit: &contextLimit})
		require.NoError(t, err)
		memberRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, chatOrg.ID)
		response, err := codersdk.NewExperimentalClient(memberRaw).ChatModels(ctx, chatOrg.ID)
		require.NoError(t, err)
		require.True(t, containsModel(response, local.ID))
		require.False(t, containsModel(response, defaultConfig.ID))
		require.False(t, containsModel(response, third.ID))
	})

	t.Run("Unauthenticated", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_, err := codersdk.NewExperimentalClient(codersdk.New(client.URL)).ChatModels(ctx, firstUser.OrganizationID)
		requireSDKError(t, err, http.StatusUnauthorized)
	})

	t.Run("CopilotOnlyUnsupported", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		provider := createAIProviderForTest(t, client, string(codersdk.AIProviderTypeCopilot), "")
		response, err := client.ChatModels(ctx, firstUser.OrganizationID)
		require.NoError(t, err)
		require.False(t, providerByID(t, response, provider.ID).Available)
		require.Equal(t, []codersdk.ChatUnsupportedProvider{{Provider: "copilot", DisplayName: "GitHub Copilot"}}, response.UnsupportedProviders)
	})

	t.Run("SupportedProviderHasNoUnsupportedEntry", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)
		response, err := client.ChatModels(ctx, firstUser.OrganizationID)
		require.NoError(t, err)
		require.Empty(t, response.UnsupportedProviders)
	})

	t.Run("CentralOnlyProviderAvailable", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		model := createChatModel(t, client)
		response, err := client.ChatModels(ctx, firstUser.OrganizationID)
		require.NoError(t, err)
		require.True(t, providerByID(t, response, model.AIProviderID).Available)
	})

	t.Run("UserOnlyProviderRequiresUserKey", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		provider := createAIProviderForTest(t, client, "anthropic", "")
		response, err := client.ChatModels(ctx, firstUser.OrganizationID)
		require.NoError(t, err)
		descriptor := providerByID(t, response, provider.ID)
		require.False(t, descriptor.Available)
		require.Equal(t, codersdk.ChatModelProviderUnavailableReasonUserAPIKeyRequired, descriptor.UnavailableReason)
		_, err = client.UpsertUserAIProviderKey(ctx, "me", provider.ID, codersdk.CreateUserAIProviderKeyRequest{APIKey: "user-key"})
		require.NoError(t, err)
		response, err = client.ChatModels(ctx, firstUser.OrganizationID)
		require.NoError(t, err)
		require.True(t, providerByID(t, response, provider.ID).Available)
	})

	t.Run("CentralAndUserWithFallback", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		provider := createAIProviderForTest(t, client, "google", "provider-key")
		response, err := client.ChatModels(ctx, firstUser.OrganizationID)
		require.NoError(t, err)
		require.True(t, providerByID(t, response, provider.ID).Available)
		_, err = client.UpsertUserAIProviderKey(ctx, "me", provider.ID, codersdk.CreateUserAIProviderKeyRequest{APIKey: "user-key"})
		require.NoError(t, err)
		response, err = client.ChatModels(ctx, firstUser.OrganizationID)
		require.NoError(t, err)
		require.True(t, providerByID(t, response, provider.ID).Available)
	})

	t.Run("SameTypeProvidersUseExactCredentialStatus", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		availableProvider := createAIProviderForTest(t, client, "openai", "available-key")
		unavailableProvider := createAIProviderForTest(t, client, "openai", "")
		response, err := client.ChatModels(ctx, firstUser.OrganizationID)
		require.NoError(t, err)
		require.True(t, providerByID(t, response, availableProvider.ID).Available)
		unavailable := providerByID(t, response, unavailableProvider.ID)
		require.False(t, unavailable.Available)
		require.NotEmpty(t, unavailable.UnavailableReason)
	})

	t.Run("UserCredentialsAreCallerIsolated", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		provider := createAIProviderForTest(t, client, "anthropic", "")
		otherRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
		other := codersdk.NewExperimentalClient(otherRaw)
		_, err := client.UpsertUserAIProviderKey(ctx, "me", provider.ID, codersdk.CreateUserAIProviderKeyRequest{APIKey: "owner-key"})
		require.NoError(t, err)
		ownerResponse, err := client.ChatModels(ctx, firstUser.OrganizationID)
		require.NoError(t, err)
		otherResponse, err := other.ChatModels(ctx, firstUser.OrganizationID)
		require.NoError(t, err)
		require.True(t, providerByID(t, ownerResponse, provider.ID).Available)
		require.True(t, providerByID(t, ownerResponse, provider.ID).HasUserAPIKey)
		require.False(t, providerByID(t, otherResponse, provider.ID).Available)
		require.False(t, providerByID(t, otherResponse, provider.ID).HasUserAPIKey)
	})

	t.Run("IncludesOnlyDefaultOrganizationModels", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		provider := createAIProviderForTest(t, client, "openai-compat", "test-api-key")
		contextLimit := int64(4096)
		defaultConfig, err := client.CreateChatModel(ctx, firstUser.OrganizationID, codersdk.CreateChatModelRequest{AIProviderID: &provider.ID, Model: "default-" + uuid.NewString(), ContextLimit: &contextLimit})
		require.NoError(t, err)
		otherOrg := dbgen.Organization(t, db, database.Organization{IsDefault: false})
		other := dbgen.ChatModelConfig(t, db, database.ChatModelConfig{AIProviderID: uuid.NullUUID{UUID: provider.ID, Valid: true}, Model: "other-" + uuid.NewString(), OrganizationID: otherOrg.ID})
		response, err := client.ChatModels(ctx, firstUser.OrganizationID)
		require.NoError(t, err)
		require.True(t, containsModel(response, defaultConfig.ID))
		require.False(t, containsModel(response, other.ID))
	})

	t.Run("DisabledProvidersAndModelsRemainVisible", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		provider := createAIProviderForTest(t, client, "openai", "test-key")
		contextLimit := int64(4096)
		enabledModel, err := client.CreateChatModel(ctx, firstUser.OrganizationID, codersdk.CreateChatModelRequest{AIProviderID: &provider.ID, Model: "enabled-" + uuid.NewString(), ContextLimit: &contextLimit})
		require.NoError(t, err)
		disabledModel, err := client.CreateChatModel(ctx, firstUser.OrganizationID, codersdk.CreateChatModelRequest{AIProviderID: &provider.ID, Model: "disabled-" + uuid.NewString(), ContextLimit: &contextLimit})
		require.NoError(t, err)
		_, err = client.UpdateChatModel(ctx, firstUser.OrganizationID, disabledModel.ID, codersdk.UpdateChatModelRequest{Enabled: ptr.Ref(false)})
		require.NoError(t, err)
		response, err := client.ChatModels(ctx, firstUser.OrganizationID)
		require.NoError(t, err)
		require.True(t, containsModel(response, enabledModel.ID))
		require.True(t, slices.ContainsFunc(response.Models, func(model codersdk.ChatModel) bool {
			return model.ID == disabledModel.ID && !model.Enabled
		}))
		_, err = client.UpdateAIProvider(ctx, provider.ID.String(), codersdk.UpdateAIProviderRequest{Enabled: ptr.Ref(false)})
		require.NoError(t, err)
		response, err = client.ChatModels(ctx, firstUser.OrganizationID)
		require.NoError(t, err)
		require.True(t, containsModel(response, enabledModel.ID))
		require.True(t, containsModel(response, disabledModel.ID))
		disabledProvider := providerByID(t, response, provider.ID)
		require.False(t, disabledProvider.Enabled)
		require.False(t, disabledProvider.Available)
	})
}

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

	setup := func(t *testing.T) (context.Context, *codersdk.ExperimentalClient, database.Store, codersdk.CreateFirstUserResponse, codersdk.ChatModel) {
		t.Helper()
		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)
		return ctx, client, db, firstUser, modelConfig
	}

	createChat := func(t *testing.T, db database.Store, firstUser codersdk.CreateFirstUserResponse, modelConfigID uuid.UUID, title string) database.Chat {
		t.Helper()
		return dbgen.Chat(t, db, database.Chat{
			OrganizationID:    firstUser.OrganizationID,
			OwnerID:           firstUser.UserID,
			LastModelConfigID: modelConfigID,
			Title:             title,
			Status:            database.ChatStatusWaiting,
		})
	}

	insertMessage := func(t *testing.T, db database.Store, firstUser codersdk.CreateFirstUserResponse, modelConfigID, chatID uuid.UUID, text string) {
		t.Helper()
		content, err := json.Marshal([]map[string]string{{"type": "text", "text": text}})
		require.NoError(t, err)
		dbgen.ChatMessage(t, db, database.ChatMessage{
			ChatID:        chatID,
			CreatedBy:     uuid.NullUUID{UUID: firstUser.UserID, Valid: true},
			ModelConfigID: uuid.NullUUID{UUID: modelConfigID, Valid: true},
			Role:          database.ChatMessageRoleUser,
			Visibility:    database.ChatMessageVisibilityBoth,
			Content:       pqtype.NullRawMessage{RawMessage: content, Valid: true},
		})
	}

	backfillSearchTsv := func(ctx context.Context, t *testing.T, db database.Store) {
		t.Helper()
		_, err := db.BackfillChatMessagesSearchTsv(dbauthz.AsSystemRestricted(ctx), 1000)
		require.NoError(t, err)
	}

	chatIDs := func(chats []codersdk.Chat) map[uuid.UUID]struct{} {
		ids := make(map[uuid.UUID]struct{}, len(chats))
		for _, chat := range chats {
			ids[chat.ID] = struct{}{}
		}
		return ids
	}

	t.Run("MatchesTitleAndMessageBody", func(t *testing.T) {
		t.Parallel()
		ctx, client, db, firstUser, modelConfig := setup(t)

		titleMatch := createChat(t, db, firstUser, modelConfig.ID, "kubernetes upgrade notes")
		bodyMatch := createChat(t, db, firstUser, modelConfig.ID, "plain title")
		insertMessage(t, db, firstUser, modelConfig.ID, bodyMatch.ID, "restart the kubernetes cluster")
		noMatch := createChat(t, db, firstUser, modelConfig.ID, "unrelated chat")
		insertMessage(t, db, firstUser, modelConfig.ID, noMatch.ID, "terraform apply failure")
		backfillSearchTsv(ctx, t, db)

		chats, err := client.ListChats(ctx, &codersdk.ListChatsOptions{
			Query: `search:"kubernetes"`,
		})
		require.NoError(t, err)
		ids := chatIDs(chats)
		require.Contains(t, ids, titleMatch.ID)
		require.Contains(t, ids, bodyMatch.ID)
		require.NotContains(t, ids, noMatch.ID)

		// The 'english' config stems both sides, so an inflected query
		// form matches the stored words.
		chats, err = client.ListChats(ctx, &codersdk.ListChatsOptions{
			Query: `search:"restarting"`,
		})
		require.NoError(t, err)
		require.Contains(t, chatIDs(chats), bodyMatch.ID)
	})

	t.Run("NoSearchableWordsReturnsEmpty", func(t *testing.T) {
		t.Parallel()
		ctx, client, db, firstUser, modelConfig := setup(t)

		// Message search uses the 'english' config, which drops
		// stopwords: search:"or" tokenizes to no lexemes and matches
		// nothing, just like search:"!!!". The title must not contain the
		// searched words because title search keeps the 'simple' config,
		// which retains stopwords. The control query proves the empty
		// results come from tokenization, not missing data.
		control := createChat(t, db, firstUser, modelConfig.ID, "plain title")
		insertMessage(t, db, firstUser, modelConfig.ID, control.ID, "fix this or that")
		backfillSearchTsv(ctx, t, db)

		chats, err := client.ListChats(ctx, &codersdk.ListChatsOptions{
			Query: `search:"fix"`,
		})
		require.NoError(t, err)
		require.Contains(t, chatIDs(chats), control.ID)

		chats, err = client.ListChats(ctx, &codersdk.ListChatsOptions{
			Query: `search:"or"`,
		})
		require.NoError(t, err)
		require.Empty(t, chats)

		chats, err = client.ListChats(ctx, &codersdk.ListChatsOptions{
			Query: `search:"!!!"`,
		})
		require.NoError(t, err)
		require.Empty(t, chats)
	})

	t.Run("ComposesWithRepoFilterAndArchivedDefault", func(t *testing.T) {
		t.Parallel()
		ctx, client, db, firstUser, modelConfig := setup(t)

		linkRepo := func(chatID uuid.UUID, remote string) {
			t.Helper()
			_, err := db.UpsertChatDiffStatusReference(
				dbauthz.AsSystemRestricted(ctx),
				database.UpsertChatDiffStatusReferenceParams{
					ChatID:          chatID,
					GitBranch:       "main",
					GitRemoteOrigin: remote,
					StaleAt:         time.Now().UTC().Add(time.Hour),
				},
			)
			require.NoError(t, err)
		}

		bothMatch := createChat(t, db, firstUser, modelConfig.ID, "kubernetes in coder repo")
		linkRepo(bothMatch.ID, "git@github.com:acme/widget.git")
		searchOnly := createChat(t, db, firstUser, modelConfig.ID, "kubernetes elsewhere")
		linkRepo(searchOnly.ID, "git@github.com:acme/other.git")
		repoOnly := createChat(t, db, firstUser, modelConfig.ID, "plain title")
		linkRepo(repoOnly.ID, "git@github.com:acme/widget.git")
		// Matches via message body, not title, so composition also covers
		// search_tsv.
		bodyMatch := createChat(t, db, firstUser, modelConfig.ID, "quiet title")
		linkRepo(bodyMatch.ID, "git@github.com:acme/widget.git")
		insertMessage(t, db, firstUser, modelConfig.ID, bodyMatch.ID, "kubernetes rollout stuck")
		bodyMatchWrongRepo := createChat(t, db, firstUser, modelConfig.ID, "quiet title two")
		linkRepo(bodyMatchWrongRepo.ID, "git@github.com:acme/other.git")
		insertMessage(t, db, firstUser, modelConfig.ID, bodyMatchWrongRepo.ID, "kubernetes rollout stuck")
		archivedMatch := createChat(t, db, firstUser, modelConfig.ID, "kubernetes archived")
		linkRepo(archivedMatch.ID, "git@github.com:acme/widget.git")
		_, err := db.ArchiveChatByID(dbauthz.AsSystemRestricted(ctx), archivedMatch.ID)
		require.NoError(t, err)
		backfillSearchTsv(ctx, t, db)

		chats, err := client.ListChats(ctx, &codersdk.ListChatsOptions{
			Query: `repo:widget search:"kubernetes"`,
		})
		require.NoError(t, err)
		ids := chatIDs(chats)
		require.Contains(t, ids, bothMatch.ID)
		require.Contains(t, ids, bodyMatch.ID)
		require.NotContains(t, ids, bodyMatchWrongRepo.ID)
		require.NotContains(t, ids, searchOnly.ID)
		require.NotContains(t, ids, repoOnly.ID)
		// Archived chats stay hidden unless archived:true is requested.
		require.NotContains(t, ids, archivedMatch.ID)
	})

	t.Run("NoSearchTermUnchanged", func(t *testing.T) {
		t.Parallel()
		ctx, client, db, firstUser, modelConfig := setup(t)

		chat := createChat(t, db, firstUser, modelConfig.ID, "kubernetes upgrade notes")
		other := createChat(t, db, firstUser, modelConfig.ID, "unrelated chat")

		chats, err := client.ListChats(ctx, nil)
		require.NoError(t, err)
		ids := chatIDs(chats)
		require.Contains(t, ids, chat.ID)
		require.Contains(t, ids, other.ID)
	})

	t.Run("MutualExclusionWithTitleReturns400", func(t *testing.T) {
		t.Parallel()
		ctx, client, _, _, _ := setup(t)

		_, err := client.ListChats(ctx, &codersdk.ListChatsOptions{
			Query: `search:alpha title:beta`,
		})
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Len(t, sdkErr.Validations, 1)
		require.Equal(t, "search", sdkErr.Validations[0].Field)
		require.Contains(t, sdkErr.Validations[0].Detail, `"title"`)
	})
}

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

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)

		conn, err := client.Dial(ctx, "/api/v2/chats/watch", nil)
		require.NoError(t, err)
		defer conn.Close(websocket.StatusNormalClosure, "done")

		createdChat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: firstUser.OrganizationID,
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "watch route created event",
				},
			},
		})
		require.NoError(t, err)

		for {
			var payload codersdk.ChatWatchEvent
			err = wsjson.Read(ctx, conn, &payload)
			require.NoError(t, err)

			if payload.Kind == codersdk.ChatWatchEventKindCreated &&
				payload.Chat.ID == createdChat.ID {
				break
			}
		}
	})
	t.Run("CreatedEventIncludesAllChatFields", func(t *testing.T) {
		t.Parallel()

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		conn, err := client.Dial(ctx, "/api/v2/chats/watch", nil)
		require.NoError(t, err)
		defer conn.Close(websocket.StatusNormalClosure, "done")

		createdChat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: firstUser.OrganizationID,
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "watch route fields completeness test",
				},
			},
		})
		require.NoError(t, err)

		var got codersdk.Chat
		testutil.Eventually(ctx, t, func(_ context.Context) bool {
			var payload codersdk.ChatWatchEvent
			if readErr := wsjson.Read(ctx, conn, &payload); readErr != nil {
				return false
			}
			if payload.Kind == codersdk.ChatWatchEventKindCreated &&
				payload.Chat.ID == createdChat.ID {
				got = payload.Chat
				return true
			}
			return false
		}, testutil.IntervalFast, "expected a created event for chat %s", createdChat.ID)

		require.Equal(t, createdChat.ID, got.ID)
		require.Equal(t, createdChat.OwnerID, got.OwnerID)
		require.Equal(t, modelConfig.ID, got.LastModelConfigID)
		require.Equal(t, createdChat.Title, got.Title)
		// CreateChat inserts new chats in the running state under the
		// chatstate state machine, so the created event carries running.
		require.Equal(t, codersdk.ChatStatusRunning, got.Status)
		require.NotNil(t, got.RootChatID)
		require.Equal(t, createdChat.ID, *got.RootChatID)
		require.NotZero(t, got.CreatedAt)
		require.NotZero(t, got.UpdatedAt)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		rawClient, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{
			DeploymentValues: coderdtest.DeploymentValues(t),
		})
		aibridgedtest.StartTestAIBridgeDaemon(t.Context(), t, api, nil)
		client := codersdk.NewExperimentalClient(rawClient)
		db := api.Database
		chatDaemon := api.ChatDaemon()
		user := coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		// Insert a chat and a diff status row.
		chat := dbgen.Chat(t, db, database.Chat{
			OrganizationID:    user.OrganizationID,
			OwnerID:           user.UserID,
			LastModelConfigID: modelConfig.ID,
			Title:             "diff status watch test",
		})
		refreshedAt := time.Now().UTC().Truncate(time.Second)
		staleAt := refreshedAt.Add(time.Hour)
		_, err := db.UpsertChatDiffStatusReference(
			dbauthz.AsSystemRestricted(ctx),
			database.UpsertChatDiffStatusReferenceParams{
				ChatID:          chat.ID,
				Url:             sql.NullString{String: "https://github.com/coder/coder/pull/99", Valid: true},
				GitBranch:       "feature/test",
				GitRemoteOrigin: "git@github.com:coder/coder.git",
				StaleAt:         staleAt,
			},
		)
		require.NoError(t, err)
		_, err = db.UpsertChatDiffStatus(
			dbauthz.AsSystemRestricted(ctx),
			database.UpsertChatDiffStatusParams{
				ChatID:           chat.ID,
				Url:              sql.NullString{String: "https://github.com/coder/coder/pull/99", Valid: true},
				GitBranch:        "feature/test",
				GitRemoteOrigin:  "git@github.com:coder/coder.git",
				PullRequestState: sql.NullString{String: "open", Valid: true},
				Additions:        42,
				Deletions:        7,
				ChangedFiles:     5,
				RefreshedAt:      refreshedAt,
				StaleAt:          staleAt,
			},
		)
		require.NoError(t, err)

		// Open the watch WebSocket.
		conn, err := client.Dial(ctx, "/api/v2/chats/watch", nil)
		require.NoError(t, err)
		defer conn.Close(websocket.StatusNormalClosure, "done")

		err = chatDaemon.PublishDiffStatusChange(dbauthz.AsChatd(ctx), chat.ID, codersdk.DiffStatusRef{
			RemoteOrigin: "git@github.com:coder/coder.git",
			GitBranch:    "feature/test",
		})
		require.NoError(t, err)

		var received codersdk.ChatWatchEvent
		for {
			err = wsjson.Read(ctx, conn, &received)
			require.NoError(t, err)

			if received.Kind == codersdk.ChatWatchEventKindDiffStatusChange &&
				received.Chat.ID == chat.ID {
				break
			}
		}

		// Verify the event carries the full DiffStatus.
		require.NotNil(t, received.Chat.DiffStatus, "diff_status_change event must include DiffStatus")
		ds := received.Chat.DiffStatus
		require.Equal(t, chat.ID, ds.ChatID)
		require.NotNil(t, ds.URL)
		require.Equal(t, "https://github.com/coder/coder/pull/99", *ds.URL)
		require.NotNil(t, ds.PullRequestState)
		require.Equal(t, "open", *ds.PullRequestState)
		require.EqualValues(t, 42, ds.Additions)
		require.EqualValues(t, 7, ds.Deletions)
		require.EqualValues(t, 5, ds.ChangedFiles)

		require.NotNil(t, received.ChangedDiffStatus)
		require.Equal(t, "git@github.com:coder/coder.git", received.ChangedDiffStatus.Ref.RemoteOrigin)
		require.Equal(t, "feature/test", received.ChangedDiffStatus.Ref.GitBranch)
		require.NotNil(t, received.ChangedDiffStatus.Status)
		require.NotNil(t, received.ChangedDiffStatus.Status.URL)
		require.Equal(
			t,
			"https://github.com/coder/coder/pull/99",
			*received.ChangedDiffStatus.Status.URL,
		)
		require.NotNil(t, received.ChangedDiffStatus.Status.PullRequestState)
		require.Equal(
			t,
			"open",
			*received.ChangedDiffStatus.Status.PullRequestState,
		)
		require.EqualValues(t, 42, received.ChangedDiffStatus.Status.Additions)
		require.EqualValues(t, 7, received.ChangedDiffStatus.Status.Deletions)
		require.EqualValues(t, 5, received.ChangedDiffStatus.Status.ChangedFiles)
	})
	t.Run("ArchiveAndUnarchiveEmitEventsForDescendants", func(t *testing.T) {
		t.Parallel()

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db, api := newChatClientWithAPIAndDatabase(t)
		user := coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		parentChat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: user.OrganizationID,
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "watch root chat",
				},
			},
		})
		require.NoError(t, err)

		// The parent chat is created via the API, so the chat worker moves
		// it to running. Archiving is only allowed from a terminal state,
		// so wait for it to settle before archiving below.
		coderdtest.WaitForChatSettled(ctx, t, api, parentChat.ID)

		childOne := dbgen.Chat(t, db, database.Chat{
			OrganizationID:    user.OrganizationID,
			OwnerID:           user.UserID,
			LastModelConfigID: modelConfig.ID,
			Title:             "watch child 1",
			ParentChatID:      uuid.NullUUID{UUID: parentChat.ID, Valid: true},
			RootChatID:        uuid.NullUUID{UUID: parentChat.ID, Valid: true},
		})

		childTwo := dbgen.Chat(t, db, database.Chat{
			OrganizationID:    user.OrganizationID,
			OwnerID:           user.UserID,
			LastModelConfigID: modelConfig.ID,
			Title:             "watch child 2",
			ParentChatID:      uuid.NullUUID{UUID: parentChat.ID, Valid: true},
			RootChatID:        uuid.NullUUID{UUID: parentChat.ID, Valid: true},
		})

		conn, err := client.Dial(ctx, "/api/v2/chats/watch", nil)
		require.NoError(t, err)
		defer conn.Close(websocket.StatusNormalClosure, "done")

		collectLifecycleEvents := func(expectedKind codersdk.ChatWatchEventKind) map[uuid.UUID]codersdk.ChatWatchEvent {
			t.Helper()

			events := make(map[uuid.UUID]codersdk.ChatWatchEvent, 3)
			for len(events) < 3 {
				var payload codersdk.ChatWatchEvent
				err = wsjson.Read(ctx, conn, &payload)
				require.NoError(t, err)
				if payload.Kind != expectedKind {
					continue
				}
				events[payload.Chat.ID] = payload
			}
			return events
		}

		assertLifecycleEvents := func(events map[uuid.UUID]codersdk.ChatWatchEvent, archived bool) {
			t.Helper()

			require.Len(t, events, 3)
			for _, chatID := range []uuid.UUID{parentChat.ID, childOne.ID, childTwo.ID} {
				payload, ok := events[chatID]
				require.True(t, ok, "missing event for chat %s", chatID)
				require.Equal(t, archived, payload.Chat.Archived)
			}
		}

		err = client.UpdateChat(ctx, parentChat.ID, codersdk.UpdateChatRequest{Archived: ptr.Ref(true)})
		require.NoError(t, err)
		deletedEvents := collectLifecycleEvents(codersdk.ChatWatchEventKindDeleted)
		assertLifecycleEvents(deletedEvents, true)

		err = client.UpdateChat(ctx, parentChat.ID, codersdk.UpdateChatRequest{Archived: ptr.Ref(false)})
		require.NoError(t, err)
		createdEvents := collectLifecycleEvents(codersdk.ChatWatchEventKindCreated)
		assertLifecycleEvents(createdEvents, false)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		_ = coderdtest.CreateFirstUser(t, client.Client)

		unauthenticatedClient := codersdk.New(client.URL)
		res, err := unauthenticatedClient.Request(
			ctx,
			http.MethodGet,
			"/api/v2/chats/watch",
			nil,
		)
		require.NoError(t, err)
		defer res.Body.Close()
		require.Equal(t, http.StatusUnauthorized, res.StatusCode)
	})
}

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

	createOpenAIProvider := func(t *testing.T, client *codersdk.ExperimentalClient, name string, enabled bool, apiKeys ...string) codersdk.AIProvider {
		t.Helper()

		provider, err := client.CreateAIProvider(testutil.Context(t, testutil.WaitLong), codersdk.CreateAIProviderRequest{
			Type:    codersdk.AIProviderTypeOpenAI,
			Name:    name,
			Enabled: enabled,
			BaseURL: "https://api.openai.example.com/v1",
			APIKeys: apiKeys,
		})
		require.NoError(t, err)
		return provider
	}

	findUserAIProviderKeyConfig := func(
		t *testing.T,
		configs []codersdk.UserAIProviderKeyConfig,
		providerID uuid.UUID,
	) *codersdk.UserAIProviderKeyConfig {
		t.Helper()

		for i := range configs {
			if configs[i].Provider.ID == providerID {
				return &configs[i]
			}
		}
		return nil
	}

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

		ctx := testutil.Context(t, testutil.WaitLong)
		adminClient := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, adminClient.Client)
		memberClientRaw, _ := coderdtest.CreateAnotherUser(t, adminClient.Client, firstUser.OrganizationID)
		memberClient := codersdk.NewExperimentalClient(memberClientRaw)

		provider := createOpenAIProvider(t, adminClient, "test-user-key-"+uuid.NewString(), true, "test-provider-api-key")

		configs, err := memberClient.ListUserAIProviderKeyConfigs(ctx, "me")
		require.NoError(t, err)
		cfg := findUserAIProviderKeyConfig(t, configs, provider.ID)
		require.NotNil(t, cfg)
		require.False(t, cfg.HasUserAPIKey)
		require.True(t, cfg.HasProviderAPIKey)
		require.True(t, cfg.BYOKEnabled)

		cfgValue, err := memberClient.UpsertUserAIProviderKey(ctx, "me", provider.ID, codersdk.CreateUserAIProviderKeyRequest{APIKey: "test-user-api-key"})
		require.NoError(t, err)
		require.Equal(t, provider.ID, cfgValue.Provider.ID)
		require.True(t, cfgValue.HasUserAPIKey)
		require.True(t, cfgValue.HasProviderAPIKey)
		require.True(t, cfgValue.BYOKEnabled)

		configs, err = memberClient.ListUserAIProviderKeyConfigs(ctx, "me")
		require.NoError(t, err)
		cfg = findUserAIProviderKeyConfig(t, configs, provider.ID)
		require.NotNil(t, cfg)
		require.True(t, cfg.HasUserAPIKey)

		cfgValue, err = memberClient.UpsertUserAIProviderKey(ctx, "me", provider.ID, codersdk.CreateUserAIProviderKeyRequest{APIKey: "replacement-user-api-key"})
		require.NoError(t, err)
		require.Equal(t, provider.ID, cfgValue.Provider.ID)
		require.True(t, cfgValue.HasUserAPIKey)

		configs, err = memberClient.ListUserAIProviderKeyConfigs(ctx, "me")
		require.NoError(t, err)
		cfg = findUserAIProviderKeyConfig(t, configs, provider.ID)
		require.NotNil(t, cfg)
		require.True(t, cfg.HasUserAPIKey)

		require.NoError(t, memberClient.DeleteUserAIProviderKey(ctx, "me", provider.ID))
		configs, err = memberClient.ListUserAIProviderKeyConfigs(ctx, "me")
		require.NoError(t, err)
		cfg = findUserAIProviderKeyConfig(t, configs, provider.ID)
		require.NotNil(t, cfg)
		require.False(t, cfg.HasUserAPIKey)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		adminClient := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, adminClient.Client)
		memberClientRaw, _ := coderdtest.CreateAnotherUser(t, adminClient.Client, firstUser.OrganizationID)
		memberClient := codersdk.NewExperimentalClient(memberClientRaw)

		provider := createOpenAIProvider(t, adminClient, "test-disabled-saved-user-key-"+uuid.NewString(), true)
		_, err := memberClient.UpsertUserAIProviderKey(ctx, "me", provider.ID, codersdk.CreateUserAIProviderKeyRequest{APIKey: "test-user-api-key"})
		require.NoError(t, err)

		enabled := false
		_, err = adminClient.UpdateAIProvider(ctx, provider.ID.String(), codersdk.UpdateAIProviderRequest{Enabled: &enabled})
		require.NoError(t, err)

		configs, err := memberClient.ListUserAIProviderKeyConfigs(ctx, "me")
		require.NoError(t, err)
		cfg := findUserAIProviderKeyConfig(t, configs, provider.ID)
		require.NotNil(t, cfg)
		require.False(t, cfg.Provider.Enabled)
		require.True(t, cfg.HasUserAPIKey)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		adminClient := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, adminClient.Client)
		memberClientRaw, _ := coderdtest.CreateAnotherUser(t, adminClient.Client, firstUser.OrganizationID)
		memberClient := codersdk.NewExperimentalClient(memberClientRaw)

		provider := createOpenAIProvider(t, adminClient, "test-disabled-user-key-"+uuid.NewString(), false)

		_, err := memberClient.UpsertUserAIProviderKey(ctx, "me", provider.ID, codersdk.CreateUserAIProviderKeyRequest{APIKey: "test-user-api-key"})
		sdkErr := requireSDKError(t, err, http.StatusPreconditionFailed)
		require.Equal(t, "AI provider is disabled.", sdkErr.Message)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		adminClient := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, adminClient.Client)
		memberClientRaw, _ := coderdtest.CreateAnotherUser(t, adminClient.Client, firstUser.OrganizationID)
		memberClient := codersdk.NewExperimentalClient(memberClientRaw)

		provider, err := adminClient.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{
			Type:    codersdk.AIProviderTypeBedrock,
			Name:    "test-bedrock-user-key-" + uuid.NewString(),
			Enabled: true,
			BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com",
			Settings: codersdk.AIProviderSettings{
				Bedrock: &codersdk.AIProviderBedrockSettings{
					Region:         "us-east-1",
					Model:          "anthropic.claude-3-5-sonnet",
					SmallFastModel: "anthropic.claude-3-5-haiku",
				},
			},
		})
		require.NoError(t, err)

		// Bedrock providers accept a user-supplied Bedrock API key, which the
		// AI gateway forwards as a bearer token.
		cfg, err := memberClient.UpsertUserAIProviderKey(ctx, "me", provider.ID, codersdk.CreateUserAIProviderKeyRequest{APIKey: "test-user-api-key"})
		require.NoError(t, err)
		require.True(t, cfg.HasUserAPIKey)

		configs, err := memberClient.ListUserAIProviderKeyConfigs(ctx, "me")
		require.NoError(t, err)
		listed := findUserAIProviderKeyConfig(t, configs, provider.ID)
		require.NotNil(t, listed)
		require.True(t, listed.HasUserAPIKey)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		adminClient := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, adminClient.Client)
		memberClientRaw, _ := coderdtest.CreateAnotherUser(t, adminClient.Client, firstUser.OrganizationID)
		memberClient := codersdk.NewExperimentalClient(memberClientRaw)

		provider := createOpenAIProvider(t, adminClient, "test-large-user-key-"+uuid.NewString(), true)

		_, err := memberClient.UpsertUserAIProviderKey(ctx, "me", provider.ID, codersdk.CreateUserAIProviderKeyRequest{APIKey: strings.Repeat("x", 10241)})
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, "API key too large.", sdkErr.Message)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		adminClient := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, adminClient.Client)
		memberClientRaw, _ := coderdtest.CreateAnotherUser(t, adminClient.Client, firstUser.OrganizationID)
		memberClient := codersdk.NewExperimentalClient(memberClientRaw)

		provider := createOpenAIProvider(t, adminClient, "test-whitespace-user-key-"+uuid.NewString(), true)

		_, err := memberClient.UpsertUserAIProviderKey(ctx, "me", provider.ID, codersdk.CreateUserAIProviderKeyRequest{APIKey: "  "})
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, "API key must not contain leading or trailing whitespace.", sdkErr.Message)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		values := coderdtest.DeploymentValues(t)
		values.AI.BridgeConfig.AllowBYOK = serpent.Bool(false)
		// The aibridged reloader logs at error level when it sees a provider
		// configured with no API key and BYOK disabled. That state is the
		// scenario under test, so suppress its error logs here.
		logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
		client := newChatClient(t, func(o *coderdtest.Options) {
			o.DeploymentValues = values
			o.Logger = &logger
		})
		_ = coderdtest.CreateFirstUser(t, client.Client)

		provider := createOpenAIProvider(t, client, "test-byok-disabled-"+uuid.NewString(), true)

		_, err := client.UpsertUserAIProviderKey(ctx, "me", provider.ID, codersdk.CreateUserAIProviderKeyRequest{APIKey: "test-user-api-key"})
		sdkErr := requireSDKError(t, err, http.StatusForbidden)
		require.Equal(t, "BYOK is disabled.", sdkErr.Message)

		configs, err := client.ListUserAIProviderKeyConfigs(ctx, "me")
		require.NoError(t, err)
		cfg := findUserAIProviderKeyConfig(t, configs, provider.ID)
		require.NotNil(t, cfg)
		require.False(t, cfg.BYOKEnabled)
		require.NoError(t, client.DeleteUserAIProviderKey(ctx, "me", provider.ID))
	})
}

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

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		_ = coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		configs, err := client.ChatModels(ctx, modelConfig.OrganizationID)
		require.NoError(t, err)
		require.NotEmpty(t, configs.Models)
		require.NotEmpty(t, configs.Providers)
		require.Empty(t, configs.UnsupportedProviders)

		found := false
		for _, config := range configs.Models {
			if config.ID == modelConfig.ID {
				found = true
				require.Equal(t, modelConfig.AIProviderID, config.AIProviderID)
				require.Equal(t, modelConfig.Model, config.Model)
				require.True(t, config.IsDefault)
			}
		}
		require.True(t, found)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)

		aiProvider := createAIProviderForTest(t, client, "openai", "test-api-key")

		contextLimit := int64(4096)
		enabled := false
		disabledConfig, err := client.CreateChatModel(ctx, firstUser.OrganizationID, codersdk.CreateChatModelRequest{
			AIProviderID: &aiProvider.ID,
			Model:        "gpt-4o-disabled",
			DisplayName:  "GPT-4o Disabled",
			Enabled:      &enabled,
			ContextLimit: &contextLimit,
		})
		require.NoError(t, err)
		require.False(t, disabledConfig.Enabled)

		configs, err := client.ChatModels(ctx, firstUser.OrganizationID)
		require.NoError(t, err)
		require.True(t, slices.ContainsFunc(configs.Models, func(config codersdk.ChatModel) bool {
			return config.ID == disabledConfig.ID && !config.Enabled
		}))
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		adminClient, db := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, adminClient.Client)
		readableConfig := createChatModel(t, adminClient)
		privateConfig := dbgen.ChatModelConfig(t, db, database.ChatModelConfig{
			AIProviderID:   uuid.NullUUID{UUID: readableConfig.AIProviderID, Valid: true},
			Model:          "private-" + uuid.NewString(),
			OrganizationID: firstUser.OrganizationID,
			GroupACL:       database.ChatACL{},
		})

		memberClientRaw, _ := coderdtest.CreateAnotherUser(t, adminClient.Client, firstUser.OrganizationID)
		memberClient := codersdk.NewExperimentalClient(memberClientRaw)

		configs, err := memberClient.ChatModels(ctx, firstUser.OrganizationID)
		require.NoError(t, err)
		modelIDs := make([]uuid.UUID, 0, len(configs.Models))
		for _, config := range configs.Models {
			modelIDs = append(modelIDs, config.ID)
		}
		require.Contains(t, modelIDs, readableConfig.ID)
		require.NotContains(t, modelIDs, privateConfig.ID)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		provider := dbgen.AIProvider(t, db, database.AIProvider{
			Type: database.AIProviderTypeBedrock,
		})

		models, err := client.ChatModels(ctx, firstUser.OrganizationID)
		require.NoError(t, err)
		for _, descriptor := range models.Providers {
			if descriptor.ID == provider.ID {
				require.False(t, descriptor.HasAPIKey)
				require.False(t, descriptor.HasUserAPIKey)
				require.True(t, descriptor.HasEffectiveAPIKey)
				require.True(t, descriptor.Available)
				require.Empty(t, descriptor.UnavailableReason)
				return
			}
		}
		t.Fatal("bedrock provider descriptor not found")
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		anthropicProvider := dbgen.AIProvider(t, db, database.AIProvider{Type: database.AIProviderTypeAnthropic})
		bedrockProvider := dbgen.AIProvider(t, db, database.AIProvider{Type: database.AIProviderTypeBedrock})
		compatProvider := dbgen.AIProvider(t, db, database.AIProvider{Type: database.AIProviderTypeOpenaiCompat})

		want := map[uuid.UUID]codersdk.ChatComputerUseProvider{}
		for _, tc := range []struct {
			provider database.AIProvider
			model    string
			want     codersdk.ChatComputerUseProvider
		}{
			{provider: anthropicProvider, model: "claude-opus-5-5", want: codersdk.ChatComputerUseProviderAnthropic},
			{provider: bedrockProvider, model: "us.anthropic.claude-opus-5-5", want: codersdk.ChatComputerUseProviderAnthropic},
			{provider: bedrockProvider, model: "openai.gpt-5.4"},
			{provider: compatProvider, model: "claude-opus-5-5", want: codersdk.ChatComputerUseProviderOpenAICompat},
		} {
			config := dbgen.ChatModelConfig(t, db, database.ChatModelConfig{
				AIProviderID:   uuid.NullUUID{UUID: tc.provider.ID, Valid: true},
				Model:          tc.model,
				OrganizationID: firstUser.OrganizationID,
			})
			want[config.ID] = tc.want
		}

		models, err := client.ChatModels(ctx, firstUser.OrganizationID)
		require.NoError(t, err)
		got := map[uuid.UUID]codersdk.ChatComputerUseProvider{}
		for _, model := range models.Models {
			if _, ok := want[model.ID]; ok {
				got[model.ID] = model.ComputerUseProvider
			}
		}
		require.Equal(t, want, got)
	})
}

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

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		_ = coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		got, err := client.ChatModel(ctx, modelConfig.OrganizationID, modelConfig.ID)
		require.NoError(t, err)
		require.Equal(t, modelConfig.ID, got.ID)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		_ = coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)
		wrongOrganization := dbgen.Organization(t, db, database.Organization{IsDefault: false})

		for _, tc := range []struct {
			name   string
			method string
			body   any
		}{
			{name: "Get", method: http.MethodGet},
			{name: "Patch", method: http.MethodPatch, body: codersdk.UpdateChatModelRequest{DisplayName: "wrong organization"}},
			{name: "Delete", method: http.MethodDelete},
		} {
			t.Run(tc.name, func(t *testing.T) {
				res, err := client.Request(ctx, tc.method, fmt.Sprintf(
					"/api/v2/organizations/%s/chats/models/%s",
					wrongOrganization.ID,
					modelConfig.ID,
				), tc.body)
				require.NoError(t, err)
				defer res.Body.Close()
				require.Equal(t, http.StatusNotFound, res.StatusCode)
			})
		}

		stored, err := db.GetChatModelConfigByID(dbauthz.AsSystemRestricted(ctx), modelConfig.ID)
		require.NoError(t, err)
		require.Equal(t, modelConfig.DisplayName, stored.DisplayName)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		adminClient := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, adminClient.Client)
		modelConfig := createChatModel(t, adminClient)
		memberClientRaw, _ := coderdtest.CreateAnotherUser(t, adminClient.Client, firstUser.OrganizationID)
		memberClient := codersdk.NewExperimentalClient(memberClientRaw)

		got, err := memberClient.ChatModel(ctx, modelConfig.OrganizationID, modelConfig.ID)
		require.NoError(t, err)
		require.Equal(t, modelConfig.ID, got.ID)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		adminClient, db := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, adminClient.Client)
		sharedConfig := createChatModel(t, adminClient)
		privateConfig := dbgen.ChatModelConfig(t, db, database.ChatModelConfig{
			AIProviderID:   uuid.NullUUID{UUID: sharedConfig.AIProviderID, Valid: true},
			Model:          "private-" + uuid.NewString(),
			OrganizationID: firstUser.OrganizationID,
			GroupACL:       database.ChatACL{},
		})
		memberClientRaw, _ := coderdtest.CreateAnotherUser(t, adminClient.Client, firstUser.OrganizationID)
		memberClient := codersdk.NewExperimentalClient(memberClientRaw)

		_, err := memberClient.ChatModel(ctx, privateConfig.OrganizationID, privateConfig.ID)
		requireSDKError(t, err, http.StatusNotFound)
	})
}

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

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

		// The write is authorized before any privileged provider lookup, so
		// a principal without create-in-org gets an identical denial whether
		// the provider ID is missing, disabled, or configured: no provider
		// state is disclosed.
		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		memberClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
		memberClient := codersdk.NewExperimentalClient(memberClientRaw)

		configuredProvider := createAIProviderForTest(t, client, "openai", "test-api-key")
		disabledProvider := createAIProviderForTest(t, client, "google", "test-api-key-2")
		enabled := false
		_, err := client.UpdateAIProvider(ctx, disabledProvider.ID.String(), codersdk.UpdateAIProviderRequest{
			Enabled: &enabled,
		})
		require.NoError(t, err)

		for _, tc := range []struct {
			name       string
			providerID uuid.UUID
		}{
			{"missing", uuid.New()},
			{"disabled", disabledProvider.ID},
			{"configured", configuredProvider.ID},
		} {
			t.Run(tc.name, func(t *testing.T) {
				contextLimit := int64(4096)
				_, err := memberClient.CreateChatModel(ctx, firstUser.OrganizationID, codersdk.CreateChatModelRequest{
					AIProviderID: &tc.providerID,
					Model:        "probe-" + tc.name,
					ContextLimit: &contextLimit,
				})
				sdkErr := requireSDKError(t, err, http.StatusForbidden)
				require.Equal(t, "Forbidden.", sdkErr.Message)
			})
		}
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		adminClient, db := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, adminClient.Client)
		provider := createAIProviderForTest(t, adminClient, "openai", "test-api-key")
		hiddenOrganization := dbgen.Organization(t, db, database.Organization{IsDefault: false})
		memberClientRaw, _ := coderdtest.CreateAnotherUser(t, adminClient.Client, firstUser.OrganizationID)
		memberClient := codersdk.NewExperimentalClient(memberClientRaw)
		contextLimit := int64(4096)

		var concealedMessage string
		for _, organizationID := range []uuid.UUID{hiddenOrganization.ID, uuid.New()} {
			_, err := memberClient.CreateChatModel(ctx, organizationID, codersdk.CreateChatModelRequest{
				AIProviderID: &provider.ID,
				Model:        "concealed-" + uuid.NewString(),
				ContextLimit: &contextLimit,
			})
			sdkErr := requireSDKError(t, err, http.StatusNotFound)
			if concealedMessage == "" {
				concealedMessage = sdkErr.Message
			}
			require.Equal(t, concealedMessage, sdkErr.Message)
		}

		configs, err := adminClient.ChatModels(ctx, hiddenOrganization.ID)
		require.NoError(t, err)
		require.Empty(t, configs.Models)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)

		aiProvider := createAIProviderForTest(t, client, "openai", "test-api-key")

		contextLimit := int64(4096)
		isDefault := true
		modelConfig, err := client.CreateChatModel(ctx, firstUser.OrganizationID, codersdk.CreateChatModelRequest{
			AIProviderID: &aiProvider.ID,
			Model:        "gpt-4o-mini",
			ContextLimit: &contextLimit,
			IsDefault:    &isDefault,
		})
		require.NoError(t, err)
		require.NotEqual(t, uuid.Nil, modelConfig.ID)
		require.Equal(t, aiProvider.ID, modelConfig.AIProviderID)
		require.Equal(t, "gpt-4o-mini", modelConfig.Model)
		require.EqualValues(t, 4096, modelConfig.ContextLimit)
		require.True(t, modelConfig.IsDefault)

		configs, err := client.ChatModels(ctx, firstUser.OrganizationID)
		require.NoError(t, err)
		require.Len(t, configs.Models, 1)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, store := newChatClientWithModelConfigHookStore(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		aiProvider := createAIProviderForTest(t, client, "openai", "test-api-key")

		store.armFailProviderReferenceLock(aiProvider.ID)

		contextLimit := int64(4096)
		_, err := client.CreateChatModel(ctx, firstUser.OrganizationID, codersdk.CreateChatModelRequest{
			AIProviderID: &aiProvider.ID,
			Model:        "gpt-4o-mini",
			ContextLimit: &contextLimit,
		})
		sdkErr := requireSDKError(t, err, http.StatusInternalServerError)
		require.Equal(t, "Failed to create chat model config.", sdkErr.Message)
		require.Contains(t, sdkErr.Detail, "get AI provider for create")
		require.NotContains(t, sdkErr.Detail, "get AI provider for update")
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)

		aiProvider := createAIProviderForTest(t, client, "openai", "test-api-key")

		// Concurrent creators race to self-elect a default while one claims
		// it via a follow-up update, mirroring a terraform apply. Unserialized,
		// the losers 409 on the single-default unique index.
		const creators = 10
		contextLimit := int64(4096)
		var claimed codersdk.ChatModel
		var eg errgroup.Group
		for i := range creators - 1 {
			eg.Go(func() error {
				_, err := client.CreateChatModel(ctx, firstUser.OrganizationID, codersdk.CreateChatModelRequest{
					AIProviderID: &aiProvider.ID,
					Model:        fmt.Sprintf("gpt-4o-mini-%d", i),
					ContextLimit: &contextLimit,
				})
				return err
			})
		}
		eg.Go(func() error {
			created, err := client.CreateChatModel(ctx, firstUser.OrganizationID, codersdk.CreateChatModelRequest{
				AIProviderID: &aiProvider.ID,
				Model:        "gpt-4o",
				ContextLimit: &contextLimit,
			})
			if err != nil {
				return xerrors.Errorf("create claimed config: %w", err)
			}
			claimed, err = client.UpdateChatModel(ctx, created.OrganizationID, created.ID, codersdk.UpdateChatModelRequest{
				IsDefault: ptr.Ref(true),
			})
			if err != nil {
				return xerrors.Errorf("promote claimed config: %w", err)
			}
			return nil
		})
		require.NoError(t, eg.Wait())

		configs, err := client.ChatModels(ctx, firstUser.OrganizationID)
		require.NoError(t, err)
		require.Len(t, configs.Models, creators)
		var defaults []uuid.UUID
		for _, cfg := range configs.Models {
			if cfg.IsDefault {
				defaults = append(defaults, cfg.ID)
			}
		}
		require.Equal(t, []uuid.UUID{claimed.ID}, defaults)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		rawDB, pubsub := dbtestutil.NewDB(t)
		client := newChatClient(t, func(opts *coderdtest.Options) {
			opts.Database = rawDB
			opts.Pubsub = pubsub
		})
		_ = coderdtest.CreateFirstUser(t, client.Client)
		defaultOrg, err := rawDB.GetDefaultOrganization(ctx)
		require.NoError(t, err)

		// A custom org role holding only chat_model_config permissions
		// passes write authorization for configs in its org and holds no
		// grant anywhere else; org role permissions are scoped to the
		// organization the role belongs to.
		role, err := rawDB.InsertCustomRole(ctx, database.InsertCustomRoleParams{
			Name:           testutil.GetRandomName(t),
			DisplayName:    "Chat Model Config Admin",
			OrganizationID: uuid.NullUUID{UUID: defaultOrg.ID, Valid: true},
			OrgPermissions: database.CustomRolePermissions{
				{
					ResourceType: rbac.ResourceChatModelConfig.Type,
					Action:       policy.ActionCreate,
				},
				{
					ResourceType: rbac.ResourceChatModelConfig.Type,
					Action:       policy.ActionRead,
				},
				{
					ResourceType: rbac.ResourceChatModelConfig.Type,
					Action:       policy.ActionUpdate,
				},
			},
		})
		require.NoError(t, err)

		// An existing org default keeps the create off the self-promotion
		// path; the writer below holds no chat_model_config:update grant
		// beyond its own org. The owner seeds the provider and the
		// default: both operations need site-wide permissions a
		// non-owner lacks.
		aiProvider := createAIProviderForTest(t, client, "openai", "test-api-key")
		_ = createAdditionalChatModel(t, client, "openai", "gpt-4o-existing")

		adminClientRaw, adminUser := coderdtest.CreateAnotherUser(
			t,
			client.Client,
			defaultOrg.ID,
		)
		_, err = client.UpdateOrganizationMemberRoles(
			ctx,
			defaultOrg.ID,
			adminUser.ID.String(),
			codersdk.UpdateRoles{Roles: []string{role.Name}},
		)
		require.NoError(t, err)
		adminClient := codersdk.NewExperimentalClient(adminClientRaw)

		contextLimit := int64(4096)
		modelConfig, err := adminClient.CreateChatModel(ctx, defaultOrg.ID, codersdk.CreateChatModelRequest{
			AIProviderID: &aiProvider.ID,
			Model:        "gpt-4o-mini",
			ContextLimit: &contextLimit,
		})
		require.NoError(t, err)

		row, err := rawDB.GetChatModelConfigByID(ctx, modelConfig.ID)
		require.NoError(t, err)
		require.Equal(t, defaultOrg.ID, row.OrganizationID)
		require.False(t, row.IsDefault)
		require.Equal(
			t,
			database.ChatACL{
				defaultOrg.ID.String(): {Permissions: []policy.Action{policy.ActionRead}},
			},
			row.GroupACL,
		)
		require.Equal(t, database.ChatACL{}, row.UserACL)
	})
	// An organization role can manage the full model lifecycle. Default
	// election must not select another organization's config.
	t.Run("OrgRoleWritesThroughLifecycle", func(t *testing.T) {
		t.Parallel()

		ctx := testutil.Context(t, testutil.WaitLong)
		rawDB, pubsub := dbtestutil.NewDB(t)
		client := newChatClient(t, func(opts *coderdtest.Options) {
			opts.Database = rawDB
			opts.Pubsub = pubsub
		})
		_ = coderdtest.CreateFirstUser(t, client.Client)
		defaultOrg, err := rawDB.GetDefaultOrganization(ctx)
		require.NoError(t, err)

		// The writer role holds chat_model_config permissions in the
		// default org and nothing else: user-scoped member permissions do
		// not carry the org-wide create grant the route needs, so the role
		// must supply every action the lifecycle touches.
		role, err := rawDB.InsertCustomRole(ctx, database.InsertCustomRoleParams{
			Name:           testutil.GetRandomName(t),
			DisplayName:    "Chat Model Config Admin",
			OrganizationID: uuid.NullUUID{UUID: defaultOrg.ID, Valid: true},
			OrgPermissions: database.CustomRolePermissions{
				{
					ResourceType: rbac.ResourceChatModelConfig.Type,
					Action:       policy.ActionCreate,
				},
				{
					ResourceType: rbac.ResourceChatModelConfig.Type,
					Action:       policy.ActionRead,
				},
				{
					ResourceType: rbac.ResourceChatModelConfig.Type,
					Action:       policy.ActionUpdate,
				},
				{
					ResourceType: rbac.ResourceChatModelConfig.Type,
					Action:       policy.ActionDelete,
				},
			},
		})
		require.NoError(t, err)

		aiProvider := createAIProviderForTest(t, client, "openai", "test-api-key")

		writerClientRaw, writerUser := coderdtest.CreateAnotherUser(
			t,
			client.Client,
			defaultOrg.ID,
		)
		_, err = client.UpdateOrganizationMemberRoles(
			ctx,
			defaultOrg.ID,
			writerUser.ID.String(),
			codersdk.UpdateRoles{Roles: []string{role.Name}},
		)
		require.NoError(t, err)
		writerClient := codersdk.NewExperimentalClient(writerClientRaw)

		contextLimit := int64(4096)
		defaultConfig, err := writerClient.CreateChatModel(ctx, defaultOrg.ID, codersdk.CreateChatModelRequest{
			AIProviderID: &aiProvider.ID,
			Model:        "gpt-4o-mini",
			ContextLimit: &contextLimit,
		})
		require.NoError(t, err)
		require.True(t, defaultConfig.IsDefault)

		updated, err := writerClient.UpdateChatModel(ctx, defaultConfig.OrganizationID, defaultConfig.ID, codersdk.UpdateChatModelRequest{
			DisplayName: "Window Write",
		})
		require.NoError(t, err)
		require.Equal(t, "Window Write", updated.DisplayName)

		candidate, err := writerClient.CreateChatModel(ctx, defaultOrg.ID, codersdk.CreateChatModelRequest{
			AIProviderID: &aiProvider.ID,
			Model:        "gpt-4o-mini-candidate",
			ContextLimit: &contextLimit,
		})
		require.NoError(t, err)

		notDefault := false
		_, err = writerClient.UpdateChatModel(ctx, defaultConfig.OrganizationID, defaultConfig.ID, codersdk.UpdateChatModelRequest{
			IsDefault: &notDefault,
		})
		require.NoError(t, err)
		candidateRow, err := rawDB.GetChatModelConfigByID(ctx, candidate.ID)
		require.NoError(t, err)
		require.True(t, candidateRow.IsDefault)

		err = writerClient.DeleteChatModel(ctx, candidate.OrganizationID, candidate.ID)
		require.NoError(t, err)
		defaultRow, err := rawDB.GetChatModelConfigByID(ctx, defaultConfig.ID)
		require.NoError(t, err)
		require.True(t, defaultRow.IsDefault)

		promoted, err := writerClient.CreateChatModel(ctx, defaultOrg.ID, codersdk.CreateChatModelRequest{
			AIProviderID: &aiProvider.ID,
			Model:        "gpt-4o-mini-promoted",
			ContextLimit: &contextLimit,
		})
		require.NoError(t, err)
		isDefault := true
		_, err = writerClient.UpdateChatModel(ctx, promoted.OrganizationID, promoted.ID, codersdk.UpdateChatModelRequest{
			IsDefault: &isDefault,
		})
		require.NoError(t, err)
		promotedRow, err := rawDB.GetChatModelConfigByID(ctx, promoted.ID)
		require.NoError(t, err)
		require.True(t, promotedRow.IsDefault)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)

		aiProvider := createAIProviderForTest(t, client, "openai", "test-api-key")

		contextLimit := int64(4096)
		modelConfig, err := client.CreateChatModel(ctx, firstUser.OrganizationID, codersdk.CreateChatModelRequest{
			AIProviderID: &aiProvider.ID,
			Model:        "gpt-4o-mini",
			ContextLimit: &contextLimit,
			ModelConfig: &codersdk.ChatModelCallConfig{
				ReasoningEffort: &codersdk.ChatModelReasoningEffortConfig{
					Default: ptr.Ref("medium"),
					Max:     ptr.Ref("xhigh"),
				},
			},
		})
		require.NoError(t, err)
		require.NotNil(t, modelConfig.ModelConfig)
		require.NotNil(t, modelConfig.ModelConfig.ReasoningEffort)
		require.NotNil(t, modelConfig.ModelConfig.ReasoningEffort.Default)
		require.Equal(t, "medium", *modelConfig.ModelConfig.ReasoningEffort.Default)
		require.NotNil(t, modelConfig.ModelConfig.ReasoningEffort.Max)
		require.Equal(t, "xhigh", *modelConfig.ModelConfig.ReasoningEffort.Max)
		require.Equal(t, []string{"none", "minimal", "low", "medium", "high", "xhigh"}, modelConfig.ReasoningEfforts)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)

		aiProvider := createAIProviderForTest(t, client, "openai", "test-api-key")

		contextLimit := int64(4096)
		_, err := client.CreateChatModel(ctx, firstUser.OrganizationID, codersdk.CreateChatModelRequest{
			AIProviderID: &aiProvider.ID,
			Model:        "gpt-4o-mini",
			ContextLimit: &contextLimit,
			ModelConfig: &codersdk.ChatModelCallConfig{
				ReasoningEffort: &codersdk.ChatModelReasoningEffortConfig{
					Default: ptr.Ref("high"),
				},
			},
		})
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, "Invalid model config.", sdkErr.Message)
		require.Equal(t, "reasoning_effort.default and reasoning_effort.max must both be set", sdkErr.Detail)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)

		aiProvider := createAIProviderForTest(t, client, "openai", "test-api-key")

		contextLimit := int64(4096)
		_, err := client.CreateChatModel(ctx, firstUser.OrganizationID, codersdk.CreateChatModelRequest{
			AIProviderID: &aiProvider.ID,
			Model:        "gpt-4o-mini",
			ContextLimit: &contextLimit,
			ModelConfig: &codersdk.ChatModelCallConfig{
				ReasoningEffort: &codersdk.ChatModelReasoningEffortConfig{
					Default: ptr.Ref(" HIGH "),
					Max:     ptr.Ref("high"),
				},
			},
		})
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, "Invalid model config.", sdkErr.Message)
		require.Equal(
			t,
			`reasoning_effort.default " HIGH " must be one of none, minimal, low, medium, high, xhigh, max`,
			sdkErr.Detail,
		)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)

		aiProvider := createAIProviderForTest(t, client, "openai", "test-api-key")

		contextLimit := int64(4096)
		_, err := client.CreateChatModel(ctx, firstUser.OrganizationID, codersdk.CreateChatModelRequest{
			AIProviderID: &aiProvider.ID,
			Model:        "gpt-4o-mini",
			ContextLimit: &contextLimit,
			ModelConfig: &codersdk.ChatModelCallConfig{
				ReasoningEffort: &codersdk.ChatModelReasoningEffortConfig{
					Default: ptr.Ref("xhigh"),
					Max:     ptr.Ref("low"),
				},
			},
		})
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, "Invalid model config.", sdkErr.Message)
		require.Contains(t, sdkErr.Detail, "must not exceed")
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		aiProvider := createAIProviderForTest(t, client, "openai", "test-api-key")

		_, err := client.CreateChatModel(ctx, firstUser.OrganizationID, codersdk.CreateChatModelRequest{
			AIProviderID: &aiProvider.ID,
			Model:        "gpt-4o-mini",
		})
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, "Context limit is required.", sdkErr.Message)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)

		contextLimit := int64(4096)
		_, err := client.CreateChatModel(ctx, firstUser.OrganizationID, codersdk.CreateChatModelRequest{
			Model:        "gpt-4o-mini",
			ContextLimit: &contextLimit,
		})
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, "AI provider ID is required.", sdkErr.Message)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)

		contextLimit := int64(4096)
		missingProviderID := uuid.New()
		_, err := client.CreateChatModel(ctx, firstUser.OrganizationID, codersdk.CreateChatModelRequest{
			AIProviderID: &missingProviderID,
			Model:        "gpt-4o-mini",
			ContextLimit: &contextLimit,
		})
		sdkErr := requireSDKError(t, err, http.StatusPreconditionFailed)
		require.Equal(t, "AI provider is not configured.", sdkErr.Message)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		provider, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{
			Type:    codersdk.AIProviderTypeOpenAI,
			Name:    "test-model-config-provider-" + uuid.NewString(),
			Enabled: true,
			BaseURL: "https://api.openai.com/v1",
		})
		require.NoError(t, err)

		contextLimit := int64(4096)
		modelConfig, err := client.CreateChatModel(ctx, firstUser.OrganizationID, codersdk.CreateChatModelRequest{
			AIProviderID: &provider.ID,
			Model:        "gpt-4o-mini",
			ContextLimit: &contextLimit,
		})
		require.NoError(t, err)
		require.NotEqual(t, uuid.Nil, modelConfig.AIProviderID)
		require.Equal(t, provider.ID, modelConfig.AIProviderID)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)

		missingProviderID := uuid.New()
		contextLimit := int64(4096)
		_, err := client.CreateChatModel(ctx, firstUser.OrganizationID, codersdk.CreateChatModelRequest{
			AIProviderID: &missingProviderID,
			Model:        "gpt-4o-mini",
			ContextLimit: &contextLimit,
		})
		sdkErr := requireSDKError(t, err, http.StatusPreconditionFailed)
		require.Equal(t, "AI provider is not configured.", sdkErr.Message)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		provider, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{
			Type:    codersdk.AIProviderTypeOpenAI,
			Name:    "test-disabled-model-provider-" + uuid.NewString(),
			Enabled: false,
			BaseURL: "https://api.openai.com/v1",
		})
		require.NoError(t, err)

		contextLimit := int64(4096)
		_, err = client.CreateChatModel(ctx, firstUser.OrganizationID, codersdk.CreateChatModelRequest{
			AIProviderID: &provider.ID,
			Model:        "gpt-4o-mini",
			ContextLimit: &contextLimit,
		})
		sdkErr := requireSDKError(t, err, http.StatusPreconditionFailed)
		require.Equal(t, "AI provider is disabled.", sdkErr.Message)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)

		aiProvider, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{
			Type:    codersdk.AIProviderTypeOpenAI,
			Name:    "openrouter",
			Enabled: true,
			BaseURL: "https://openrouter.ai/api/v1",
			APIKeys: []string{"test-api-key"},
		})
		require.NoError(t, err)

		contextLimit := int64(4096)
		_, err = client.CreateChatModel(ctx, firstUser.OrganizationID, codersdk.CreateChatModelRequest{
			AIProviderID: &aiProvider.ID,
			Model:        "anthropic/claude-opus-4.6",
			ContextLimit: &contextLimit,
		})
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, "OpenRouter-like provider configured as type openai does not support slash-namespaced models.", sdkErr.Message)
		require.Contains(t, sdkErr.Detail, "Change the AI provider type to openrouter or openai-compat.")
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		adminClient := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, adminClient.Client)
		memberClientRaw, _ := coderdtest.CreateAnotherUser(t, adminClient.Client, firstUser.OrganizationID)
		memberClient := codersdk.NewExperimentalClient(memberClientRaw)

		aiProvider := createAIProviderForTest(t, adminClient, "openai", "test-api-key")

		contextLimit := int64(4096)
		_, err := memberClient.CreateChatModel(ctx, firstUser.OrganizationID, codersdk.CreateChatModelRequest{
			AIProviderID: &aiProvider.ID,
			Model:        "gpt-4o-mini",
			ContextLimit: &contextLimit,
		})
		// A plain org member holds no chat model config permissions in the
		// org, so the create is rejected by the write pre-check before any
		// privileged provider lookup.
		sdkErr := requireSDKError(t, err, http.StatusForbidden)
		require.Equal(t, "Forbidden.", sdkErr.Message)
	})
}

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

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		_ = coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		contextLimit := int64(8192)
		updated, err := client.UpdateChatModel(ctx, modelConfig.OrganizationID, modelConfig.ID, codersdk.UpdateChatModelRequest{
			DisplayName:  "GPT-4o Mini Updated",
			ContextLimit: &contextLimit,
		})
		require.NoError(t, err)
		require.Equal(t, modelConfig.ID, updated.ID)
		require.Equal(t, "GPT-4o Mini Updated", updated.DisplayName)
		require.EqualValues(t, 8192, updated.ContextLimit)

		configs, err := client.ChatModels(ctx, modelConfig.OrganizationID)
		require.NoError(t, err)
		require.Len(t, configs.Models, 1)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		updated, err := client.UpdateChatModel(ctx, modelConfig.OrganizationID, modelConfig.ID, codersdk.UpdateChatModelRequest{
			IsDefault: ptr.Ref(false),
		})
		require.NoError(t, err)
		require.True(t, updated.IsDefault)

		defaultConfig, err := db.GetDefaultChatModelConfig(dbauthz.AsSystemRestricted(ctx), firstUser.OrganizationID)
		require.NoError(t, err)
		require.Equal(t, modelConfig.ID, defaultConfig.ID)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		defaultConfig := createChatModel(t, client)
		candidateConfig := createAdditionalChatModel(
			t,
			client,
			coderdtest.TestChatProviderOpenAICompat,
			"promotion-candidate",
		)
		otherOrg := dbgen.Organization(t, db, database.Organization{})
		otherDefault := dbgen.ChatModelConfig(t, db, database.ChatModelConfig{
			Model:          "other-org-default-" + uuid.NewString(),
			AIProviderID:   uuid.NullUUID{UUID: defaultConfig.AIProviderID, Valid: true},
			OrganizationID: otherOrg.ID,
			IsDefault:      true,
		})

		updated, err := client.UpdateChatModel(ctx, candidateConfig.OrganizationID, candidateConfig.ID, codersdk.UpdateChatModelRequest{
			IsDefault: ptr.Ref(true),
		})
		require.NoError(t, err)
		require.True(t, updated.IsDefault)

		orgDefault, err := db.GetDefaultChatModelConfig(dbauthz.AsSystemRestricted(ctx), firstUser.OrganizationID)
		require.NoError(t, err)
		require.Equal(t, candidateConfig.ID, orgDefault.ID)
		unchangedOtherDefault, err := db.GetDefaultChatModelConfig(dbauthz.AsSystemRestricted(ctx), otherOrg.ID)
		require.NoError(t, err)
		require.Equal(t, otherDefault.ID, unchangedOtherDefault.ID)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		_ = coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		updated, err := client.UpdateChatModel(ctx, modelConfig.OrganizationID, modelConfig.ID, codersdk.UpdateChatModelRequest{
			Model: "gpt-4o-mini-updated",
		})
		require.NoError(t, err)
		require.Equal(t, modelConfig.ID, updated.ID)
		require.NotEqual(t, uuid.Nil, updated.AIProviderID)
		require.Equal(t, modelConfig.AIProviderID, updated.AIProviderID)
		require.Equal(t, "gpt-4o-mini-updated", updated.Model)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)

		aiProvider, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{
			Type:    codersdk.AIProviderTypeOpenAI,
			Name:    "openrouter",
			Enabled: true,
			BaseURL: "https://openrouter.ai/api/v1",
			APIKeys: []string{"test-api-key"},
		})
		require.NoError(t, err)

		contextLimit := int64(4096)
		modelConfig, err := client.CreateChatModel(ctx, firstUser.OrganizationID, codersdk.CreateChatModelRequest{
			AIProviderID: &aiProvider.ID,
			Model:        "gpt-4o-mini",
			ContextLimit: &contextLimit,
		})
		require.NoError(t, err)

		_, err = client.UpdateChatModel(ctx, modelConfig.OrganizationID, modelConfig.ID, codersdk.UpdateChatModelRequest{
			Model: "anthropic/claude-opus-4.6",
		})
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, "OpenRouter-like provider configured as type openai does not support slash-namespaced models.", sdkErr.Message)
		require.Contains(t, sdkErr.Detail, "Change the AI provider type to openrouter or openai-compat.")
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		_ = coderdtest.CreateFirstUser(t, client.Client)

		aiProvider, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{
			Type:    codersdk.AIProviderTypeOpenAI,
			Name:    "openrouter",
			Enabled: true,
			BaseURL: "https://openrouter.ai/api/v1",
			APIKeys: []string{"test-api-key"},
		})
		require.NoError(t, err)

		modelConfig := dbgen.ChatModelConfig(t, db, database.ChatModelConfig{
			Model:        "anthropic/claude-opus-4.6",
			AIProviderID: uuid.NullUUID{UUID: aiProvider.ID, Valid: true},
		})

		updated, err := client.UpdateChatModel(ctx, modelConfig.OrganizationID, modelConfig.ID, codersdk.UpdateChatModelRequest{
			DisplayName: "Existing OpenRouter Config",
		})
		require.NoError(t, err)
		require.Equal(t, "Existing OpenRouter Config", updated.DisplayName)
		require.Equal(t, modelConfig.Model, updated.Model)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)

		validProvider, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{
			Type:    codersdk.AIProviderTypeOpenrouter,
			Name:    "openrouter-valid",
			Enabled: true,
			BaseURL: "https://openrouter.ai/api/v1",
			APIKeys: []string{"test-api-key"},
		})
		require.NoError(t, err)
		misconfiguredProvider, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{
			Type:    codersdk.AIProviderTypeOpenAI,
			Name:    "openrouter",
			Enabled: true,
			BaseURL: "https://openrouter.ai/api/v1",
			APIKeys: []string{"test-api-key"},
		})
		require.NoError(t, err)

		contextLimit := int64(4096)
		modelConfig, err := client.CreateChatModel(ctx, firstUser.OrganizationID, codersdk.CreateChatModelRequest{
			AIProviderID: &validProvider.ID,
			Model:        "anthropic/claude-opus-4.6",
			ContextLimit: &contextLimit,
		})
		require.NoError(t, err)

		_, err = client.UpdateChatModel(ctx, modelConfig.OrganizationID, modelConfig.ID, codersdk.UpdateChatModelRequest{
			AIProviderID: &misconfiguredProvider.ID,
		})
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, "OpenRouter-like provider configured as type openai does not support slash-namespaced models.", sdkErr.Message)
		require.Contains(t, sdkErr.Detail, "Change the AI provider type to openrouter or openai-compat.")
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		adminClient := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, adminClient.Client)
		memberClientRaw, _ := coderdtest.CreateAnotherUser(t, adminClient.Client, firstUser.OrganizationID)
		memberClient := codersdk.NewExperimentalClient(memberClientRaw)
		modelConfig := createChatModel(t, adminClient)

		enabled := false
		updated, err := adminClient.UpdateChatModel(ctx, modelConfig.OrganizationID, modelConfig.ID, codersdk.UpdateChatModelRequest{
			Enabled: &enabled,
		})
		require.NoError(t, err)
		require.Equal(t, modelConfig.ID, updated.ID)
		require.False(t, updated.Enabled)

		adminConfigs, err := adminClient.ChatModels(ctx, firstUser.OrganizationID)
		require.NoError(t, err)

		require.True(t, slices.ContainsFunc(adminConfigs.Models, func(config codersdk.ChatModel) bool {
			return config.ID == modelConfig.ID && !config.Enabled
		}))

		collection, err := memberClient.ChatModels(ctx, firstUser.OrganizationID)
		require.NoError(t, err)
		require.True(t, slices.ContainsFunc(collection.Models, func(config codersdk.ChatModel) bool {
			return config.ID == modelConfig.ID && !config.Enabled
		}))

		_, err = memberClient.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: firstUser.OrganizationID,
			Content: []codersdk.ChatInputPart{{
				Type: codersdk.ChatInputPartTypeText,
				Text: "use a disabled model config",
			}},
			ModelConfigID: &modelConfig.ID,
		})
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, "Invalid model_config_id: model config not found or disabled.", sdkErr.Message)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		adminClient := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, adminClient.Client)
		memberClientRaw, _ := coderdtest.CreateAnotherUser(t, adminClient.Client, firstUser.OrganizationID)
		memberClient := codersdk.NewExperimentalClient(memberClientRaw)

		aiProvider := createAIProviderForTest(t, adminClient, "openai", "test-api-key")

		contextLimit := int64(4096)
		enabled := false
		modelConfig, err := adminClient.CreateChatModel(ctx, firstUser.OrganizationID, codersdk.CreateChatModelRequest{
			AIProviderID: &aiProvider.ID,
			Model:        "gpt-4o-reenable",
			DisplayName:  "GPT-4o Re-enable",
			Enabled:      &enabled,
			ContextLimit: &contextLimit,
		})
		require.NoError(t, err)
		require.False(t, modelConfig.Enabled)

		collection, err := memberClient.ChatModels(ctx, firstUser.OrganizationID)
		require.NoError(t, err)
		require.True(t, slices.ContainsFunc(collection.Models, func(config codersdk.ChatModel) bool {
			return config.ID == modelConfig.ID && !config.Enabled
		}))

		enabled = true
		updated, err := adminClient.UpdateChatModel(ctx, modelConfig.OrganizationID, modelConfig.ID, codersdk.UpdateChatModelRequest{
			Enabled: &enabled,
		})
		require.NoError(t, err)
		require.Equal(t, modelConfig.ID, updated.ID)
		require.True(t, updated.Enabled)

		collection, err = memberClient.ChatModels(ctx, firstUser.OrganizationID)
		require.NoError(t, err)
		require.True(t, slices.ContainsFunc(collection.Models, func(config codersdk.ChatModel) bool {
			return config.ID == modelConfig.ID && config.Enabled
		}))
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		_ = coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)
		provider, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{
			Type:    codersdk.AIProviderTypeAnthropic,
			Name:    "test-update-model-provider-" + uuid.NewString(),
			Enabled: true,
			BaseURL: "https://api.anthropic.com",
		})
		require.NoError(t, err)

		updated, err := client.UpdateChatModel(ctx, modelConfig.OrganizationID, modelConfig.ID, codersdk.UpdateChatModelRequest{
			AIProviderID: &provider.ID,
			Model:        "claude-3-5-sonnet-latest",
		})
		require.NoError(t, err)
		require.NotEqual(t, uuid.Nil, updated.AIProviderID)
		require.Equal(t, provider.ID, updated.AIProviderID)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		_ = coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)
		provider, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{
			Type:    codersdk.AIProviderTypeAnthropic,
			Name:    "test-preserve-model-provider-" + uuid.NewString(),
			Enabled: true,
			BaseURL: "https://api.anthropic.com",
		})
		require.NoError(t, err)

		updated, err := client.UpdateChatModel(ctx, modelConfig.OrganizationID, modelConfig.ID, codersdk.UpdateChatModelRequest{
			AIProviderID: &provider.ID,
			Model:        "claude-3-5-sonnet-latest",
		})
		require.NoError(t, err)
		require.NotEqual(t, uuid.Nil, updated.AIProviderID)

		updated, err = client.UpdateChatModel(ctx, modelConfig.OrganizationID, modelConfig.ID, codersdk.UpdateChatModelRequest{
			Model: "claude-3-5-haiku-latest",
		})
		require.NoError(t, err)
		require.NotEqual(t, uuid.Nil, updated.AIProviderID)
		require.Equal(t, provider.ID, updated.AIProviderID)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		_ = coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		missingProviderID := uuid.New()
		_, err := client.UpdateChatModel(ctx, modelConfig.OrganizationID, modelConfig.ID, codersdk.UpdateChatModelRequest{
			AIProviderID: &missingProviderID,
		})
		sdkErr := requireSDKError(t, err, http.StatusPreconditionFailed)
		require.Equal(t, "AI provider is not configured.", sdkErr.Message)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		_ = coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)
		provider, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{
			Type:    codersdk.AIProviderTypeOpenAI,
			Name:    "test-update-disabled-model-provider-" + uuid.NewString(),
			Enabled: false,
			BaseURL: "https://api.openai.com/v1",
		})
		require.NoError(t, err)

		_, err = client.UpdateChatModel(ctx, modelConfig.OrganizationID, modelConfig.ID, codersdk.UpdateChatModelRequest{
			AIProviderID: &provider.ID,
		})
		sdkErr := requireSDKError(t, err, http.StatusPreconditionFailed)
		require.Equal(t, "AI provider is disabled.", sdkErr.Message)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		_ = coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		missingProviderID := uuid.New()
		_, err := client.UpdateChatModel(ctx, modelConfig.OrganizationID, modelConfig.ID, codersdk.UpdateChatModelRequest{
			AIProviderID: &missingProviderID,
		})
		sdkErr := requireSDKError(t, err, http.StatusPreconditionFailed)
		require.Equal(t, "AI provider is not configured.", sdkErr.Message)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, store := newChatClientWithModelConfigHookStore(t)
		_ = coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		store.armFailNextUpdate(modelConfig.ID)

		_, err := client.UpdateChatModel(ctx, modelConfig.OrganizationID, modelConfig.ID, codersdk.UpdateChatModelRequest{
			DisplayName: "missing in tx",
		})
		requireSDKError(t, err, http.StatusNotFound)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, store := newChatClientWithModelConfigHookStore(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		defaultConfig := createChatModel(t, client)

		aiProvider := createAIProviderForTest(t, client, "anthropic", "candidate-api-key")

		contextLimit := int64(4096)
		isDefault := false
		candidateConfig, err := client.CreateChatModel(ctx, firstUser.OrganizationID, codersdk.CreateChatModelRequest{
			AIProviderID: &aiProvider.ID,
			Model:        "claude-3-5-sonnet",
			ContextLimit: &contextLimit,
			IsDefault:    &isDefault,
		})
		require.NoError(t, err)

		store.armFailNextUpdate(candidateConfig.ID)

		_, err = client.UpdateChatModel(ctx, defaultConfig.OrganizationID, defaultConfig.ID, codersdk.UpdateChatModelRequest{
			IsDefault: ptr.Ref(false),
		})
		sdkErr := requireSDKError(t, err, http.StatusInternalServerError)
		require.Equal(t, "Failed to update chat model config.", sdkErr.Message)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)

		_, err := client.UpdateChatModel(ctx, firstUser.OrganizationID, uuid.New(), codersdk.UpdateChatModelRequest{
			DisplayName: "missing",
		})
		requireSDKError(t, err, http.StatusNotFound)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		_ = coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		contextLimit := int64(0)
		_, err := client.UpdateChatModel(ctx, modelConfig.OrganizationID, modelConfig.ID, codersdk.UpdateChatModelRequest{
			ContextLimit: &contextLimit,
		})
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, "Context limit must be greater than zero.", sdkErr.Message)
	})

	// A row that disappears between the pre-read and the locked read must
	// 404, not silently no-op the update.
	t.Run("NotFoundWhenTargetRowDisappearsAtLockedRead", func(t *testing.T) {
		t.Parallel()

		ctx := testutil.Context(t, testutil.WaitLong)
		client, store := newChatClientWithModelConfigHookStore(t)
		_ = coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		store.armVanishAtLockedRead(modelConfig.ID)

		_, err := client.UpdateChatModel(ctx, modelConfig.OrganizationID, modelConfig.ID, codersdk.UpdateChatModelRequest{
			DisplayName: "vanished before update",
		})
		requireSDKError(t, err, http.StatusNotFound)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, store := newChatClientWithModelConfigHookStore(t)
		_ = coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		const sentinel = "locked-copy-sentinel-model"
		store.armMutateAtLockedRead(modelConfig.ID, sentinel)

		updated, err := client.UpdateChatModel(ctx, modelConfig.OrganizationID, modelConfig.ID, codersdk.UpdateChatModelRequest{
			DisplayName: "only-display-name",
		})
		require.NoError(t, err)
		require.Equal(t, sentinel, updated.Model)
		require.Equal(t, "only-display-name", updated.DisplayName)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		_ = coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		disabled := false
		_, err := client.UpdateAIProvider(ctx, modelConfig.AIProviderID.String(), codersdk.UpdateAIProviderRequest{
			Enabled: &disabled,
		})
		require.NoError(t, err)

		_, err = client.UpdateChatModel(ctx, modelConfig.OrganizationID, modelConfig.ID, codersdk.UpdateChatModelRequest{
			Model: "gpt-4o-different",
		})
		sdkErr := requireSDKError(t, err, http.StatusPreconditionFailed)
		require.Equal(t, "AI provider is disabled.", sdkErr.Message)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		_ = coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		err := client.DeleteAIProvider(ctx, modelConfig.AIProviderID.String())
		require.NoError(t, err)

		_, err = client.UpdateChatModel(ctx, modelConfig.OrganizationID, modelConfig.ID, codersdk.UpdateChatModelRequest{
			Model: "gpt-4o-different",
		})
		sdkErr := requireSDKError(t, err, http.StatusPreconditionFailed)
		require.Equal(t, "AI provider is not configured.", sdkErr.Message)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		_ = coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		for _, tc := range []struct {
			name  string
			key   string
			value any
		}{
			{name: "group_acl/object", key: "group_acl", value: map[string]any{}},
			{name: "group_acl/null", key: "group_acl", value: nil},
			{name: "user_acl/object", key: "user_acl", value: map[string]any{}},
			{name: "user_acl/null", key: "user_acl", value: nil},
		} {
			t.Run(tc.name, func(t *testing.T) {
				res, err := client.Request(ctx, http.MethodPatch, fmt.Sprintf(
					"/api/v2/organizations/%s/chats/models/%s",
					modelConfig.OrganizationID,
					modelConfig.ID,
				), map[string]any{tc.key: tc.value})
				require.NoError(t, err)
				defer res.Body.Close()
				err = codersdk.ReadBodyAsError(res)
				sdkErr := requireSDKError(t, err, http.StatusBadRequest)
				require.Contains(t, sdkErr.Message, "nested /acl endpoint")
			})
		}
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		_ = coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		_, err := client.UpdateChatModel(ctx, modelConfig.OrganizationID, modelConfig.ID, codersdk.UpdateChatModelRequest{
			CompressionThreshold: ptr.Ref(int32(150)),
		})
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, "Invalid compression threshold.", sdkErr.Message)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		_ = coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		updated, err := client.UpdateChatModel(ctx, modelConfig.OrganizationID, modelConfig.ID, codersdk.UpdateChatModelRequest{
			CompressionThreshold: ptr.Ref(int32(55)),
		})
		require.NoError(t, err)
		require.Equal(t, int32(55), updated.CompressionThreshold)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		_ = coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		updated, err := client.UpdateChatModel(ctx, modelConfig.OrganizationID, modelConfig.ID, codersdk.UpdateChatModelRequest{
			ModelConfig: &codersdk.ChatModelCallConfig{
				ReasoningEffort: &codersdk.ChatModelReasoningEffortConfig{
					Default: ptr.Ref("high"),
					Max:     ptr.Ref("high"),
				},
			},
		})
		require.NoError(t, err)
		require.NotNil(t, updated.ModelConfig)
		require.NotNil(t, updated.ModelConfig.ReasoningEffort)
		require.NotNil(t, updated.ModelConfig.ReasoningEffort.Default)
		require.Equal(t, "high", *updated.ModelConfig.ReasoningEffort.Default)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)

		res, err := client.Request(
			ctx,
			http.MethodPatch,
			fmt.Sprintf("/api/v2/organizations/%s/chats/models/not-a-uuid", firstUser.OrganizationID),
			codersdk.UpdateChatModelRequest{DisplayName: "ignored"},
		)
		require.NoError(t, err)
		defer res.Body.Close()

		err = codersdk.ReadBodyAsError(res)
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, `Invalid UUID "not-a-uuid".`, sdkErr.Message)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		adminClient := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, adminClient.Client)
		memberClientRaw, _ := coderdtest.CreateAnotherUser(t, adminClient.Client, firstUser.OrganizationID)
		memberClient := codersdk.NewExperimentalClient(memberClientRaw)

		modelConfig := createChatModel(t, adminClient)
		_, err := memberClient.UpdateChatModel(ctx, modelConfig.OrganizationID, modelConfig.ID, codersdk.UpdateChatModelRequest{
			DisplayName: "member update",
		})
		// Action-aware item admission conceals a caller without update access.
		sdkErr := requireSDKError(t, err, http.StatusNotFound)
		require.Equal(t, "Resource not found or you do not have access to this resource", sdkErr.Message)
	})
}

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

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		_ = coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		err := client.DeleteChatModel(ctx, modelConfig.OrganizationID, modelConfig.ID)
		require.NoError(t, err)

		configs, err := client.ChatModels(ctx, modelConfig.OrganizationID)
		require.NoError(t, err)
		for _, config := range configs.Models {
			require.NotEqual(t, modelConfig.ID, config.ID)
		}
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		defaultConfig := createChatModel(t, client)
		candidateConfig := createAdditionalChatModel(
			t,
			client,
			coderdtest.TestChatProviderOpenAICompat,
			"delete-candidate",
		)
		otherOrg := dbgen.Organization(t, db, database.Organization{})
		otherDefault := dbgen.ChatModelConfig(t, db, database.ChatModelConfig{
			Model:          "other-org-default-" + uuid.NewString(),
			AIProviderID:   uuid.NullUUID{UUID: defaultConfig.AIProviderID, Valid: true},
			OrganizationID: otherOrg.ID,
			IsDefault:      true,
		})

		err := client.DeleteChatModel(ctx, defaultConfig.OrganizationID, defaultConfig.ID)
		require.NoError(t, err)

		orgDefault, err := db.GetDefaultChatModelConfig(dbauthz.AsSystemRestricted(ctx), firstUser.OrganizationID)
		require.NoError(t, err)
		require.Equal(t, candidateConfig.ID, orgDefault.ID)
		unchangedOtherDefault, err := db.GetDefaultChatModelConfig(dbauthz.AsSystemRestricted(ctx), otherOrg.ID)
		require.NoError(t, err)
		require.Equal(t, otherDefault.ID, unchangedOtherDefault.ID)
	})

	// Deleting the default must not promote a config whose provider is
	// disabled while a usable candidate exists.
	t.Run("PromotesUsableConfigOverDisabledProvider", func(t *testing.T) {
		t.Parallel()

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		_ = coderdtest.CreateFirstUser(t, client.Client)

		defaultConfig := createChatModel(t, client)
		// Same provider type as the enabled candidate with an
		// alphabetically earlier model, so it sorts first in the
		// reselection order.
		createProviderDisabledChatModel(
			t,
			client,
			coderdtest.TestChatProviderOpenAICompat,
			"a-provider-disabled-model",
		)
		enabledConfig := createAdditionalChatModel(
			t,
			client,
			coderdtest.TestChatProviderOpenAICompat,
			"z-enabled-model",
		)

		err := client.DeleteChatModel(ctx, defaultConfig.OrganizationID, defaultConfig.ID)
		require.NoError(t, err)

		configs, err := client.ChatModels(ctx, defaultConfig.OrganizationID)
		require.NoError(t, err)
		defaultID := uuid.Nil
		for _, config := range configs.Models {
			if config.IsDefault {
				defaultID = config.ID
			}
		}
		require.Equal(t, enabledConfig.ID, defaultID)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)

		err := client.DeleteChatModel(ctx, firstUser.OrganizationID, uuid.New())
		requireSDKError(t, err, http.StatusNotFound)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, store := newChatClientWithModelConfigHookStore(t)
		_ = coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		store.armFailNextDelete(modelConfig.ID)

		err := client.DeleteChatModel(ctx, modelConfig.OrganizationID, modelConfig.ID)
		requireSDKError(t, err, http.StatusNotFound)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)

		res, err := client.Request(
			ctx,
			http.MethodDelete,
			fmt.Sprintf("/api/v2/organizations/%s/chats/models/not-a-uuid", firstUser.OrganizationID),
			nil,
		)
		require.NoError(t, err)
		defer res.Body.Close()

		err = codersdk.ReadBodyAsError(res)
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, `Invalid UUID "not-a-uuid".`, sdkErr.Message)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		adminClient := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, adminClient.Client)
		memberClientRaw, _ := coderdtest.CreateAnotherUser(t, adminClient.Client, firstUser.OrganizationID)
		memberClient := codersdk.NewExperimentalClient(memberClientRaw)

		modelConfig := createChatModel(t, adminClient)
		err := memberClient.DeleteChatModel(ctx, modelConfig.OrganizationID, modelConfig.ID)
		// Action-aware item admission conceals a caller without delete access.
		sdkErr := requireSDKError(t, err, http.StatusNotFound)
		require.Equal(t, "Resource not found or you do not have access to this resource", sdkErr.Message)
	})
}

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

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		createdChat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: firstUser.OrganizationID,
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "get chat route payload",
				},
			},
		})
		require.NoError(t, err)

		chatResult, err := client.GetChat(ctx, createdChat.ID)
		require.NoError(t, err)
		messagesResult, err := client.GetChatMessages(ctx, createdChat.ID, nil)
		require.NoError(t, err)
		require.Equal(t, createdChat.ID, chatResult.ID)
		require.Equal(t, firstUser.UserID, chatResult.OwnerID)
		require.Equal(t, modelConfig.ID, chatResult.LastModelConfigID)
		require.Equal(t, "get chat route payload", chatResult.Title)
		require.NotZero(t, chatResult.CreatedAt)
		require.NotZero(t, chatResult.UpdatedAt)
		require.NotEmpty(t, messagesResult.Messages)
		require.Empty(t, messagesResult.QueuedMessages)

		foundUserMessage := false
		for _, message := range messagesResult.Messages {
			require.Equal(t, createdChat.ID, message.ChatID)
			require.NotEqual(t, codersdk.ChatMessageRoleSystem, message.Role)
			for _, part := range message.Content {
				if message.Role == codersdk.ChatMessageRoleUser &&
					part.Type == codersdk.ChatMessagePartTypeText &&
					part.Text == "get chat route payload" {
					foundUserMessage = true
				}
			}
		}
		require.True(t, foundUserMessage)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)

		createdChat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: firstUser.OrganizationID,
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "private chat",
				},
			},
		})
		require.NoError(t, err)

		otherClientRaw, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
		otherClient := codersdk.NewExperimentalClient(otherClientRaw)
		_, err = otherClient.GetChat(ctx, createdChat.ID)
		requireSDKError(t, err, http.StatusNotFound)
	})

	// AIGatewayDisabled regression-tests that getChat is a pure DB read that
	// still works when the AI Gateway is disabled and api.chatDaemon is nil.
	// It builds the server without starting the test AI bridge daemon
	// (unlike every other subtest here) and seeds the chat directly into the
	// database, since the create-chat route itself requires the daemon.
	t.Run("AIGatewayDisabled", func(t *testing.T) {
		t.Parallel()

		ctx := testutil.Context(t, testutil.WaitLong)
		values := coderdtest.DeploymentValues(t)
		require.NoError(t, values.AI.BridgeConfig.Enabled.Set("false"))
		opts := newChatTestOptions(t, values)
		rawClient, _, api := coderdtest.NewWithAPI(t, opts)
		client := codersdk.NewExperimentalClient(rawClient)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)

		modelConfig := dbgen.ChatModelConfig(t, api.Database, database.ChatModelConfig{})
		seededChat := dbgen.Chat(t, api.Database, database.Chat{
			OrganizationID:    firstUser.OrganizationID,
			OwnerID:           firstUser.UserID,
			LastModelConfigID: modelConfig.ID,
			Title:             "ai gateway disabled chat",
		})

		chatResult, err := client.GetChat(ctx, seededChat.ID)
		require.NoError(t, err)
		require.Equal(t, seededChat.ID, chatResult.ID)
		require.Equal(t, firstUser.UserID, chatResult.OwnerID)
		require.Equal(t, modelConfig.ID, chatResult.LastModelConfigID)
		require.Equal(t, "ai gateway disabled chat", chatResult.Title)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)

		// Upload a file.
		pngData := append([]byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}, make([]byte, 64)...)
		uploadResp, err := client.UploadChatFile(ctx, firstUser.OrganizationID, "image/png", "hydrated.png", bytes.NewReader(pngData))
		require.NoError(t, err)

		// Create a chat with a text + file part.
		chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: firstUser.OrganizationID,
			Content: []codersdk.ChatInputPart{
				{Type: codersdk.ChatInputPartTypeText, Text: "check file hydration"}, {Type: codersdk.ChatInputPartTypeFile, FileID: uploadResp.ID},
			},
		})
		require.NoError(t, err)

		// GET the chat — files must be hydrated with all metadata fields.
		chatResult, err := client.GetChat(ctx, chat.ID)
		require.NoError(t, err)
		require.Len(t, chatResult.Files, 1)
		f := chatResult.Files[0]
		require.Equal(t, uploadResp.ID, f.ID)
		require.Equal(t, firstUser.UserID, f.OwnerID)
		require.NotEqual(t, uuid.Nil, f.OrganizationID)
		require.Equal(t, "image/png", f.MimeType)
		require.Equal(t, "hydrated.png", f.Name)
		require.NotZero(t, f.CreatedAt)
	})

	// ToolCreatedFilesLinked exercises the DB path that chatd uses
	// when a tool (e.g. propose_plan) creates a file: InsertChatFile
	// then LinkChatFiles. This is a DB-level test because driving
	// the full chatd tool-call pipeline requires an LLM mock.
	t.Run("ToolCreatedFilesLinked", func(t *testing.T) {
		t.Parallel()

		ctx := testutil.Context(t, testutil.WaitLong)
		client, store := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)

		// Create a chat via the API so all metadata is set up.
		chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: firstUser.OrganizationID,
			Content: []codersdk.ChatInputPart{
				{Type: codersdk.ChatInputPartTypeText, Text: "tool file test"},
			},
		})
		require.NoError(t, err)

		// Mimic what chatd's StoreFile closure does:
		// 1. InsertChatFile
		// 2. LinkChatFiles
		//nolint:gocritic // Using AsChatd to mimic the chatd background worker.
		chatdCtx := dbauthz.AsChatd(ctx)
		fileRow, err := store.InsertChatFile(chatdCtx, database.InsertChatFileParams{
			OwnerID:        firstUser.UserID,
			OrganizationID: firstUser.OrganizationID,
			Name:           "plan.md",
			Mimetype:       "text/markdown",
			Data:           []byte("# Plan"),
		})
		require.NoError(t, err)

		rejected, err := store.LinkChatFiles(chatdCtx, database.LinkChatFilesParams{
			ChatID:       chat.ID,
			MaxFileLinks: int32(codersdk.DefaultChatMaxAttachmentsPerChat),
			FileIds:      []uuid.UUID{fileRow.ID},
		})
		require.NoError(t, err)
		require.Equal(t, int32(0), rejected, "0 rejected = all files linked")

		// Verify via the API that the file appears in the chat.
		chatResult, err := client.GetChat(ctx, chat.ID)
		require.NoError(t, err)
		require.Len(t, chatResult.Files, 1)
		f := chatResult.Files[0]
		require.Equal(t, fileRow.ID, f.ID)
		require.Equal(t, firstUser.UserID, f.OwnerID)
		require.Equal(t, firstUser.OrganizationID, f.OrganizationID)
		require.Equal(t, "plan.md", f.Name)
		require.Equal(t, "text/markdown", f.MimeType)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db, api := newChatClientWithAPIAndDatabase(t)
		user := coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		parentChat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: user.OrganizationID,
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "parent for getChat",
				},
			},
		})
		require.NoError(t, err)

		// The parent chat is created via the API, so the chat worker moves
		// it to running. Archiving is only allowed from a terminal state,
		// so wait for it to settle before archiving below.
		coderdtest.WaitForChatSettled(ctx, t, api, parentChat.ID)

		child := dbgen.Chat(t, db, database.Chat{
			OrganizationID:    user.OrganizationID,
			OwnerID:           user.UserID,
			LastModelConfigID: modelConfig.ID,
			Title:             "child for getChat",
			ParentChatID:      uuid.NullUUID{UUID: parentChat.ID, Valid: true},
			RootChatID:        uuid.NullUUID{UUID: parentChat.ID, Valid: true},
		})

		// Fetching the root chat should embed its children.
		result, err := client.GetChat(ctx, parentChat.ID)
		require.NoError(t, err)
		require.Len(t, result.Children, 1)
		require.Equal(t, child.ID, result.Children[0].ID)
		require.NotNil(t, result.Children[0].ParentChatID)
		require.Equal(t, parentChat.ID, *result.Children[0].ParentChatID)

		// Fetching a child chat should not have children.
		childResult, err := client.GetChat(ctx, child.ID)
		require.NoError(t, err)
		require.NotNil(t, childResult.Children)
		require.Empty(t, childResult.Children)

		// An archived root should still embed its cascaded
		// archived children (guards against the filter getting
		// hardcoded to false).
		err = client.UpdateChat(ctx, parentChat.ID, codersdk.UpdateChatRequest{Archived: ptr.Ref(true)})
		require.NoError(t, err)

		archivedResult, err := client.GetChat(ctx, parentChat.ID)
		require.NoError(t, err)
		require.True(t, archivedResult.Archived, "root should be archived")
		require.Len(t, archivedResult.Children, 1, "archived root should embed its archived child")
		require.Equal(t, child.ID, archivedResult.Children[0].ID)
		require.True(t, archivedResult.Children[0].Archived, "embedded child should be archived")
	})
}

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

	insertUserMessage := func(
		t *testing.T,
		ctx context.Context,
		db database.Store,
		chatID uuid.UUID,
		modelConfigID uuid.UUID,
		userID uuid.UUID,
		parts []codersdk.ChatMessagePart,
		visibility database.ChatMessageVisibility,
		deleted bool,
	) database.ChatMessage {
		t.Helper()
		content, err := chatprompt.MarshalParts(parts)
		require.NoError(t, err)
		msgs, err := db.InsertChatMessages(dbauthz.AsSystemRestricted(ctx), database.InsertChatMessagesParams{
			ChatID:              chatID,
			CreatedBy:           []uuid.UUID{userID},
			ModelConfigID:       []uuid.UUID{modelConfigID},
			Role:                []database.ChatMessageRole{database.ChatMessageRoleUser},
			ContentVersion:      []int16{chatprompt.CurrentContentVersion},
			Content:             []string{string(content.RawMessage)},
			Visibility:          []database.ChatMessageVisibility{visibility},
			InputTokens:         []int64{0},
			OutputTokens:        []int64{0},
			TotalTokens:         []int64{0},
			ReasoningTokens:     []int64{0},
			CacheCreationTokens: []int64{0},
			CacheReadTokens:     []int64{0},
			ContextLimit:        []int64{0},
			Compressed:          []bool{false},
			RuntimeMs:           []int64{0},
		})
		require.NoError(t, err)
		require.Len(t, msgs, 1)
		if deleted {
			require.NoError(t, db.SoftDeleteChatMessageByID(dbauthz.AsSystemRestricted(ctx), msgs[0].ID))
		}
		return database.ChatMessage(msgs[0])
	}

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		user := coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		chat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{
			OrganizationID:    user.OrganizationID,
			Status:            database.ChatStatusWaiting,
			ClientType:        database.ChatClientTypeUi,
			OwnerID:           user.UserID,
			LastModelConfigID: modelConfig.ID,
			Title:             "prompts route test",
		})
		require.NoError(t, err)

		// Older user prompt with multiple text parts that need
		// concatenation in original order.
		want1 := insertUserMessage(t, ctx, db, chat.ID, modelConfig.ID, user.UserID,
			[]codersdk.ChatMessagePart{
				{Type: codersdk.ChatMessagePartTypeText, Text: "first "},
				{Type: codersdk.ChatMessagePartTypeText, Text: "prompt"},
			},
			database.ChatMessageVisibilityBoth, false,
		)

		// User prompt with a non-text part interleaved; only text
		// parts should appear in the response, joined verbatim.
		want2 := insertUserMessage(t, ctx, db, chat.ID, modelConfig.ID, user.UserID,
			[]codersdk.ChatMessagePart{
				{Type: codersdk.ChatMessagePartTypeText, Text: "hello "},
				{Type: codersdk.ChatMessagePartTypeFile, MediaType: "text/plain", Data: []byte("x")},
				{Type: codersdk.ChatMessagePartTypeText, Text: "world"},
			},
			database.ChatMessageVisibilityBoth, false,
		)

		// Whitespace-only prompt; must be filtered out by the
		// HAVING clause so cycling never lands on a blank entry.
		insertUserMessage(t, ctx, db, chat.ID, modelConfig.ID, user.UserID,
			[]codersdk.ChatMessagePart{
				{Type: codersdk.ChatMessagePartTypeText, Text: "   \n\t  "},
			},
			database.ChatMessageVisibilityBoth, false,
		)

		// Assistant-role message with otherwise-valid content;
		// the SQL filter cm.role = 'user' must exclude it from
		// the response.
		assistantContent, err := chatprompt.MarshalParts([]codersdk.ChatMessagePart{
			{Type: codersdk.ChatMessagePartTypeText, Text: "assistant reply"},
		})
		require.NoError(t, err)
		_, err = db.InsertChatMessages(dbauthz.AsSystemRestricted(ctx), database.InsertChatMessagesParams{
			ChatID:              chat.ID,
			CreatedBy:           []uuid.UUID{user.UserID},
			ModelConfigID:       []uuid.UUID{modelConfig.ID},
			Role:                []database.ChatMessageRole{database.ChatMessageRoleAssistant},
			ContentVersion:      []int16{chatprompt.CurrentContentVersion},
			Content:             []string{string(assistantContent.RawMessage)},
			Visibility:          []database.ChatMessageVisibility{database.ChatMessageVisibilityBoth},
			InputTokens:         []int64{0},
			OutputTokens:        []int64{0},
			TotalTokens:         []int64{0},
			ReasoningTokens:     []int64{0},
			CacheCreationTokens: []int64{0},
			CacheReadTokens:     []int64{0},
			ContextLimit:        []int64{0},
			Compressed:          []bool{false},
			RuntimeMs:           []int64{0},
		})
		require.NoError(t, err)

		// Legacy V0 user message stored as a scalar JSON string
		// (predates migration 000434). The jsonb_typeof guard in
		// GetChatUserPromptsByChatID must silently exclude this row;
		// without the guard, jsonb_array_elements would raise
		// "cannot extract elements from a scalar" and the request
		// would 500.
		_, err = db.InsertChatMessages(dbauthz.AsSystemRestricted(ctx), database.InsertChatMessagesParams{
			ChatID:              chat.ID,
			CreatedBy:           []uuid.UUID{user.UserID},
			ModelConfigID:       []uuid.UUID{modelConfig.ID},
			Role:                []database.ChatMessageRole{database.ChatMessageRoleUser},
			ContentVersion:      []int16{0},
			Content:             []string{`"plain text from V0"`},
			Visibility:          []database.ChatMessageVisibility{database.ChatMessageVisibilityBoth},
			InputTokens:         []int64{0},
			OutputTokens:        []int64{0},
			TotalTokens:         []int64{0},
			ReasoningTokens:     []int64{0},
			CacheCreationTokens: []int64{0},
			CacheReadTokens:     []int64{0},
			ContextLimit:        []int64{0},
			Compressed:          []bool{false},
			RuntimeMs:           []int64{0},
		})
		require.NoError(t, err)

		// Soft-deleted prompt; must not appear.
		insertUserMessage(t, ctx, db, chat.ID, modelConfig.ID, user.UserID,
			[]codersdk.ChatMessagePart{
				{Type: codersdk.ChatMessagePartTypeText, Text: "deleted prompt"},
			},
			database.ChatMessageVisibilityBoth, true,
		)

		// Model-only visibility prompt; must not appear (composer
		// only shows what the user actually typed).
		insertUserMessage(t, ctx, db, chat.ID, modelConfig.ID, user.UserID,
			[]codersdk.ChatMessagePart{
				{Type: codersdk.ChatMessagePartTypeText, Text: "model only"},
			},
			database.ChatMessageVisibilityModel, false,
		)

		// Newest user-visible prompt; should come first in the
		// response.
		want3 := insertUserMessage(t, ctx, db, chat.ID, modelConfig.ID, user.UserID,
			[]codersdk.ChatMessagePart{
				{Type: codersdk.ChatMessagePartTypeText, Text: "newest prompt"},
			},
			database.ChatMessageVisibilityUser, false,
		)

		resp, err := client.GetChatPrompts(ctx, chat.ID, nil)
		require.NoError(t, err)
		require.Len(t, resp.Prompts, 3, "expected exactly the three user-visible non-blank prompts")

		require.Equal(t, want3.ID, resp.Prompts[0].ID)
		require.Equal(t, "newest prompt", resp.Prompts[0].Text)
		require.Equal(t, want2.ID, resp.Prompts[1].ID)
		require.Equal(t, "hello world", resp.Prompts[1].Text)
		require.Equal(t, want1.ID, resp.Prompts[2].ID)
		require.Equal(t, "first prompt", resp.Prompts[2].Text)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		user := coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		chat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{
			OrganizationID:    user.OrganizationID,
			Status:            database.ChatStatusWaiting,
			ClientType:        database.ChatClientTypeUi,
			OwnerID:           user.UserID,
			LastModelConfigID: modelConfig.ID,
			Title:             "prompts limit test",
		})
		require.NoError(t, err)

		for i := range 5 {
			insertUserMessage(t, ctx, db, chat.ID, modelConfig.ID, user.UserID,
				[]codersdk.ChatMessagePart{
					{Type: codersdk.ChatMessagePartTypeText, Text: fmt.Sprintf("prompt %d", i)},
				},
				database.ChatMessageVisibilityBoth, false,
			)
		}

		resp, err := client.GetChatPrompts(ctx, chat.ID, &codersdk.ChatPromptsOptions{Limit: 2})
		require.NoError(t, err)
		require.Len(t, resp.Prompts, 2)
		require.Equal(t, "prompt 4", resp.Prompts[0].Text)
		require.Equal(t, "prompt 3", resp.Prompts[1].Text)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		user := coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		chat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{
			OrganizationID:    user.OrganizationID,
			Status:            database.ChatStatusWaiting,
			ClientType:        database.ChatClientTypeUi,
			OwnerID:           user.UserID,
			LastModelConfigID: modelConfig.ID,
			Title:             "prompts invalid limit test",
		})
		require.NoError(t, err)

		_, err = client.GetChatPrompts(ctx, chat.ID, &codersdk.ChatPromptsOptions{Limit: 5000})
		require.Error(t, err)
		var sdkErr *codersdk.Error
		require.ErrorAs(t, err, &sdkErr)
		require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		chat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{
			OrganizationID:    firstUser.OrganizationID,
			Status:            database.ChatStatusWaiting,
			ClientType:        database.ChatClientTypeUi,
			OwnerID:           firstUser.UserID,
			LastModelConfigID: modelConfig.ID,
			Title:             "prompts cross-owner test",
		})
		require.NoError(t, err)

		insertUserMessage(t, ctx, db, chat.ID, modelConfig.ID, firstUser.UserID,
			[]codersdk.ChatMessagePart{
				{Type: codersdk.ChatMessagePartTypeText, Text: "private prompt"},
			},
			database.ChatMessageVisibilityBoth, false,
		)

		memberClient, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
		memberExp := codersdk.NewExperimentalClient(memberClient)
		_, err = memberExp.GetChatPrompts(ctx, chat.ID, nil)
		require.Error(t, err)
		var sdkErr *codersdk.Error
		require.ErrorAs(t, err, &sdkErr)
		require.Equal(t, http.StatusNotFound, sdkErr.StatusCode())
	})

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

		// Boundary: a chat with no user-visible prompts must
		// serialize to {"prompts":[]}, not {"prompts":null}, so
		// the composer's cycle code can branch on len() without
		// guarding against nil. We exercise both branches: a chat
		// with zero messages, and a chat that has only an
		// assistant message (the SQL filter excludes it).
		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		user := coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		emptyChat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{
			OrganizationID:    user.OrganizationID,
			Status:            database.ChatStatusWaiting,
			ClientType:        database.ChatClientTypeUi,
			OwnerID:           user.UserID,
			LastModelConfigID: modelConfig.ID,
			Title:             "prompts empty chat test",
		})
		require.NoError(t, err)

		resp, err := client.GetChatPrompts(ctx, emptyChat.ID, nil)
		require.NoError(t, err)
		require.NotNil(t, resp.Prompts, "prompts must be [] not nil")
		require.Empty(t, resp.Prompts)

		assistantOnlyChat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{
			OrganizationID:    user.OrganizationID,
			Status:            database.ChatStatusWaiting,
			ClientType:        database.ChatClientTypeUi,
			OwnerID:           user.UserID,
			LastModelConfigID: modelConfig.ID,
			Title:             "prompts assistant-only chat test",
		})
		require.NoError(t, err)

		assistantContent, err := chatprompt.MarshalParts([]codersdk.ChatMessagePart{
			{Type: codersdk.ChatMessagePartTypeText, Text: "assistant reply"},
		})
		require.NoError(t, err)
		_, err = db.InsertChatMessages(dbauthz.AsSystemRestricted(ctx), database.InsertChatMessagesParams{
			ChatID:              assistantOnlyChat.ID,
			CreatedBy:           []uuid.UUID{user.UserID},
			ModelConfigID:       []uuid.UUID{modelConfig.ID},
			Role:                []database.ChatMessageRole{database.ChatMessageRoleAssistant},
			ContentVersion:      []int16{chatprompt.CurrentContentVersion},
			Content:             []string{string(assistantContent.RawMessage)},
			Visibility:          []database.ChatMessageVisibility{database.ChatMessageVisibilityBoth},
			InputTokens:         []int64{0},
			OutputTokens:        []int64{0},
			TotalTokens:         []int64{0},
			ReasoningTokens:     []int64{0},
			CacheCreationTokens: []int64{0},
			CacheReadTokens:     []int64{0},
			ContextLimit:        []int64{0},
			Compressed:          []bool{false},
			RuntimeMs:           []int64{0},
		})
		require.NoError(t, err)

		resp, err = client.GetChatPrompts(ctx, assistantOnlyChat.ID, nil)
		require.NoError(t, err)
		require.NotNil(t, resp.Prompts, "prompts must be [] not nil")
		require.Empty(t, resp.Prompts)
	})
}

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

	insertMessage := func(
		t *testing.T,
		ctx context.Context,
		db database.Store,
		chat database.Chat,
		role database.ChatMessageRole,
		parts []codersdk.ChatMessagePart,
		visibility database.ChatMessageVisibility,
		deleted bool,
	) database.ChatMessage {
		t.Helper()
		content, err := chatprompt.MarshalParts(parts)
		require.NoError(t, err)
		msgs, err := db.InsertChatMessages(dbauthz.AsSystemRestricted(ctx), database.InsertChatMessagesParams{
			ChatID:              chat.ID,
			CreatedBy:           []uuid.UUID{chat.OwnerID},
			ModelConfigID:       []uuid.UUID{chat.LastModelConfigID},
			Role:                []database.ChatMessageRole{role},
			ContentVersion:      []int16{chatprompt.CurrentContentVersion},
			Content:             []string{string(content.RawMessage)},
			Visibility:          []database.ChatMessageVisibility{visibility},
			InputTokens:         []int64{0},
			OutputTokens:        []int64{0},
			TotalTokens:         []int64{0},
			ReasoningTokens:     []int64{0},
			CacheCreationTokens: []int64{0},
			CacheReadTokens:     []int64{0},
			ContextLimit:        []int64{0},
			Compressed:          []bool{false},
			RuntimeMs:           []int64{0},
		})
		require.NoError(t, err)
		require.Len(t, msgs, 1)
		if deleted {
			require.NoError(t, db.SoftDeleteChatMessageByID(dbauthz.AsSystemRestricted(ctx), msgs[0].ID))
		}
		return database.ChatMessage(msgs[0])
	}

	insertChat := func(t *testing.T, ctx context.Context, db database.Store, user codersdk.CreateFirstUserResponse, modelConfigID uuid.UUID) database.Chat {
		t.Helper()
		chat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{
			OrganizationID:    user.OrganizationID,
			Status:            database.ChatStatusWaiting,
			ClientType:        database.ChatClientTypeUi,
			OwnerID:           user.UserID,
			LastModelConfigID: modelConfigID,
			Title:             "attachments route test",
		})
		require.NoError(t, err)
		return chat
	}

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		user := coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)
		chat := insertChat(t, ctx, db, user, modelConfig.ID)

		fileA := codersdk.ChatMessageFile(uuid.New(), "image/png", "a.png")
		fileB := codersdk.ChatMessageFile(uuid.New(), "text/plain", "b.txt")
		fileC := codersdk.ChatMessageFile(uuid.New(), "text/plain", "c.txt")
		fileD := codersdk.ChatMessageFile(uuid.New(), "image/png", "d.png")
		text := codersdk.ChatMessagePart{Type: codersdk.ChatMessagePartTypeText, Text: "hello"}

		userUpload := insertMessage(t, ctx, db, chat, database.ChatMessageRoleUser,
			[]codersdk.ChatMessagePart{text, fileA},
			database.ChatMessageVisibilityBoth, false)
		agentFiles := insertMessage(t, ctx, db, chat, database.ChatMessageRoleAssistant,
			[]codersdk.ChatMessagePart{fileB, text, fileC},
			database.ChatMessageVisibilityBoth, false)
		// Each of these hides its file part from the user, so none may
		// appear in the response.
		insertMessage(t, ctx, db, chat, database.ChatMessageRoleUser,
			[]codersdk.ChatMessagePart{codersdk.ChatMessageFile(uuid.New(), "text/plain", "deleted.txt")},
			database.ChatMessageVisibilityBoth, true)
		insertMessage(t, ctx, db, chat, database.ChatMessageRoleUser,
			[]codersdk.ChatMessagePart{codersdk.ChatMessageFile(uuid.New(), "text/plain", "model.txt")},
			database.ChatMessageVisibilityModel, false)
		insertMessage(t, ctx, db, chat, database.ChatMessageRoleTool,
			[]codersdk.ChatMessagePart{codersdk.ChatMessageFile(uuid.New(), "image/png", "tool.png")},
			database.ChatMessageVisibilityBoth, false)
		insertMessage(t, ctx, db, chat, database.ChatMessageRoleUser,
			[]codersdk.ChatMessagePart{text},
			database.ChatMessageVisibilityBoth, false)
		// The hook-context part is stripped from client responses, so the
		// file part's index must shift to match the messages endpoint.
		hookPrompt := insertMessage(t, ctx, db, chat, database.ChatMessageRoleUser,
			[]codersdk.ChatMessagePart{
				{Type: codersdk.ChatMessagePartTypeHookContext, Text: "model only"},
				fileD,
			},
			database.ChatMessageVisibilityUser, false)
		// An inline part keeps its bytes because it has no stored file.
		inlinePaste := insertMessage(t, ctx, db, chat, database.ChatMessageRoleUser,
			[]codersdk.ChatMessagePart{
				{Type: codersdk.ChatMessagePartTypeFile, MediaType: "text/plain", Data: []byte("pasted")},
			},
			database.ChatMessageVisibilityBoth, false)

		resp, err := client.GetChatAttachments(ctx, chat.ID, nil)
		require.NoError(t, err)

		type attachment struct {
			MessageID int64
			PartIndex int
			Role      codersdk.ChatAttachmentRole
			FileID    uuid.UUID
		}
		got := make([]attachment, 0, len(resp.Attachments))
		for _, a := range resp.Attachments {
			got = append(got, attachment{a.MessageID, a.PartIndex, a.Role, a.File.FileID.UUID})
		}
		require.Equal(t, []attachment{
			{inlinePaste.ID, 0, codersdk.ChatAttachmentRoleUser, uuid.Nil},
			{hookPrompt.ID, 0, codersdk.ChatAttachmentRoleUser, fileD.FileID.UUID},
			{agentFiles.ID, 2, codersdk.ChatAttachmentRoleAssistant, fileC.FileID.UUID},
			{agentFiles.ID, 0, codersdk.ChatAttachmentRoleAssistant, fileB.FileID.UUID},
			{userUpload.ID, 1, codersdk.ChatAttachmentRoleUser, fileA.FileID.UUID},
		}, got)

		messages, err := client.GetChatMessages(ctx, chat.ID, nil)
		require.NoError(t, err)
		contentByID := make(map[int64][]codersdk.ChatMessagePart, len(messages.Messages))
		for _, m := range messages.Messages {
			contentByID[m.ID] = m.Content
		}
		for _, a := range resp.Attachments {
			content := contentByID[a.MessageID]
			require.Less(t, a.PartIndex, len(content))
			part := content[a.PartIndex]
			require.Equal(t, codersdk.ChatMessagePartTypeFile, part.Type)
			require.Equal(t, codersdk.ChatAttachmentFile{
				Type:      codersdk.ChatAttachmentFileTypeFile,
				MediaType: part.MediaType,
				Name:      part.Name,
				Data:      part.Data,
				FileID:    part.FileID,
			}, a.File, "attachment must carry the messages endpoint part's file fields")
		}

		before, err := client.GetChatAttachments(ctx, chat.ID, &codersdk.ChatAttachmentsOptions{BeforeID: agentFiles.ID})
		require.NoError(t, err)
		require.Len(t, before.Attachments, 1)
		require.Equal(t, userUpload.ID, before.Attachments[0].MessageID)

		none, err := client.GetChatAttachments(ctx, chat.ID, &codersdk.ChatAttachmentsOptions{BeforeID: userUpload.ID})
		require.NoError(t, err)
		require.NotNil(t, none.Attachments, "attachments must be [] not nil")
		require.Empty(t, none.Attachments)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		user := coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)
		chat := insertChat(t, ctx, db, user, modelConfig.ID)
		stored := codersdk.ChatMessageFile(uuid.New(), "text/plain", "stored.txt")
		insertMessage(t, ctx, db, chat, database.ChatMessageRoleUser,
			[]codersdk.ChatMessagePart{
				stored,
				{Type: codersdk.ChatMessagePartTypeFile, MediaType: "text/plain", Data: []byte("pasted")},
			},
			database.ChatMessageVisibilityBoth, false)

		res, err := client.Request(ctx, http.MethodGet,
			fmt.Sprintf("/api/v2/chats/%s/attachments", chat.ID), nil)
		require.NoError(t, err)
		defer res.Body.Close()
		require.Equal(t, http.StatusOK, res.StatusCode)
		var body struct {
			Attachments []struct {
				PartIndex int                        `json:"part_index"`
				File      map[string]json.RawMessage `json:"file"`
			} `json:"attachments"`
		}
		require.NoError(t, json.NewDecoder(res.Body).Decode(&body))
		files := make(map[int]map[string]json.RawMessage, len(body.Attachments))
		for _, a := range body.Attachments {
			files[a.PartIndex] = a.File
		}
		require.Len(t, files, 2)

		require.JSONEq(t, fmt.Sprintf("%q", stored.FileID.UUID), string(files[0]["file_id"]))
		require.NotContains(t, files[1], "file_id", "an inline file must omit file_id, not send null")
		var data string
		require.NoError(t, json.Unmarshal(files[1]["data"], &data), "data must be a base64 string")
		require.Equal(t, base64.StdEncoding.EncodeToString([]byte("pasted")), data)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		user := coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)
		chat := insertChat(t, ctx, db, user, modelConfig.ID)

		res, err := client.Request(ctx, http.MethodGet,
			fmt.Sprintf("/api/v2/chats/%s/attachments?before_id=-1", chat.ID), nil)
		require.NoError(t, err)
		defer res.Body.Close()
		require.Equal(t, http.StatusBadRequest, res.StatusCode)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)
		chat := insertChat(t, ctx, db, firstUser, modelConfig.ID)
		insertMessage(t, ctx, db, chat, database.ChatMessageRoleUser,
			[]codersdk.ChatMessagePart{codersdk.ChatMessageFile(uuid.New(), "text/plain", "private.txt")},
			database.ChatMessageVisibilityBoth, false)

		memberClient, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
		memberExp := codersdk.NewExperimentalClient(memberClient)
		_, err := memberExp.GetChatAttachments(ctx, chat.ID, nil)
		require.Error(t, err)
		var sdkErr *codersdk.Error
		require.ErrorAs(t, err, &sdkErr)
		require.Equal(t, http.StatusNotFound, sdkErr.StatusCode())
	})
}

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

	createChat := func(ctx context.Context, t *testing.T, client *codersdk.ExperimentalClient, orgID uuid.UUID, text string) codersdk.Chat {
		t.Helper()

		chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: orgID,
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: text,
				},
			},
		})
		require.NoError(t, err)
		return chat
	}

	getChat := func(ctx context.Context, t *testing.T, client *codersdk.ExperimentalClient, chatID uuid.UUID) codersdk.Chat {
		t.Helper()

		chat, err := client.GetChat(ctx, chatID)
		require.NoError(t, err)
		return chat
	}

	createStoredChat := func(
		ctx context.Context,
		t *testing.T,
		db database.Store,
		ownerID uuid.UUID,
		orgID uuid.UUID,
		modelConfigID uuid.UUID,
		title string,
	) codersdk.Chat {
		t.Helper()

		dbChat := dbgen.Chat(t, db, database.Chat{
			OrganizationID:    orgID,
			OwnerID:           ownerID,
			LastModelConfigID: modelConfigID,
			Title:             title,
		})
		return db2sdk.Chat(dbChat, nil, nil)
	}

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

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

			ctx := testutil.Context(t, testutil.WaitLong)
			mAudit := audit.NewMock()
			client := newChatClient(t, func(opts *coderdtest.Options) {
				opts.Auditor = mAudit
			})
			firstUser := coderdtest.CreateFirstUser(t, client.Client)
			_ = createChatModel(t, client)

			chat := createChat(ctx, t, client, firstUser.OrganizationID, "set plan mode")
			err := client.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{
				PlanMode: ptr.Ref(codersdk.ChatPlanModePlan),
			})
			require.NoError(t, err)

			updated := getChat(ctx, t, client, chat.ID)
			require.Equal(t, codersdk.ChatPlanModePlan, updated.PlanMode)
			require.True(t, mAudit.Contains(t, database.AuditLog{
				Action:       database.AuditActionWrite,
				ResourceType: database.ResourceTypeChat,
				ResourceID:   chat.ID,
				UserID:       firstUser.UserID,
			}))
		})

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

			ctx := testutil.Context(t, testutil.WaitLong)
			mAudit := audit.NewMock()
			client := newChatClient(t, func(opts *coderdtest.Options) {
				opts.Auditor = mAudit
			})
			firstUser := coderdtest.CreateFirstUser(t, client.Client)
			_ = createChatModel(t, client)

			chat := createChat(ctx, t, client, firstUser.OrganizationID, "clear plan mode")
			err := client.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{
				PlanMode: ptr.Ref(codersdk.ChatPlanModePlan),
			})
			require.NoError(t, err)

			err = client.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{
				PlanMode: ptr.Ref(codersdk.ChatPlanMode("")),
			})
			require.NoError(t, err)

			updated := getChat(ctx, t, client, chat.ID)
			require.Empty(t, updated.PlanMode)
			require.True(t, mAudit.Contains(t, database.AuditLog{
				Action:       database.AuditActionWrite,
				ResourceType: database.ResourceTypeChat,
				ResourceID:   chat.ID,
				UserID:       firstUser.UserID,
			}))
		})

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

			ctx := testutil.Context(t, testutil.WaitLong)
			mAudit := audit.NewMock()
			client := newChatClient(t, func(opts *coderdtest.Options) {
				opts.Auditor = mAudit
			})
			firstUser := coderdtest.CreateFirstUser(t, client.Client)
			_ = createChatModel(t, client)

			chat := createChat(ctx, t, client, firstUser.OrganizationID, "invalid plan mode")
			err := client.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{
				PlanMode: ptr.Ref(codersdk.ChatPlanMode("invalid")),
			})
			sdkErr := requireSDKError(t, err, http.StatusBadRequest)
			require.Equal(t, "Invalid plan_mode value.", sdkErr.Message)
			require.True(t, mAudit.Contains(t, database.AuditLog{
				Action:       database.AuditActionWrite,
				ResourceType: database.ResourceTypeChat,
				ResourceID:   chat.ID,
				UserID:       firstUser.UserID,
			}))
		})
	})

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

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

			ctx := testutil.Context(t, testutil.WaitLong)
			mAudit := audit.NewMock()
			client, db := newChatClientWithDatabase(t, func(opts *coderdtest.Options) {
				opts.Auditor = mAudit
			})
			firstUser := coderdtest.CreateFirstUser(t, client.Client)
			modelConfig := createChatModel(t, client)

			workspaceBuild := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
				OrganizationID: firstUser.OrganizationID,
				OwnerID:        firstUser.UserID,
			}).Seed(database.WorkspaceBuild{
				HasExternalAgent: sql.NullBool{Bool: true, Valid: true},
			}).WithAgent().Do()
			chat := createStoredChat(
				ctx,
				t,
				db,
				firstUser.UserID,
				firstUser.OrganizationID,
				modelConfig.ID,
				"bind workspace",
			)

			err := client.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{
				WorkspaceID: &workspaceBuild.Workspace.ID,
			})
			require.NoError(t, err)

			updated := getChat(ctx, t, client, chat.ID)
			require.NotNil(t, updated.WorkspaceID)
			require.Equal(t, workspaceBuild.Workspace.ID, *updated.WorkspaceID)
			require.True(t, mAudit.Contains(t, database.AuditLog{
				Action:       database.AuditActionWrite,
				ResourceType: database.ResourceTypeChat,
				ResourceID:   chat.ID,
				UserID:       firstUser.UserID,
			}))
		})

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

			ctx := testutil.Context(t, testutil.WaitLong)
			mAudit := audit.NewMock()
			client, db := newChatClientWithDatabase(t, func(opts *coderdtest.Options) {
				opts.Auditor = mAudit
			})
			firstUser := coderdtest.CreateFirstUser(t, client.Client)
			modelConfig := createChatModel(t, client)

			chat := createStoredChat(
				ctx,
				t,
				db,
				firstUser.UserID,
				firstUser.OrganizationID,
				modelConfig.ID,
				"missing workspace",
			)
			workspaceID := uuid.New()
			err := client.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{
				WorkspaceID: &workspaceID,
			})
			sdkErr := requireSDKError(t, err, http.StatusBadRequest)
			require.Equal(t, "Workspace not found or you do not have access to this resource", sdkErr.Message)
			require.True(t, mAudit.Contains(t, database.AuditLog{
				Action:       database.AuditActionWrite,
				ResourceType: database.ResourceTypeChat,
				ResourceID:   chat.ID,
				UserID:       firstUser.UserID,
			}))
		})

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

			ctx := testutil.Context(t, testutil.WaitLong)
			mAudit := audit.NewMock()
			client, db := newChatClientWithDatabase(t, func(opts *coderdtest.Options) {
				opts.Auditor = mAudit
			})
			firstUser := coderdtest.CreateFirstUser(t, client.Client)
			modelConfig := createChatModel(t, client)

			secondOrg := dbgen.Organization(t, db, database.Organization{})
			dbgen.OrganizationMember(t, db, database.OrganizationMember{
				OrganizationID: secondOrg.ID,
				UserID:         firstUser.UserID,
			})
			workspaceBuild := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
				OrganizationID: secondOrg.ID,
				OwnerID:        firstUser.UserID,
			}).WithAgent().Do()
			chat := createStoredChat(
				ctx,
				t,
				db,
				firstUser.UserID,
				firstUser.OrganizationID,
				modelConfig.ID,
				"cross org workspace binding",
			)

			err := client.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{
				WorkspaceID: &workspaceBuild.Workspace.ID,
			})
			sdkErr := requireSDKError(t, err, http.StatusBadRequest)
			require.Equal(t, "Workspace does not belong to this chat's organization.", sdkErr.Message)
			require.True(t, mAudit.Contains(t, database.AuditLog{
				Action:       database.AuditActionWrite,
				ResourceType: database.ResourceTypeChat,
				ResourceID:   chat.ID,
				UserID:       firstUser.UserID,
			}))
		})

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

			ctx := testutil.Context(t, testutil.WaitLong)
			mAudit := audit.NewMock()
			client, db := newChatClientWithDatabase(t, func(opts *coderdtest.Options) {
				opts.Auditor = mAudit
			})
			firstUser := coderdtest.CreateFirstUser(t, client.Client)
			modelConfig := createChatModel(t, client)

			workspaceBuild := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
				OrganizationID: firstUser.OrganizationID,
				OwnerID:        firstUser.UserID,
			}).WithAgent().Do()
			chat := createStoredChat(
				ctx,
				t,
				db,
				firstUser.UserID,
				firstUser.OrganizationID,
				modelConfig.ID,
				"clear workspace binding",
			)

			err := client.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{
				WorkspaceID: &workspaceBuild.Workspace.ID,
			})
			require.NoError(t, err)

			workspaceID := uuid.Nil
			err = client.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{
				WorkspaceID: &workspaceID,
			})
			require.NoError(t, err)

			updated := getChat(ctx, t, client, chat.ID)
			require.Nil(t, updated.WorkspaceID)
			require.Nil(t, updated.BuildID)
			require.Nil(t, updated.AgentID)
			require.True(t, mAudit.Contains(t, database.AuditLog{
				Action:       database.AuditActionWrite,
				ResourceType: database.ResourceTypeChat,
				ResourceID:   chat.ID,
				UserID:       firstUser.UserID,
			}))
		})

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

			ctx := testutil.Context(t, testutil.WaitLong)
			client, db := newChatClientWithDatabase(t)
			firstUser := coderdtest.CreateFirstUser(t, client.Client)
			modelConfig := createChatModel(t, client)
			_, member := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)

			memberWorkspace := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
				OrganizationID: firstUser.OrganizationID,
				OwnerID:        member.ID,
			}).WithAgent().Do()
			// The deployment owner can SSH into its own workspace, so only
			// the ownership check can stop this rebind.
			ownerWorkspace := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
				OrganizationID: firstUser.OrganizationID,
				OwnerID:        firstUser.UserID,
			}).WithAgent().Do()
			chat := dbgen.Chat(t, db, database.Chat{
				OrganizationID:    firstUser.OrganizationID,
				OwnerID:           member.ID,
				LastModelConfigID: modelConfig.ID,
				Title:             "member chat",
				WorkspaceID:       uuid.NullUUID{UUID: memberWorkspace.Workspace.ID, Valid: true},
			})

			for _, workspaceID := range []uuid.UUID{ownerWorkspace.Workspace.ID, uuid.Nil} {
				err := client.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{
					Title:       ptr.Ref("renamed by admin"),
					WorkspaceID: &workspaceID,
				})
				sdkErr := requireSDKError(t, err, http.StatusForbidden)
				require.Equal(t, "Only the chat owner can change its workspace.", sdkErr.Message)

				persisted := getChat(ctx, t, client, chat.ID)
				require.Equal(t, "member chat", persisted.Title)
				require.NotNil(t, persisted.WorkspaceID)
				require.Equal(t, memberWorkspace.Workspace.ID, *persisted.WorkspaceID)
			}
		})
	})

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

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

			ctx := testutil.Context(t, testutil.WaitLong)
			client, api := newChatClientWithAPI(t)
			firstUser := coderdtest.CreateFirstUser(t, client.Client)
			_ = createChatModel(t, client)

			chat := createChat(ctx, t, client, firstUser.OrganizationID, "original title")

			coderdtest.WaitForChatSettled(ctx, t, api, chat.ID)

			err := client.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{
				Title: ptr.Ref("renamed title"),
			})
			require.NoError(t, err)

			updated := getChat(ctx, t, client, chat.ID)
			require.Equal(t, "renamed title", updated.Title)
		})

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

			ctx := testutil.Context(t, testutil.WaitLong)
			client, api := newChatClientWithAPI(t)
			firstUser := coderdtest.CreateFirstUser(t, client.Client)
			_ = createChatModel(t, client)

			chat := createChat(ctx, t, client, firstUser.OrganizationID, "before trim")

			coderdtest.WaitForChatSettled(ctx, t, api, chat.ID)

			err := client.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{
				Title: ptr.Ref("   padded title   "),
			})
			require.NoError(t, err)

			updated := getChat(ctx, t, client, chat.ID)
			require.Equal(t, "padded title", updated.Title)
		})

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

			ctx := testutil.Context(t, testutil.WaitLong)
			client := newChatClient(t)
			firstUser := coderdtest.CreateFirstUser(t, client.Client)
			_ = createChatModel(t, client)

			chat := createChat(ctx, t, client, firstUser.OrganizationID, "keep original")

			err := client.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{
				Title: ptr.Ref("   "),
			})
			requireSDKError(t, err, http.StatusBadRequest)

			updated := getChat(ctx, t, client, chat.ID)
			require.Equal(t, chat.Title, updated.Title)
		})

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

			ctx := testutil.Context(t, testutil.WaitLong)
			client, db := newChatClientWithDatabase(t)
			firstUser := coderdtest.CreateFirstUser(t, client.Client)
			modelConfig := createChatModel(t, client)
			chat := createStoredChat(ctx, t, db, firstUser.UserID, firstUser.OrganizationID, modelConfig.ID, "stored title")
			require.Equal(t, codersdk.ChatTitleSourceFallback, chat.TitleSource)

			invalid := []struct {
				field string
				req   codersdk.UpdateChatRequest
			}{
				{"labels", codersdk.UpdateChatRequest{Labels: &map[string]string{"": "bad"}}},
				{"archived", codersdk.UpdateChatRequest{Archived: new(false)}},
				{"pin_order", codersdk.UpdateChatRequest{PinOrder: new(int32(-1))}},
				{"workspace_id", codersdk.UpdateChatRequest{WorkspaceID: new(uuid.New())}},
				{"plan_mode", codersdk.UpdateChatRequest{PlanMode: new(codersdk.ChatPlanMode("invalid"))}},
			}
			for _, tc := range invalid {
				for _, title := range []string{chat.Title, "new title"} {
					req := tc.req
					req.Title = &title
					err := client.UpdateChat(ctx, chat.ID, req)
					requireSDKError(t, err, http.StatusBadRequest)

					stored := getChat(ctx, t, client, chat.ID)
					require.Equal(t, chat.Title, stored.Title, "invalid %s, title %q", tc.field, title)
					require.Equal(t, chat.TitleSource, stored.TitleSource, "invalid %s, title %q", tc.field, title)
					require.True(t, chat.TitleUpdatedAt.Equal(stored.TitleUpdatedAt), "invalid %s, title %q", tc.field, title)
				}
			}
		})

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

			cases := []struct {
				name     string
				title    string
				expectOK bool
				storedAs string
			}{
				{
					name:     "ExactlyMaxASCII",
					title:    strings.Repeat("a", 200),
					expectOK: true,
					storedAs: strings.Repeat("a", 200),
				},
				{
					name:     "OneOverMaxASCII",
					title:    strings.Repeat("a", 201),
					expectOK: false,
				},
				{
					name:     "ExactlyMaxMultiByte",
					title:    strings.Repeat("é", 200),
					expectOK: true,
					storedAs: strings.Repeat("é", 200),
				},
				{
					name:     "OneOverMaxMultiByte",
					title:    strings.Repeat("é", 201),
					expectOK: false,
				},
				{
					name:     "TrimsDownToMax",
					title:    "   " + strings.Repeat("a", 200) + "   ",
					expectOK: true,
					storedAs: strings.Repeat("a", 200),
				},
			}

			for _, tc := range cases {
				t.Run(tc.name, func(t *testing.T) {
					t.Parallel()

					ctx := testutil.Context(t, testutil.WaitLong)
					client, api := newChatClientWithAPI(t)
					firstUser := coderdtest.CreateFirstUser(t, client.Client)
					_ = createChatModel(t, client)
					chat := createChat(ctx, t, client, firstUser.OrganizationID, "boundary baseline")
					coderdtest.WaitForChatSettled(ctx, t, api, chat.ID)

					err := client.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{
						Title: ptr.Ref(tc.title),
					})
					updated := getChat(ctx, t, client, chat.ID)
					if tc.expectOK {
						require.NoError(t, err)
						require.Equal(t, tc.storedAs, updated.Title)
					} else {
						requireSDKError(t, err, http.StatusBadRequest)
						require.Equal(t, chat.Title, updated.Title)
					}
				})
			}
		})

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

			ctx := testutil.Context(t, testutil.WaitLong)
			db, ps, sqlDB := dbtestutil.NewDBWithSQLDB(t)
			clientRaw, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{
				DeploymentValues: coderdtest.DeploymentValues(t),
				Database:         db,
				Pubsub:           ps,
			})
			aibridgedtest.StartTestAIBridgeDaemon(t.Context(), t, api, nil)
			client := codersdk.NewExperimentalClient(clientRaw)
			firstUser := coderdtest.CreateFirstUser(t, client.Client)
			_ = createChatModel(t, client)

			chat := createChat(ctx, t, client, firstUser.OrganizationID, "steady title")
			coderdtest.WaitForChatSettled(ctx, t, api, chat.ID)

			past := time.Now().UTC().Add(-2 * time.Hour).Truncate(time.Second)
			_, err := sqlDB.ExecContext(ctx,
				"UPDATE chats SET title = $1, title_source = 'fallback', updated_at = $2 WHERE id = $3",
				"steady title", past, chat.ID,
			)
			require.NoError(t, err)

			err = client.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{
				Title: new("steady title"),
			})
			require.NoError(t, err)

			updated := getChat(ctx, t, client, chat.ID)
			require.Equal(t, "steady title", updated.Title)
			require.Equal(t, codersdk.ChatTitleSourceUser, updated.TitleSource)
			require.WithinDuration(t, past, updated.UpdatedAt, time.Second)

			err = client.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{
				Title: new("renamed in place"),
			})
			require.NoError(t, err)

			renamed := getChat(ctx, t, client, chat.ID)
			require.Equal(t, "renamed in place", renamed.Title)
			require.Equal(t, codersdk.ChatTitleSourceUser, renamed.TitleSource)
			require.True(t, renamed.TitleUpdatedAt.After(updated.TitleUpdatedAt))
			require.WithinDuration(t, past, renamed.UpdatedAt, time.Second)
		})

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

			ctx := testutil.Context(t, testutil.WaitLong)
			client, api := newChatClientWithAPI(t)
			firstUser := coderdtest.CreateFirstUser(t, client.Client)
			_ = createChatModel(t, client)

			chat := createChat(ctx, t, client, firstUser.OrganizationID, "announce me")

			coderdtest.WaitForChatSettled(ctx, t, api, chat.ID)

			conn, err := client.Dial(ctx, "/api/v2/chats/watch", nil)
			require.NoError(t, err)
			defer conn.Close(websocket.StatusNormalClosure, "done")

			go func() {
				_ = client.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{
					Title: ptr.Ref("announced name"),
				})
			}()

			var received codersdk.ChatWatchEvent
			for {
				if err := wsjson.Read(ctx, conn, &received); err != nil {
					break
				}
				if received.Kind == codersdk.ChatWatchEventKindTitleChange &&
					received.Chat.ID == chat.ID {
					require.Equal(t, "announced name", received.Chat.Title)
					return
				}
			}
			t.Fatalf("did not observe title_change event for chat %s", chat.ID)
		})
	})
}

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

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

		ctx := testutil.Context(t, testutil.WaitLong)
		mAudit := audit.NewMock()
		client, api := newChatClientWithAPI(t, func(o *coderdtest.Options) {
			o.Auditor = mAudit
		})
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)

		chatToArchive, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: firstUser.OrganizationID,
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "archive me",
				},
			},
		})
		require.NoError(t, err)

		chatToKeep, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: firstUser.OrganizationID,
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "keep me",
				},
			},
		})
		require.NoError(t, err)
		coderdtest.WaitForChatSettled(ctx, t, api, chatToArchive.ID)
		coderdtest.WaitForChatSettled(ctx, t, api, chatToKeep.ID)

		chatsBeforeArchive, err := client.ListChats(ctx, nil)
		require.NoError(t, err)
		require.Len(t, chatsBeforeArchive, 2)

		err = client.UpdateChat(ctx, chatToArchive.ID, codersdk.UpdateChatRequest{Archived: ptr.Ref(true)})
		require.NoError(t, err)

		// Default (no filter) returns only non-archived chats.
		allChats, err := client.ListChats(ctx, nil)
		require.NoError(t, err)
		require.Len(t, allChats, 1)
		require.Equal(t, chatToKeep.ID, allChats[0].ID)

		// archived:false returns only non-archived chats.
		activeChats, err := client.ListChats(ctx, &codersdk.ListChatsOptions{
			Query: "archived:false",
		})
		require.NoError(t, err)
		require.Len(t, activeChats, 1)
		require.Equal(t, chatToKeep.ID, activeChats[0].ID)
		require.False(t, activeChats[0].Archived)

		// archived:true returns only archived chats.
		archivedChats, err := client.ListChats(ctx, &codersdk.ListChatsOptions{
			Query: "archived:true",
		})
		require.NoError(t, err)
		require.Len(t, archivedChats, 1)
		require.Equal(t, chatToArchive.ID, archivedChats[0].ID)
		require.True(t, archivedChats[0].Archived)

		require.True(t, mAudit.Contains(t, database.AuditLog{
			Action:         database.AuditActionWrite,
			ResourceType:   database.ResourceTypeChat,
			ResourceID:     chatToArchive.ID,
			ResourceTarget: chatToArchive.ID.String()[:8],
			UserID:         firstUser.UserID,
		}))
	})
	t.Run("NotFound", func(t *testing.T) {
		t.Parallel()

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		_ = coderdtest.CreateFirstUser(t, client.Client)

		err := client.UpdateChat(ctx, uuid.New(), codersdk.UpdateChatRequest{Archived: ptr.Ref(true)})
		requireSDKError(t, err, http.StatusNotFound)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db, api := newChatClientWithAPIAndDatabase(t)
		user := coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		// Create a parent chat via the API.
		parentChat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: user.OrganizationID,
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "parent chat",
				},
			},
		})
		require.NoError(t, err)
		coderdtest.WaitForChatSettled(ctx, t, api, parentChat.ID)

		// Insert child chats directly via the database.
		child1 := dbgen.Chat(t, db, database.Chat{
			OrganizationID:    user.OrganizationID,
			OwnerID:           user.UserID,
			LastModelConfigID: modelConfig.ID,
			Title:             "child 1",
			ParentChatID:      uuid.NullUUID{UUID: parentChat.ID, Valid: true},
			RootChatID:        uuid.NullUUID{UUID: parentChat.ID, Valid: true},
		})

		child2 := dbgen.Chat(t, db, database.Chat{
			OrganizationID:    user.OrganizationID,
			OwnerID:           user.UserID,
			LastModelConfigID: modelConfig.ID,
			Title:             "child 2",
			ParentChatID:      uuid.NullUUID{UUID: parentChat.ID, Valid: true},
			RootChatID:        uuid.NullUUID{UUID: parentChat.ID, Valid: true},
		})

		// Archive the parent via the API.
		err = client.UpdateChat(ctx, parentChat.ID, codersdk.UpdateChatRequest{Archived: ptr.Ref(true)})
		require.NoError(t, err)

		// archived:false should exclude the entire archived family.
		activeChats, err := client.ListChats(ctx, &codersdk.ListChatsOptions{
			Query: "archived:false",
		})
		require.NoError(t, err)
		for _, c := range activeChats {
			require.NotEqual(t, parentChat.ID, c.ID, "parent should not appear")
			require.NotEqual(t, child1.ID, c.ID, "child1 should not appear")
			require.NotEqual(t, child2.ID, c.ID, "child2 should not appear")
		}

		// Verify children are archived directly in the DB.
		dbChild1, err := db.GetChatByID(dbauthz.AsSystemRestricted(ctx), child1.ID)
		require.NoError(t, err)
		require.True(t, dbChild1.Archived, "child1 should be archived")

		dbChild2, err := db.GetChatByID(dbauthz.AsSystemRestricted(ctx), child2.ID)
		require.NoError(t, err)
		require.True(t, dbChild2.Archived, "child2 should be archived")

		// archived:true should return the parent with both
		// cascaded children embedded.
		archivedChats, err := client.ListChats(ctx, &codersdk.ListChatsOptions{
			Query: "archived:true",
		})
		require.NoError(t, err)
		var foundParent *codersdk.Chat
		for _, chat := range archivedChats {
			if chat.ID == parentChat.ID {
				foundParent = &chat
				break
			}
		}
		require.NotNil(t, foundParent, "parent should appear in archived list")
		require.True(t, foundParent.Archived, "parent should be archived")
		require.Len(t, foundParent.Children, 2, "both archived children should be embedded under the archived parent")
		childIDs := map[uuid.UUID]bool{}
		for _, child := range foundParent.Children {
			require.True(t, child.Archived, "embedded child should be archived")
			childIDs[child.ID] = true
		}
		require.True(t, childIDs[child1.ID], "child1 should be embedded under archived parent")
		require.True(t, childIDs[child2.ID], "child2 should be embedded under archived parent")
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db, api := newChatClientWithAPIAndDatabase(t)
		user := coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		// Create a parent chat via the API.
		parentChat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: user.OrganizationID,
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "parent",
				},
			},
		})
		require.NoError(t, err)
		coderdtest.WaitForChatSettled(ctx, t, api, parentChat.ID)

		// Insert a child chat directly via the database.
		child := dbgen.Chat(t, db, database.Chat{
			OrganizationID:    user.OrganizationID,
			OwnerID:           user.UserID,
			LastModelConfigID: modelConfig.ID,
			Title:             "child",
			ParentChatID:      uuid.NullUUID{UUID: parentChat.ID, Valid: true},
			RootChatID:        uuid.NullUUID{UUID: parentChat.ID, Valid: true},
		})

		// Archive state changes must target the root chat and cascade.
		// Child archive attempts are rejected to preserve the family invariant.
		err = client.UpdateChat(ctx, child.ID, codersdk.UpdateChatRequest{Archived: ptr.Ref(true)})
		requireSDKError(t, err, http.StatusBadRequest)

		dbChild, err := db.GetChatByID(dbauthz.AsSystemRestricted(ctx), child.ID)
		require.NoError(t, err)
		require.False(t, dbChild.Archived, "child should remain active")

		dbParent, err := db.GetChatByID(dbauthz.AsSystemRestricted(ctx), parentChat.ID)
		require.NoError(t, err)
		require.False(t, dbParent.Archived, "parent should stay active")
	})
}

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

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, api := newChatClientWithAPI(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)

		chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: firstUser.OrganizationID,
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "archive then unarchive me",
				},
			},
		})
		require.NoError(t, err)
		coderdtest.WaitForChatSettled(ctx, t, api, chat.ID)

		// Archive the chat first.
		err = client.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{Archived: ptr.Ref(true)})
		require.NoError(t, err)

		// Verify it's archived.
		archivedChats, err := client.ListChats(ctx, &codersdk.ListChatsOptions{
			Query: "archived:true",
		})
		require.NoError(t, err)
		require.Len(t, archivedChats, 1)
		require.True(t, archivedChats[0].Archived)
		// Unarchive the chat.
		err = client.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{Archived: ptr.Ref(false)})
		require.NoError(t, err)

		// Verify it's no longer archived.
		activeChats, err := client.ListChats(ctx, &codersdk.ListChatsOptions{
			Query: "archived:false",
		})
		require.NoError(t, err)
		require.Len(t, activeChats, 1)
		require.Equal(t, chat.ID, activeChats[0].ID)
		require.False(t, activeChats[0].Archived)

		// No archived chats remain.
		archivedChats, err = client.ListChats(ctx, &codersdk.ListChatsOptions{
			Query: "archived:true",
		})
		require.NoError(t, err)
		require.Empty(t, archivedChats)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db, api := newChatClientWithAPIAndDatabase(t)
		user := coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		parentChat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: user.OrganizationID,
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "parent chat",
				},
			},
		})
		require.NoError(t, err)
		coderdtest.WaitForChatSettled(ctx, t, api, parentChat.ID)

		child1 := dbgen.Chat(t, db, database.Chat{
			OrganizationID:    user.OrganizationID,
			OwnerID:           user.UserID,
			LastModelConfigID: modelConfig.ID,
			Title:             "child 1",
			ParentChatID:      uuid.NullUUID{UUID: parentChat.ID, Valid: true},
			RootChatID:        uuid.NullUUID{UUID: parentChat.ID, Valid: true},
		})

		child2 := dbgen.Chat(t, db, database.Chat{
			OrganizationID:    user.OrganizationID,
			OwnerID:           user.UserID,
			LastModelConfigID: modelConfig.ID,
			Title:             "child 2",
			ParentChatID:      uuid.NullUUID{UUID: parentChat.ID, Valid: true},
			RootChatID:        uuid.NullUUID{UUID: parentChat.ID, Valid: true},
		})

		err = client.UpdateChat(ctx, parentChat.ID, codersdk.UpdateChatRequest{Archived: ptr.Ref(true)})
		require.NoError(t, err)

		err = client.UpdateChat(ctx, parentChat.ID, codersdk.UpdateChatRequest{Archived: ptr.Ref(false)})
		require.NoError(t, err)

		activeChats, err := client.ListChats(ctx, &codersdk.ListChatsOptions{
			Query: "archived:false",
		})
		require.NoError(t, err)

		// Children no longer appear as top-level entries.
		// They are embedded inside the parent's Children field.
		var foundParent *codersdk.Chat
		for _, chat := range activeChats {
			require.NotEqual(t, child1.ID, chat.ID, "child1 should not appear at top level")
			require.NotEqual(t, child2.ID, chat.ID, "child2 should not appear at top level")
			if chat.ID == parentChat.ID {
				foundParent = &chat
			}
		}
		require.NotNil(t, foundParent, "parent should be listed as active")
		require.False(t, foundParent.Archived)

		// Verify children are embedded and unarchived.
		require.Len(t, foundParent.Children, 2)
		childIDs := map[uuid.UUID]bool{}
		for _, child := range foundParent.Children {
			require.False(t, child.Archived)
			childIDs[child.ID] = true
		}
		require.True(t, childIDs[child1.ID], "child1 should be embedded")
		require.True(t, childIDs[child2.ID], "child2 should be embedded")

		archivedChats, err := client.ListChats(ctx, &codersdk.ListChatsOptions{
			Query: "archived:true",
		})
		require.NoError(t, err)
		for _, chat := range archivedChats {
			require.NotEqual(t, parentChat.ID, chat.ID, "parent should not remain archived")
		}

		dbParent, err := db.GetChatByID(dbauthz.AsSystemRestricted(ctx), parentChat.ID)
		require.NoError(t, err)
		require.False(t, dbParent.Archived, "parent should be unarchived")

		dbChild1, err := db.GetChatByID(dbauthz.AsSystemRestricted(ctx), child1.ID)
		require.NoError(t, err)
		require.False(t, dbChild1.Archived, "child1 should be unarchived")

		dbChild2, err := db.GetChatByID(dbauthz.AsSystemRestricted(ctx), child2.ID)
		require.NoError(t, err)
		require.False(t, dbChild2.Archived, "child2 should be unarchived")
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, api := newChatClientWithAPI(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)

		chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: firstUser.OrganizationID,
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "not archived",
				},
			},
		})
		require.NoError(t, err)
		coderdtest.WaitForChatSettled(ctx, t, api, chat.ID)

		// Trying to unarchive a non-archived chat should fail.
		err = client.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{Archived: ptr.Ref(false)})
		requireSDKError(t, err, http.StatusBadRequest)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db, api := newChatClientWithAPIAndDatabase(t)
		user := coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		// Create a parent chat via the API.
		parentChat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: user.OrganizationID,
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "parent",
				},
			},
		})
		require.NoError(t, err)
		coderdtest.WaitForChatSettled(ctx, t, api, parentChat.ID)

		// Insert a child directly via the database, then archive the
		// parent so the whole family is archived (cascade).
		child := dbgen.Chat(t, db, database.Chat{
			OrganizationID:    user.OrganizationID,
			OwnerID:           user.UserID,
			LastModelConfigID: modelConfig.ID,
			Title:             "child",
			ParentChatID:      uuid.NullUUID{UUID: parentChat.ID, Valid: true},
			RootChatID:        uuid.NullUUID{UUID: parentChat.ID, Valid: true},
		})

		err = client.UpdateChat(ctx, parentChat.ID, codersdk.UpdateChatRequest{Archived: ptr.Ref(true)})
		require.NoError(t, err)

		// Unarchiving the child while the parent stays archived
		// must be rejected. Otherwise the child becomes a ghost
		// (active list excludes the parent, archived list's child
		// query filters archived=true so the now-unarchived child
		// is also excluded).
		err = client.UpdateChat(ctx, child.ID, codersdk.UpdateChatRequest{Archived: ptr.Ref(false)})
		requireSDKError(t, err, http.StatusBadRequest)

		dbChild, err := db.GetChatByID(dbauthz.AsSystemRestricted(ctx), child.ID)
		require.NoError(t, err)
		require.True(t, dbChild.Archived, "child should still be archived")
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db, api := newChatClientWithAPIAndDatabase(t)
		user := coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		parentChat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: user.OrganizationID,
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "parent",
				},
			},
		})
		require.NoError(t, err)
		coderdtest.WaitForChatSettled(ctx, t, api, parentChat.ID)

		// Simulate legacy lone-archived child (from before the
		// child-archive gate existed) by inserting it directly
		// with archived=true while the parent is not archived.
		child := dbgen.Chat(t, db, database.Chat{
			OrganizationID:    user.OrganizationID,
			OwnerID:           user.UserID,
			LastModelConfigID: modelConfig.ID,
			Title:             "legacy child",
			ParentChatID:      uuid.NullUUID{UUID: parentChat.ID, Valid: true},
			RootChatID:        uuid.NullUUID{UUID: parentChat.ID, Valid: true},
		})

		_, err = db.ArchiveChatByID(dbauthz.AsSystemRestricted(ctx), child.ID)
		require.NoError(t, err)

		// Archive state changes must target the root chat, even when
		// the child is a legacy lone-archived row.
		err = client.UpdateChat(ctx, child.ID, codersdk.UpdateChatRequest{Archived: ptr.Ref(false)})
		requireSDKError(t, err, http.StatusBadRequest)

		dbChild, err := db.GetChatByID(dbauthz.AsSystemRestricted(ctx), child.ID)
		require.NoError(t, err)
		require.True(t, dbChild.Archived, "child should remain archived")
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		_ = coderdtest.CreateFirstUser(t, client.Client)

		err := client.UpdateChat(ctx, uuid.New(), codersdk.UpdateChatRequest{Archived: ptr.Ref(false)})
		requireSDKError(t, err, http.StatusNotFound)
	})
}

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

	createChat := func(ctx context.Context, t *testing.T, client *codersdk.ExperimentalClient, orgID uuid.UUID, title string) codersdk.Chat {
		t.Helper()

		chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: orgID,
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: title,
				},
			},
		})
		require.NoError(t, err)
		return chat
	}

	getChat := func(ctx context.Context, t *testing.T, client *codersdk.ExperimentalClient, chatID uuid.UUID) codersdk.Chat {
		t.Helper()

		chat, err := client.GetChat(ctx, chatID)
		require.NoError(t, err)
		return chat
	}

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)

		first := createChat(ctx, t, client, firstUser.OrganizationID, "first pinned chat")
		second := createChat(ctx, t, client, firstUser.OrganizationID, "second pinned chat")
		third := createChat(ctx, t, client, firstUser.OrganizationID, "third pinned chat")

		err := client.UpdateChat(ctx, first.ID, codersdk.UpdateChatRequest{PinOrder: ptr.Ref(int32(1))})
		require.NoError(t, err)
		err = client.UpdateChat(ctx, second.ID, codersdk.UpdateChatRequest{PinOrder: ptr.Ref(int32(1))})
		require.NoError(t, err)
		err = client.UpdateChat(ctx, third.ID, codersdk.UpdateChatRequest{PinOrder: ptr.Ref(int32(1))})
		require.NoError(t, err)

		first = getChat(ctx, t, client, first.ID)
		second = getChat(ctx, t, client, second.ID)
		third = getChat(ctx, t, client, third.ID)
		require.EqualValues(t, 1, first.PinOrder)
		require.EqualValues(t, 2, second.PinOrder)
		require.EqualValues(t, 3, third.PinOrder)

		err = client.UpdateChat(ctx, third.ID, codersdk.UpdateChatRequest{PinOrder: ptr.Ref(int32(1))})
		require.NoError(t, err)

		first = getChat(ctx, t, client, first.ID)
		second = getChat(ctx, t, client, second.ID)
		third = getChat(ctx, t, client, third.ID)
		require.EqualValues(t, 2, first.PinOrder)
		require.EqualValues(t, 3, second.PinOrder)
		require.EqualValues(t, 1, third.PinOrder)

		err = client.UpdateChat(ctx, first.ID, codersdk.UpdateChatRequest{PinOrder: ptr.Ref(int32(0))})
		require.NoError(t, err)

		first = getChat(ctx, t, client, first.ID)
		second = getChat(ctx, t, client, second.ID)
		third = getChat(ctx, t, client, third.ID)
		require.Zero(t, first.PinOrder)
		require.EqualValues(t, 2, second.PinOrder)
		require.EqualValues(t, 1, third.PinOrder)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, api := newChatClientWithAPI(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)

		first := createChat(ctx, t, client, firstUser.OrganizationID, "pinned then archived")
		second := createChat(ctx, t, client, firstUser.OrganizationID, "stays pinned")
		coderdtest.WaitForChatSettled(ctx, t, api, first.ID)
		coderdtest.WaitForChatSettled(ctx, t, api, second.ID)

		// Pin both.
		err := client.UpdateChat(ctx, first.ID, codersdk.UpdateChatRequest{PinOrder: ptr.Ref(int32(1))})
		require.NoError(t, err)
		err = client.UpdateChat(ctx, second.ID, codersdk.UpdateChatRequest{PinOrder: ptr.Ref(int32(1))})
		require.NoError(t, err)

		// Archive the first — pin_order should be cleared.
		err = client.UpdateChat(ctx, first.ID, codersdk.UpdateChatRequest{Archived: ptr.Ref(true)})
		require.NoError(t, err)

		first = getChat(ctx, t, client, first.ID)
		second = getChat(ctx, t, client, second.ID)
		require.Zero(t, first.PinOrder, "archived chat should have pin_order 0")
		require.True(t, first.Archived)
		// The remaining pin keeps its original position. The next
		// pin/unpin/reorder operation compacts via ROW_NUMBER().
		require.EqualValues(t, 2, second.PinOrder, "remaining pin keeps original position")
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)

		chat := createChat(ctx, t, client, firstUser.OrganizationID, "negative pin order")
		err := client.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{PinOrder: ptr.Ref(int32(-1))})
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, "Pin order must be non-negative.", sdkErr.Message)

		chat = getChat(ctx, t, client, chat.ID)
		require.Zero(t, chat.PinOrder)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		modelConfig := createChatModel(t, client)

		parentChat := createChat(ctx, t, client, firstUser.OrganizationID, "parent chat")

		child := dbgen.Chat(t, db, database.Chat{
			OrganizationID:    firstUser.OrganizationID,
			OwnerID:           firstUser.UserID,
			LastModelConfigID: modelConfig.ID,
			Title:             "child chat",
			Status:            database.ChatStatusWaiting,
			ParentChatID:      uuid.NullUUID{UUID: parentChat.ID, Valid: true},
			RootChatID:        uuid.NullUUID{UUID: parentChat.ID, Valid: true},
		})

		err := client.UpdateChat(ctx, child.ID, codersdk.UpdateChatRequest{PinOrder: ptr.Ref(int32(1))})

		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, "Cannot pin a child chat.", sdkErr.Message)

		result := getChat(ctx, t, client, child.ID)
		require.Zero(t, result.PinOrder)
	})
}

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

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)

		chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: firstUser.OrganizationID,
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: "initial message for post route test",
				},
			},
		})
		require.NoError(t, err)

		hasTextPart := func(parts []codersdk.ChatMessagePart, want string) bool {
			for _, part := range parts {
				if part.Type == codersdk.ChatMessagePartTypeText && part.Text == want {
					return true
				}
			}
			return false
		}

		messageText := "post message route success " + uuid.NewString()
		created, err := client.CreateChatMessage(ctx, chat.ID, codersdk.CreateChatMessageRequest{
			Content: []codersdk.ChatInputPart{
				{
					Type: codersdk.ChatInputPartTypeText,
					Text: messageText,
				},
			},
		})
		require.NoError(t, err)

		if created.Queued {
			require.Nil(t, created.Message)
			require.NotNil(t, created.QueuedMessage)
			require.Equal(t, chat.ID, created.QueuedMessage.ChatID)
			require.NotZero(t, created.QueuedMessage.ID)
			require.True(t, hasTextPart(created.QueuedMessage.Content, messageText))

			require.Eventually(t, func() bool {
				messagesResult, getErr := client.GetChatMessages(ctx, chat.ID, nil)
				if getErr != nil {
					return false
				}

				for _, queued := range messagesResult.QueuedMessages {
					if queued.ID == created.QueuedMessage.ID &&
						queued.ChatID == chat.ID &&
						hasTextPart(queued.Content, messageText) {
						return true
					}
				}
				for _, message := range messagesResult.Messages {
					if message.Role == codersdk.ChatMessageRoleUser && hasTextPart(message.Content, messageText) {
						return true
					}
				}
				return false
			}, testutil.WaitLong, testutil.IntervalFast)
		} else {
			require.Nil(t, created.QueuedMessage)
			require.NotNil(t, created.Message)
			require.Equal(t, chat.ID, created.Message.ChatID)
			require.Equal(t, codersdk.ChatMessageRoleUser, created.Message.Role)
			require.NotZero(t, created.Message.ID)
			require.True(t, hasTextPart(created.Message.Content, messageText))

			require.Eventually(t, func() bool {
				messagesResult, getErr := client.GetChatMessages(ctx, chat.ID, nil)
				if getErr != nil {
					return false
				}
				for _, message := range messagesResult.Messages {
					if message.ID == created.Message.ID &&
						message.Role == codersdk.ChatMessageRoleUser &&
						hasTextPart(message.Content, messageText) {
						return true
					}
				}
				return false
			}, testutil.WaitLong, testutil.IntervalFast)
		}
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)

		chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: firstUser.OrganizationID,
			Content: []codersdk.ChatInputPart{{
				Type: codersdk.ChatInputPartTypeText,
				Text: "initial message before disabled provider switch",
			}},
		})
		require.NoError(t, err)

		providerDisabledConfig := createProviderDisabledChatModel(
			t,
			client,
			"openai",
			"gpt-4o-send-provider-disabled-"+uuid.NewString(),
		)

		_, err = client.CreateChatMessage(ctx, chat.ID, codersdk.CreateChatMessageRequest{
			Content: []codersdk.ChatInputPart{{
				Type: codersdk.ChatInputPartTypeText,
				Text: "switch to a provider-disabled model",
			}},
			ModelConfigID: ptr.Ref(providerDisabledConfig.ID),
		})
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, "Invalid model_config_id: provider is not enabled for this model.", sdkErr.Message)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		defaultConfig := createChatModel(t, client)
		chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: firstUser.OrganizationID,
			Content: []codersdk.ChatInputPart{{
				Type: codersdk.ChatInputPartTypeText,
				Text: "initial message before cross-org switch",
			}},
		})
		require.NoError(t, err)

		otherOrg := dbgen.Organization(t, db, database.Organization{IsDefault: false})
		otherConfig := dbgen.ChatModelConfig(t, db, database.ChatModelConfig{
			AIProviderID:   uuid.NullUUID{UUID: defaultConfig.AIProviderID, Valid: true},
			Model:          "cross-org-send-" + uuid.NewString(),
			Enabled:        true,
			OrganizationID: otherOrg.ID,
		})
		_, err = client.CreateChatMessage(ctx, chat.ID, codersdk.CreateChatMessageRequest{
			Content: []codersdk.ChatInputPart{{
				Type: codersdk.ChatInputPartTypeText,
				Text: "reject another organization's model",
			}},
			ModelConfigID: ptr.Ref(otherConfig.ID),
		})
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, "Invalid model_config_id: model config not found or disabled.", sdkErr.Message)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		foreignConfig := createChatModel(t, client)
		provider := createAIProviderForTest(t, client, "openai-compat", "test-api-key")
		chatOrg := dbgen.Organization(t, db, database.Organization{IsDefault: false})
		localConfig := dbgen.ChatModelConfig(t, db, database.ChatModelConfig{
			AIProviderID:   uuid.NullUUID{UUID: provider.ID, Valid: true},
			Model:          "local-send-" + uuid.NewString(),
			Enabled:        true,
			IsDefault:      true,
			OrganizationID: chatOrg.ID,
		})
		memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, chatOrg.ID)
		memberClient := codersdk.NewExperimentalClient(memberClientRaw)
		chat := dbgen.Chat(t, db, database.Chat{
			OrganizationID:    chatOrg.ID,
			OwnerID:           member.ID,
			LastModelConfigID: foreignConfig.ID,
			Title:             "historical foreign model",
		})

		resp, err := memberClient.CreateChatMessage(ctx, chat.ID, codersdk.CreateChatMessageRequest{
			Content: []codersdk.ChatInputPart{{
				Type: codersdk.ChatInputPartTypeText,
				Text: "use the local fallback",
			}},
		})
		require.NoError(t, err)
		require.NotNil(t, resp.Message)
		require.NotNil(t, resp.Message.ModelConfigID)
		require.Equal(t, localConfig.ID, *resp.Message.ModelConfigID)
		require.Equal(t, firstUser.OrganizationID, foreignConfig.OrganizationID)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client, db := newChatClientWithDatabase(t)
		_ = coderdtest.CreateFirstUser(t, client.Client)
		foreignConfig := createChatModel(t, client)
		chatOrg := dbgen.Organization(t, db, database.Organization{IsDefault: false})
		memberClientRaw, member := coderdtest.CreateAnotherUser(t, client.Client, chatOrg.ID)
		memberClient := codersdk.NewExperimentalClient(memberClientRaw)
		chat := dbgen.Chat(t, db, database.Chat{
			OrganizationID:    chatOrg.ID,
			OwnerID:           member.ID,
			LastModelConfigID: foreignConfig.ID,
			Title:             "historical foreign model without fallback",
		})

		_, err := memberClient.CreateChatMessage(ctx, chat.ID, codersdk.CreateChatMessageRequest{
			Content: []codersdk.ChatInputPart{{
				Type: codersdk.ChatInputPartTypeText,
				Text: "require a local fallback",
			}},
		})
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, "No chat model is available in this organization.", sdkErr.Message)
		require.Equal(t, "Ask an organization administrator to configure and enable a chat model.", sdkErr.Detail)
	})

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

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		defaultConfig := createChatModel(t, client)

		chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: firstUser.OrganizationID,
			Content: []codersdk.ChatInputPart{{
				Type: codersdk.ChatInputPartTypeText,
				Text: "initial message before provider disable",
			}},
		})
		require.NoError(t, err)

		_, err = client.UpdateAIProvider(ctx, defaultConfig.AIProviderID.String(), codersdk.UpdateAIProviderRequest{
			Enabled: ptr.Ref(false),
		})
		require.NoError(t, err)

		// Without an explicit model the fallback walks last model ->
		// default, both under the now-disabled provider.
		_, err = client.CreateChatMessage(ctx, chat.ID, codersdk.CreateChatMessageRequest{
			Content: []codersdk.ChatInputPart{{
				Type: codersdk.ChatInputPartTypeText,
				Text: "message after provider disable",
			}},
		})
		sdkErr := requireSDKError(t, err, http.StatusBadRequest)
		require.Equal(t, "No chat model is available in this organization.", sdkErr.Message)
		require.Equal(t, "Ask an organization administrator to configure and enable a chat model.", sdkErr.Detail)
	})

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

		ctx := testutil.Context(t,