package workspace

import (
	"bytes"
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

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

	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/runtimecontrol"
	workspaceservice "github.com/tidbcloud/AgentTeam/internal/service/workspace"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/app/runtime"
	"github.com/tidbcloud/AgentTeam/internal/store/identity"
)

type fakeMachineRuntime struct {
	disconnected      string
	connectedMachines map[string]bool
	runningAgents     map[machineAgentCall]bool
	sessionIDs        map[machineAgentCall]string
	runtimeStatuses   map[machineAgentCall]machineAgentRuntimeStatus
	runtimeRuntimes   map[string][]string
	runtimeDefaults   map[string]map[string]runtime.RuntimeDefaults
	directories       []agent.WorkspaceDirectory
	page              *agent.WorkspaceDirectoryPage
	lastScanOpts      agent.WorkspaceListOptions
	deleted           bool
	lastRemoteRequest runtimecontrol.RemoteRequestContext
	syncMachineCalls  []string
	restartCalls      []machineAgentCall

	dashboardSupported   map[string]bool
	dashboard            runtimecontrol.MachineDashboardResponse
	dashboardErr         error
	lastDashboardRequest runtimecontrol.MachineDashboardRequest
	dashboardCallCount   int

	directoryBrowseSupported map[string]bool
	directoryPage            *agent.MachineDirectoryPage
	directoryErr             error
	lastDirectoryListRequest runtimecontrol.RemoteRequestContext
	lastDirectoryListArgs    machineDirectoryListArgs
	directoryListCallCount   int

	// Machine Detail tab remotes (settings / runtimes / mcp-servers). When
	// machineDetailErr is set, every detail method returns it; otherwise reads
	// return machineDetailPayload (or a minimal default) and writes echo their
	// input payload back to the caller.
	machineDetailPayload   json.RawMessage
	machineDetailErr       error
	machineDetailCallCount int
}

type machineDirectoryListArgs struct {
	MachineID string
	Path      string
	Options   agent.WorkspaceListOptions
}

type machineAgentCall struct{ MachineID, AgentID string }
type machineAgentRuntimeStatus struct {
	Status   string
	Activity string
	Detail   string
}

func (fake *fakeMachineRuntime) DeleteMachineWorkspaceDirectory(ctx context.Context, _ string, _ string) (bool, error) {
	fake.lastRemoteRequest = runtimecontrol.RemoteRequestContextFromContext(ctx)
	return fake.deleted, nil
}

func (fake *fakeMachineRuntime) DisconnectMachine(machineID string) {
	fake.disconnected = machineID
}

func (fake *fakeMachineRuntime) MachineConnected(machineID string) bool {
	return fake.connectedMachines[machineID]
}

func (fake *fakeMachineRuntime) AgentRuntimeRunning(machineID string, agentID string) bool {
	return fake.runningAgents[machineAgentCall{machineID, agentID}]
}

func (fake *fakeMachineRuntime) AgentRuntimeSessionID(machineID string, agentID string) (string, bool) {
	sessionID := fake.sessionIDs[machineAgentCall{machineID, agentID}]
	return sessionID, sessionID != ""
}

func (fake *fakeMachineRuntime) AgentRuntimeStatus(machineID string, agentID string) (string, string, string, bool) {
	status, ok := fake.runtimeStatuses[machineAgentCall{machineID, agentID}]
	if !ok {
		return "", "", "", false
	}
	return status.Status, status.Activity, status.Detail, true
}

func (fake *fakeMachineRuntime) MachineRuntimeCatalog(machineID string) ([]string, map[string]runtime.RuntimeDefaults, bool) {
	runtimes, ok := fake.runtimeRuntimes[machineID]
	if !ok {
		return nil, nil, false
	}
	return runtimes, fake.runtimeDefaults[machineID], true
}

func (fake *fakeMachineRuntime) ScanMachineWorkspace(_ context.Context, _ string) ([]agent.WorkspaceDirectory, error) {
	return fake.directories, nil
}

func (fake *fakeMachineRuntime) ScanMachineWorkspacePage(ctx context.Context, _ string, opts agent.WorkspaceListOptions) (agent.WorkspaceDirectoryPage, error) {
	fake.lastRemoteRequest = runtimecontrol.RemoteRequestContextFromContext(ctx)
	fake.lastScanOpts = opts
	if fake.page != nil {
		return *fake.page, nil
	}
	return agent.WorkspaceDirectoryPage{
		Directories: fake.directories,
		Limit:       opts.Limit,
		Cursor:      opts.Cursor,
	}, nil
}

func (fake *fakeMachineRuntime) SyncMachineCatalog(_ context.Context, machineID string) error {
	fake.syncMachineCalls = append(fake.syncMachineCalls, machineID)
	return nil
}

func (fake *fakeMachineRuntime) RestartAgent(_ context.Context, machineID string, agentID string) error {
	fake.restartCalls = append(fake.restartCalls, machineAgentCall{MachineID: machineID, AgentID: agentID})
	return nil
}

func (fake *fakeMachineRuntime) MachineDashboardSupported(machineID string) bool {
	if fake == nil || fake.dashboardSupported == nil {
		return false
	}
	return fake.dashboardSupported[machineID]
}

func (fake *fakeMachineRuntime) QueryMachineDashboard(ctx context.Context, machineID string, request runtimecontrol.MachineDashboardRequest) (runtimecontrol.MachineDashboardResponse, error) {
	fake.lastRemoteRequest = runtimecontrol.RemoteRequestContextFromContext(ctx)
	fake.dashboardCallCount++
	fake.lastDashboardRequest = request
	if fake.dashboardErr != nil {
		return runtimecontrol.MachineDashboardResponse{}, fake.dashboardErr
	}
	if fake.dashboard.MachineID != "" || fake.dashboard.WorkspaceID != "" {
		return fake.dashboard, nil
	}
	return runtimecontrol.MachineDashboardResponse{
		MachineID:    machineID,
		WorkspaceID:  request.WorkspaceID,
		Window:       string(request.Window),
		AgentCount:   len(request.AgentIDs),
		ActiveAgents: 1,
		MessageSummary: runtimecontrol.MachineDashboardMessageSummary{
			Received: 2,
			Sent:     3,
			Total:    5,
		},
	}, nil
}

func (fake *fakeMachineRuntime) MachineDirectoryBrowseSupported(machineID string) bool {
	if fake == nil || fake.directoryBrowseSupported == nil {
		return false
	}
	return fake.directoryBrowseSupported[machineID]
}

func (fake *fakeMachineRuntime) ListMachineDirectoriesPage(ctx context.Context, machineID string, path string, opts agent.WorkspaceListOptions) (agent.MachineDirectoryPage, error) {
	fake.lastDirectoryListRequest = runtimecontrol.RemoteRequestContextFromContext(ctx)
	fake.lastDirectoryListArgs = machineDirectoryListArgs{MachineID: machineID, Path: path, Options: opts}
	fake.directoryListCallCount++
	if fake.directoryErr != nil {
		return agent.MachineDirectoryPage{}, fake.directoryErr
	}
	if fake.directoryPage != nil {
		return *fake.directoryPage, nil
	}
	return agent.MachineDirectoryPage{
		Directories: nil,
		Limit:       opts.Limit,
		Cursor:      opts.Cursor,
	}, nil
}

// --- Machine Detail tab remote stubs --------------------------------------
//
// The fake returns a minimal valid JSON document for each read endpoint and
// echoes the request body for each write endpoint. Tests that need to assert
// on specific payload shapes can populate the matching fields on
// fakeMachineRuntime (machineDetailPayload / machineDetailErr) before invoking
// the handler.

func (fake *fakeMachineRuntime) QueryMachineSettings(ctx context.Context, _, _ string) (json.RawMessage, error) {
	fake.recordMachineDetailCall(ctx)
	if fake.machineDetailErr != nil {
		return nil, fake.machineDetailErr
	}
	if len(fake.machineDetailPayload) > 0 {
		return fake.machineDetailPayload, nil
	}
	return json.RawMessage(`{"desktop":{},"system":{},"daemon":{}}`), nil
}

func (fake *fakeMachineRuntime) UpdateMachineSettings(ctx context.Context, _, _ string, desktop json.RawMessage) (json.RawMessage, error) {
	fake.recordMachineDetailCall(ctx)
	if fake.machineDetailErr != nil {
		return nil, fake.machineDetailErr
	}
	if len(desktop) == 0 {
		return json.RawMessage(`{"ok":true}`), nil
	}
	return desktop, nil
}

func (fake *fakeMachineRuntime) QueryMachineRuntimesBundle(ctx context.Context, _, _ string) (json.RawMessage, error) {
	fake.recordMachineDetailCall(ctx)
	if fake.machineDetailErr != nil {
		return nil, fake.machineDetailErr
	}
	if len(fake.machineDetailPayload) > 0 {
		return fake.machineDetailPayload, nil
	}
	return json.RawMessage(`{"summary":{},"runtimes":[],"agents":[],"detected":[]}`), nil
}

func (fake *fakeMachineRuntime) QueryMachineRuntimeConfig(ctx context.Context, _, _, _ string) (json.RawMessage, error) {
	fake.recordMachineDetailCall(ctx)
	if fake.machineDetailErr != nil {
		return nil, fake.machineDetailErr
	}
	if len(fake.machineDetailPayload) > 0 {
		return fake.machineDetailPayload, nil
	}
	return json.RawMessage(`{"runtime":"","configPath":"","exists":false,"editable":{},"offline":{}}`), nil
}

func (fake *fakeMachineRuntime) UpdateMachineRuntimeConfig(ctx context.Context, _, _, _ string, config json.RawMessage) (json.RawMessage, error) {
	fake.recordMachineDetailCall(ctx)
	if fake.machineDetailErr != nil {
		return nil, fake.machineDetailErr
	}
	if len(config) == 0 {
		return json.RawMessage(`{"runtime":"","configPath":"","exists":false,"editable":{},"offline":{}}`), nil
	}
	return config, nil
}

func (fake *fakeMachineRuntime) ValidateMachineRuntimeConfig(ctx context.Context, _, _, _ string, _ json.RawMessage) (json.RawMessage, error) {
	fake.recordMachineDetailCall(ctx)
	if fake.machineDetailErr != nil {
		return nil, fake.machineDetailErr
	}
	if len(fake.machineDetailPayload) > 0 {
		return fake.machineDetailPayload, nil
	}
	return json.RawMessage(`{"offline":{"ok":true},"online":{"ok":true,"models":[]}}`), nil
}

func (fake *fakeMachineRuntime) DetectMachineRuntimes(ctx context.Context, _, _ string) (json.RawMessage, error) {
	fake.recordMachineDetailCall(ctx)
	if fake.machineDetailErr != nil {
		return nil, fake.machineDetailErr
	}
	if len(fake.machineDetailPayload) > 0 {
		return fake.machineDetailPayload, nil
	}
	return json.RawMessage(`{"summary":{},"runtimes":[],"agents":[],"detected":[]}`), nil
}

func (fake *fakeMachineRuntime) QueryMachineMCPServers(ctx context.Context, _, _ string) (json.RawMessage, error) {
	fake.recordMachineDetailCall(ctx)
	if fake.machineDetailErr != nil {
		return nil, fake.machineDetailErr
	}
	if len(fake.machineDetailPayload) > 0 {
		return fake.machineDetailPayload, nil
	}
	return json.RawMessage(`{"servers":[]}`), nil
}

func (fake *fakeMachineRuntime) UpsertMachineMCPServer(ctx context.Context, _, _ string, server json.RawMessage) (json.RawMessage, error) {
	fake.recordMachineDetailCall(ctx)
	if fake.machineDetailErr != nil {
		return nil, fake.machineDetailErr
	}
	if len(server) == 0 {
		return json.RawMessage(`{}`), nil
	}
	return server, nil
}

func (fake *fakeMachineRuntime) DeleteMachineMCPServer(ctx context.Context, _, _, _ string) (json.RawMessage, error) {
	fake.recordMachineDetailCall(ctx)
	if fake.machineDetailErr != nil {
		return nil, fake.machineDetailErr
	}
	return json.RawMessage(`{"ok":true}`), nil
}

func (fake *fakeMachineRuntime) TestMachineMCPServer(ctx context.Context, _, _, _ string) (json.RawMessage, error) {
	fake.recordMachineDetailCall(ctx)
	if fake.machineDetailErr != nil {
		return nil, fake.machineDetailErr
	}
	if len(fake.machineDetailPayload) > 0 {
		return fake.machineDetailPayload, nil
	}
	return json.RawMessage(`{"ok":true}`), nil
}

func (fake *fakeMachineRuntime) recordMachineDetailCall(ctx context.Context) {
	fake.lastRemoteRequest = runtimecontrol.RemoteRequestContextFromContext(ctx)
	fake.machineDetailCallCount++
}

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

	handler := &MachineHandler{}
	router := gin.New()
	router.GET("/api/v1/workspaces/:workspaceId/machines", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
	}, handleWorkspaceAuth(handler.List))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/workspaces/wsp_1/machines?scope=everything", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusBadRequest, recorder.Code)
	require.Contains(t, recorder.Body.String(), "scope must be own or all")
}

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

	fake := &fakeWorkspaceService{
		allMachines: []workspaceservice.Machine{{ID: "mach_1", Name: "Machine 1"}},
	}
	handler := &MachineHandler{workspaceService: fake}
	router := gin.New()
	router.GET("/api/v1/workspaces/:workspaceId/machines", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
	}, handleWorkspaceAuth(handler.List))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/workspaces/wsp_1/machines?scope=all&limit=10&cursor=cursor_1", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, workspaceservice.MachineListOptions{Limit: 10, Cursor: "cursor_1"}, fake.allMachineOptions)
	require.JSONEq(t, `{"machines":[{"id":"mach_1","name":"Machine 1","status":"","apiKeyPrefix":"","canManage":false,"runtimes":[],"runtimeDefaults":{},"createdAt":"0001-01-01T00:00:00Z","directoryBrowseSupported":false,"dashboardSupported":false}],"latestDaemonVersion":"","hasMore":false}`, recorder.Body.String())
}

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

	fake := &fakeWorkspaceService{
		ownMachines: []workspaceservice.Machine{{ID: "mach_own", Name: "Machine Own"}},
	}
	handler := &MachineHandler{workspaceService: fake}
	router := gin.New()
	router.GET("/api/v1/workspaces/:workspaceId/machines", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
	}, handleWorkspaceAuth(handler.List))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/workspaces/wsp_1/machines", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, workspaceservice.MachineListOptions{}, fake.ownMachineOptions)
	require.JSONEq(t, `{"machines":[{"id":"mach_own","name":"Machine Own","status":"","apiKeyPrefix":"","canManage":false,"runtimes":[],"runtimeDefaults":{},"createdAt":"0001-01-01T00:00:00Z","directoryBrowseSupported":false,"dashboardSupported":false}],"latestDaemonVersion":"","hasMore":false}`, recorder.Body.String())
}

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

	fake := &fakeWorkspaceService{
		ownMachines: []workspaceservice.Machine{
			{ID: "mach_online", Name: "Machine Online", Status: "offline"},
			{ID: "mach_offline", Name: "Machine Offline", Status: "offline"},
		},
	}
	runtime := &fakeMachineRuntime{
		connectedMachines:  map[string]bool{"mach_online": true},
		runtimeRuntimes:    map[string][]string{"mach_online": {"codex"}},
		dashboardSupported: map[string]bool{"mach_online": true},
		runtimeDefaults: map[string]map[string]runtime.RuntimeDefaults{
			"mach_online": {"codex": {Model: "gpt-5.4", ReasoningEffort: "high"}},
		},
	}
	handler := &MachineHandler{workspaceService: fake, runtime: runtime}
	router := gin.New()
	router.GET("/api/v1/workspaces/:workspaceId/machines", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
	}, handleWorkspaceAuth(handler.List))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/workspaces/wsp_1/machines", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.JSONEq(t, `{
		"machines":[
			{"id":"mach_online","name":"Machine Online","status":"online","apiKeyPrefix":"","canManage":false,"runtimes":["codex"],"runtimeDefaults":{"codex":{"model":"gpt-5.4","reasoningEffort":"high"}},"createdAt":"0001-01-01T00:00:00Z","directoryBrowseSupported":false,"dashboardSupported":true},
			{"id":"mach_offline","name":"Machine Offline","status":"offline","apiKeyPrefix":"","canManage":false,"runtimes":[],"runtimeDefaults":{},"createdAt":"0001-01-01T00:00:00Z","directoryBrowseSupported":false,"dashboardSupported":false}
		],
		"latestDaemonVersion":"",
		"hasMore":false
	}`, recorder.Body.String())
}

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

	handler := &MachineHandler{}
	router := gin.New()
	router.GET("/api/v1/workspaces/:workspaceId/machines", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
	}, handleWorkspaceAuth(handler.List))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/workspaces/wsp_1/machines?limit=zero", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusBadRequest, recorder.Code)
	require.Contains(t, recorder.Body.String(), "limit must be an integer")
}

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

	runtime := &fakeMachineRuntime{}
	handler := &MachineHandler{workspaceService: &fakeWorkspaceService{rotatedKey: "next-key"}, runtime: runtime}
	router := gin.New()
	router.POST("/api/v1/workspaces/:workspaceId/machines/:machineId/api-key-rotations", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
	}, handleWorkspaceAuth(handler.RotateKey))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/v1/workspaces/wsp_1/machines/mach_1/api-key-rotations", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.JSONEq(t, `{"apiKey":"next-key"}`, recorder.Body.String())
	require.Equal(t, "mach_1", runtime.disconnected)
}

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

	runtime := &fakeMachineRuntime{}
	handler := &MachineHandler{workspaceService: &fakeWorkspaceService{}, runtime: runtime}
	router := gin.New()
	router.POST("/api/v1/workspaces/:workspaceId/machines/:machineId/evictions", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
	}, handleWorkspaceAuth(handler.Evict))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/v1/workspaces/wsp_1/machines/mach_1/evictions", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.JSONEq(t, `{"evicted":true,"machineId":"mach_1","affectedAgents":["agt_1"]}`, recorder.Body.String())
	require.Equal(t, "mach_1", runtime.disconnected)
}

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

	fake := &fakeWorkspaceService{}
	handler := &MachineHandler{workspaceService: fake}
	router := gin.New()
	router.GET("/api/v1/workspaces/:workspaceId/machines/:machineId/workspace-directories", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
	}, handleWorkspaceAuth(handler.ListWorkspaceDirectories))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/workspaces/wsp_1/machines/mach_1/workspace-directories", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusServiceUnavailable, recorder.Code)
	require.Contains(t, recorder.Body.String(), "Machine is offline")
	require.True(t, fake.requireManagementHit)
}

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

	runtime := &fakeMachineRuntime{
		connectedMachines: map[string]bool{"mach_1": true},
		runningAgents:     map[machineAgentCall]bool{{MachineID: "mach_1", AgentID: "agt_local"}: true},
		runtimeStatuses: map[machineAgentCall]machineAgentRuntimeStatus{
			{MachineID: "mach_1", AgentID: "agt_local"}: {Activity: "working", Detail: "Indexing"},
		},
		page: &agent.WorkspaceDirectoryPage{
			Directories: []agent.WorkspaceDirectory{
				{DirectoryName: "agt_local", TotalSizeBytes: 10, FileCount: 2},
				{DirectoryName: "orphan_dir", TotalSizeBytes: 5, FileCount: 1},
			},
			Limit:      2,
			Cursor:     "4",
			HasMore:    true,
			NextCursor: "6",
		},
	}
	fake := &fakeWorkspaceService{
		agents: []agent.AppAgent{
			{ID: "agt_local", Name: "Broad agent", DisplayName: "Wrong broad agent", Status: "inactive"},
		},
		machineAgents: []agent.AppAgent{
			{ID: "agt_local", Name: "Local agent", DisplayName: "Local display", MachineID: "mach_1", Status: "inactive", Activity: "offline"},
		},
	}
	handler := &MachineHandler{workspaceService: fake, runtime: runtime}
	router := gin.New()
	router.GET("/api/v1/workspaces/:workspaceId/machines/:machineId/workspace-directories", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
	}, handleWorkspaceAuth(handler.ListWorkspaceDirectories))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/workspaces/wsp_1/machines/mach_1/workspace-directories?limit=2&cursor=4", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, agent.WorkspaceListOptions{Limit: 2, Cursor: "4"}, runtime.lastScanOpts)
	require.Equal(t, runtimecontrol.RemoteRequestOriginUserInteractive, runtime.lastRemoteRequest.Origin)
	require.Equal(t, runtimecontrol.RemoteRequestCauseWebAPI, runtime.lastRemoteRequest.Cause)
	require.False(t, fake.listAgentsForWorkspaceHit, "machine workspace scan must not load every workspace agent")
	require.Equal(t, []string{"usr_1", "wsp_1", "mach_1"}, fake.machineAgentArgs)
	require.JSONEq(t, `{
		"workspaces":[
			{"directoryName":"agt_local","agentName":"Local display","status":"active","totalSizeBytes":10,"fileCount":2,"lastModified":"0001-01-01T00:00:00Z"},
			{"directoryName":"orphan_dir","status":"orphan","totalSizeBytes":5,"fileCount":1,"lastModified":"0001-01-01T00:00:00Z"}
		],
		"limit":2,
		"cursor":"4",
		"hasMore":true,
		"nextCursor":"6"
	}`, recorder.Body.String())
}

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

	fake := &fakeWorkspaceService{
		machineAgents: []agent.AppAgent{
			{ID: "agt_machine", Name: "machine-agent", DisplayName: "Machine Agent", Runtime: "codex", MachineID: "mach_1", CanManage: true, Status: "inactive", Activity: "offline"},
		},
	}
	runtime := &fakeMachineRuntime{
		connectedMachines: map[string]bool{"mach_1": true},
		runningAgents:     map[machineAgentCall]bool{{MachineID: "mach_1", AgentID: "agt_machine"}: true},
		sessionIDs:        map[machineAgentCall]string{{MachineID: "mach_1", AgentID: "agt_machine"}: "sess-machine"},
	}
	handler := &MachineHandler{workspaceService: fake, runtime: runtime}
	router := gin.New()
	router.GET("/api/v1/workspaces/:workspaceId/machines/:machineId/agents", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
	}, handleWorkspaceAuth(handler.Agents))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/workspaces/wsp_1/machines/mach_1/agents", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.False(t, fake.listAgentsForWorkspaceHit, "machine agent read must not load every workspace agent")
	require.Equal(t, []string{"usr_1", "wsp_1", "mach_1"}, fake.machineAgentArgs)
	require.Contains(t, recorder.Body.String(), "Machine Agent")
	require.Contains(t, recorder.Body.String(), `"status":"active"`)
	require.Contains(t, recorder.Body.String(), `"activity":"online"`)
	require.Contains(t, recorder.Body.String(), `"sessionId":"sess-machine"`)
}

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

	runtime := &fakeMachineRuntime{connectedMachines: map[string]bool{"mach_1": true}}
	fake := &fakeWorkspaceService{
		agents: []agent.AppAgent{{ID: "agt_broad", MachineID: "mach_other"}},
		machineAgents: []agent.AppAgent{
			{ID: "agt_one", Name: "One", MachineID: "mach_1"},
			{ID: "agt_two", Name: "Two", MachineID: "mach_1"},
		},
	}
	handler := &MachineHandler{workspaceService: fake, runtime: runtime}
	router := gin.New()
	router.GET("/api/v1/workspaces/:workspaceId/machines/:machineId/dashboard", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
	}, handleWorkspaceAuth(handler.Dashboard))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/workspaces/wsp_1/machines/mach_1/dashboard?window=6h&agentId=agt_two&agentId=agt_two", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.False(t, fake.listAgentsForWorkspaceHit, "dashboard must not load every workspace agent")
	require.Equal(t, []string{"usr_1", "wsp_1", "mach_1"}, fake.machineAgentArgs)
	require.Equal(t, 1, runtime.dashboardCallCount)
	require.Equal(t, runtimecontrol.MachineDashboardRequest{
		WorkspaceID: "wsp_1",
		MachineID:   "mach_1",
		Window:      runtimecontrol.MachineDashboardWindow6Hours,
		AgentIDs:    []string{"agt_two"},
	}, runtime.lastDashboardRequest)
	require.Equal(t, runtimecontrol.RemoteRequestOriginUserInteractive, runtime.lastRemoteRequest.Origin)
	require.Equal(t, runtimecontrol.RemoteRequestCauseWebAPI, runtime.lastRemoteRequest.Cause)
	require.JSONEq(t, `{"machineId":"mach_1","workspaceId":"wsp_1","window":"6h","agentCount":1,"activeAgents":1,"messageSummary":{"received":2,"sent":3,"total":5},"messageTrend":{"interval":0,"data":null},"messageBar":{"interval":0,"series":null,"data":null},"tokenSummary":{"count":0,"input":0,"cached":0,"output":0,"total":0},"tokenTrend":{"interval":0,"data":null},"inputTokenBar":{"interval":0,"series":null,"data":null},"cachedTokenBar":{"interval":0,"series":null,"data":null},"outputTokenBar":{"interval":0,"series":null,"data":null},"tokenSources":{"available":false,"trend":{"series":null,"data":null}},"agents":null}`, recorder.Body.String())
}

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

	runtime := &fakeMachineRuntime{connectedMachines: map[string]bool{"mach_1": true}}
	fake := &fakeWorkspaceService{machineAgents: []agent.AppAgent{{ID: "agt_local", MachineID: "mach_1"}}}
	handler := &MachineHandler{workspaceService: fake, runtime: runtime}
	router := gin.New()
	router.GET("/api/v1/workspaces/:workspaceId/machines/:machineId/dashboard", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
	}, handleWorkspaceAuth(handler.Dashboard))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/api/v1/workspaces/wsp_1/machines/mach_1/dashboard?agentId=agt_foreign", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusBadRequest, recorder.Code)
	require.Equal(t, 0, runtime.dashboardCallCount)
	require.Contains(t, recorder.Body.String(), "agentId does not belong to this machine")
}

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

	handler := &MachineHandler{workspaceService: &fakeWorkspaceService{exchange: dto.MachineConnectExchangeResponse{
		MachineID:   "mach_1",
		APIKey:      "api-key",
		ServerURL:   "https://agentteam.example.com",
		WorkspaceID: "wsp_1",
	}}}
	router := gin.New()
	router.POST("/api/v1/machine-connect-exchanges", handler.ConnectExchange)

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/v1/machine-connect-exchanges", bytes.NewBufferString(`{"token":"one-time"}`))
	request.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.JSONEq(t, `{"machineId":"mach_1","apiKey":"api-key","serverUrl":"https://agentteam.example.com","workspaceId":"wsp_1"}`, recorder.Body.String())
}

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

	handler := &MachineHandler{workspaceService: &fakeWorkspaceService{exchange: dto.MachineConnectExchangeResponse{
		MachineID:   "mach_1",
		WorkspaceID: "wsp_1",
	}}}
	router := gin.New()
	router.POST("/api/v1/machine-auth-resolutions", handler.ResolveAuth)

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/v1/machine-auth-resolutions", bytes.NewBufferString(`{"apiKey":"api-key","serverUrl":"https://agentteam.example.com/"}`))
	request.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.JSONEq(t, `{"machineId":"mach_1","apiKey":"api-key","serverUrl":"https://agentteam.example.com/","workspaceId":"wsp_1"}`, recorder.Body.String())
}

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

	fakeService := &fakeWorkspaceService{}
	fakeRuntime := &fakeMachineRuntime{}
	handler := &MachineHandler{workspaceService: fakeService, runtime: fakeRuntime}
	router := gin.New()
	router.DELETE("/api/v1/workspaces/:workspaceId/machines/:machineId", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
	}, handleWorkspaceAuth(handler.Delete))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodDelete, "/api/v1/workspaces/wsp_1/machines/mac_1", nil)
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, "mac_1", fakeService.deletedMachineID)
	require.Equal(t, "mac_1", fakeRuntime.disconnected)
}

// TestMachineHandlerUpdateSettingsReachesRuntime pins the body-routing fix
// for the machine-detail write endpoints: PUT /settings MUST read the
// request body and forward it to the runtime (200, callCount 1, body echoed).
// Before the fix the body was consumed by the JSON adapter and the handler
// returned 400 "request body is required" without ever calling the runtime.
func TestMachineHandlerUpdateSettingsReachesRuntime(t *testing.T) {
	t.Parallel()

	runtime := &fakeMachineRuntime{connectedMachines: map[string]bool{"mach_1": true}}
	handler := &MachineHandler{workspaceService: &fakeWorkspaceService{}, runtime: runtime}
	router := gin.New()
	router.PUT("/api/v1/workspaces/:workspaceId/machines/:machineId/settings", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
	}, handleWorkspaceAuth(handler.UpdateSettings))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPut, "/api/v1/workspaces/wsp_1/machines/mach_1/settings", bytes.NewBufferString(`{"desktop":{"theme":"dark"}}`))
	request.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, 1, runtime.machineDetailCallCount, "UpdateSettings MUST forward the body to the runtime")
	require.JSONEq(t, `{"desktop":{"theme":"dark"}}`, recorder.Body.String())
}

// TestMachineHandlerUpdateSettingsAuthRunsBeforeRuntimeChecks pins auth-first
// ordering: when RequireMachineManagementForUser rejects, the handler MUST
// return the auth error without touching the (nil) runtime.
func TestMachineHandlerUpdateSettingsAuthRunsBeforeRuntimeChecks(t *testing.T) {
	t.Parallel()

	authErr := api.NewHTTPError(http.StatusForbidden, "not allowed")
	fake := &fakeWorkspaceService{machineManagementErr: authErr}
	handler := &MachineHandler{workspaceService: fake}
	router := gin.New()
	router.PUT("/api/v1/workspaces/:workspaceId/machines/:machineId/settings", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
	}, handleWorkspaceAuth(handler.UpdateSettings))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPut, "/api/v1/workspaces/wsp_1/machines/mach_1/settings", bytes.NewBufferString(`{"desktop":{"theme":"dark"}}`))
	request.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusForbidden, recorder.Code, "auth error MUST take precedence over runtime-nil 503")
	require.True(t, fake.requireManagementHit, "RequireMachineManagementForUser MUST run before any runtime check")
}

// TestMachineHandlerUpdateSettingsReturnsUnavailableWhenOffline pins that an
// offline machine yields a typed 503 without reaching the runtime write
// (and, implicitly, that the body was read without a 400).
func TestMachineHandlerUpdateSettingsReturnsUnavailableWhenOffline(t *testing.T) {
	t.Parallel()

	runtime := &fakeMachineRuntime{connectedMachines: map[string]bool{}}
	handler := &MachineHandler{workspaceService: &fakeWorkspaceService{}, runtime: runtime}
	router := gin.New()
	router.PUT("/api/v1/workspaces/:workspaceId/machines/:machineId/settings", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
	}, handleWorkspaceAuth(handler.UpdateSettings))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPut, "/api/v1/workspaces/wsp_1/machines/mach_1/settings", bytes.NewBufferString(`{"desktop":{"theme":"dark"}}`))
	request.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusServiceUnavailable, recorder.Code)
	require.Equal(t, 0, runtime.machineDetailCallCount, "offline machine MUST NOT reach the runtime write")
}

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

	runtime := &fakeMachineRuntime{connectedMachines: map[string]bool{"mach_1": true}}
	handler := &MachineHandler{workspaceService: &fakeWorkspaceService{}, runtime: runtime}
	router := gin.New()
	router.PUT("/api/v1/workspaces/:workspaceId/machines/:machineId/runtimes/:name/config", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
	}, handleWorkspaceAuth(handler.UpdateRuntimeConfig))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPut, "/api/v1/workspaces/wsp_1/machines/mach_1/runtimes/codex/config", bytes.NewBufferString(`{"editable":{"model":"gpt-5"}}`))
	request.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, 1, runtime.machineDetailCallCount, "UpdateRuntimeConfig MUST forward the body to the runtime")
	require.JSONEq(t, `{"editable":{"model":"gpt-5"}}`, recorder.Body.String())
}

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

	runtime := &fakeMachineRuntime{connectedMachines: map[string]bool{"mach_1": true}}
	handler := &MachineHandler{workspaceService: &fakeWorkspaceService{}, runtime: runtime}
	router := gin.New()
	router.POST("/api/v1/workspaces/:workspaceId/machines/:machineId/runtimes/:name/config/validate", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
	}, handleWorkspaceAuth(handler.ValidateRuntimeConfig))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/v1/workspaces/wsp_1/machines/mach_1/runtimes/codex/config/validate", bytes.NewBufferString(`{"editable":{"model":"gpt-5"}}`))
	request.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, 1, runtime.machineDetailCallCount, "ValidateRuntimeConfig MUST forward the body to the runtime")
}

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

	runtime := &fakeMachineRuntime{connectedMachines: map[string]bool{"mach_1": true}}
	handler := &MachineHandler{workspaceService: &fakeWorkspaceService{}, runtime: runtime}
	router := gin.New()
	router.POST("/api/v1/workspaces/:workspaceId/machines/:machineId/mcp-servers", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
	}, handleWorkspaceAuth(handler.UpsertMCPServer))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/v1/workspaces/wsp_1/machines/mach_1/mcp-servers", bytes.NewBufferString(`{"id":"srv1","transport":"stdio","stdio":{"command":"run"}}`))
	request.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, 1, runtime.machineDetailCallCount, "UpsertMCPServer MUST forward the body to the runtime")
	require.JSONEq(t, `{"id":"srv1","transport":"stdio","stdio":{"command":"run"}}`, recorder.Body.String())
}
