package webagent

import (
	"encoding/json"
	"errors"
	"net/http"
	"reflect"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/api/testkit/httputil"
	"github.com/tidbcloud/AgentTeam/internal/runtimecontrol"
	"github.com/tidbcloud/AgentTeam/internal/service/agentmemory"
	"github.com/tidbcloud/AgentTeam/internal/service/maas"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/organization"
	"github.com/tidbcloud/AgentTeam/internal/store/rewind"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

func TestCreateAgentPermissionModeIsNotCreateContract(t *testing.T) {
	_, ok := reflect.TypeOf(dto.WebAgentCreateRequest{}).FieldByName("PermissionMode")
	require.False(t, ok)
	_, ok = reflect.TypeOf(dto.WebAgentCreateFromRoleRequest{}).FieldByName("PermissionMode")
	require.False(t, ok)
	_, ok = reflect.TypeOf(agent.CreateAgentParams{}).FieldByName("PermissionMode")
	require.False(t, ok)
}

func TestCollectionHandlers(t *testing.T) {
	t.Run("list returns workspace agents", func(t *testing.T) {
		fake := newFakeStore()
		fake.agents = []agent.AppAgent{{ID: "agt_1", Name: "agent-one", PermissionMode: agent.AgentPermissionModeWorkspaceWrite}}
		router := newWebAgentRouter(Dependencies{AgentStore: fake, ReviewStore: fake})

		resp := httputil.DoJSON(t, router, http.MethodGet, "/api/v1/workspaces/wsp_1/agents", nil)
		require.Equal(t, http.StatusOK, resp.Code)

		var out []agent.AppAgent
		require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &out))
		require.Len(t, out, 1)
		require.Equal(t, "agent-one", out[0].Name)
		require.Equal(t, agent.AgentPermissionModeFullAccess, out[0].PermissionMode)
		require.Equal(t, agent.AppAgentListOptions{}, fake.lastListOptions)
	})

	t.Run("list owned scope returns current user agents", func(t *testing.T) {
		fake := newFakeStore()
		fake.agents = []agent.AppAgent{{ID: "agt_workspace", Name: "workspace-agent"}}
		fake.ownedAgents = []agent.AppAgent{{ID: "agt_owned", Name: "owned-agent"}}
		router := newWebAgentRouter(Dependencies{AgentStore: fake, ReviewStore: fake})

		resp := httputil.DoJSON(t, router, http.MethodGet, "/api/v1/workspaces/wsp_1/agents?scope=owned", nil)
		require.Equal(t, http.StatusOK, resp.Code)

		var out []agent.AppAgent
		require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &out))
		require.Len(t, out, 1)
		require.Equal(t, "owned-agent", out[0].Name)
		require.Equal(t, agent.AppAgentListOptions{OwnedOnly: true}, fake.lastListOptions)
	})

	t.Run("list parses bounded diagnostics options", func(t *testing.T) {
		fake := newFakeStore()
		fake.agents = []agent.AppAgent{{ID: "agt_workspace", Name: "workspace-agent"}}
		router := newWebAgentRouter(Dependencies{AgentStore: fake, ReviewStore: fake})

		resp := httputil.DoJSON(t, router, http.MethodGet, "/api/v1/workspaces/wsp_1/agents?limit=250&includeRuntimeFailures=true", nil)
		require.Equal(t, http.StatusOK, resp.Code)
		require.Equal(t, agent.AppAgentListOptions{Limit: maxAgentListLimit, IncludeRuntimeFailures: true}, fake.lastListOptions)
	})

	t.Run("list rejects unknown scope", func(t *testing.T) {
		fake := newFakeStore()
		router := newWebAgentRouter(Dependencies{AgentStore: fake, ReviewStore: fake})

		resp := httputil.DoJSON(t, router, http.MethodGet, "/api/v1/workspaces/wsp_1/agents?scope=workspace", nil)
		require.Equal(t, http.StatusBadRequest, resp.Code)
		require.Contains(t, resp.Body.String(), "invalid agent scope")
	})

	t.Run("create trims name, stores params, and emits realtime", func(t *testing.T) {
		fake := newFakeStore()
		fake.createdAgent = agent.AgentView{AgentID: "agt_new", MachineID: "mach_1"}
		realtime := &fakeRealtime{}
		runtime := &fakeRuntime{}
		router := newWebAgentRouter(Dependencies{
			AgentStore:    fake,
			ReviewStore:   fake,
			Organizations: fakeOrganizationProvider{providers: []organization.OrganizationProviderView{{ID: "opv_1"}}},
			Runtime:       runtime,
			Realtime:      realtime,
		})

		resp := httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/agents", map[string]any{
			"name": "  New Agent  ", "machineId": " mach_1 ", "description": "desc", "runtime": "codex", "model": "gpt-5.4", "reasoningEffort": "medium", "llmMode": "agentteam_proxy",
		})
		require.Equal(t, http.StatusOK, resp.Code)
		require.Equal(t, "mach_1", fake.lastCreate.MachineID)
		require.Empty(t, fake.lastCreate.Params.Name)
		require.Equal(t, "New Agent", fake.lastCreate.Params.DisplayName)
		require.Equal(t, "desc", fake.lastCreate.Params.Description)
		require.Equal(t, "agentteam_proxy", fake.lastCreate.Params.LLMMode)
		require.Equal(t, []string{"mach_1"}, runtime.syncMachineCalls)
		require.Equal(t, "wsp_1", realtime.createdWorkspace)
	})

	t.Run("create rolls back agent when cloud memory enable fails", func(t *testing.T) {
		fake := newFakeStore()
		fake.createdAgent = agent.AgentView{AgentID: "agt_new", MachineID: "mach_1"}
		memory := &fakeCloudMemoryProvisioner{
			err: agentmemory.DependencyError{
				Status:  http.StatusConflict,
				Message: `{"message":"memory quota exceeded"}`,
			},
		}
		realtime := &fakeRealtime{}
		runtime := &fakeRuntime{}
		router := newWebAgentRouter(Dependencies{
			AgentStore:    fake,
			ReviewStore:   fake,
			CloudMemory:   memory,
			Runtime:       runtime,
			Realtime:      realtime,
			Organizations: fakeOrganizationProvider{providers: []organization.OrganizationProviderView{{ID: "opv_1"}}},
		})

		resp := httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/agents", map[string]any{
			"name": "New Agent", "machineId": "mach_1", "runtime": "codex", "model": "gpt-5.4", "llmMode": "agentteam_proxy",
		})

		require.Equal(t, http.StatusConflict, resp.Code)
		require.Contains(t, resp.Body.String(), "memory quota exceeded")
		require.Len(t, memory.calls, 1)
		require.Equal(t, "usr_1", memory.calls[0].UserID)
		require.Equal(t, "wsp_1", memory.calls[0].WorkspaceID)
		require.Equal(t, "agt_new", memory.calls[0].AgentID)
		require.True(t, memory.calls[0].ImportExistingLocalContent)
		require.Equal(t, "agt_new", fake.lastDeleteID)
		require.Empty(t, runtime.syncMachineCalls)
		require.Empty(t, runtime.notifyMemoryCalls)
		require.Empty(t, realtime.createdWorkspace)
	})

	t.Run("create rejects legacy permission mode key", func(t *testing.T) {
		fake := newFakeStore()
		fake.createdAgent = agent.AgentView{AgentID: "agt_new", MachineID: "mach_1"}
		router := newWebAgentRouter(Dependencies{AgentStore: fake, ReviewStore: fake})

		resp := httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/agents", map[string]any{
			"name": "Full Access Agent", "machineId": "mach_1", "runtime": "codex", "permissionMode": "agent_workspace_write",
		})

		require.Equal(t, http.StatusBadRequest, resp.Code)
		require.Contains(t, resp.Body.String(), "permissionMode is not supported when creating agents")
		require.Empty(t, fake.lastCreate.UserID)
	})

	t.Run("create validates hosted binding and stores without credentials", func(t *testing.T) {
		fake := newFakeStore()
		fake.createdAgent = agent.AgentView{AgentID: "agt_new", MachineID: "mach_1"}
		hosted := &fakeHostedAgentValidator{}
		supported := true
		runtime := &fakeRuntime{maasDirectOK: &supported}
		router := newWebAgentRouter(Dependencies{
			AgentStore:   fake,
			ReviewStore:  fake,
			HostedAgents: hosted,
			Runtime:      runtime,
		})

		resp := httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/agents", map[string]any{
			"name": "Hosted Agent", "machineId": "mach_1", "runtime": "claude", "model": "agentteam/deepseek-v4-pro", "llmMode": "agentteam_hosted", "apiKey": "ignored",
		})

		require.Equal(t, http.StatusOK, resp.Code)
		require.Equal(t, 1, hosted.calls)
		require.Equal(t, "usr_1", hosted.last.UserID)
		require.Equal(t, "wsp_1", hosted.last.WorkspaceID)
		require.Equal(t, "agentteam/deepseek-v4-pro", hosted.last.ModelReference)
		require.Equal(t, agent.AgentLLMModeAgentTeamHosted, hosted.last.LLMMode)
		require.NotNil(t, hosted.last.MachineSupportsDirectMaaS)
		require.True(t, *hosted.last.MachineSupportsDirectMaaS)
		require.Equal(t, agent.AgentLLMModeAgentTeamHosted, fake.lastCreate.Params.LLMMode)
		require.Empty(t, fake.lastCreate.Params.APIKey)
		require.Empty(t, fake.lastCreate.Params.BaseURL)
		require.Empty(t, fake.lastCreate.Params.WireAPI)
	})

	t.Run("create allows builtin with agentteam proxy model", func(t *testing.T) {
		fake := newFakeStore()
		fake.createdAgent = agent.AgentView{AgentID: "agt_new", MachineID: "mach_1"}
		hosted := &fakeHostedAgentValidator{}
		router := newWebAgentRouter(Dependencies{
			AgentStore:    fake,
			ReviewStore:   fake,
			Organizations: fakeOrganizationProvider{providers: []organization.OrganizationProviderView{{ID: "opv_1"}}},
			HostedAgents:  hosted,
		})

		resp := httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/agents", map[string]any{
			"name": "Builtin Proxy Agent", "machineId": "mach_1", "runtime": "builtin", "model": "gpt-5.4", "llmMode": "agentteam_proxy",
		})

		require.Equal(t, http.StatusOK, resp.Code)
		require.Zero(t, hosted.calls)
		require.Equal(t, "builtin", fake.lastCreate.Params.Runtime)
		require.Equal(t, "gpt-5.4", fake.lastCreate.Params.Model)
		require.Equal(t, agent.AgentLLMModeAgentTeamProxy, fake.lastCreate.Params.LLMMode)
		require.Equal(t, "workspace-proxy-token-wsp_1", fake.lastCreate.Params.APIKey)
	})

	t.Run("create rejects hosted binding when admission fails", func(t *testing.T) {
		fake := newFakeStore()
		hosted := &fakeHostedAgentValidator{err: storeerr.Conflict(maas.ReasonZeroBalance)}
		router := newWebAgentRouter(Dependencies{
			AgentStore:   fake,
			ReviewStore:  fake,
			HostedAgents: hosted,
		})

		resp := httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/agents", map[string]any{
			"name": "Hosted Agent", "machineId": "mach_1", "runtime": "claude", "model": "agentteam/deepseek-v4-pro", "llmMode": "agentteam_hosted",
		})

		require.Equal(t, http.StatusConflict, resp.Code)
		require.Contains(t, resp.Body.String(), maas.ReasonZeroBalance)
		require.Empty(t, fake.lastCreate.UserID)
	})

	t.Run("create validates required fields", func(t *testing.T) {
		fake := newFakeStore()
		router := newWebAgentRouter(Dependencies{AgentStore: fake, ReviewStore: fake})
		resp := httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/agents", map[string]any{"name": " ", "machineId": "mach_1"})
		require.Equal(t, http.StatusBadRequest, resp.Code)
		require.Contains(t, resp.Body.String(), "name is required")

		resp = httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/agents", map[string]any{"name": "agent", "machineId": " "})
		require.Equal(t, http.StatusBadRequest, resp.Code)
		require.Contains(t, resp.Body.String(), "machineId is required")
	})
}

func TestCollectionPatchRuntimeSideEffects(t *testing.T) {
	t.Run("runtime config change restarts active agent without marking it manually stopped", func(t *testing.T) {
		fake := newFakeStore()
		fake.agentView = agent.AgentView{AgentID: "agt_1", Runtime: "codex", Model: "old", ReasoningEffort: "medium", MachineID: "mach_1", Status: "active"}
		fake.updateResult = fake.agentView
		fake.appAgents["agt_1"] = agent.AppAgent{ID: "agt_1", Name: "agent-one", DisplayName: "Agent One", MachineID: "mach_1", Runtime: "codex"}
		runtime := &fakeRuntime{}
		router := newWebAgentRouter(Dependencies{
			AgentStore:    fake,
			ReviewStore:   fake,
			Organizations: fakeOrganizationProvider{providers: []organization.OrganizationProviderView{{ID: "opv_1"}}},
			Runtime:       runtime,
		})

		resp := httputil.DoJSON(t, router, http.MethodPatch, "/api/v1/workspaces/wsp_1/agents/agt_1", map[string]any{"model": "new", "llmMode": "agentteam_proxy"})
		require.Equal(t, http.StatusOK, resp.Code)
		require.Empty(t, runtime.stopCalls)
		require.Len(t, runtime.restartCalls, 1)
		require.Equal(t, agentCall{MachineID: "mach_1", AgentID: "agt_1"}, runtime.restartCalls[0])
		require.NotNil(t, fake.lastUpdate.Model)
		require.Equal(t, "new", *fake.lastUpdate.Model)
		require.True(t, fake.lastUpdate.LLMSet)
		require.NotNil(t, fake.lastUpdate.LLMMode)
		require.Equal(t, "agentteam_proxy", *fake.lastUpdate.LLMMode)
	})

	t.Run("switch to hosted validates admission and stores hosted binding", func(t *testing.T) {
		fake := newFakeStore()
		fake.agentView = agent.AgentView{AgentID: "agt_1", Runtime: "claude", Model: "old", ReasoningEffort: "medium", MachineID: "mach_1", Status: "active"}
		fake.updateResult = fake.agentView
		fake.appAgents["agt_1"] = agent.AppAgent{ID: "agt_1", Name: "agent-one", DisplayName: "Agent One", MachineID: "mach_1", Runtime: "claude"}
		hosted := &fakeHostedAgentValidator{}
		supported := true
		runtime := &fakeRuntime{maasDirectOK: &supported}
		router := newWebAgentRouter(Dependencies{
			AgentStore:   fake,
			ReviewStore:  fake,
			HostedAgents: hosted,
			Runtime:      runtime,
		})

		resp := httputil.DoJSON(t, router, http.MethodPatch, "/api/v1/workspaces/wsp_1/agents/agt_1", map[string]any{
			"model": "agentteam/deepseek-v4-pro", "llmMode": "agentteam_hosted", "apiKey": "ignored",
		})

		require.Equal(t, http.StatusOK, resp.Code)
		require.Equal(t, 1, hosted.calls)
		require.Equal(t, "agentteam/deepseek-v4-pro", hosted.last.ModelReference)
		require.Equal(t, agent.AgentLLMModeAgentTeamHosted, hosted.last.LLMMode)
		require.NotNil(t, hosted.last.MachineSupportsDirectMaaS)
		require.True(t, *hosted.last.MachineSupportsDirectMaaS)
		require.True(t, fake.lastUpdate.LLMSet)
		require.NotNil(t, fake.lastUpdate.LLMMode)
		require.Equal(t, agent.AgentLLMModeAgentTeamHosted, *fake.lastUpdate.LLMMode)
		require.NotNil(t, fake.lastUpdate.APIKey)
		require.Empty(t, *fake.lastUpdate.APIKey)
		require.Len(t, runtime.restartCalls, 1)
	})

	t.Run("runtime config change for inactive agent only syncs catalog", func(t *testing.T) {
		fake := newFakeStore()
		fake.agentView = agent.AgentView{AgentID: "agt_1", Runtime: "codex", Model: "old", ReasoningEffort: "medium", MachineID: "mach_1", Status: "inactive", Activity: "offline"}
		fake.updateResult = fake.agentView
		fake.appAgents["agt_1"] = agent.AppAgent{ID: "agt_1", Name: "agent-one", DisplayName: "Agent One", MachineID: "mach_1", Runtime: "claude"}
		runtime := &fakeRuntime{}
		router := newWebAgentRouter(Dependencies{
			AgentStore:    fake,
			ReviewStore:   fake,
			Organizations: fakeOrganizationProvider{providers: []organization.OrganizationProviderView{{ID: "opv_1"}}},
			Runtime:       runtime,
		})

		resp := httputil.DoJSON(t, router, http.MethodPatch, "/api/v1/workspaces/wsp_1/agents/agt_1", map[string]any{"runtime": "claude"})
		require.Equal(t, http.StatusOK, resp.Code)
		require.Empty(t, runtime.stopCalls)
		require.Empty(t, runtime.restartCalls)
		require.Equal(t, []string{"mach_1"}, runtime.syncMachineCalls)
	})

	t.Run("legacy persisted workspace permission normalizes without restart", func(t *testing.T) {
		fake := newFakeStore()
		fake.agentView = agent.AgentView{AgentID: "agt_1", Runtime: "codex", PermissionMode: "agent_workspace_write", Model: "same", MachineID: "mach_1", Status: "active"}
		fake.updateResult = fake.agentView
		fake.appAgents["agt_1"] = agent.AppAgent{ID: "agt_1", Name: "agent-one", DisplayName: "Agent One", MachineID: "mach_1", Runtime: "codex", PermissionMode: "full_access"}
		runtime := &fakeRuntime{}
		router := newWebAgentRouter(Dependencies{AgentStore: fake, ReviewStore: fake, Runtime: runtime})

		resp := httputil.DoJSON(t, router, http.MethodPatch, "/api/v1/workspaces/wsp_1/agents/agt_1", map[string]any{"permissionMode": "full_access"})

		require.Equal(t, http.StatusOK, resp.Code)
		require.Empty(t, runtime.restartCalls)
		require.NotNil(t, fake.lastUpdate.PermissionMode)
		require.Equal(t, agent.AgentPermissionModeFullAccess, *fake.lastUpdate.PermissionMode)
	})

	t.Run("legacy workspace permission request normalizes without in-flight restart", func(t *testing.T) {
		fake := newFakeStore()
		fake.agentView = agent.AgentView{AgentID: "agt_1", Runtime: "codex", PermissionMode: "full_access", Model: "same", MachineID: "mach_1", Status: "inactive", Activity: "offline"}
		fake.updateResult = fake.agentView
		fake.appAgents["agt_1"] = agent.AppAgent{ID: "agt_1", Name: "agent-one", DisplayName: "Agent One", MachineID: "mach_1", Runtime: "codex", PermissionMode: "agent_workspace_write"}
		runtime := &fakeRuntime{runningAgents: map[agentCall]bool{{MachineID: "mach_1", AgentID: "agt_1"}: true}}
		router := newWebAgentRouter(Dependencies{AgentStore: fake, ReviewStore: fake, Runtime: runtime})

		resp := httputil.DoJSON(t, router, http.MethodPatch, "/api/v1/workspaces/wsp_1/agents/agt_1", map[string]any{"permissionMode": "agent_workspace_write"})

		require.Equal(t, http.StatusOK, resp.Code)
		require.Empty(t, runtime.restartCalls)
		require.NotNil(t, fake.lastUpdate.PermissionMode)
		require.Equal(t, agent.AgentPermissionModeFullAccess, *fake.lastUpdate.PermissionMode)
	})

	t.Run("legacy workspace permission request does not recheck in-flight status", func(t *testing.T) {
		fake := newFakeStore()
		fake.agentView = agent.AgentView{AgentID: "agt_1", Runtime: "codex", PermissionMode: "full_access", Model: "same", MachineID: "mach_1", Status: "inactive", Activity: "offline"}
		fake.updateResult = fake.agentView
		fake.appAgents["agt_1"] = agent.AppAgent{ID: "agt_1", Name: "agent-one", DisplayName: "Agent One", MachineID: "mach_1", Runtime: "codex", PermissionMode: "agent_workspace_write"}
		runtime := &fakeRuntime{runningResults: []bool{false, true}}
		router := newWebAgentRouter(Dependencies{AgentStore: fake, ReviewStore: fake, Runtime: runtime})

		resp := httputil.DoJSON(t, router, http.MethodPatch, "/api/v1/workspaces/wsp_1/agents/agt_1", map[string]any{"permissionMode": "agent_workspace_write"})

		require.Equal(t, http.StatusOK, resp.Code)
		require.Empty(t, runtime.restartCalls)
		require.Len(t, runtime.runningResults, 2)
	})

	t.Run("legacy workspace permission post-update race no longer requires permission capability", func(t *testing.T) {
		fake := newFakeStore()
		fake.agentView = agent.AgentView{AgentID: "agt_1", Runtime: "codex", PermissionMode: "full_access", Model: "same", MachineID: "mach_1", Status: "inactive", Activity: "offline", SessionID: "sess_keep"}
		fake.updateResult = fake.agentView
		fake.appAgents["agt_1"] = agent.AppAgent{ID: "agt_1", Name: "agent-one", DisplayName: "Agent One", MachineID: "mach_1", Runtime: "codex", PermissionMode: "agent_workspace_write"}
		permissionModeOK := false
		runtime := &fakeRuntime{
			runningResults:   []bool{false, true},
			permissionModeOK: &permissionModeOK,
		}
		router := newWebAgentRouter(Dependencies{AgentStore: fake, ReviewStore: fake, Runtime: runtime})

		resp := httputil.DoJSON(t, router, http.MethodPatch, "/api/v1/workspaces/wsp_1/agents/agt_1", map[string]any{"permissionMode": "agent_workspace_write"})

		require.Equal(t, http.StatusOK, resp.Code)
		require.Empty(t, runtime.restartCalls)
		require.Empty(t, fake.restoreCalls)
	})

	t.Run("legacy workspace permission restart does not require capable daemon", func(t *testing.T) {
		fake := newFakeStore()
		fake.agentView = agent.AgentView{AgentID: "agt_1", Runtime: "codex", PermissionMode: "full_access", Model: "same", MachineID: "mach_1", Status: "inactive", Activity: "offline"}
		fake.updateResult = fake.agentView
		fake.appAgents["agt_1"] = agent.AppAgent{ID: "agt_1", Name: "agent-one", DisplayName: "Agent One", MachineID: "mach_1", Runtime: "codex", PermissionMode: "agent_workspace_write"}
		permissionModeOK := false
		runtime := &fakeRuntime{
			runningAgents:    map[agentCall]bool{{MachineID: "mach_1", AgentID: "agt_1"}: true},
			permissionModeOK: &permissionModeOK,
		}
		router := newWebAgentRouter(Dependencies{AgentStore: fake, ReviewStore: fake, Runtime: runtime})

		resp := httputil.DoJSON(t, router, http.MethodPatch, "/api/v1/workspaces/wsp_1/agents/agt_1", map[string]any{"permissionMode": "agent_workspace_write"})

		require.Equal(t, http.StatusOK, resp.Code)
		require.NotNil(t, fake.lastUpdate.PermissionMode)
		require.Equal(t, agent.AgentPermissionModeFullAccess, *fake.lastUpdate.PermissionMode)
		require.Empty(t, runtime.restartCalls)
	})

	t.Run("legacy workspace permission normalization does not rollback on offline runtime", func(t *testing.T) {
		fake := newFakeStore()
		fake.agentView = agent.AgentView{AgentID: "agt_1", Runtime: "codex", PermissionMode: "full_access", Model: "same", MachineID: "mach_1", Status: "active", SessionID: "sess_keep"}
		fake.updateResult = fake.agentView
		fake.appAgents["agt_1"] = agent.AppAgent{ID: "agt_1", Name: "agent-one", DisplayName: "Agent One", MachineID: "mach_1", Runtime: "codex", PermissionMode: "agent_workspace_write"}
		runtime := &fakeRuntime{restartErr: runtimecontrol.ErrMachineOffline}
		router := newWebAgentRouter(Dependencies{AgentStore: fake, ReviewStore: fake, Runtime: runtime})

		resp := httputil.DoJSON(t, router, http.MethodPatch, "/api/v1/workspaces/wsp_1/agents/agt_1", map[string]any{"permissionMode": "agent_workspace_write"})

		require.Equal(t, http.StatusOK, resp.Code)
		require.Empty(t, runtime.restartCalls)
		require.Len(t, fake.updateCalls, 1)
		require.NotNil(t, fake.updateCalls[0].Params.PermissionMode)
		require.Equal(t, agent.AgentPermissionModeFullAccess, *fake.updateCalls[0].Params.PermissionMode)
		require.Empty(t, fake.restoreCalls)
	})

	t.Run("legacy workspace permission mixed config change keeps restart behavior without rollback", func(t *testing.T) {
		fake := newFakeStore()
		fake.agentView = agent.AgentView{AgentID: "agt_1", Runtime: "codex", PermissionMode: "full_access", Model: "old", MachineID: "mach_1", Status: "active", SessionID: "sess_keep"}
		fake.updateResult = fake.agentView
		fake.appAgents["agt_1"] = agent.AppAgent{ID: "agt_1", Name: "agent-one", DisplayName: "Agent One", MachineID: "mach_1", Runtime: "codex", PermissionMode: "agent_workspace_write", Model: "new"}
		runtime := &fakeRuntime{restartErr: runtimecontrol.ErrMachineOffline}
		router := newWebAgentRouter(Dependencies{AgentStore: fake, ReviewStore: fake, Runtime: runtime})

		resp := httputil.DoJSON(t, router, http.MethodPatch, "/api/v1/workspaces/wsp_1/agents/agt_1", map[string]any{"permissionMode": "agent_workspace_write", "model": "new"})

		require.Equal(t, http.StatusOK, resp.Code)
		require.Len(t, runtime.restartCalls, 1)
		require.Empty(t, fake.restoreCalls)
	})

	t.Run("legacy workspace permission normalization does not restart before response fetch", func(t *testing.T) {
		fake := newFakeStore()
		fake.agentView = agent.AgentView{AgentID: "agt_1", Runtime: "codex", PermissionMode: "full_access", Model: "same", MachineID: "mach_1", Status: "active"}
		fake.updateResult = fake.agentView
		fake.appAgentErr = errors.New("agent response failed")
		runtime := &fakeRuntime{}
		router := newWebAgentRouter(Dependencies{AgentStore: fake, ReviewStore: fake, Runtime: runtime})

		resp := httputil.DoJSON(t, router, http.MethodPatch, "/api/v1/workspaces/wsp_1/agents/agt_1", map[string]any{"permissionMode": "agent_workspace_write"})

		require.Equal(t, http.StatusInternalServerError, resp.Code)
		require.Empty(t, runtime.restartCalls)
		require.Empty(t, fake.restoreCalls)
	})

	t.Run("rename action derives handle and updates running identity", func(t *testing.T) {
		fake := newFakeStore()
		fake.agentView = agent.AgentView{AgentID: "agt_1", Runtime: "codex", Model: "same", MachineID: "mach_1"}
		fake.appAgents["agt_1"] = agent.AppAgent{ID: "agt_1", Name: "new-name", DisplayName: "New Name", MachineID: "mach_1"}
		runtime := &fakeRuntime{}
		router := newWebAgentRouter(Dependencies{AgentStore: fake, ReviewStore: fake, Runtime: runtime})

		resp := httputil.DoJSON(t, router, http.MethodPatch, "/api/v1/workspaces/wsp_1/agents/agt_1", map[string]any{"displayName": "New Name", "deriveName": true})
		require.Equal(t, http.StatusOK, resp.Code)
		require.Nil(t, fake.lastUpdate.Name)
		require.NotNil(t, fake.lastUpdate.DisplayName)
		require.Equal(t, "New Name", *fake.lastUpdate.DisplayName)
		require.True(t, fake.lastUpdate.DeriveName)
		require.Empty(t, runtime.stopCalls)
		require.Len(t, runtime.identityCalls, 1)
		require.Equal(t, identityCall{MachineID: "mach_1", AgentID: "agt_1", Name: "new-name", DisplayName: "New Name"}, runtime.identityCalls[0])
	})

	t.Run("display name-only patch keeps handle and allows spaces", func(t *testing.T) {
		fake := newFakeStore()
		fake.agentView = agent.AgentView{AgentID: "agt_1", Runtime: "codex", Model: "same", MachineID: "mach_1"}
		fake.appAgents["agt_1"] = agent.AppAgent{ID: "agt_1", Name: "old-name", DisplayName: "Helper App", MachineID: "mach_1"}
		runtime := &fakeRuntime{}
		router := newWebAgentRouter(Dependencies{AgentStore: fake, ReviewStore: fake, Runtime: runtime})

		resp := httputil.DoJSON(t, router, http.MethodPatch, "/api/v1/workspaces/wsp_1/agents/agt_1", map[string]any{"displayName": "Helper App"})

		require.Equal(t, http.StatusOK, resp.Code)
		require.Nil(t, fake.lastUpdate.Name)
		require.NotNil(t, fake.lastUpdate.DisplayName)
		require.Equal(t, "Helper App", *fake.lastUpdate.DisplayName)
		require.Len(t, runtime.identityCalls, 1)
		require.Equal(t, identityCall{MachineID: "mach_1", AgentID: "agt_1", Name: "old-name", DisplayName: "Helper App"}, runtime.identityCalls[0])
	})

	t.Run("display name metadata edit does not update handle", func(t *testing.T) {
		fake := newFakeStore()
		fake.agentView = agent.AgentView{AgentID: "agt_1", Runtime: "codex", Model: "same", MachineID: "mach_1"}
		fake.appAgents["agt_1"] = agent.AppAgent{ID: "agt_1", Name: "old-name", DisplayName: "New Display Name", MachineID: "mach_1"}
		router := newWebAgentRouter(Dependencies{AgentStore: fake, ReviewStore: fake})

		resp := httputil.DoJSON(t, router, http.MethodPatch, "/api/v1/workspaces/wsp_1/agents/agt_1", map[string]any{
			"displayName": "New Display Name",
			"description": "metadata edit",
		})

		require.Equal(t, http.StatusOK, resp.Code)
		require.Nil(t, fake.lastUpdate.Name)
		require.NotNil(t, fake.lastUpdate.DisplayName)
		require.Equal(t, "New Display Name", *fake.lastUpdate.DisplayName)
		require.NotNil(t, fake.lastUpdate.Description)
		require.Equal(t, "metadata edit", *fake.lastUpdate.Description)
	})

	t.Run("null optional string is converted to empty string", func(t *testing.T) {
		fake := newFakeStore()
		router := newWebAgentRouter(Dependencies{AgentStore: fake, ReviewStore: fake})
		resp := httputil.DoRaw(t, router, http.MethodPatch, "/api/v1/workspaces/wsp_1/agents/agt_1", authJSONHeaders(), []byte(`{"description":null}`))
		require.Equal(t, http.StatusOK, resp.Code)
		require.NotNil(t, fake.lastUpdate.Description)
		require.Equal(t, "", *fake.lastUpdate.Description)
	})
}

func TestCollectionDeleteStopsAndGCsRecoveryPoints(t *testing.T) {
	fake := newFakeStore()
	fake.recoveryPoints = []rewind.MessageRecoveryPoint{{MessageID: "msg_1", AgentID: "agt_1"}}
	runtime := &fakeRuntime{}
	realtime := &fakeRealtime{}
	router := newWebAgentRouter(Dependencies{AgentStore: fake, ReviewStore: fake, Runtime: runtime, Realtime: realtime})

	resp := httputil.DoJSON(t, router, http.MethodDelete, "/api/v1/workspaces/wsp_1/agents/agt_1", nil)
	require.Equal(t, http.StatusOK, resp.Code)
	require.Equal(t, "agt_1", fake.lastDeleteID)
	require.Len(t, runtime.stopCalls, 1)
	require.Len(t, runtime.gcCalls, 1)
	require.Equal(t, fake.recoveryPoints, runtime.gcCalls[0].Points)
	require.Equal(t, "wsp_1", realtime.deletedWorkspace)
}

func authJSONHeaders() http.Header {
	return http.Header{
		"Authorization":  {"Bearer token"},
		"X-Workspace-Id": {"wsp_1"},
		"Content-Type":   {"application/json"},
	}
}

func TestPOSTUpdateAliasBehavesLikePATCH(t *testing.T) {
	type outcome struct {
		status  int
		body    string
		agentID string
		model   string
		llmMode string
	}
	run := func(t *testing.T, method, path string) outcome {
		t.Helper()
		fake := newFakeStore()
		fake.agentView = agent.AgentView{AgentID: "agt_1", Runtime: "codex", MachineID: "mach_1", Status: "active"}
		fake.updateResult = fake.agentView
		fake.appAgents["agt_1"] = agent.AppAgent{ID: "agt_1", Name: "agent-one", DisplayName: "Agent One", MachineID: "mach_1", Runtime: "codex"}
		router := newWebAgentRouter(Dependencies{
			AgentStore:    fake,
			ReviewStore:   fake,
			Organizations: fakeOrganizationProvider{providers: []organization.OrganizationProviderView{{ID: "opv_1"}}},
			Runtime:       &fakeRuntime{},
		})
		resp := httputil.DoJSON(t, router, method, path, map[string]any{"model": "new", "llmMode": "agentteam_proxy"})
		require.NotNil(t, fake.lastUpdate.Model)
		require.NotNil(t, fake.lastUpdate.LLMMode)
		return outcome{
			status:  resp.Code,
			body:    resp.Body.String(),
			agentID: fake.lastUpdateID,
			model:   *fake.lastUpdate.Model,
			llmMode: *fake.lastUpdate.LLMMode,
		}
	}
	patch := run(t, http.MethodPatch, "/api/v1/workspaces/wsp_1/agents/agt_1")
	post := run(t, http.MethodPost, "/api/v1/workspaces/wsp_1/agents/agt_1/update")
	require.Equal(t, patch, post)

	t.Run("POST update without auth returns 401", func(t *testing.T) {
		router := gin.New()
		RegisterRoutes(router, Dependencies{})
		recorder := httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/agents/agt_1/update", map[string]any{"model": "new"})
		require.Equal(t, http.StatusUnauthorized, recorder.Code)
	})

	t.Run("POST update surfaces store errors like PATCH", func(t *testing.T) {
		fake := newFakeStore()
		fake.agentView = agent.AgentView{AgentID: "agt_1", Runtime: "codex", MachineID: "mach_1", Status: "active"}
		fake.appAgents["agt_1"] = agent.AppAgent{ID: "agt_1", Name: "agent-one", DisplayName: "Agent One", MachineID: "mach_1", Runtime: "codex"}
		fake.updateErr = storeerr.Forbidden("not your agent")
		router := newWebAgentRouter(Dependencies{AgentStore: fake, ReviewStore: fake})

		resp := httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/agents/agt_1/update", map[string]any{"model": "new"})
		require.Equal(t, http.StatusForbidden, resp.Code)
	})
}
