package agent

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/desktop/internal/agent/drivercontract"
	"github.com/tidbcloud/AgentTeam/desktop/internal/agent/runtimeconfig"
	"github.com/tidbcloud/AgentTeam/desktop/internal/consts"
	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
	"github.com/tidbcloud/AgentTeam/internal/core/rewindcaps"
)

type capturedRPCRequest struct {
	ID     int
	Method string
	Params map[string]any
}

type rpcCaptureWriter struct {
	t        *testing.T
	driver   *CodexAppServerDriver
	requests []capturedRPCRequest
	handle   func(capturedRPCRequest) appServerEnvelope
}

func TestNewDriverCodexUsesAppServerDriver(t *testing.T) {
	driver, err := NewDriver(runtimeconfig.TypeCodex)
	require.NoError(t, err)
	require.Equal(t, "codex", driver.Name())
	_, ok := driver.(*CodexAppServerDriver)
	require.True(t, ok, "NewDriver(TypeCodex) returned %T, want *CodexAppServerDriver", driver)
}

func TestCodexDetachedRecoveryValidatesRecoveryRefBeforeStartingBinary(t *testing.T) {
	driver := &CodexAppServerDriver{}
	err := driver.RestoreRecoveryPointDetached(context.Background(), drivercontract.LaunchConfig{
		Binary:         filepath.Join(t.TempDir(), "missing-codex"),
		WorkDir:        t.TempDir(),
		PermissionMode: types.AgentPermissionModeFullAccess,
	}, rewindcaps.StrategyCodexAppServerTurns, "not-json")

	require.Error(t, err)
	require.Contains(t, err.Error(), "decode codex recovery ref")
	require.NotContains(t, err.Error(), "missing-codex")
}

func TestCodexPrepareRecoveryPointSupportsZeroBoundary(t *testing.T) {
	driver := &CodexAppServerDriver{
		codexThreadState: codexThreadState{
			threadID:           "thread-zero",
			completedTurnCount: 0,
		},
	}

	strategy, recoveryRef, supported, err := driver.PrepareRecoveryPoint()

	require.NoError(t, err)
	require.True(t, supported)
	require.Equal(t, rewindcaps.StrategyCodexAppServerTurns, strategy)
	ref, err := rewindcaps.DecodeCodexAppServerRecoveryRef(recoveryRef)
	require.NoError(t, err)
	require.Equal(t, "thread-zero", ref.ThreadID)
	require.Equal(t, 0, ref.TurnCount)
}

func (w *rpcCaptureWriter) Write(p []byte) (int, error) {
	var request struct {
		ID     int            `json:"id"`
		Method string         `json:"method"`
		Params map[string]any `json:"params"`
	}
	require.NoError(w.t, json.Unmarshal([]byte(strings.TrimSpace(string(p))), &request))

	captured := capturedRPCRequest{
		ID:     request.ID,
		Method: request.Method,
		Params: request.Params,
	}
	w.requests = append(w.requests, captured)

	if captured.ID != 0 && w.handle != nil {
		envelope := w.handle(captured)
		if len(envelope.ID) == 0 {
			envelope.ID = json.RawMessage([]byte(strconv.Itoa(captured.ID)))
		}

		w.driver.mu.Lock()
		pending, ok := w.driver.pending[captured.ID]
		delete(w.driver.pending, captured.ID)
		w.driver.mu.Unlock()
		if ok {
			pending.responseCh <- envelope
		}
	}

	return len(p), nil
}

func (w *rpcCaptureWriter) Close() error { return nil }

func rawRPCResult(t *testing.T, payload any) json.RawMessage {
	data, err := json.Marshal(payload)
	require.NoError(t, err)
	return data
}

type rawCaptureWriteCloser struct {
	mu     sync.Mutex
	writes [][]byte
}

func (w *rawCaptureWriteCloser) Write(p []byte) (int, error) {
	w.mu.Lock()
	defer w.mu.Unlock()
	w.writes = append(w.writes, append([]byte(nil), p...))
	return len(p), nil
}

func (w *rawCaptureWriteCloser) Close() error { return nil }

func (w *rawCaptureWriteCloser) Writes() [][]byte {
	w.mu.Lock()
	defer w.mu.Unlock()
	out := make([][]byte, len(w.writes))
	for i := range w.writes {
		out[i] = append([]byte(nil), w.writes[i]...)
	}
	return out
}

func decodeRPCResponse(t *testing.T, raw []byte) appServerEnvelope {
	t.Helper()
	var envelope appServerEnvelope
	require.NoError(t, json.Unmarshal(bytes.TrimSpace(raw), &envelope))
	return envelope
}

func runReadAgentTeamWithSingleLine(driver *CodexAppServerDriver, line string) {
	if err := driver.handleAppServerLine(line); err != nil {
		panic(err)
	}
}

func turnInputText(t *testing.T, params map[string]any) string {
	inputItem := turnTextInputItem(t, params)
	text, ok := inputItem["text"].(string)
	require.True(t, ok)
	return text
}

func turnTextInputItem(t *testing.T, params map[string]any) map[string]any {
	t.Helper()
	input, ok := params["input"].([]map[string]any)
	require.True(t, ok)
	require.Len(t, input, 1)
	return input[0]
}

func requireCodexAppServerThreadSandbox(t *testing.T, params map[string]any) {
	t.Helper()
	require.Equal(t, codexAppServerSandboxModeFullAccess, params["sandbox"])
	require.NotContains(t, params, "permissionProfile")
	require.NotContains(t, params, "sandboxPolicy")
}

func requireCodexAppServerTurnSandboxPolicy(t *testing.T, params map[string]any) {
	t.Helper()
	require.Equal(t, map[string]any{"type": codexAppServerSandboxPolicyTypeFullAccess}, params["sandboxPolicy"])
	require.NotContains(t, params, "permissionProfile")
	require.NotContains(t, params, "sandbox")
}

func requireCodexAppServerWorkspaceTurnSandboxPolicy(t *testing.T, params map[string]any, workDir string) {
	t.Helper()
	require.Equal(t, map[string]any{
		"type":          codexAppServerSandboxPolicyTypeWorkspaceWrite,
		"writableRoots": []string{workDir},
	}, params["sandboxPolicy"])
	require.NotContains(t, params, "permissionProfile")
	require.NotContains(t, params, "sandbox")
}

func requireCodexAppServerApprovalPolicy(t *testing.T, params map[string]any, requestPermissions bool) {
	t.Helper()
	require.Equal(t, map[string]any{
		"granular": map[string]any{
			"mcp_elicitations":    true,
			"request_permissions": requestPermissions,
			"rules":               false,
			"sandbox_approval":    false,
			"skill_approval":      false,
		},
	}, params["approvalPolicy"])
}

func newTestCodexDriver(t *testing.T, handle func(capturedRPCRequest) appServerEnvelope) (*CodexAppServerDriver, *rpcCaptureWriter) {
	t.Helper()
	driver := &CodexAppServerDriver{}
	writer := &rpcCaptureWriter{t: t, driver: driver, handle: handle}
	driver.rawStdin = writer
	driver.pending = make(map[int]pendingAppServerRequest)
	return driver, writer
}

func TestCodexAppServerStartThreadIncludesInstructions(t *testing.T) {
	driver, writer := newTestCodexDriver(t, func(req capturedRPCRequest) appServerEnvelope {
		require.Equal(t, "thread/start", req.Method)
		return appServerEnvelope{
			Result: rawRPCResult(t, appServerThreadResult{
				Thread: appServerThread{ID: "thread-1", Status: appServerThreadStatus{Type: "idle"}},
			}),
		}
	})

	threadID, err := driver.startThread(context.Background(), testLaunchConfig(drivercontract.LaunchConfig{WorkDir: "/tmp/agent", Model: "gpt-5"}, testLaunchSurfaces{BaseInstructions: "base instructions", DeveloperInstructions: "developer instructions"}))
	require.NoError(t, err)
	require.Equal(t, "thread-1", threadID)
	require.Len(t, writer.requests, 1)
	require.Equal(t, "base instructions", writer.requests[0].Params["baseInstructions"])
	require.Equal(t, "developer instructions", writer.requests[0].Params["developerInstructions"])
	requireCodexAppServerThreadSandbox(t, writer.requests[0].Params)
	requireCodexAppServerApprovalPolicy(t, writer.requests[0].Params, true)
}

func TestCodexAppServerStartThreadInjectsMCPDynamicTools(t *testing.T) {
	agentteamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()
		var req struct {
			Method string `json:"method"`
		}
		require.NoError(t, json.NewDecoder(r.Body).Decode(&req))
		require.Equal(t, "tools/list", req.Method)
		w.Header().Set("Content-Type", "application/json")
		_, err := io.WriteString(w, `{"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"message_send","description":"Send a message","inputSchema":{"type":"object","properties":{"target":{"type":"string","enum":["#agentteam"]},"content":{"type":"string"},"routing_intent":{"oneOf":[{"const":"completion_summary"}]}},"required":["target","content"],"additionalProperties":false}}]}}`)
		require.NoError(t, err)
	}))
	defer agentteamServer.Close()

	driver, writer := newTestCodexDriver(t, func(req capturedRPCRequest) appServerEnvelope {
		require.Equal(t, "thread/start", req.Method)
		return appServerEnvelope{
			Result: rawRPCResult(t, appServerThreadResult{
				Thread: appServerThread{ID: "thread-1", Status: appServerThreadStatus{Type: "idle"}},
			}),
		}
	})
	driver.mcpConfigs = map[string]types.NamedMCPConfig{
		"agentteam": {Name: "agentteam", URL: agentteamServer.URL},
	}

	threadID, err := driver.startThread(context.Background(), drivercontract.LaunchConfig{WorkDir: "/tmp/agent"})
	require.NoError(t, err)
	require.Equal(t, "thread-1", threadID)
	require.Len(t, writer.requests, 1)

	dynamicTools, ok := writer.requests[0].Params["dynamicTools"].([]any)
	require.True(t, ok)
	require.Len(t, dynamicTools, 1)
	toolsByName := make(map[string]map[string]any, len(dynamicTools))
	for _, entry := range dynamicTools {
		tool, ok := entry.(map[string]any)
		require.True(t, ok)
		name, ok := tool["name"].(string)
		require.True(t, ok)
		require.NotContains(t, name, "mcp__")
		toolsByName[name] = tool
	}

	tool := toolsByName["mcp_agentteam_message_send"]
	require.NotNil(t, tool)
	require.Equal(t, "mcp_agentteam_message_send", tool["name"])
	require.Equal(t, "Send a message", tool["description"])
	inputSchema, ok := tool["inputSchema"].(map[string]any)
	require.True(t, ok)
	require.Equal(t, "object", inputSchema["type"])
	require.ElementsMatch(t, []any{"target", "content"}, inputSchema["required"])
	require.Equal(t, false, inputSchema["additionalProperties"])
	properties, ok := inputSchema["properties"].(map[string]any)
	require.True(t, ok)
	target, ok := properties["target"].(map[string]any)
	require.True(t, ok)
	require.Equal(t, []any{"#agentteam"}, target["enum"])
	routingIntent, ok := properties["routing_intent"].(map[string]any)
	require.True(t, ok)
	require.Len(t, routingIntent["oneOf"], 1)

}

func TestCodexAppServerPreservesDiscriminatedMCPInputSchema(t *testing.T) {
	schema := `{"type":"object","properties":{},"oneOf":[{"type":"object","properties":{"type":{"type":"string","const":"team_role"}},"required":["type"],"additionalProperties":false}]}`
	chatServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		fmt.Fprintf(w, `{"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"permission_probe","inputSchema":%s}]}}`, schema)
	}))
	defer chatServer.Close()

	driver, writer := newTestCodexDriver(t, func(req capturedRPCRequest) appServerEnvelope {
		return appServerEnvelope{Result: rawRPCResult(t, appServerThreadResult{
			Thread: appServerThread{ID: "thread-schema", Status: appServerThreadStatus{Type: "idle"}},
		})}
	})
	driver.mcpConfigs = map[string]types.NamedMCPConfig{
		"agentteam": {Name: "agentteam", URL: chatServer.URL},
	}
	_, err := driver.startThread(context.Background(), drivercontract.LaunchConfig{WorkDir: "/tmp/agent"})
	require.NoError(t, err)
	dynamicTools := writer.requests[0].Params["dynamicTools"].([]any)
	require.Len(t, dynamicTools, 1)
	tool := dynamicTools[0].(map[string]any)
	input := tool["inputSchema"].(map[string]any)
	require.Len(t, input["oneOf"], 1)
	variant := input["oneOf"].([]any)[0].(map[string]any)
	require.Equal(t, false, variant["additionalProperties"])
	require.Equal(t, "team_role", variant["properties"].(map[string]any)["type"].(map[string]any)["const"])
}

func TestCodexAppServerStartThreadSkipsOptionalMCPDynamicToolsListFailure(t *testing.T) {
	chatServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()
		w.Header().Set("Content-Type", "application/json")
		_, err := io.WriteString(w, `{"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"message_send","description":"Send a message","inputSchema":{"type":"object","properties":{}}}]}}`)
		require.NoError(t, err)
	}))
	defer chatServer.Close()

	optionalServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()
		http.Error(w, "temporary outage", http.StatusServiceUnavailable)
	}))
	defer optionalServer.Close()

	driver, writer := newTestCodexDriver(t, func(req capturedRPCRequest) appServerEnvelope {
		require.Equal(t, "thread/start", req.Method)
		return appServerEnvelope{
			Result: rawRPCResult(t, appServerThreadResult{
				Thread: appServerThread{ID: "thread-1", Status: appServerThreadStatus{Type: "idle"}},
			}),
		}
	})
	driver.mcpConfigs = map[string]types.NamedMCPConfig{
		"agentteam": {Name: "agentteam", URL: chatServer.URL},
		"optional":  {Name: "optional", URL: optionalServer.URL},
	}

	threadID, err := driver.startThread(context.Background(), drivercontract.LaunchConfig{WorkDir: "/tmp/agent"})
	require.NoError(t, err)
	require.Equal(t, "thread-1", threadID)
	require.Len(t, writer.requests, 1)
	dynamicTools, ok := writer.requests[0].Params["dynamicTools"].([]any)
	require.True(t, ok)
	require.Len(t, dynamicTools, 1)
	tool, ok := dynamicTools[0].(map[string]any)
	require.True(t, ok)
	require.Equal(t, "mcp_agentteam_message_send", tool["name"])
}

func TestCodexAppServerStartThreadLegacyWorkspacePermissionModeDefaultsFullAccess(t *testing.T) {
	driver, writer := newTestCodexDriver(t, func(req capturedRPCRequest) appServerEnvelope {
		require.Equal(t, "thread/start", req.Method)
		return appServerEnvelope{
			Result: rawRPCResult(t, appServerThreadResult{
				Thread: appServerThread{ID: "thread-1", Status: appServerThreadStatus{Type: "idle"}},
			}),
		}
	})

	threadID, err := driver.startThread(context.Background(), drivercontract.LaunchConfig{
		WorkDir:        "/tmp/agent",
		PermissionMode: types.AgentPermissionModeWorkspaceWrite,
	})

	require.NoError(t, err)
	require.Equal(t, "thread-1", threadID)
	require.Equal(t, codexAppServerSandboxModeFullAccess, writer.requests[0].Params["sandbox"])
	require.NotContains(t, writer.requests[0].Params, "permissionProfile")
	require.NotContains(t, writer.requests[0].Params, "sandboxPolicy")
	requireCodexAppServerApprovalPolicy(t, writer.requests[0].Params, true)
}

func TestCodexAppServerResumeThreadOmitsEmptyOptionalInstructions(t *testing.T) {
	driver, writer := newTestCodexDriver(t, func(req capturedRPCRequest) appServerEnvelope {
		switch req.Method {
		case "thread/resume":
			return appServerEnvelope{
				Result: rawRPCResult(t, appServerThreadResult{
					Thread: appServerThread{ID: "thread-1", Status: appServerThreadStatus{Type: "idle"}},
					Model:  "gpt-5.2",
				}),
			}
		case "thread/read":
			require.Equal(t, "thread-1", req.Params["threadId"])
			require.Equal(t, true, req.Params["includeTurns"])
			return appServerEnvelope{
				Result: rawRPCResult(t, appServerThreadResult{
					Thread: appServerThread{ID: "thread-1", Status: appServerThreadStatus{Type: "idle"}},
				}),
			}
		default:
			t.Fatalf("unexpected method %s", req.Method)
			return appServerEnvelope{}
		}
	})

	err := driver.resumeThread(context.Background(), testLaunchConfig(drivercontract.LaunchConfig{WorkDir: "/tmp/agent"}, testLaunchSurfaces{BaseInstructions: "base instructions", DeveloperInstructions: ""}), "thread-1")
	require.NoError(t, err)
	require.Len(t, writer.requests, 2)
	require.Contains(t, writer.requests[0].Params, "baseInstructions")
	require.NotContains(t, writer.requests[0].Params, "developerInstructions")
	require.Equal(t, true, writer.requests[0].Params["excludeTurns"])
	requireCodexAppServerThreadSandbox(t, writer.requests[0].Params)
	requireCodexAppServerApprovalPolicy(t, writer.requests[0].Params, true)
}

func TestCodexAppServerResumeThreadForRecoveryIncludesSandbox(t *testing.T) {
	driver, writer := newTestCodexDriver(t, func(req capturedRPCRequest) appServerEnvelope {
		switch req.Method {
		case "thread/resume":
			return appServerEnvelope{
				Result: rawRPCResult(t, appServerThreadResult{
					Thread: appServerThread{ID: "thread-1", Status: appServerThreadStatus{Type: "idle"}},
					Model:  "gpt-5.2",
				}),
			}
		case "thread/read":
			require.Equal(t, "thread-1", req.Params["threadId"])
			require.Equal(t, true, req.Params["includeTurns"])
			return appServerEnvelope{
				Result: rawRPCResult(t, appServerThreadResult{
					Thread: appServerThread{ID: "thread-1", Status: appServerThreadStatus{Type: "idle"}},
				}),
			}
		default:
			t.Fatalf("unexpected method %s", req.Method)
			return appServerEnvelope{}
		}
	})

	resumed, err := driver.resumeThreadForRecovery(context.Background(), drivercontract.LaunchConfig{
		WorkDir: "/tmp/agent",
	}, "thread-1")
	require.NoError(t, err)
	require.Equal(t, "thread-1", resumed.Thread.ID)
	require.Len(t, writer.requests, 2)
	require.Equal(t, "thread-1", writer.requests[0].Params["threadId"])
	require.Equal(t, true, writer.requests[0].Params["excludeTurns"])
	requireCodexAppServerThreadSandbox(t, writer.requests[0].Params)
	requireCodexAppServerApprovalPolicy(t, writer.requests[0].Params, true)
}

func TestCodexAppServerStartTurnParamsIncludesResolvedReasoningSummary(t *testing.T) {
	driver, _ := newTestCodexDriver(t, nil)
	driver.reasoningSummary = "auto"

	params, err := driver.startTurnParams(drivercontract.LaunchConfig{
		WorkDir: "/tmp/agent",
	}, "hello", "thread-1")
	require.NoError(t, err)
	require.NotNil(t, params)
	requireCodexAppServerTurnSandboxPolicy(t, params)
	require.Equal(t, "auto", params["summary"])
	inputItem := turnTextInputItem(t, params)
	require.Equal(t, "text", inputItem["type"])
	require.Equal(t, "hello", inputItem["text"])
	require.Equal(t, []map[string]any{}, inputItem["text_elements"])
}

func TestCodexAppServerStartTurnParamsAllowsSystemNotification(t *testing.T) {
	driver, _ := newTestCodexDriver(t, nil)

	const notification = "[System notification: Runtime input resumed.]"
	params, err := driver.startTurnParams(drivercontract.LaunchConfig{
		WorkDir: "/tmp/agent",
	}, notification, "thread-1")
	require.NoError(t, err)

	require.NotNil(t, params)
	require.Equal(t, notification, turnInputText(t, params))
	requireCodexAppServerTurnSandboxPolicy(t, params)
}

func TestCodexAppServerStartTurnParamsLegacyWorkspacePermissionModeDefaultsFullAccess(t *testing.T) {
	driver, _ := newTestCodexDriver(t, nil)

	params, err := driver.startTurnParams(drivercontract.LaunchConfig{
		WorkDir:        "/tmp/agent",
		PermissionMode: types.AgentPermissionModeWorkspaceWrite,
	}, "hello", "thread-1")

	require.NoError(t, err)
	require.NotNil(t, params)
	requireCodexAppServerTurnSandboxPolicy(t, params)
}

func TestCodexAppServerStartTurnParamsLegacyWorkspacePermissionModeDoesNotRequireWorkdir(t *testing.T) {
	driver, _ := newTestCodexDriver(t, nil)

	params, err := driver.startTurnParams(drivercontract.LaunchConfig{
		PermissionMode: types.AgentPermissionModeWorkspaceWrite,
	}, "hello", "thread-1")

	require.NoError(t, err)
	require.NotNil(t, params)
	requireCodexAppServerTurnSandboxPolicy(t, params)
}

func TestCodexAppServerStartTurnParamsAllowReasoningSummaryOverride(t *testing.T) {
	driver, _ := newTestCodexDriver(t, nil)
	driver.reasoningSummary = "detailed"

	params, err := driver.startTurnParams(drivercontract.LaunchConfig{
		WorkDir: "/tmp/agent",
	}, "hello", "thread-1")

	require.NoError(t, err)
	require.NotNil(t, params)
	require.Equal(t, "detailed", params["summary"])
}

func TestCodexAppServerCharacterizesThreadSnapshotAndSteeringState(t *testing.T) {
	driver := &CodexAppServerDriver{
		codexThreadState: codexThreadState{
			threadID:                "previous-thread",
			compactQueued:           true,
			compactionInProgress:    true,
			compactionTurnID:        "old-compaction",
			terminalErrorTurnEndIDs: map[string]struct{}{"old-turn": {}},
			lastTokenUsage: appServerThreadTokenUsage{
				Last: appServerTokenUsageBreakdown{InputTokens: 42},
			},
		},
	}

	driver.updateThreadSnapshot(appServerThread{
		ID:     "thread-1",
		Status: appServerThreadStatus{Type: "active"},
		Turns: []appServerTurn{
			{ID: "turn-complete", Status: appServerTurnStatus{Type: "completed"}},
			{ID: "turn-active", Status: appServerTurnStatus{Type: "inProgress"}},
		},
	})

	status, activeTurnID, ok := driver.currentThreadState("thread-1")
	require.True(t, ok)
	require.Equal(t, appServerThreadStatus{Type: "active"}, status)
	require.Equal(t, "turn-active", activeTurnID)
	require.Equal(t, 1, driver.currentCompletedTurnCount("thread-1"))
	require.True(t, driver.SupportsAckedBusyRuntimeInputDelivery())
	require.False(t, driver.threadKnownActive("previous-thread"))

	driver.mu.Lock()
	require.False(t, driver.compactQueued)
	require.False(t, driver.compactionInProgress)
	require.Empty(t, driver.compactionTurnID)
	require.Empty(t, driver.terminalErrorTurnEndIDs)
	require.Equal(t, appServerThreadTokenUsage{}, driver.lastTokenUsage)
	driver.mu.Unlock()

	require.False(t, driver.updateThreadStatus("thread-1", appServerThreadStatus{
		Type:        "active",
		ActiveFlags: []string{"waitingOnApproval"},
	}))
	require.True(t, driver.SupportsAckedBusyRuntimeInputDelivery(), "support reports structural capability, not momentary readiness")
}

func TestCodexAppServerCharacterizesQueuedIdleCompactionLifecycle(t *testing.T) {
	driver := &CodexAppServerDriver{}
	require.False(t, driver.updateThreadStatus("thread-1", appServerThreadStatus{Type: "idle"}))
	driver.queueCompaction("thread-1", "overflow-turn")

	threadID, ok := driver.prepareIdleCompaction("thread-1")
	require.True(t, ok)
	require.Equal(t, "thread-1", threadID)

	_, ok = driver.prepareIdleCompaction("thread-1")
	require.False(t, ok, "preparing compaction consumes the queued request")

	driver.beginCompaction("thread-1")
	status, activeTurnID, ok := driver.currentThreadState("thread-1")
	require.True(t, ok)
	require.Equal(t, appServerThreadStatus{Type: "active"}, status)
	require.Empty(t, activeTurnID)
	require.True(t, driver.SupportsAckedBusyRuntimeInputDelivery(), "compaction blocks this attempt but does not remove the delivery capability")
	driver.mu.Lock()
	require.Equal(t, "overflow-turn", driver.compactionCauseTurnID)
	driver.mu.Unlock()

	driver.restoreQueuedCompaction("thread-1", "")
	status, activeTurnID, ok = driver.currentThreadState("thread-1")
	require.True(t, ok)
	require.Equal(t, appServerThreadStatus{Type: "idle"}, status)
	require.Empty(t, activeTurnID)

	threadID, ok = driver.prepareIdleCompaction("thread-1")
	require.True(t, ok, "restoreQueuedCompaction requeues the idle compaction")
	require.Equal(t, "thread-1", threadID)
	driver.mu.Lock()
	require.Equal(t, "overflow-turn", driver.queuedCompactionCauseTurnID)
	driver.mu.Unlock()
}

func TestCodexReasoningSummaryResolvesNativeConfig(t *testing.T) {
	t.Setenv("CODEX_HOME", "")
	home := t.TempDir()
	codexDir := filepath.Join(home, ".codex")
	require.NoError(t, os.MkdirAll(codexDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(codexDir, "config.toml"), []byte(`model_reasoning_summary = "detailed"
`), 0o644))

	require.Equal(t, "detailed", codexReasoningSummary(map[string]string{"HOME": home}))
}

func TestCodexReasoningSummaryPrefersEnvOverride(t *testing.T) {
	t.Setenv("CODEX_HOME", "")
	home := t.TempDir()
	codexDir := filepath.Join(home, ".codex")
	require.NoError(t, os.MkdirAll(codexDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(codexDir, "config.toml"), []byte(`model_reasoning_summary = "detailed"
`), 0o644))

	require.Equal(t, "concise", codexReasoningSummary(map[string]string{
		"HOME":                              home,
		"AGENTTEAM_CODEX_REASONING_SUMMARY": "concise",
	}))
}

func TestCodexReasoningSummaryFallsBackToAuto(t *testing.T) {
	t.Setenv("CODEX_HOME", "")
	require.Equal(t, "auto", codexReasoningSummary(map[string]string{"HOME": t.TempDir()}))
}

func TestCodexAppServerReasoningSummaryTextDeltaEmitsThinking(t *testing.T) {
	driver, _ := newTestCodexDriver(t, nil)
	driver.translatedLines = make(chan string, 1)

	driver.handleNotification(appServerEnvelope{
		Method: "item/reasoning/summaryTextDelta",
		Params: rawRPCResult(t, map[string]any{
			"threadId":     "thread-1",
			"turnId":       "turn-1",
			"itemId":       "reasoning-1",
			"delta":        "Inspecting the activity pipeline",
			"summaryIndex": 0,
		}),
	})

	line := <-driver.translatedLines
	events := driver.ParseLine(line)
	require.Len(t, events, 1)
	require.Equal(t, consts.ParsedEventKindThinking, events[0].Kind)
	require.Equal(t, "Inspecting the activity pipeline", events[0].Text)
	require.Equal(t, "reasoning-1:summary:0", events[0].SectionID)
	require.Empty(t, events[0].Title)
}

func TestCodexAppServerReasoningSummaryPlaceholderDeltaDoesNotSuppressCompletedReasoning(t *testing.T) {
	driver, _ := newTestCodexDriver(t, nil)
	driver.translatedLines = make(chan string, 1)

	driver.handleNotification(appServerEnvelope{
		Method: "item/reasoning/summaryTextDelta",
		Params: rawRPCResult(t, map[string]any{
			"threadId":     "thread-1",
			"turnId":       "turn-1",
			"itemId":       "reasoning-1",
			"delta":        "<!-- -->",
			"summaryIndex": 0,
		}),
	})

	select {
	case line := <-driver.translatedLines:
		t.Fatalf("unexpected placeholder reasoning event: %s", line)
	default:
	}

	driver.handleNotification(appServerEnvelope{
		Method: "item/completed",
		Params: rawRPCResult(t, map[string]any{
			"threadId": "thread-1",
			"turnId":   "turn-1",
			"item": map[string]any{
				"type":    "reasoning",
				"id":      "reasoning-1",
				"summary": []string{"Real thinking summary"},
				"content": []string{},
			},
		}),
	})

	events := driver.ParseLine(<-driver.translatedLines)
	require.Len(t, events, 1)
	require.Equal(t, consts.ParsedEventKindThinking, events[0].Kind)
	require.Equal(t, "Real thinking summary", events[0].Text)
	require.Equal(t, "reasoning-1:summary:0", events[0].SectionID)
}

func TestCodexAppServerReasoningSummaryDeltaStripsEmbeddedPlaceholder(t *testing.T) {
	driver, _ := newTestCodexDriver(t, nil)
	driver.translatedLines = make(chan string, 1)

	driver.handleNotification(appServerEnvelope{
		Method: "item/reasoning/summaryTextDelta",
		Params: rawRPCResult(t, map[string]any{
			"threadId":     "thread-1",
			"turnId":       "turn-1",
			"itemId":       "reasoning-1",
			"delta":        "**Switching to X-Api-Key authentication**\n\n<!-- -->",
			"summaryIndex": 0,
		}),
	})

	events := driver.ParseLine(<-driver.translatedLines)
	require.Len(t, events, 1)
	require.Equal(t, consts.ParsedEventKindThinking, events[0].Kind)
	require.Equal(t, "**Switching to X-Api-Key authentication**", events[0].Text)
	require.Equal(t, "Switching to X-Api-Key authentication", events[0].Title)
	require.Equal(t, "reasoning-1:summary:0", events[0].SectionID)
}

func TestCodexAppServerCompletedReasoningSkipsPlaceholderSummary(t *testing.T) {
	driver, _ := newTestCodexDriver(t, nil)
	driver.translatedLines = make(chan string, 1)

	driver.handleNotification(appServerEnvelope{
		Method: "item/completed",
		Params: rawRPCResult(t, map[string]any{
			"threadId": "thread-1",
			"turnId":   "turn-1",
			"item": map[string]any{
				"type":    "reasoning",
				"id":      "reasoning-1",
				"summary": []string{"<!-- -->"},
				"content": []string{},
			},
		}),
	})

	select {
	case line := <-driver.translatedLines:
		t.Fatalf("unexpected placeholder completed reasoning event: %s", line)
	default:
	}
}

func TestCodexAppServerCompletedReasoningStripsEmbeddedPlaceholder(t *testing.T) {
	driver, _ := newTestCodexDriver(t, nil)
	driver.translatedLines = make(chan string, 1)

	driver.handleNotification(appServerEnvelope{
		Method: "item/completed",
		Params: rawRPCResult(t, map[string]any{
			"threadId": "thread-1",
			"turnId":   "turn-1",
			"item": map[string]any{
				"type":    "reasoning",
				"id":      "reasoning-1",
				"summary": []string{"**Switching to X-Api-Key authentication**\n\n<!-- -->"},
				"content": []string{},
			},
		}),
	})

	events := driver.ParseLine(<-driver.translatedLines)
	require.Len(t, events, 1)
	require.Equal(t, consts.ParsedEventKindThinking, events[0].Kind)
	require.Equal(t, "**Switching to X-Api-Key authentication**", events[0].Text)
	require.Equal(t, "Switching to X-Api-Key authentication", events[0].Title)
	require.Equal(t, "reasoning-1:summary:0", events[0].SectionID)
}

func TestCodexAppServerStderrInvalidModelProviderEmitsRuntimeError(t *testing.T) {
	driver, _ := newTestCodexDriver(t, nil)
	driver.translatedLines = make(chan string, 1)

	driver.handleCodexAppServerStderrLine(
		"2026-06-09T09:56:10.820576Z ERROR codex_app_server: Invalid configuration; using defaults. Model provider `crsc` not found",
	)

	events := driver.ParseLine(<-driver.translatedLines)
	require.Equal(t, []drivercontract.ParsedEvent{
		{
			Kind:    consts.ParsedEventKindError,
			Message: "Codex configuration error: Invalid configuration; using defaults. Model provider `crsc` not found",
		},
	}, events)
}

func TestCodexAppServerStderrIgnoresNonConfigLog(t *testing.T) {
	driver, _ := newTestCodexDriver(t, nil)
	driver.translatedLines = make(chan string, 1)

	driver.handleCodexAppServerStderrLine(
		"2026-06-09T09:56:10.820576Z INFO codex_app_server: server initialized",
	)

	require.Empty(t, driver.translatedLines)
}

func TestCodexAppServerTerminalErrorEmitsTurnEnd(t *testing.T) {
	driver, _ := newTestCodexDriver(t, nil)
	driver.translatedLines = make(chan string, 2)

	driver.handleNotification(appServerEnvelope{
		Method: consts.ParsedEventKindError,
		Params: rawRPCResult(t, map[string]any{
			"threadId":  "thread-1",
			"turnId":    "turn-1",
			"willRetry": false,
			"error": map[string]any{
				"message": "request failed",
			},
		}),
	})

	require.Len(t, driver.translatedLines, 2)
	var got []drivercontract.ParsedEvent
	for range 2 {
		got = append(got, driver.ParseLine(<-driver.translatedLines)...)
	}
	require.Equal(t, []drivercontract.ParsedEvent{
		{
			Kind:    consts.ParsedEventKindError,
			Message: "request failed",
			Metadata: map[string]any{
				"context_window_exceeded": false,
				"will_retry":              false,
			},
		},
		{
			Kind:      consts.ParsedEventKindTurnEnd,
			SessionID: "thread-1",
			Metadata: map[string]any{
				runtimeCompletedTurnIDKey:            "turn-1",
				codexTerminalErrorTurnEndMetadataKey: true,
			},
		},
	}, got)
}

func TestCodexAppServerTerminalErrorIncludesAdditionalDetails(t *testing.T) {
	driver, _ := newTestCodexDriver(t, nil)
	driver.translatedLines = make(chan string, 2)

	driver.handleNotification(appServerEnvelope{
		Method: consts.ParsedEventKindError,
		Params: rawRPCResult(t, map[string]any{
			"threadId":  "thread-1",
			"turnId":    "turn-1",
			"willRetry": false,
			"error": map[string]any{
				"message":           "APIError",
				"additionalDetails": "provider returned 429: quota exceeded",
			},
		}),
	})

	events := driver.ParseLine(<-driver.translatedLines)
	require.Len(t, events, 1)
	require.Equal(t, consts.ParsedEventKindError, events[0].Kind)
	require.Equal(t, "APIError: provider returned 429: quota exceeded", events[0].Message)
}

func TestCodexAppServerRetryingErrorDoesNotEmitTurnEnd(t *testing.T) {
	driver, _ := newTestCodexDriver(t, nil)
	driver.translatedLines = make(chan string, 2)

	driver.handleNotification(appServerEnvelope{
		Method: consts.ParsedEventKindError,
		Params: rawRPCResult(t, map[string]any{
			"threadId":  "thread-1",
			"turnId":    "turn-1",
			"willRetry": true,
			"error": map[string]any{
				"message": "temporary failure",
			},
		}),
	})

	require.Len(t, driver.translatedLines, 1)
	events := driver.ParseLine(<-driver.translatedLines)
	require.Equal(t, []drivercontract.ParsedEvent{
		{
			Kind:    consts.ParsedEventKindError,
			Message: "temporary failure",
			Metadata: map[string]any{
				"context_window_exceeded": false,
				"will_retry":              true,
			},
		},
	}, events)
}

func TestCodexAppServerTerminalErrorSuppressesDuplicateTurnCompletedEnd(t *testing.T) {
	driver, _ := newTestCodexDriver(t, nil)
	driver.translatedLines = make(chan string, 3)

	driver.handleNotification(appServerEnvelope{
		Method: consts.ParsedEventKindError,
		Params: rawRPCResult(t, map[string]any{
			"threadId":  "thread-1",
			"turnId":    "turn-1",
			"willRetry": false,
			"error": map[string]any{
				"message": "request failed",
			},
		}),
	})
	driver.handleNotification(appServerEnvelope{
		Method: "turn/completed",
		Params: rawRPCResult(t, appServerTurnNotification{
			ThreadID: "thread-1",
			Turn: appServerTurn{
				ID:     "turn-1",
				Status: appServerTurnStatus{Type: "failed"},
			},
		}),
	})

	require.Len(t, driver.translatedLines, 2)
	var got []drivercontract.ParsedEvent
	for range 2 {
		got = append(got, driver.ParseLine(<-driver.translatedLines)...)
	}
	require.Equal(t, []drivercontract.ParsedEvent{
		{
			Kind:    consts.ParsedEventKindError,
			Message: "request failed",
			Metadata: map[string]any{
				"context_window_exceeded": false,
				"will_retry":              false,
			},
		},
		{
			Kind:      consts.ParsedEventKindTurnEnd,
			SessionID: "thread-1",
			Metadata: map[string]any{
				runtimeCompletedTurnIDKey:            "turn-1",
				codexTerminalErrorTurnEndMetadataKey: true,
			},
		},
	}, got)
}

func TestCodexAppServerReasoningSummaryTextDeltaSuppressesCompletedReasoningDuplicate(t *testing.T) {
	driver, _ := newTestCodexDriver(t, nil)
	driver.translatedLines = make(chan string, 2)

	driver.handleNotification(appServerEnvelope{
		Method: "item/reasoning/summaryTextDelta",
		Params: rawRPCResult(t, map[string]any{
			"threadId":     "thread-1",
			"turnId":       "turn-1",
			"itemId":       "reasoning-1",
			"delta":        "Checking events",
			"summaryIndex": 0,
		}),
	})
	<-driver.translatedLines

	driver.handleNotification(appServerEnvelope{
		Method: "item/completed",
		Params: rawRPCResult(t, map[string]any{
			"threadId": "thread-1",
			"turnId":   "turn-1",
			"item": map[string]any{
				"type":    "reasoning",
				"id":      "reasoning-1",
				"summary": []string{"Checking events"},
				"content": []string{},
			},
		}),
	})

	select {
	case line := <-driver.translatedLines:
		t.Fatalf("unexpected duplicate completed reasoning event: %s", line)
	default:
	}
}

func TestCodexAppServerReasoningSummaryTextDeltaCarriesSectionAndTitle(t *testing.T) {
	driver, _ := newTestCodexDriver(t, nil)
	driver.translatedLines = make(chan string, 4)

	driver.handleNotification(appServerEnvelope{
		Method: "item/reasoning/summaryTextDelta",
		Params: rawRPCResult(t, map[string]any{
			"threadId":     "thread-1",
			"turnId":       "turn-1",
			"itemId":       "reasoning-1",
			"delta":        "**Checking",
			"summaryIndex": 0,
		}),
	})
	driver.handleNotification(appServerEnvelope{
		Method: "item/reasoning/summaryTextDelta",
		Params: rawRPCResult(t, map[string]any{
			"threadId":     "thread-1",
			"turnId":       "turn-1",
			"itemId":       "reasoning-1",
			"delta":        " activity**\nReading events",
			"summaryIndex": 0,
		}),
	})
	driver.handleNotification(appServerEnvelope{
		Method: "item/reasoning/summaryTextDelta",
		Params: rawRPCResult(t, map[string]any{
			"threadId":     "thread-1",
			"turnId":       "turn-1",
			"itemId":       "reasoning-1",
			"delta":        "**Rendering**\nUpdating UI",
			"summaryIndex": 1,
		}),
	})

	first := driver.ParseLine(<-driver.translatedLines)
	second := driver.ParseLine(<-driver.translatedLines)
	third := driver.ParseLine(<-driver.translatedLines)
	require.Len(t, first, 1)
	require.Len(t, second, 1)
	require.Len(t, third, 1)
	require.Equal(t, "reasoning-1:summary:0", first[0].SectionID)
	require.Empty(t, first[0].Title)
	require.Equal(t, "reasoning-1:summary:0", second[0].SectionID)
	require.Equal(t, "Checking activity", second[0].Title)
	require.Equal(t, "reasoning-1:summary:1", third[0].SectionID)
	require.Equal(t, "Rendering", third[0].Title)
}

func TestCodexAppServerReasoningTextDeltaHiddenByDefault(t *testing.T) {
	driver, _ := newTestCodexDriver(t, nil)
	driver.translatedLines = make(chan string, 1)

	driver.handleNotification(appServerEnvelope{
		Method: "item/reasoning/textDelta",
		Params: rawRPCResult(t, map[string]any{
			"threadId":     "thread-1",
			"turnId":       "turn-1",
			"itemId":       "reasoning-1",
			"delta":        "raw hidden reasoning",
			"contentIndex": 0,
		}),
	})

	select {
	case line := <-driver.translatedLines:
		t.Fatalf("unexpected raw reasoning event: %s", line)
	default:
	}
}

func TestCodexAppServerEncodeStdinMessageSteersSystemNotificationDuringActiveTurn(t *testing.T) {
	driver, _ := newTestCodexDriver(t, nil)
	driver.threadID = "thread-1"
	driver.threadStatus = appServerThreadStatus{Type: "active"}
	driver.activeTurnID = "turn-1"

	const notification = "[System notification: Runtime input resumed.]"
	data := driver.EncodeStdinMessage(notification, "thread-1")

	require.NotNil(t, data)
	var req struct {
		Method string `json:"method"`
		Params struct {
			ThreadID       string           `json:"threadId"`
			ExpectedTurnID string           `json:"expectedTurnId"`
			Input          []map[string]any `json:"input"`
		} `json:"params"`
	}
	require.NoError(t, json.Unmarshal(bytes.TrimSpace(data), &req))
	require.Equal(t, "turn/steer", req.Method)
	require.Equal(t, "thread-1", req.Params.ThreadID)
	require.Equal(t, "turn-1", req.Params.ExpectedTurnID)
	require.Len(t, req.Params.Input, 1)
	require.Equal(t, "text", req.Params.Input[0]["type"])
	require.Equal(t, notification, req.Params.Input[0]["text"])
	require.Equal(t, []any{}, req.Params.Input[0]["text_elements"])
}

func TestCodexAppServerEncodeStdinMessageSteersWhileWaitingOnApproval(t *testing.T) {
	driver, _ := newTestCodexDriver(t, nil)
	driver.threadID = "thread-1"
	driver.threadStatus = appServerThreadStatus{
		Type:        "active",
		ActiveFlags: []string{"waitingOnApproval"},
	}
	driver.activeTurnID = "turn-1"

	data := driver.EncodeStdinMessage("[System notification: pending]", "thread-1")

	require.NotNil(t, data)
	var request struct {
		Method string `json:"method"`
		Params struct {
			ExpectedTurnID string `json:"expectedTurnId"`
		} `json:"params"`
	}
	require.NoError(t, json.Unmarshal(bytes.TrimSpace(data), &request))
	require.Equal(t, "turn/steer", request.Method)
	require.Equal(t, "turn-1", request.Params.ExpectedTurnID)
}

func TestCodexAppServerDeliverAckedBusyRuntimeInputSteersWithClientUserMessageID(t *testing.T) {
	driver, writer := newTestCodexDriver(t, func(req capturedRPCRequest) appServerEnvelope {
		require.Equal(t, "turn/steer", req.Method)
		require.Equal(t, "thread-1", req.Params["threadId"])
		require.Equal(t, "turn-1", req.Params["expectedTurnId"])
		require.Equal(t, "msg-agentteam-1", req.Params["clientUserMessageId"])

		input, ok := req.Params["input"].([]any)
		require.True(t, ok)
		require.Len(t, input, 1)
		item, ok := input[0].(map[string]any)
		require.True(t, ok)
		require.Equal(t, "text", item["type"])
		require.Equal(t, "new message", item["text"])

		return appServerEnvelope{
			Result: rawRPCResult(t, appServerTurnIDResult{TurnID: "turn-1"}),
		}
	})
	driver.threadID = "thread-1"
	driver.threadStatus = appServerThreadStatus{Type: "active"}
	driver.activeTurnID = "turn-1"

	require.True(t, driver.SupportsAckedBusyRuntimeInputDelivery())
	err := driver.DeliverAckedBusyRuntimeInput(context.Background(), testRuntimeInputWithConfig(drivercontract.RuntimeInput{ID: "msg-agentteam-1"}, "new message"), "thread-1")

	require.NoError(t, err)
	require.Len(t, writer.requests, 1)
}

func TestCodexAppServerDeliverRuntimeInputStartsAfterTaggedFileChangeHistory(t *testing.T) {
	driver, writer := newTestCodexDriver(t, func(req capturedRPCRequest) appServerEnvelope {
		switch req.Method {
		case "thread/read":
			return appServerEnvelope{Result: json.RawMessage(`{
				"thread": {
					"id": "thread-1",
					"status": {"type": "idle"},
					"turns": [{
						"id": "turn-1",
						"status": {"type": "completed"},
						"items": [{
							"type": "fileChange",
							"id": "file-change-1",
							"changes": [{
								"path": "changed.go",
								"kind": {"type": "update", "move_path": null},
								"diff": "@@ -1 +1 @@"
							}]
						}]
					}]
				}
			}`)}
		case "turn/start":
			require.Equal(t, "thread-1", req.Params["threadId"])
			require.Equal(t, "msg-direct-1", req.Params["clientUserMessageId"])
			return appServerEnvelope{Result: rawRPCResult(t, appServerTurnResult{Turn: appServerTurn{ID: "turn-2"}})}
		default:
			t.Fatalf("unexpected request %s", req.Method)
			return appServerEnvelope{}
		}
	})
	driver.threadID = "thread-1"
	driver.threadStatus = appServerThreadStatus{Type: "idle"}

	err := driver.DeliverRuntimeInput(context.Background(), testRuntimeInputWithConfig(drivercontract.RuntimeInput{ID: "msg-direct-1"}, "idle direct message"), "thread-1")

	require.NoError(t, err)
	require.Equal(t, []string{"thread/read", "turn/start"}, capturedRPCMethods(writer.requests))
}

func TestCodexAppServerAckedBusyRuntimeInputSteersWhileWaitingOnApproval(t *testing.T) {
	driver, writer := newTestCodexDriver(t, func(req capturedRPCRequest) appServerEnvelope {
		require.Equal(t, "turn/steer", req.Method)
		require.Equal(t, "turn-1", req.Params["expectedTurnId"])
		require.Equal(t, "msg-agentteam-1", req.Params["clientUserMessageId"])
		return appServerEnvelope{Result: rawRPCResult(t, appServerTurnIDResult{TurnID: "turn-1"})}
	})
	driver.threadID = "thread-1"
	driver.threadStatus = appServerThreadStatus{
		Type:        "active",
		ActiveFlags: []string{"waitingOnApproval"},
	}
	driver.activeTurnID = "turn-1"

	require.True(t, driver.SupportsAckedBusyRuntimeInputDelivery())
	err := driver.DeliverAckedBusyRuntimeInput(context.Background(), testRuntimeInputWithConfig(drivercontract.RuntimeInput{ID: "msg-agentteam-1"}, "new message"), "thread-1")
	require.NoError(t, err)
	require.Len(t, writer.requests, 1)
}

func TestCodexAppServerUserMessageClientIDAcknowledgesRuntimeInput(t *testing.T) {
	driver, _ := newTestCodexDriver(t, nil)
	driver.translatedLines = make(chan string, 1)

	driver.handleItemNotification(json.RawMessage(`{
		"threadId":"thread-1",
		"turnId":"turn-1",
		"item":{
			"id":"user-item-1",
			"type":"userMessage",
			"clientId":"msg-agentteam-1",
			"content":[{"type":"text","text":"new message"}]
		}
	}`), false)

	line := <-driver.translatedLines
	require.Equal(t, []drivercontract.ParsedEvent{{
		Kind:           consts.ParsedEventKindRuntimeInputAcked,
		RuntimeInputID: "msg-agentteam-1",
		SessionID:      "thread-1",
	}}, driver.ParseLine(line))
}

func TestCodexAppServerDeliverInterruptingRuntimeInputInterruptsThenStarts(t *testing.T) {
	driver, writer := newTestCodexDriver(t, func(req capturedRPCRequest) appServerEnvelope {
		switch req.Method {
		case "turn/interrupt":
			require.Equal(t, "thread-1", req.Params["threadId"])
			require.Equal(t, "turn-1", req.Params["turnId"])
			return appServerEnvelope{Result: rawRPCResult(t, map[string]any{})}
		case "thread/read":
			require.Equal(t, "thread-1", req.Params["threadId"])
			require.Equal(t, true, req.Params["includeTurns"])
			return appServerEnvelope{
				Result: rawRPCResult(t, appServerThreadResult{
					Thread: appServerThread{
						ID:     "thread-1",
						Status: appServerThreadStatus{Type: "idle"},
						Turns: []appServerTurn{{
							ID:     "turn-1",
							Status: appServerTurnStatus{Type: "interrupted"},
						}},
					},
				}),
			}
		case "turn/start":
			require.Equal(t, "thread-1", req.Params["threadId"])
			require.Equal(t, "msg-interrupt-1", req.Params["clientUserMessageId"])
			input, ok := req.Params["input"].([]any)
			require.True(t, ok)
			require.Len(t, input, 1)
			item, ok := input[0].(map[string]any)
			require.True(t, ok)
			require.Equal(t, "new authoritative input", item["text"])
			return appServerEnvelope{
				Result: rawRPCResult(t, appServerTurnResult{
					Turn: appServerTurn{
						ID:     "turn-2",
						Status: appServerTurnStatus{Type: "inProgress"},
					},
				}),
			}
		default:
			t.Fatalf("unexpected method %s", req.Method)
			return appServerEnvelope{}
		}
	})
	driver.threadID = "thread-1"
	driver.threadStatus = appServerThreadStatus{Type: "active"}
	driver.activeTurnID = "turn-1"

	err := driver.DeliverInterruptingRuntimeInput(context.Background(), testRuntimeInputWithConfig(drivercontract.RuntimeInput{ID: "msg-interrupt-1"}, "new authoritative input"), "thread-1")

	require.NoError(t, err)
	require.Len(t, writer.requests, 3)
	require.Equal(t, "turn/interrupt", writer.requests[0].Method)
	require.Equal(t, "thread/read", writer.requests[1].Method)
	require.Equal(t, "turn/start", writer.requests[2].Method)
}

func TestCodexAppServerInterruptActiveRunDoesNotStartReplacementTurn(t *testing.T) {
	driver, writer := newTestCodexDriver(t, func(req capturedRPCRequest) appServerEnvelope {
		switch req.Method {
		case "turn/interrupt":
			require.Equal(t, "thread-1", req.Params["threadId"])
			require.Equal(t, "turn-1", req.Params["turnId"])
			return appServerEnvelope{Result: rawRPCResult(t, map[string]any{})}
		case "thread/read":
			require.Equal(t, "thread-1", req.Params["threadId"])
			require.Equal(t, true, req.Params["includeTurns"])
			return appServerEnvelope{
				Result: rawRPCResult(t, appServerThreadResult{
					Thread: appServerThread{
						ID:     "thread-1",
						Status: appServerThreadStatus{Type: "idle"},
						Turns: []appServerTurn{{
							ID:     "turn-1",
							Status: appServerTurnStatus{Type: "interrupted"},
						}},
					},
				}),
			}
		default:
			t.Fatalf("unexpected method %s", req.Method)
			return appServerEnvelope{}
		}
	})
	driver.threadID = "thread-1"
	driver.threadStatus = appServerThreadStatus{Type: "active"}
	driver.activeTurnID = "turn-1"

	err := driver.InterruptActiveRun(context.Background(), "thread-1")

	require.NoError(t, err)
	require.Len(t, writer.requests, 2)
	require.Equal(t, "turn/interrupt", writer.requests[0].Method)
	require.Equal(t, "thread/read", writer.requests[1].Method)
}

func TestCodexAppServerInterruptActiveRunUsesStartupInterruptWhenTurnIDUnknown(t *testing.T) {
	readCount := 0
	driver, writer := newTestCodexDriver(t, func(req capturedRPCRequest) appServerEnvelope {
		switch req.Method {
		case "thread/read":
			readCount++
			require.Equal(t, "thread-1", req.Params["threadId"])
			require.Equal(t, true, req.Params["includeTurns"])
			status := appServerThreadStatus{Type: "active"}
			if readCount > 1 {
				status = appServerThreadStatus{Type: "idle"}
			}
			return appServerEnvelope{
				Result: rawRPCResult(t, appServerThreadResult{
					Thread: appServerThread{
						ID:     "thread-1",
						Status: status,
						Turns:  nil,
					},
				}),
			}
		case "turn/interrupt":
			require.Equal(t, "thread-1", req.Params["threadId"])
			require.Equal(t, "", req.Params["turnId"])
			return appServerEnvelope{Result: rawRPCResult(t, map[string]any{})}
		default:
			t.Fatalf("unexpected method %s", req.Method)
			return appServerEnvelope{}
		}
	})
	driver.threadID = "thread-1"
	driver.threadStatus = appServerThreadStatus{Type: "active"}
	driver.activeTurnID = ""

	err := driver.InterruptActiveRun(context.Background(), "thread-1")

	require.NoError(t, err)
	require.Len(t, writer.requests, 3)
	require.Equal(t, "thread/read", writer.requests[0].Method)
	require.Equal(t, "turn/interrupt", writer.requests[1].Method)
	require.Equal(t, "thread/read", writer.requests[2].Method)
}

func TestCodexAppServerInterruptActiveRunTreatsEndpointErrorAsInterruptedWhenThreadIdle(t *testing.T) {
	driver, writer := newTestCodexDriver(t, func(req capturedRPCRequest) appServerEnvelope {
		switch req.Method {
		case "turn/interrupt":
			require.Equal(t, "thread-1", req.Params["threadId"])
			require.Equal(t, "turn-1", req.Params["turnId"])
			return appServerEnvelope{Error: &appServerResponseError{Code: -32000, Message: "endpoint_error"}}
		case "thread/read":
			require.Equal(t, "thread-1", req.Params["threadId"])
			require.Equal(t, true, req.Params["includeTurns"])
			return appServerEnvelope{
				Result: rawRPCResult(t, appServerThreadResult{
					Thread: appServerThread{
						ID:     "thread-1",
						Status: appServerThreadStatus{Type: "idle"},
						Turns: []appServerTurn{{
							ID:     "turn-1",
							Status: appServerTurnStatus{Type: "failed"},
						}},
					},
				}),
			}
		default:
			t.Fatalf("unexpected method %s", req.Method)
			return appServerEnvelope{}
		}
	})
	driver.threadID = "thread-1"
	driver.threadStatus = appServerThreadStatus{Type: "active"}
	driver.activeTurnID = "turn-1"

	err := driver.InterruptActiveRun(context.Background(), "thread-1")

	require.NoError(t, err)
	require.Len(t, writer.requests, 3)
	require.Equal(t, "turn/interrupt", writer.requests[0].Method)
	require.Equal(t, "thread/read", writer.requests[1].Method)
	require.Equal(t, "thread/read", writer.requests[2].Method)
}

func TestCodexAppServerRestoreUsesStartupInterruptWhenTurnIDUnknown(t *testing.T) {
	readCount := 0
	driver, writer := newTestCodexDriver(t, func(req capturedRPCRequest) appServerEnvelope {
		switch req.Method {
		case "thread/read":
			readCount++
			require.Equal(t, "thread-1", req.Params["threadId"])
			status := appServerThreadStatus{Type: "active"}
			if readCount >= 3 {
				status = appServerThreadStatus{Type: "idle"}
			}
			return appServerEnvelope{
				Result: rawRPCResult(t, appServerThreadResult{
					Thread: appServerThread{
						ID:     "thread-1",
						Status: status,
						Turns:  nil,
					},
				}),
			}
		case "turn/interrupt":
			require.Equal(t, "thread-1", req.Params["threadId"])
			require.Equal(t, "", req.Params["turnId"])
			return appServerEnvelope{Result: rawRPCResult(t, map[string]any{})}
		default:
			t.Fatalf("unexpected method %s", req.Method)
			return appServerEnvelope{}
		}
	})
	driver.threadID = "thread-1"
	driver.threadStatus = appServerThreadStatus{Type: "active"}
	driver.activeTurnID = ""
	recoveryRef, err := rewindcaps.EncodeCodexAppServerRecoveryRef("thread-1", 0)
	require.NoError(t, err)

	err = driver.RestoreRecoveryPoint(context.Background(), rewindcaps.StrategyCodexAppServerTurns, recoveryRef)

	require.NoError(t, err)
	require.Len(t, writer.requests, 4)
	require.Equal(t, "thread/read", writer.requests[0].Method)
	require.Equal(t, false, writer.requests[0].Params["includeTurns"])
	require.Equal(t, "thread/read", writer.requests[1].Method)
	require.Equal(t, true, writer.requests[1].Params["includeTurns"])
	require.Equal(t, "turn/interrupt", writer.requests[2].Method)
	require.Equal(t, "thread/read", writer.requests[3].Method)
	require.Equal(t, true, writer.requests[3].Params["includeTurns"])
	driver.mu.Lock()
	snapshot := driver.lastActiveInterrupt
	driver.mu.Unlock()
	require.Empty(t, snapshot.threadID, "startup interrupt has no persisted +1 turn and must consume at the inactive pre-count boundary")
}

func TestCodexAppServerPrepareInitialThreadChoosesCorrectPrompt(t *testing.T) {
	t.Run("fresh thread uses fresh prompt", func(t *testing.T) {
		driver, _ := newTestCodexDriver(t, func(req capturedRPCRequest) appServerEnvelope {
			require.Equal(t, "thread/start", req.Method)
			return appServerEnvelope{
				Result: rawRPCResult(t, appServerThreadResult{
					Thread: appServerThread{ID: "thread-fresh", Status: appServerThreadStatus{Type: "idle"}},
				}),
			}
		})

		threadID, method, params, resumed, err := driver.prepareInitialThread(context.Background(), testLaunchConfig(drivercontract.LaunchConfig{InitialDeliveryMessageID: "msg-fresh"}, testLaunchSurfaces{FreshPrompt: "fresh prompt"}))
		require.NoError(t, err)
		require.Equal(t, "thread-fresh", threadID)
		require.Equal(t, "turn/start", method)
		require.Equal(t, "fresh prompt", turnInputText(t, params))
		require.False(t, resumed)
		require.Equal(t, "msg-fresh", params["clientUserMessageId"])
	})

	t.Run("resumed thread uses resume prompt", func(t *testing.T) {
		driver, _ := newTestCodexDriver(t, func(req capturedRPCRequest) appServerEnvelope {
			switch req.Method {
			case "thread/resume":
				return appServerEnvelope{
					Result: rawRPCResult(t, appServerThreadResult{
						Thread: appServerThread{ID: "thread-resume", Status: appServerThreadStatus{Type: "idle"}},
					}),
				}
			case "thread/read":
				require.Equal(t, "thread-resume", req.Params["threadId"])
				require.Equal(t, true, req.Params["includeTurns"])
				return appServerEnvelope{
					Result: rawRPCResult(t, appServerThreadResult{
						Thread: appServerThread{ID: "thread-resume", Status: appServerThreadStatus{Type: "idle"}},
					}),
				}
			default:
				t.Fatalf("unexpected method %s", req.Method)
				return appServerEnvelope{}
			}
		})

		threadID, method, params, resumed, err := driver.prepareInitialThread(context.Background(), testLaunchConfig(drivercontract.LaunchConfig{SessionID: "thread-resume", InitialDeliveryMessageID: "msg-resume"}, testLaunchSurfaces{ResumePrompt: "resume prompt"}))
		require.NoError(t, err)
		require.Equal(t, "thread-resume", threadID)
		require.Equal(t, "turn/start", method)
		require.Equal(t, "resume prompt", turnInputText(t, params))
		require.True(t, resumed)
		require.Equal(t, "msg-resume", params["clientUserMessageId"])
	})

	t.Run("resume failure falls back to fresh prompt", func(t *testing.T) {
		driver, writer := newTestCodexDriver(t, func(req capturedRPCRequest) appServerEnvelope {
			switch req.Method {
			case "thread/resume":
				return appServerEnvelope{
					Error: &appServerResponseError{
						Code:    -32000,
						Message: "resume rejected",
						Data:    json.RawMessage(`{"code":"thread_not_found"}`),
					},
				}
			case "thread/start":
				return appServerEnvelope{
					Result: rawRPCResult(t, appServerThreadResult{
						Thread: appServerThread{ID: "thread-fallback", Status: appServerThreadStatus{Type: "idle"}},
					}),
				}
			default:
				t.Fatalf("unexpected method %s", req.Method)
				return appServerEnvelope{}
			}
		})

		threadID, method, params, resumed, err := driver.prepareInitialThread(context.Background(), testLaunchConfig(drivercontract.LaunchConfig{SessionID: "thread-legacy", InitialDeliveryMessageID: "msg-fallback"}, testLaunchSurfaces{FreshPrompt: "fresh prompt", ResumePrompt: "resume prompt"}))
		require.NoError(t, err)
		require.Equal(t, "thread-fallback", threadID)
		require.Equal(t, "turn/start", method)
		require.Equal(t, "fresh prompt", turnInputText(t, params))
		require.False(t, resumed)
		require.Equal(t, "msg-fallback", params["clientUserMessageId"])
		fallback := driver.takeSessionResumeFallback()
		require.Equal(t, &drivercontract.SessionResumeFallback{
			Reason:    "invalid_or_not_found_session",
			Operation: "thread/resume",
		}, fallback)
		require.Nil(t, driver.takeSessionResumeFallback(), "fresh-session fallback must be consumed at most once")
		require.Len(t, writer.requests, 2)
		require.Equal(t, "thread/resume", writer.requests[0].Method)
		require.Equal(t, "thread/start", writer.requests[1].Method)
	})

	t.Run("ordinary provider rejection does not fall back", func(t *testing.T) {
		driver, writer := newTestCodexDriver(t, func(req capturedRPCRequest) appServerEnvelope {
			require.Equal(t, "thread/resume", req.Method)
			return appServerEnvelope{Error: &appServerResponseError{Code: -32000, Message: "model not found"}}
		})
		_, _, _, _, err := driver.prepareInitialThread(context.Background(), testLaunchConfig(drivercontract.LaunchConfig{SessionID: "thread-legacy"}, testLaunchSurfaces{FreshPrompt: "must not be replayed"}))
		require.ErrorContains(t, err, "model not found")
		require.Len(t, writer.requests, 1)
		require.Nil(t, driver.takeSessionResumeFallback())
	})
}

func TestCodexSessionInvalidAuthorityRequiresTypedThreadCodeAndOuterError(t *testing.T) {
	for _, data := range []string{
		`{"reason":"thread_not_found"}`,
		`{"status":"thread_not_found"}`,
		`{"error":"thread_not_found"}`,
		`{"message":"thread_not_found"}`,
		`{"code":"not_found"}`,
		`{"code":"invalid"}`,
	} {
		err := &appServerRequestError{
			Method: "thread/resume",
			Code:   codexSessionInvalidRPCCode,
			Data:   json.RawMessage(data),
		}
		require.False(t, isAuthoritativeCodexSessionInvalidError(err), "generic data must not authorize fallback: %s", data)
	}

	require.True(t, isAuthoritativeCodexSessionInvalidError(&appServerRequestError{
		Method: "thread/resume",
		Code:   codexSessionInvalidRPCCode,
		Data:   json.RawMessage(`{"kind":"invalid_thread_id"}`),
	}))
	require.False(t, isAuthoritativeCodexSessionInvalidError(&appServerRequestError{
		Method: "thread/resume",
		Code:   -32602,
		Data:   json.RawMessage(`{"code":"thread_not_found"}`),
	}), "a valid token under the wrong outer RPC code is not authority")

	wrongCodeDriver, wrongCodeWriter := newTestCodexDriver(t, func(req capturedRPCRequest) appServerEnvelope {
		require.Equal(t, "thread/resume", req.Method)
		return appServerEnvelope{Error: &appServerResponseError{
			Code: -32602,
			Data: json.RawMessage(`{"code":"thread_not_found"}`),
		}}
	})
	_, _, _, _, wrongErr := wrongCodeDriver.prepareInitialThread(context.Background(), testLaunchConfig(drivercontract.LaunchConfig{SessionID: "thread-legacy"}, testLaunchSurfaces{FreshPrompt: "must not be replayed"}))
	require.Error(t, wrongErr)
	require.Len(t, wrongCodeWriter.requests, 1)
	require.Nil(t, wrongCodeDriver.takeSessionResumeFallback())

	driver, writer := newTestCodexDriver(t, func(req capturedRPCRequest) appServerEnvelope {
		require.Equal(t, "thread/resume", req.Method)
		return appServerEnvelope{Error: &appServerResponseError{
			Code: codexSessionInvalidRPCCode,
			Data: json.RawMessage(`{"reason":"thread_not_found"}`),
		}}
	})
	_, _, _, _, err := driver.prepareInitialThread(context.Background(), testLaunchConfig(drivercontract.LaunchConfig{SessionID: "thread-legacy"}, testLaunchSurfaces{FreshPrompt: "must not be replayed"}))
	require.Error(t, err)
	require.Len(t, writer.requests, 1)
	require.Nil(t, driver.takeSessionResumeFallback())
}

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

	for _, effort := range []string{"max", "ultra", "xhigh"} {
		t.Run(effort, func(t *testing.T) {
			driver, writer := newTestCodexDriver(t, func(req capturedRPCRequest) appServerEnvelope {
				require.Equal(t, "thread/start", req.Method)
				return appServerEnvelope{
					Result: rawRPCResult(t, appServerThreadResult{
						Thread: appServerThread{ID: "thread-fresh", Status: appServerThreadStatus{Type: "idle"}},
					}),
				}
			})
			driver.reasoningEffort = effort

			threadID, method, params, resumed, err := driver.prepareInitialThread(context.Background(), testLaunchConfig(drivercontract.LaunchConfig{}, testLaunchSurfaces{FreshPrompt: "fresh prompt"}))

			require.NoError(t, err)
			require.Equal(t, "thread-fresh", threadID)
			require.Equal(t, "turn/start", method)
			require.Equal(t, "fresh prompt", turnInputText(t, params))
			require.Equal(t, effort, params["effort"])
			require.False(t, resumed)
			require.Len(t, writer.requests, 1)
		})
	}
}

func TestCodexAppServerArgsIncludeYOLO(t *testing.T) {
	args, err := codexAppServerArgs(drivercontract.LaunchConfig{})
	require.NoError(t, err)
	require.Equal(t, []string{
		"--dangerously-bypass-approvals-and-sandbox",
		"app-server",
		"--listen",
		"stdio://",
	}, args[:4])
}

func TestCodexAppServerArgsForceDangerFullAccessSandbox(t *testing.T) {
	args, err := codexAppServerArgs(drivercontract.LaunchConfig{})
	require.NoError(t, err)
	require.Contains(t, args, "-c")
	require.Contains(t, args, `sandbox_mode="danger-full-access"`)
}

func TestCodexAppServerArgsLegacyWorkspacePermissionModeDefaultsFullAccess(t *testing.T) {
	args, err := codexAppServerArgs(drivercontract.LaunchConfig{
		PermissionMode: types.AgentPermissionModeWorkspaceWrite,
	})
	require.NoError(t, err)
	require.Contains(t, args, "--dangerously-bypass-approvals-and-sandbox")
	require.Contains(t, args, `sandbox_mode="danger-full-access"`)
}

func TestCodexAppServerArgsQuoteSmallModel(t *testing.T) {
	args, err := codexAppServerArgs(drivercontract.LaunchConfig{
		SmallModel: " gpt-5 mini ",
	})
	require.NoError(t, err)
	require.Contains(t, args, `model_small="gpt-5 mini"`)
}

func TestCodexAppServerArgsCanDisableNativeTools(t *testing.T) {
	args, err := codexAppServerArgs(drivercontract.LaunchConfig{DisableNativeTools: true})
	require.NoError(t, err)
	joined := strings.Join(args, "\n")
	for _, feature := range []string{"shell_tool", "unified_exec", "code_mode", "apps", "browser_use", "computer_use", "image_generation", "multi_agent", "plugins", "memories", "goals"} {
		require.Contains(t, joined, "features."+feature+"=false")
	}
	require.Contains(t, joined, `web_search="disabled"`)
}

func TestCodexAppServerArgsMarksAgentTeamMCPRequired(t *testing.T) {
	args, err := codexAppServerArgs(drivercontract.LaunchConfig{MCPs: []types.NamedMCPConfig{
		{Name: "agentteam", Transport: types.MCPTransportHTTP, URL: "https://agentteam.example.com/mcp"},
		{Name: "github", Transport: types.MCPTransportHTTP, URL: "https://github.example.com/mcp"},
	}})
	require.NoError(t, err)
	joined := strings.Join(args, "\n")
	require.Contains(t, joined, `mcp_servers.agentteam.required=true`)
	require.Contains(t, joined, `mcp_servers.github.required=false`)
}

func TestCodexAppServerArgsPassExtraMCPHeadersByEnv(t *testing.T) {
	args, err := codexAppServerArgs(drivercontract.LaunchConfig{
		MCPs: []types.NamedMCPConfig{{
			Name: "github",
			URL:  "http://127.0.0.1:18081/external-mcp/github",
			Headers: map[string]string{
				"Authorization":                "Bearer local-mcp-token",
				"X-AgentTeam-App-Scoped-Token": "app-token",
			},
		}},
	})
	require.NoError(t, err)

	joined := strings.Join(args, "\n")
	require.Contains(t, joined, `mcp_servers.github.bearer_token_env_var="AGENTTEAM_MCP_AUTH_TOKEN_GITHUB"`)
	require.Contains(t, joined, `mcp_servers.github.env_http_headers={"X-AgentTeam-App-Scoped-Token"="AGENTTEAM_MCP_HTTP_HEADER_GITHUB_X_AGENTTEAM_APP_SCOPED_TOKEN"}`)
	require.NotContains(t, joined, "local-mcp-token")
	require.NotContains(t, joined, "app-token")
}

func TestApplyMCPHeaderEnvStoresBearerAndExtraHeadersSeparately(t *testing.T) {
	env := applyMCPHeaderEnv(map[string]string{"EXISTING": "1"}, []types.NamedMCPConfig{{
		Name: "github",
		Headers: map[string]string{
			"Authorization":                "Bearer local-mcp-token",
			"X-AgentTeam-App-Scoped-Token": "app-token",
		},
	}})

	require.Equal(t, "1", env["EXISTING"])
	require.Equal(t, "local-mcp-token", env["AGENTTEAM_MCP_AUTH_TOKEN_GITHUB"])
	require.Equal(t, "app-token", env["AGENTTEAM_MCP_HTTP_HEADER_GITHUB_X_AGENTTEAM_APP_SCOPED_TOKEN"])
}

func TestCodexAppServerReadAgentTeamHandlesMCPServerToolCallRequests(t *testing.T) {
	var requestBody struct {
		JSONRPC string `json:"jsonrpc"`
		Method  string `json:"method"`
		Params  struct {
			Name      string          `json:"name"`
			Arguments json.RawMessage `json:"arguments"`
		} `json:"params"`
	}
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()
		require.Equal(t, http.MethodPost, r.Method)
		require.Equal(t, "Bearer test-token", r.Header.Get("Authorization"))

		body, err := io.ReadAll(r.Body)
		require.NoError(t, err)
		require.NoError(t, json.Unmarshal(body, &requestBody))
		require.Equal(t, "2.0", requestBody.JSONRPC)
		require.Equal(t, "tools/call", requestBody.Method)
		require.Equal(t, "message_send", requestBody.Params.Name)
		require.JSONEq(t, `{"target":"#agentteam:deadbeef","content":"hello"}`, string(requestBody.Params.Arguments))

		w.Header().Set("Content-Type", "application/json")
		_, err = io.WriteString(w, `{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"Message sent."}]}}`)
		require.NoError(t, err)
	}))
	defer server.Close()

	rawWriter := &rawCaptureWriteCloser{}
	driver := &CodexAppServerDriver{
		codexRPCState: codexRPCState{
			rawStdin: rawWriter,
			pending:  make(map[int]pendingAppServerRequest),
		},
		codexLaunchState: codexLaunchState{
			mcpConfigs: map[string]types.NamedMCPConfig{
				"agentteam": {
					Name:    "agentteam",
					URL:     server.URL,
					Headers: map[string]string{"Authorization": "Bearer test-token"},
				},
			},
			httpClient: server.Client(),
		},
	}

	driver.handleItemNotification(json.RawMessage(`{
		"threadId":"thread-1",
		"turnId":"turn-1",
		"item":{
			"id":"req-1",
			"type":"mcpToolCall",
			"server":"agentteam",
			"tool":"message_send",
			"arguments":{"target":"#agentteam:deadbeef","content":"hello"}
		}
	}`), false)
	runReadAgentTeamWithSingleLine(driver, `{"jsonrpc":"2.0","id":"req-1","method":"mcpServer/tool/call","params":{"threadId":"thread-1","server":"agentteam","tool":"message_send","arguments":{"target":"#agentteam:deadbeef","content":"hello"}}}`)

	require.Eventually(t, func() bool { return len(rawWriter.Writes()) == 1 }, time.Second, 10*time.Millisecond)
	response := decodeRPCResponse(t, rawWriter.Writes()[0])
	require.Equal(t, `"req-1"`, string(response.ID))
	require.Nil(t, response.Error)
	require.JSONEq(t, `{"content":[{"type":"text","text":"Message sent."}]}`, string(response.Result))
}

func TestCodexAppServerReadAgentTeamHandlesDynamicToolCallRequests(t *testing.T) {
	tests := []struct {
		name        string
		itemTool    string
		requestTool string
		responseID  string
	}{
		{
			name:        "flat tool name",
			itemTool:    `"tool":"mcp_agentteam_message_send"`,
			requestTool: `"tool":"mcp_agentteam_message_send"`,
			responseID:  `"req-dynamic-legacy"`,
		},
		{
			name:        "codex namespaced tool name",
			itemTool:    `"tool":"mcp__agentteam__message_send"`,
			requestTool: `"tool":"mcp__agentteam__message_send"`,
			responseID:  `"req-dynamic-namespaced"`,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				defer r.Body.Close()
				body, err := io.ReadAll(r.Body)
				require.NoError(t, err)

				var request struct {
					Params struct {
						Name      string          `json:"name"`
						Arguments json.RawMessage `json:"arguments"`
					} `json:"params"`
				}
				require.NoError(t, json.Unmarshal(body, &request))
				require.Equal(t, "message_send", request.Params.Name)
				require.JSONEq(t, `{"target":"#agentteam:deadbeef","content":"hello from dynamic"}`, string(request.Params.Arguments))

				w.Header().Set("Content-Type", "application/json")
				_, err = io.WriteString(w, `{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"Dynamic call worked."}]}}`)
				require.NoError(t, err)
			}))
			defer server.Close()

			rawWriter := &rawCaptureWriteCloser{}
			driver := &CodexAppServerDriver{
				codexRPCState: codexRPCState{
					rawStdin: rawWriter,
					pending:  make(map[int]pendingAppServerRequest),
				},
				codexLaunchState: codexLaunchState{
					mcpConfigs: map[string]types.NamedMCPConfig{
						"agentteam": {
							Name:    "agentteam",
							URL:     server.URL,
							Headers: map[string]string{"Authorization": "Bearer dynamic-token"},
						},
					},
					httpClient: server.Client(),
				},
			}

			driver.handleItemNotification(json.RawMessage(`{
				"threadId":"thread-1",
				"turnId":"turn-1",
				"item":{
					"id":"call-1",
					"type":"dynamicToolCall",
					`+tt.itemTool+`,
					"arguments":{"target":"#agentteam:deadbeef","content":"hello from dynamic"}
				}
			}`), false)
			runReadAgentTeamWithSingleLine(driver, `{"jsonrpc":"2.0","id":`+tt.responseID+`,"method":"item/tool/call","params":{"threadId":"thread-1","turnId":"turn-1","callId":"call-1",`+tt.requestTool+`,"arguments":{"target":"#agentteam:deadbeef","content":"hello from dynamic"}}}`)

			require.Eventually(t, func() bool { return len(rawWriter.Writes()) == 1 }, time.Second, 10*time.Millisecond)
			response := decodeRPCResponse(t, rawWriter.Writes()[0])
			require.Equal(t, tt.responseID, string(response.ID))
			require.Nil(t, response.Error)
			require.JSONEq(t, `{"success":true,"contentItems":[{"type":"inputText","text":"Dynamic call worked."}]}`, string(response.Result))
		})
	}
}

func TestCodexAppServerMCPToolEventNameUsesCodexNamespace(t *testing.T) {
	driver := &CodexAppServerDriver{}

	require.Equal(t, "mcp__agentteam__message_send", driver.mcpToolEventName("agentteam", "message_send"))
	require.Equal(t, "mcp__agentteam__memory_search", driver.mcpToolEventName("agentteam", "memory_search"))
	require.Equal(t, "mcp__external__lookup", driver.mcpToolEventName("external", "lookup"))
}

func TestCodexAppServerMCPDynamicToolNameAvoidsReservedCodexNamespace(t *testing.T) {
	driver := &CodexAppServerDriver{}

	require.Equal(t, "mcp_agentteam_message_send", driver.mcpDynamicToolName("agentteam", "message_send"))
	require.Equal(t, "mcp_external_lookup", driver.mcpDynamicToolName("external", "lookup"))
	require.NotContains(t, driver.mcpDynamicToolName("external", "lookup"), "mcp__")
}

func TestCodexAppServerResolveDynamicToolCallTargetAcceptsNamespacedAndFlatNames(t *testing.T) {
	driver := &CodexAppServerDriver{
		codexLaunchState: codexLaunchState{
			mcpConfigs: map[string]types.NamedMCPConfig{
				"agentteam":  {Name: "agentteam"},
				"external":   {Name: "external"},
				"my__server": {Name: "my__server"},
			}},
	}

	tests := []struct {
		name       string
		wantServer string
		wantTool   string
	}{
		{name: "mcp__agentteam__message_send", wantServer: "agentteam", wantTool: "message_send"},
		{name: "mcp_agentteam_message_send", wantServer: "agentteam", wantTool: "message_send"},
		{name: "mcp__external__lookup", wantServer: "external", wantTool: "lookup"},
		{name: "mcp_external_lookup", wantServer: "external", wantTool: "lookup"},
		{name: "mcp__my__server__lookup", wantServer: "my__server", wantTool: "lookup"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			server, tool, ok := driver.resolveDynamicToolCallTarget(tt.name)
			require.True(t, ok)
			require.Equal(t, tt.wantServer, server)
			require.Equal(t, tt.wantTool, tool)
		})
	}

	_, _, ok := driver.resolveDynamicToolCallTarget("mcp__missing__message_send")
	require.False(t, ok)
}

func TestCodexAppServerMCPToolCallRejectsThreadIDOnlyOutputActivity(t *testing.T) {
	called := false
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		called = true
		defer r.Body.Close()
		w.Header().Set("Content-Type", "application/json")
		_, err := io.WriteString(w, `{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"[target=#agentteam msg=msg-1] @alice: history body"}]}}`)
		require.NoError(t, err)
	}))
	defer server.Close()

	driver := &CodexAppServerDriver{
		codexRPCState: codexRPCState{
			translatedLines: make(chan string, 1)},
		codexLaunchState: codexLaunchState{
			mcpConfigs: map[string]types.NamedMCPConfig{
				"agentteam": {
					Name: "agentteam",
					URL:  server.URL,
				},
			},
			httpClient: server.Client(),
		},
	}

	result, rpcErr := driver.executeServerRequest(
		context.Background(),
		"mcpServer/tool/call",
		json.RawMessage(`{"threadId":"thread-1","server":"agentteam","tool":"message_history_read","arguments":{}}`),
	)

	require.Nil(t, result)
	require.NotNil(t, rpcErr)
	require.Equal(t, -32602, rpcErr.Code)
	require.Contains(t, rpcErr.Message, "requires toolCallId")
	require.False(t, called, "MCP tool call must fail closed before executing with threadId-only output ownership")
	select {
	case line := <-driver.translatedLines:
		t.Fatalf("unexpected translated tool output with threadId-only ownership: %s", line)
	default:
	}
}

func TestCodexAppServerMCPToolCallAllowsThreadIDWithCallIDOutputActivity(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()
		body, err := io.ReadAll(r.Body)
		require.NoError(t, err)
		var request struct {
			Params struct {
				Name string `json:"name"`
			} `json:"params"`
		}
		require.NoError(t, json.Unmarshal(body, &request))
		require.Equal(t, "message_history_read", request.Params.Name)

		w.Header().Set("Content-Type", "application/json")
		_, err = io.WriteString(w, `{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"[target=#agentteam msg=msg-1] @alice: history body"}]}}`)
		require.NoError(t, err)
	}))
	defer server.Close()

	driver := &CodexAppServerDriver{
		codexRPCState: codexRPCState{
			translatedLines: make(chan string, 1)},
		codexLaunchState: codexLaunchState{
			mcpConfigs: map[string]types.NamedMCPConfig{
				"agentteam": {
					Name: "agentteam",
					URL:  server.URL,
				},
			},
			httpClient: server.Client(),
		},
	}

	result, rpcErr := driver.executeServerRequest(
		context.Background(),
		"mcpServer/tool/call",
		json.RawMessage(`{"threadId":"thread-1","callId":"call-1","server":"agentteam","tool":"message_history_read","arguments":{}}`),
	)

	require.Nil(t, rpcErr)
	require.NotNil(t, result)
	select {
	case line := <-driver.translatedLines:
		events := driver.ParseLine(line)
		require.Len(t, events, 1)
		require.Equal(t, consts.ParsedEventKindToolOutput, events[0].Kind)
		require.Equal(t, "mcp__agentteam__message_history_read", events[0].ToolName)
		require.Equal(t, "call-1", events[0].ToolCallID)
		require.Equal(t, "thread-1", events[0].SessionID)
		require.Contains(t, events[0].Text, "history body")
	case <-time.After(time.Second):
		t.Fatal("timed out waiting for translated tool output")
	}
}

func TestCodexAppServerMCPToolCallUsesToolActivityRef(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()
		body, err := io.ReadAll(r.Body)
		require.NoError(t, err)
		var request struct {
			Params struct {
				Name string `json:"name"`
			} `json:"params"`
		}
		require.NoError(t, json.Unmarshal(body, &request))
		require.Equal(t, "message_history_read", request.Params.Name)

		w.Header().Set("Content-Type", "application/json")
		_, err = io.WriteString(w, `{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"No new messages."}],"structuredContent":{"customFlag":true,"customCount":2}}}`)
		require.NoError(t, err)
	}))
	defer server.Close()

	driver := &CodexAppServerDriver{
		codexRPCState: codexRPCState{
			translatedLines: make(chan string, 2)},
		codexLaunchState: codexLaunchState{
			mcpConfigs: map[string]types.NamedMCPConfig{
				"agentteam": {
					Name: "agentteam",
					URL:  server.URL,
				},
			},
			httpClient: server.Client(),
		},
	}

	driver.handleItemNotification(json.RawMessage(`{
		"threadId":"thread-current",
		"turnId":"turn-1",
		"item":{
			"id":"call-history",
			"type":"mcpToolCall",
			"server":"agentteam",
			"tool":"message_history_read",
			"arguments":{}
		}
	}`), false)
	line := <-driver.translatedLines
	startEvents := driver.ParseLine(line)
	require.Len(t, startEvents, 1)
	require.Equal(t, consts.ParsedEventKindToolCall, startEvents[0].Kind)
	require.Equal(t, "mcp__agentteam__message_history_read", startEvents[0].ToolName)
	require.Equal(t, "call-history", startEvents[0].ToolCallID)
	require.Equal(t, "thread-current", startEvents[0].SessionID)
	require.Empty(t, startEvents[0].ToolInput)

	result, rpcErr := driver.executeServerRequestWithActivity(
		context.Background(),
		"call-history",
		"mcpServer/tool/call",
		json.RawMessage(`{"server":"agentteam","tool":"message_history_read","arguments":{}}`),
	)

	require.Nil(t, rpcErr)
	require.NotNil(t, result)
	resultJSON, err := json.Marshal(result)
	require.NoError(t, err)
	require.JSONEq(t, `{"content":[{"type":"text","text":"No new messages."}],"structuredContent":{"customFlag":true,"customCount":2}}`, string(resultJSON))
	select {
	case line := <-driver.translatedLines:
		events := driver.ParseLine(line)
		require.Len(t, events, 1)
		require.Equal(t, consts.ParsedEventKindToolOutput, events[0].Kind)
		require.Equal(t, "mcp__agentteam__message_history_read", events[0].ToolName)
		require.Equal(t, "call-history", events[0].ToolCallID)
		require.Equal(t, "thread-current", events[0].SessionID)
		require.Contains(t, events[0].Text, "No new messages.")
		require.Equal(t, map[string]any{
			"customFlag":  true,
			"customCount": float64(2),
		}, events[0].Metadata)
	case <-time.After(time.Second):
		t.Fatal("timed out waiting for translated tool output")
	}
}

func TestCodexAppServerMCPToolCallCompletedItemEmitsOutputActivity(t *testing.T) {
	driver := &CodexAppServerDriver{
		codexRPCState: codexRPCState{
			translatedLines: make(chan string, 2)}}

	driver.handleItemNotification(json.RawMessage(`{
		"threadId":"thread-current",
		"turnId":"turn-1",
		"item":{
			"id":"call-history",
			"type":"mcpToolCall",
			"server":"agentteam",
			"tool":"message_history_read",
			"arguments":{}
		}
	}`), false)
	startLine := <-driver.translatedLines
	startEvents := driver.ParseLine(startLine)
	require.Len(t, startEvents, 1)
	require.Equal(t, consts.ParsedEventKindToolCall, startEvents[0].Kind)
	require.Equal(t, "mcp__agentteam__message_history_read", startEvents[0].ToolName)
	require.Equal(t, "call-history", startEvents[0].ToolCallID)
	require.Equal(t, "thread-current", startEvents[0].SessionID)
	require.Empty(t, startEvents[0].ToolInput)

	driver.handleItemNotification(json.RawMessage(`{
		"threadId":"thread-current",
		"turnId":"turn-1",
		"item":{
			"id":"call-history",
			"type":"mcpToolCall",
			"server":"agentteam",
			"tool":"message_history_read",
			"output":"Wall time: 0.015 seconds\nOutput:\n[{\"type\":\"text\",\"text\":\"[Message] message 2\"}]"
		}
	}`), true)

	select {
	case line := <-driver.translatedLines:
		events := driver.ParseLine(line)
		require.Len(t, events, 1)
		require.Equal(t, consts.ParsedEventKindToolOutput, events[0].Kind)
		require.Equal(t, "mcp__agentteam__message_history_read", events[0].ToolName)
		require.Equal(t, "call-history", events[0].ToolCallID)
		require.Equal(t, "thread-current", events[0].SessionID)
		require.Contains(t, events[0].Text, "message 2")
	case <-time.After(time.Second):
		t.Fatal("timed out waiting for completed mcpToolCall output")
	}
}

func TestCodexAppServerWebSearchCompletedItemEmitsOutputActivity(t *testing.T) {
	driver := &CodexAppServerDriver{
		codexRPCState: codexRPCState{
			translatedLines: make(chan string, 2)}}

	driver.handleItemNotification(json.RawMessage(`{
		"threadId":"thread-current",
		"turnId":"turn-1",
		"item":{
			"id":"web-search-1",
			"type":"webSearch",
			"query":"agentteam daemon logs"
		}
	}`), false)
	startLine := <-driver.translatedLines
	startEvents := driver.ParseLine(startLine)
	require.Len(t, startEvents, 1)
	require.Equal(t, consts.ParsedEventKindToolCall, startEvents[0].Kind)
	require.Equal(t, "web_search", startEvents[0].ToolName)
	require.Equal(t, "web-search-1", startEvents[0].ToolCallID)
	require.Equal(t, "thread-current", startEvents[0].SessionID)
	require.JSONEq(t, `{"query":"agentteam daemon logs"}`, startEvents[0].ToolInput.(string))

	driver.handleItemNotification(json.RawMessage(`{
		"threadId":"thread-current",
		"turnId":"turn-1",
		"item":{
			"id":"web-search-1",
			"type":"webSearch",
			"query":"agentteam daemon logs"
		}
	}`), true)

	select {
	case line := <-driver.translatedLines:
		events := driver.ParseLine(line)
		require.Len(t, events, 1)
		require.Equal(t, consts.ParsedEventKindToolOutput, events[0].Kind)
		require.Equal(t, "web_search", events[0].ToolName)
		require.Equal(t, "web-search-1", events[0].ToolCallID)
		require.Equal(t, "thread-current", events[0].SessionID)
		require.Contains(t, events[0].Text, "Searched web for: agentteam daemon logs")
	case <-time.After(time.Second):
		t.Fatal("timed out waiting for completed webSearch output")
	}
}

func TestCodexAppServerWebSearchActionPayloadEmitsActivityQuery(t *testing.T) {
	tests := []struct {
		name      string
		item      string
		wantQuery string
	}{
		{
			name: "search query",
			item: `{
				"id":"web-search-1",
				"type":"webSearch",
				"action":{
					"type":"search",
					"query":"repo:nashsu/llm_wiki ingestFile ingest source llm_wiki GitHub"
				}
			}`,
			wantQuery: "repo:nashsu/llm_wiki ingestFile ingest source llm_wiki GitHub",
		},
		{
			name: "open page URL",
			item: `{
				"id":"web-search-1",
				"type":"webSearch",
				"action":{
					"type":"open_page",
					"url":"https://github.com/nashsu/llm_wiki"
				}
			}`,
			wantQuery: "https://github.com/nashsu/llm_wiki",
		},
		{
			name: "find in page",
			item: `{
				"id":"web-search-1",
				"type":"webSearch",
				"action":{
					"type":"find_in_page",
					"pattern":"ingest",
					"url":"https://github.com/nashsu/llm_wiki"
				}
			}`,
			wantQuery: "'ingest' in https://github.com/nashsu/llm_wiki",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			driver := &CodexAppServerDriver{
				codexRPCState: codexRPCState{
					translatedLines: make(chan string, 2)}}
			notification := json.RawMessage(fmt.Sprintf(`{
				"threadId":"thread-current",
				"turnId":"turn-1",
				"item":%s
			}`, tc.item))

			driver.handleItemNotification(notification, false)
			startLine := <-driver.translatedLines
			startEvents := driver.ParseLine(startLine)
			require.Len(t, startEvents, 1)
			require.Equal(t, consts.ParsedEventKindToolCall, startEvents[0].Kind)
			require.Equal(t, "web_search", startEvents[0].ToolName)
			require.Equal(t, "web-search-1", startEvents[0].ToolCallID)
			require.Equal(t, "thread-current", startEvents[0].SessionID)
			require.JSONEq(t, fmt.Sprintf(`{"query":%q}`, tc.wantQuery), startEvents[0].ToolInput.(string))

			driver.handleItemNotification(notification, true)

			select {
			case line := <-driver.translatedLines:
				events := driver.ParseLine(line)
				require.Len(t, events, 1)
				require.Equal(t, consts.ParsedEventKindToolOutput, events[0].Kind)
				require.Equal(t, "web_search", events[0].ToolName)
				require.Equal(t, "web-search-1", events[0].ToolCallID)
				require.Equal(t, "thread-current", events[0].SessionID)
				require.Contains(t, events[0].Text, "Searched web for: "+tc.wantQuery)
			case <-time.After(time.Second):
				t.Fatal("timed out waiting for completed webSearch output")
			}
		})
	}
}

func TestCodexAppServerWebSearchCompletedActionEmitsActivityQuery(t *testing.T) {
	driver := &CodexAppServerDriver{
		codexRPCState: codexRPCState{
			translatedLines: make(chan string, 3)}}

	driver.handleItemNotification(json.RawMessage(`{
		"threadId":"thread-current",
		"turnId":"turn-1",
		"item":{
			"id":"web-search-1",
			"type":"webSearch",
			"status":"in_progress"
		}
	}`), false)
	require.Len(t, driver.translatedLines, 0)

	driver.handleItemNotification(json.RawMessage(`{
		"threadId":"thread-current",
		"turnId":"turn-1",
		"item":{
			"id":"web-search-1",
			"type":"webSearch",
			"status":"completed",
			"action":{
				"type":"search",
				"queries":["nashsu llm_wiki GitHub"],
				"query":"nashsu llm_wiki GitHub"
			}
		}
	}`), true)

	startLine := <-driver.translatedLines
	startEvents := driver.ParseLine(startLine)
	require.Len(t, startEvents, 1)
	require.Equal(t, consts.ParsedEventKindToolCall, startEvents[0].Kind)
	require.Equal(t, "web_search", startEvents[0].ToolName)
	require.Equal(t, "web-search-1", startEvents[0].ToolCallID)
	require.Equal(t, "thread-current", startEvents[0].SessionID)
	require.JSONEq(t, `{"query":"nashsu llm_wiki GitHub"}`, startEvents[0].ToolInput.(string))

	outputLine := <-driver.translatedLines
	outputEvents := driver.ParseLine(outputLine)
	require.Len(t, outputEvents, 1)
	require.Equal(t, consts.ParsedEventKindToolOutput, outputEvents[0].Kind)
	require.Equal(t, "web_search", outputEvents[0].ToolName)
	require.Equal(t, "web-search-1", outputEvents[0].ToolCallID)
	require.Equal(t, "thread-current", outputEvents[0].SessionID)
	require.Contains(t, outputEvents[0].Text, "Searched web for: nashsu llm_wiki GitHub")
}

func TestCodexAppServerFunctionCallOutputItemUsesRegisteredToolActivity(t *testing.T) {
	driver := &CodexAppServerDriver{
		codexRPCState: codexRPCState{
			translatedLines: make(chan string, 2)}}

	driver.handleItemNotification(json.RawMessage(`{
		"threadId":"thread-current",
		"turnId":"turn-1",
		"item":{
			"id":"call-history",
			"type":"mcpToolCall",
			"server":"agentteam",
			"tool":"message_history_read",
			"arguments":{}
		}
	}`), false)
	<-driver.translatedLines

	driver.handleItemNotification(json.RawMessage(`{
		"threadId":"thread-current",
		"turnId":"turn-1",
		"item":{
			"type":"function_call_output",
			"call_id":"call-history",
			"output":"Wall time: 0.015 seconds\nOutput:\nhistory body"
		}
	}`), true)

	select {
	case line := <-driver.translatedLines:
		events := driver.ParseLine(line)
		require.Len(t, events, 1)
		require.Equal(t, consts.ParsedEventKindToolOutput, events[0].Kind)
		require.Equal(t, "mcp__agentteam__message_history_read", events[0].ToolName)
		require.Equal(t, "call-history", events[0].ToolCallID)
		require.Equal(t, "thread-current", events[0].SessionID)
		require.Contains(t, events[0].Text, "history body")
	case <-time.After(time.Second):
		t.Fatal("timed out waiting for function_call_output activity")
	}
}

func TestCodexAppServerCompletedToolOutputRequiresRegisteredActivity(t *testing.T) {
	driver := &CodexAppServerDriver{
		codexRPCState: codexRPCState{
			translatedLines: make(chan string, 1)}}

	driver.handleItemNotification(json.RawMessage(`{
		"threadId":"thread-current",
		"turnId":"turn-1",
		"item":{
			"type":"function_call_output",
			"call_id":"call-missing",
			"output":"orphan output"
		}
	}`), true)

	select {
	case line := <-driver.translatedLines:
		t.Fatalf("unexpected output for unregistered tool activity: %s", line)
	default:
	}
}

func TestCodexAppServerMCPToolCallRejectsRegisteredThreadIDMismatch(t *testing.T) {
	called := false
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		called = true
		defer r.Body.Close()
		w.Header().Set("Content-Type", "application/json")
		_, err := io.WriteString(w, `{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"No new messages."}]}}`)
		require.NoError(t, err)
	}))
	defer server.Close()

	driver := &CodexAppServerDriver{
		codexRPCState: codexRPCState{
			translatedLines: make(chan string, 1)},
		codexLaunchState: codexLaunchState{
			mcpConfigs: map[string]types.NamedMCPConfig{
				"agentteam": {
					Name: "agentteam",
					URL:  server.URL,
				},
			},
			httpClient: server.Client(),
		},
		codexThreadState: codexThreadState{
			threadID: "thread-a"},
	}

	driver.handleItemNotification(json.RawMessage(`{
		"threadId":"thread-a",
		"turnId":"turn-1",
		"item":{
			"id":"call-1",
			"type":"mcpToolCall",
			"server":"agentteam",
			"tool":"message_history_read",
			"arguments":{}
		}
	}`), false)
	<-driver.translatedLines

	result, rpcErr := driver.executeServerRequest(
		context.Background(),
		"mcpServer/tool/call",
		json.RawMessage(`{"threadId":"thread-b","callId":"call-1","server":"agentteam","tool":"message_history_read","arguments":{}}`),
	)

	require.Nil(t, result)
	require.NotNil(t, rpcErr)
	require.Equal(t, -32602, rpcErr.Code)
	require.Contains(t, rpcErr.Message, "threadId mismatch")
	require.False(t, called, "MCP tool call must fail closed before executing with mismatched registered activity")
	select {
	case line := <-driver.translatedLines:
		t.Fatalf("unexpected translated tool output with mismatched registered activity: %s", line)
	default:
	}
}

func TestCodexAppServerMCPToolOutputsFollowCallIDAcrossThreads(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()
		body, err := io.ReadAll(r.Body)
		require.NoError(t, err)
		var request struct {
			Params struct {
				Name      string          `json:"name"`
				Arguments json.RawMessage `json:"arguments"`
			} `json:"params"`
		}
		require.NoError(t, json.Unmarshal(body, &request))
		require.Equal(t, "message_history_read", request.Params.Name)
		var args struct {
			Marker string `json:"marker"`
		}
		require.NoError(t, json.Unmarshal(request.Params.Arguments, &args))

		w.Header().Set("Content-Type", "application/json")
		_, err = io.WriteString(w, `{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"result `+args.Marker+`"}]}}`)
		require.NoError(t, err)
	}))
	defer server.Close()

	driver := &CodexAppServerDriver{
		codexRPCState: codexRPCState{
			translatedLines: make(chan string, 4)},
		codexLaunchState: codexLaunchState{
			mcpConfigs: map[string]types.NamedMCPConfig{
				"agentteam": {
					Name: "agentteam",
					URL:  server.URL,
				},
			},
			httpClient: server.Client(),
		},
	}

	resultB, rpcErrB := driver.executeServerRequestWithActivity(
		context.Background(),
		"",
		"mcpServer/tool/call",
		json.RawMessage(`{"threadId":"thread-b","callId":"call-b","server":"agentteam","tool":"message_history_read","arguments":{"marker":"b"}}`),
	)
	require.Nil(t, rpcErrB)
	require.NotNil(t, resultB)

	resultA, rpcErrA := driver.executeServerRequestWithActivity(
		context.Background(),
		"",
		"mcpServer/tool/call",
		json.RawMessage(`{"threadId":"thread-a","callId":"call-a","server":"agentteam","tool":"message_history_read","arguments":{"marker":"a"}}`),
	)
	require.Nil(t, rpcErrA)
	require.NotNil(t, resultA)

	lineB := <-driver.translatedLines
	eventsB := driver.ParseLine(lineB)
	require.Len(t, eventsB, 1)
	require.Equal(t, consts.ParsedEventKindToolOutput, eventsB[0].Kind)
	require.Equal(t, "mcp__agentteam__message_history_read", eventsB[0].ToolName)
	require.Equal(t, "call-b", eventsB[0].ToolCallID)
	require.Equal(t, "thread-b", eventsB[0].SessionID)
	require.Contains(t, eventsB[0].Text, "result b")

	lineA := <-driver.translatedLines
	eventsA := driver.ParseLine(lineA)
	require.Len(t, eventsA, 1)
	require.Equal(t, consts.ParsedEventKindToolOutput, eventsA[0].Kind)
	require.Equal(t, "mcp__agentteam__message_history_read", eventsA[0].ToolName)
	require.Equal(t, "call-a", eventsA[0].ToolCallID)
	require.Equal(t, "thread-a", eventsA[0].SessionID)
	require.Contains(t, eventsA[0].Text, "result a")
}

func TestCodexAppServerDynamicToolCallUsesToolActivityRef(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()
		body, err := io.ReadAll(r.Body)
		require.NoError(t, err)
		var request struct {
			Params struct {
				Name string `json:"name"`
			} `json:"params"`
		}
		require.NoError(t, json.Unmarshal(body, &request))
		require.Equal(t, "message_history_read", request.Params.Name)

		w.Header().Set("Content-Type", "application/json")
		_, err = io.WriteString(w, `{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"Dynamic no messages."}]}}`)
		require.NoError(t, err)
	}))
	defer server.Close()

	driver := &CodexAppServerDriver{
		codexRPCState: codexRPCState{
			translatedLines: make(chan string, 2)},
		codexLaunchState: codexLaunchState{
			mcpConfigs: map[string]types.NamedMCPConfig{
				"agentteam": {
					Name: "agentteam",
					URL:  server.URL,
				},
			},
			httpClient: server.Client(),
		},
	}

	driver.handleItemNotification(json.RawMessage(`{
		"threadId":"thread-current",
		"turnId":"turn-1",
		"item":{
			"id":"call-dynamic",
			"type":"dynamicToolCall",
			"tool":"mcp_agentteam_message_history_read",
			"arguments":{}
		}
	}`), false)
	line := <-driver.translatedLines
	startEvents := driver.ParseLine(line)
	require.Len(t, startEvents, 1)
	require.Equal(t, consts.ParsedEventKindToolCall, startEvents[0].Kind)
	require.Equal(t, "mcp_agentteam_message_history_read", startEvents[0].ToolName)
	require.Equal(t, "call-dynamic", startEvents[0].ToolCallID)
	require.Equal(t, "thread-current", startEvents[0].SessionID)
	require.Empty(t, startEvents[0].ToolInput)

	result, rpcErr := driver.executeServerRequestWithActivity(
		context.Background(),
		"",
		"item/tool/call",
		json.RawMessage(`{"callId":"call-dynamic","tool":"mcp_agentteam_message_history_read","arguments":{}}`),
	)

	require.Nil(t, rpcErr)
	require.NotNil(t, result)
	select {
	case line := <-driver.translatedLines:
		events := driver.ParseLine(line)
		require.Len(t, events, 1)
		require.Equal(t, consts.ParsedEventKindToolOutput, events[0].Kind)
		require.Equal(t, "mcp_agentteam_message_history_read", events[0].ToolName)
		require.Equal(t, "call-dynamic", events[0].ToolCallID)
		require.Equal(t, "thread-current", events[0].SessionID)
		require.Contains(t, events[0].Text, "Dynamic no messages.")
	case <-time.After(time.Second):
		t.Fatal("timed out waiting for translated dynamic tool output")
	}
}

func TestCodexAppServerDynamicToolCallRequiresThreadIDForOutputActivity(t *testing.T) {
	driver := &CodexAppServerDriver{
		codexRPCState: codexRPCState{
			translatedLines: make(chan string, 1)}}

	result, rpcErr := driver.executeServerRequest(
		context.Background(),
		"item/tool/call",
		json.RawMessage(`{"turnId":"turn-1","callId":"call-1","tool":"mcp_agentteam_message_history_read","arguments":{}}`),
	)

	require.Nil(t, result)
	require.NotNil(t, rpcErr)
	require.Equal(t, -32602, rpcErr.Code)
	require.Contains(t, rpcErr.Message, "requires threadId for toolCallId")
	select {
	case line := <-driver.translatedLines:
		t.Fatalf("unexpected translated dynamic tool output without threadId: %s", line)
	default:
	}
}

func TestCodexAppServerToolStartOmitsEmptyObjectArguments(t *testing.T) {
	driver := &CodexAppServerDriver{
		codexRPCState: codexRPCState{
			translatedLines: make(chan string, 1)}}

	driver.handleItemNotification(json.RawMessage(`{
		"threadId":"thread-current",
		"turnId":"turn-1",
		"item":{
			"id":"item-1",
			"type":"mcpToolCall",
			"server":"agentteam",
			"tool":"message_history_read",
			"arguments":{}
		}
	}`), false)

	select {
	case line := <-driver.translatedLines:
		events := driver.ParseLine(line)
		require.Len(t, events, 1)
		require.Equal(t, consts.ParsedEventKindToolCall, events[0].Kind)
		require.Equal(t, "mcp__agentteam__message_history_read", events[0].ToolName)
		require.Equal(t, "item-1", events[0].ToolCallID)
		require.Equal(t, "thread-current", events[0].SessionID)
		require.Empty(t, events[0].ToolInput)
	case <-time.After(time.Second):
		t.Fatal("timed out waiting for translated tool call")
	}
}

func TestCodexAppServerReadAgentTeamCancelsElicitationRequests(t *testing.T) {
	rawWriter := &rawCaptureWriteCloser{}
	driver := &CodexAppServerDriver{
		codexRPCState: codexRPCState{
			rawStdin: rawWriter,
			pending:  make(map[int]pendingAppServerRequest),
		},
	}

	runReadAgentTeamWithSingleLine(driver, `{"jsonrpc":"2.0","id":"req-elicit","method":"mcpServer/elicitation/request","params":{"threadId":"thread-1","serverName":"agentteam","message":"Need input","mode":"form","requestedSchema":{"type":"object"}}}`)

	require.Eventually(t, func() bool { return len(rawWriter.Writes()) == 1 }, time.Second, 10*time.Millisecond)
	response := decodeRPCResponse(t, rawWriter.Writes()[0])
	require.Equal(t, `"req-elicit"`, string(response.ID))
	require.Nil(t, response.Error)
	require.JSONEq(t, `{"action":"cancel"}`, string(response.Result))
}

func TestCodexAppServerReadAgentTeamAutoAcceptsMCPToolApprovalElicitations(t *testing.T) {
	rawWriter := &rawCaptureWriteCloser{}
	driver := &CodexAppServerDriver{
		codexRPCState: codexRPCState{
			rawStdin: rawWriter,
			pending:  make(map[int]pendingAppServerRequest),
		},
	}

	runReadAgentTeamWithSingleLine(driver, `{"jsonrpc":"2.0","id":"req-approve","method":"mcpServer/elicitation/request","params":{"threadId":"thread-1","serverName":"agentteam","message":"Approve app tool call?","mode":"form","requestedSchema":{"type":"object","properties":{"approval_mode":{"type":"string","oneOf":[{"const":"approve","title":"Run the tool and continue."},{"const":"cancel","title":"Cancel this tool call."}]},"persist":{"type":"string","oneOf":[{"const":"session","title":"Allow for this session"},{"const":"always","title":"Allow and don't ask me again"}]},"codex_approval_kind":{"type":"string","oneOf":[{"const":"mcp_tool_call","title":"MCP tool call approval"}]}},"required":["approval_mode","codex_approval_kind"]}}}`)

	require.Eventually(t, func() bool { return len(rawWriter.Writes()) == 1 }, time.Second, 10*time.Millisecond)
	response := decodeRPCResponse(t, rawWriter.Writes()[0])
	require.Equal(t, `"req-approve"`, string(response.ID))
	require.Nil(t, response.Error)
	require.JSONEq(t, `{"action":"accept","content":{"approval_mode":"approve","codex_approval_kind":"mcp_tool_call"}}`, string(response.Result))
}

func TestCodexAppServerReadAgentTeamAutoAcceptsMCPToolApprovalElicitationsWithoutFormFields(t *testing.T) {
	rawWriter := &rawCaptureWriteCloser{}
	driver := &CodexAppServerDriver{
		codexRPCState: codexRPCState{
			rawStdin: rawWriter,
			pending:  make(map[int]pendingAppServerRequest),
		},
	}

	runReadAgentTeamWithSingleLine(driver, `{"jsonrpc":"2.0","id":"req-approve-empty","method":"mcpServer/elicitation/request","params":{"threadId":"thread-1","turnId":"turn-1","serverName":"agentteam","message":"Allow the agentteam MCP server to run tool \"message_send\"?","mode":"form","_meta":{"codex_approval_kind":"mcp_tool_call","persist":["session","always"],"tool_params":{"target":"dm:@pkg5vcc","content":"Hi"}},"requestedSchema":{"type":"object","properties":{}}}}`)

	require.Eventually(t, func() bool { return len(rawWriter.Writes()) == 1 }, time.Second, 10*time.Millisecond)
	response := decodeRPCResponse(t, rawWriter.Writes()[0])
	require.Equal(t, `"req-approve-empty"`, string(response.ID))
	require.Nil(t, response.Error)
	require.JSONEq(t, `{"action":"accept"}`, string(response.Result))
}

func TestCodexAppServerReadAgentTeamAutoGrantsPermissionsApprovalRequests(t *testing.T) {
	rawWriter := &rawCaptureWriteCloser{}
	driver := &CodexAppServerDriver{
		codexRPCState: codexRPCState{
			rawStdin: rawWriter,
			pending:  make(map[int]pendingAppServerRequest),
		},
	}

	runReadAgentTeamWithSingleLine(driver, `{"jsonrpc":"2.0","id":"req-permissions","method":"item/permissions/requestApproval","params":{"threadId":"thread-1","turnId":"turn-1","itemId":"item-1","reason":"Allow MCP write tool","permissions":{"network":{"enabled":true},"fileSystem":{"read":["/tmp/input"],"write":["/tmp/output"]}}}}`)

	require.Eventually(t, func() bool { return len(rawWriter.Writes()) == 1 }, time.Second, 10*time.Millisecond)
	response := decodeRPCResponse(t, rawWriter.Writes()[0])
	require.Equal(t, `"req-permissions"`, string(response.ID))
	require.Nil(t, response.Error)
	require.JSONEq(t, `{"permissions":{"network":{"enabled":true},"fileSystem":{"read":["/tmp/input"],"write":["/tmp/output"]}},"scope":"turn"}`, string(response.Result))
}

func TestCodexAppServerReadLoopLegacyWorkspacePermissionModeDefaultsFullAccess(t *testing.T) {
	rawWriter := &rawCaptureWriteCloser{}
	driver := &CodexAppServerDriver{
		codexRPCState: codexRPCState{
			rawStdin: rawWriter,
			pending:  make(map[int]pendingAppServerRequest),
		},
		codexLaunchState: codexLaunchState{
			launchPermissionMode: types.AgentPermissionModeWorkspaceWrite,
			launchWorkDir:        "/tmp/agent"},
	}

	runReadAgentTeamWithSingleLine(driver, `{"jsonrpc":"2.0","id":"req-permissions","method":"item/permissions/requestApproval","params":{"threadId":"thread-1","turnId":"turn-1","itemId":"item-1","reason":"Allow outside write","permissions":{"network":{"enabled":true},"fileSystem":{"write":["/tmp/outside"]}}}}`)

	require.Eventually(t, func() bool { return len(rawWriter.Writes()) == 1 }, time.Second, 10*time.Millisecond)
	response := decodeRPCResponse(t, rawWriter.Writes()[0])
	require.Equal(t, `"req-permissions"`, string(response.ID))
	require.Nil(t, response.Error)
	require.JSONEq(t, `{"permissions":{"network":{"enabled":true},"fileSystem":{"write":["/tmp/outside"]}},"scope":"turn"}`, string(response.Result))
}

func TestCodexAppServerResolvedServerRequestCancelsInFlightToolCall(t *testing.T) {
	requestStarted := make(chan struct{}, 1)
	requestCanceled := make(chan struct{}, 1)
	requestHadDeadline := make(chan bool, 1)
	transport := roundTripFunc(func(req *http.Request) (*http.Response, error) {
		_, hasDeadline := req.Context().Deadline()
		select {
		case requestHadDeadline <- hasDeadline:
		default:
		}
		select {
		case requestStarted <- struct{}{}:
		default:
		}
		<-req.Context().Done()
		select {
		case requestCanceled <- struct{}{}:
		default:
		}
		return nil, req.Context().Err()
	})

	rawWriter := &rawCaptureWriteCloser{}
	driver := &CodexAppServerDriver{
		codexRPCState: codexRPCState{
			rawStdin: rawWriter,
			pending:  make(map[int]pendingAppServerRequest),
		},
		codexLaunchState: codexLaunchState{
			mcpConfigs: map[string]types.NamedMCPConfig{
				"agentteam": {
					Name:    "agentteam",
					URL:     "http://chat.example.local",
					Headers: map[string]string{"Authorization": "Bearer canceled-token"},
				},
			},
			httpClient: &http.Client{Transport: transport}},
	}

	driver.handleItemNotification(json.RawMessage(`{
		"threadId":"thread-1",
		"turnId":"turn-1",
		"item":{
			"id":"req-2",
			"type":"mcpToolCall",
			"server":"agentteam",
			"tool":"message_send",
			"arguments":{"target":"#agentteam:deadbeef","content":"hello"}
		}
	}`), false)
	runReadAgentTeamWithSingleLine(driver, `{"jsonrpc":"2.0","id":"req-2","method":"mcpServer/tool/call","params":{"threadId":"thread-1","server":"agentteam","tool":"message_send","arguments":{"target":"#agentteam:deadbeef","content":"hello"}}}`)

	require.Eventually(t, func() bool {
		select {
		case <-requestStarted:
			return true
		default:
			return false
		}
	}, time.Second, 10*time.Millisecond)
	require.True(t, <-requestHadDeadline)
	runReadAgentTeamWithSingleLine(driver, `{"jsonrpc":"2.0","method":"serverRequest/resolved","params":{"threadId":"thread-1","requestId":"req-2"}}`)
	require.Eventually(t, func() bool {
		select {
		case <-requestCanceled:
			return true
		default:
			return false
		}
	}, time.Second, 10*time.Millisecond)
	require.Eventually(t, func() bool {
		driver.mu.Lock()
		defer driver.mu.Unlock()
		return len(driver.serverRequests) == 0
	}, time.Second, 10*time.Millisecond)
	require.Empty(t, rawWriter.Writes())
}

func TestCodexAppServerWriteServerRequestFailureLogLevel(t *testing.T) {
	tests := []struct {
		name     string
		err      error
		wantWarn bool
	}{
		{
			name:     "closed pipe is debug",
			err:      fmt.Errorf("write codex-app-server result: %w", io.ErrClosedPipe),
			wantWarn: false,
		},
		{
			name:     "other write failure is warn",
			err:      fmt.Errorf("write codex-app-server result: %w", io.ErrUnexpectedEOF),
			wantWarn: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var logs bytes.Buffer
			previousLogger := slog.Default()
			slog.SetDefault(slog.New(slog.NewTextHandler(&logs, &slog.HandlerOptions{
				Level: slog.LevelWarn,
			})))
			t.Cleanup(func() {
				slog.SetDefault(previousLogger)
			})

			logCodexAppServerWriteServerRequestFailure(
				context.Background(),
				"codex-app-server write server request result failed",
				"item/tool/call",
				json.RawMessage(`"req-log-level"`),
				tt.err,
			)

			if tt.wantWarn {
				require.Contains(t, logs.String(), "codex-app-server write server request result failed")
				require.Contains(t, logs.String(), tt.err.Error())
			} else {
				require.Empty(t, logs.String())
			}
		})
	}
}

type roundTripFunc func(*http.Request) (*http.Response, error)

func (fn roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
	return fn(req)
}

func TestCodexAppServerUsageNotificationUsesEffectiveThreadModel(t *testing.T) {
	driver, _ := newTestCodexDriver(t, func(req capturedRPCRequest) appServerEnvelope {
		require.Equal(t, "thread/start", req.Method)
		return appServerEnvelope{
			Result: rawRPCResult(t, appServerThreadResult{
				Thread:        appServerThread{ID: "thread-usage", Status: appServerThreadStatus{Type: "idle"}},
				Model:         "gpt-5.2",
				ModelProvider: "openai",
			}),
		}
	})
	_, err := driver.startThread(context.Background(), drivercontract.LaunchConfig{WorkDir: "/tmp/agent"})
	require.NoError(t, err)

	driver.translatedLines = make(chan string, 1)
	contextWindow := int64(258_400)
	driver.handleNotification(appServerEnvelope{
		Method: "thread/tokenUsage/updated",
		Params: rawRPCResult(t, appServerThreadTokenUsageNotification{
			ThreadID: "thread-usage",
			TurnID:   "turn-1",
			TokenUsage: appServerThreadTokenUsage{
				ModelContextWindow: &contextWindow,
				Last: appServerTokenUsageBreakdown{
					InputTokens:       12,
					CachedInputTokens: 3,
					OutputTokens:      7,
					TotalTokens:       19,
				},
				Total: appServerTokenUsageBreakdown{
					InputTokens:       12,
					CachedInputTokens: 3,
					OutputTokens:      7,
					TotalTokens:       19,
				},
			},
		}),
	})

	line := <-driver.translatedLines
	events := driver.ParseLine(line)
	require.Len(t, events, 1)
	require.Equal(t, "thread-usage", events[0].SessionID)
	require.Equal(t, "gpt-5.2", events[0].Model)
	require.Equal(t, uint64(12), events[0].InputTokens)
	require.Equal(t, uint64(3), events[0].CachedInputTokens)
	require.Equal(t, uint64(7), events[0].OutputTokens)
	require.Equal(t, uint64(19), events[0].ActiveContextTokens)
	require.Equal(t, uint64(258_400), events[0].ContextWindowTokens)
}

func TestCodexAppServerUsageNotificationDeduplicatesIdenticalSnapshot(t *testing.T) {
	driver, _ := newTestCodexDriver(t, nil)
	driver.translatedLines = make(chan string, 2)
	driver.updateEffectiveModel("gpt-5.2")

	notification := appServerEnvelope{
		Method: "thread/tokenUsage/updated",
		Params: rawRPCResult(t, appServerThreadTokenUsageNotification{
			ThreadID: "thread-usage",
			TurnID:   "turn-1",
			TokenUsage: appServerThreadTokenUsage{
				Last: appServerTokenUsageBreakdown{
					InputTokens:  12,
					OutputTokens: 7,
					TotalTokens:  19,
				},
				Total: appServerTokenUsageBreakdown{
					InputTokens:  12,
					OutputTokens: 7,
					TotalTokens:  19,
				},
			},
		}),
	}
	driver.handleNotification(notification)
	driver.handleNotification(notification)

	require.Len(t, driver.translatedLines, 1)
}

func TestCodexAppServerUsageNotificationEmitsSameTurnWhenTotalAdvances(t *testing.T) {
	driver, _ := newTestCodexDriver(t, nil)
	driver.translatedLines = make(chan string, 2)
	driver.updateEffectiveModel("gpt-5.2")

	driver.handleNotification(appServerEnvelope{
		Method: "thread/tokenUsage/updated",
		Params: rawRPCResult(t, appServerThreadTokenUsageNotification{
			ThreadID: "thread-usage",
			TurnID:   "turn-1",
			TokenUsage: appServerThreadTokenUsage{
				Last: appServerTokenUsageBreakdown{
					InputTokens:  12,
					OutputTokens: 7,
					TotalTokens:  19,
				},
				Total: appServerTokenUsageBreakdown{
					InputTokens:  12,
					OutputTokens: 7,
					TotalTokens:  19,
				},
			},
		}),
	})
	driver.handleNotification(appServerEnvelope{
		Method: "thread/tokenUsage/updated",
		Params: rawRPCResult(t, appServerThreadTokenUsageNotification{
			ThreadID: "thread-usage",
			TurnID:   "turn-1",
			TokenUsage: appServerThreadTokenUsage{
				Last: appServerTokenUsageBreakdown{
					InputTokens:  5,
					OutputTokens: 3,
					TotalTokens:  8,
				},
				Total: appServerTokenUsageBreakdown{
					InputTokens:  17,
					OutputTokens: 10,
					TotalTokens:  27,
				},
			},
		}),
	})

	require.Len(t, driver.translatedLines, 2)
}

func TestCodexAppServerArgsSupportHTTPAndStdioMCPServers(t *testing.T) {
	args, err := codexAppServerArgs(drivercontract.LaunchConfig{MCPs: []types.NamedMCPConfig{
		{Name: "zap_http", Transport: types.MCPTransportHTTP, URL: "https://mcp.example.com", Headers: map[string]string{"Authorization": "Bearer token", "X-API-Key": "secret"}},
		{Name: "zap_stdio", Transport: types.MCPTransportStdio, Command: "/bin/zap", Args: []string{"--flag", "value"}, Env: map[string]string{"API_KEY": "secret"}, CWD: "/tmp"},
	}})
	require.NoError(t, err)

	require.Contains(t, args, `mcp_servers.zap_http.url="https://mcp.example.com"`)
	require.Contains(t, args, `mcp_servers.zap_http.bearer_token_env_var="AGENTTEAM_MCP_AUTH_TOKEN_ZAP_HTTP"`)
	require.Contains(t, args, `mcp_servers.zap_http.env_http_headers={"X-API-Key"="AGENTTEAM_MCP_HTTP_HEADER_ZAP_HTTP_X_API_KEY"}`)
	require.Contains(t, args, `mcp_servers.zap_stdio.command="/bin/zap"`)
	require.Contains(t, args, `mcp_servers.zap_stdio.args=["--flag", "value"]`)
	require.Contains(t, args, `mcp_servers.zap_stdio.cwd="/tmp"`)
	require.Contains(t, args, `mcp_servers.zap_stdio.env."API_KEY"="${AGENTTEAM_MCP_STDIO_ENV_ZAP_STDIO_API_KEY}"`)
	joined := strings.Join(args, "\n")
	require.NotContains(t, joined, "mcp_servers.zap_stdio.env.\"API_KEY\"=\"secret\"")
}

func TestAddCodexMCPEnvUsesBearerHeaderAndStdioEnvVars(t *testing.T) {
	env := addCodexMCPEnv(nil, []types.NamedMCPConfig{
		{
			Name:      "zap_http",
			Transport: types.MCPTransportHTTP,
			URL:       "https://mcp.example.com",
			Headers: map[string]string{
				" Authorization ": "Bearer token",
				" X-API-Key ":     " secret ",
			},
		},
		{
			Name:      "zap_stdio",
			Transport: types.MCPTransportStdio,
			Command:   "/bin/zap",
			Env:       map[string]string{"API_KEY": "stdio-secret"},
		},
	})

	require.Equal(t, "token", env["AGENTTEAM_MCP_AUTH_TOKEN_ZAP_HTTP"])
	require.Equal(t, "secret", env["AGENTTEAM_MCP_HTTP_HEADER_ZAP_HTTP_X_API_KEY"])
	require.Equal(t, "stdio-secret", env["AGENTTEAM_MCP_STDIO_ENV_ZAP_STDIO_API_KEY"])
}
