package agent

import (
	"bufio"
	"context"
	"encoding/json"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"

	"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"
)

func TestClaudeInterruptActiveRunWritesControlRequestAndWaitsForAck(t *testing.T) {
	reader, writer := io.Pipe()
	t.Cleanup(func() { _ = reader.Close() })
	driver := &ClaudeDriver{stdinPipe: writer}

	done := make(chan error, 1)
	go func() {
		done <- driver.InterruptActiveRun(context.Background(), "claude-session")
	}()

	line, err := bufio.NewReader(reader).ReadString('\n')
	require.NoError(t, err)
	var request struct {
		Type      string `json:"type"`
		RequestID string `json:"request_id"`
		Request   struct {
			Subtype string `json:"subtype"`
		} `json:"request"`
	}
	require.NoError(t, json.Unmarshal([]byte(line), &request))
	require.Equal(t, "control_request", request.Type)
	require.NotEmpty(t, request.RequestID)
	require.Equal(t, "interrupt", request.Request.Subtype)

	response, err := json.Marshal(map[string]any{
		"type": "control_response",
		"response": map[string]any{
			"subtype":    "success",
			"request_id": request.RequestID,
		},
	})
	require.NoError(t, err)
	driver.ParseLine(string(response))
	require.NoError(t, <-done)
}

func TestClaudeStartIdleCompactionWritesCompactSlashCommand(t *testing.T) {
	stdin := &recordingWriteCloser{}
	driver := &ClaudeDriver{
		stdinPipe:     stdin,
		compactQueued: true,
	}

	started, err := driver.StartIdleCompaction(context.Background(), "claude-session")
	require.NoError(t, err)
	require.True(t, started)

	var payload struct {
		Type      string `json:"type"`
		SessionID string `json:"session_id"`
		Message   struct {
			Content []struct {
				Type string `json:"type"`
				Text string `json:"text"`
			} `json:"content"`
		} `json:"message"`
	}
	require.NoError(t, json.Unmarshal([]byte(strings.TrimSpace(stdin.String())), &payload))
	require.Equal(t, "user", payload.Type)
	require.Equal(t, "claude-session", payload.SessionID)
	require.Equal(t, "/compact", payload.Message.Content[0].Text)
	driver.mu.Lock()
	require.False(t, driver.compactQueued)
	require.True(t, driver.compactionInProgress)
	driver.mu.Unlock()
}

func TestClaudeCompactBoundaryParsesAsRuntimeCompacting(t *testing.T) {
	driver := &ClaudeDriver{}

	events := driver.ParseLine(`{"type":"system","subtype":"compact_boundary","session_id":"sess-compact"}`)

	require.Equal(t, []drivercontract.ParsedEvent{{
		Kind:      consts.ParsedEventKindRuntimeCompacting,
		SessionID: "sess-compact",
	}}, events)
}

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

	text, err := buildClaudeAppendSystemPrompt(testLaunchConfig(drivercontract.LaunchConfig{}, testLaunchSurfaces{BaseInstructions: "base instructions", DeveloperInstructions: "developer instructions"}))
	require.NoError(t, err)
	require.Equal(t, "base instructions\n\ndeveloper instructions", text)

	text, err = buildClaudeAppendSystemPrompt(drivercontract.LaunchConfig{})
	require.NoError(t, err)
	require.Empty(t, text)
}

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

	_, err := buildClaudeAppendSystemPrompt(testLaunchConfig(drivercontract.LaunchConfig{}, testLaunchSurfaces{BaseInstructions: strings.Repeat("a", claudeAppendSystemPromptMaxBytes+1)}))
	require.Error(t, err)
	require.Contains(t, err.Error(), "appended system prompt too large")
}

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

	args := []string{
		"--output-format", "stream-json",
		"--append-system-prompt", strings.Repeat("a", claudeWindowsCommandLineMaxChars),
	}
	err := validateClaudeLaunchCommandLine(`C:\Users\alice\AppData\Roaming\npm\claude.cmd`, args, "windows")

	require.Error(t, err)
	require.Contains(t, err.Error(), "AgentTeam generated Claude startup arguments exceed the Windows process launch limit")
	require.Contains(t, err.Error(), "clicking Start")
	require.Contains(t, err.Error(), "not a user shell command")
	require.Contains(t, err.Error(), "--append-system-prompt-file")
}

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

	args := []string{"--append-system-prompt", strings.Repeat("a", claudeWindowsCommandLineMaxChars)}
	require.NoError(t, validateClaudeLaunchCommandLine("/usr/local/bin/claude", args, "linux"))
}

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

	args := []string{"--append-system-prompt", strings.Repeat("a", claudeWindowsCommandLineMaxChars)}
	require.NoError(t, validateClaudeLaunchCommandLine(`C:\tools\claude.exe`, args, "windows"))
}

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

	require.Equal(t, "resume prompt", resolveClaudeInitialPrompt(testLaunchConfig(drivercontract.LaunchConfig{SessionID: "session-1"}, testLaunchSurfaces{Prompt: "legacy prompt", FreshPrompt: "fresh prompt", ResumePrompt: "resume prompt"})))
	require.Equal(t, "fresh prompt", resolveClaudeInitialPrompt(testLaunchConfig(drivercontract.LaunchConfig{}, testLaunchSurfaces{Prompt: "legacy prompt", FreshPrompt: "fresh prompt"})))
	require.Equal(t, "legacy prompt", resolveClaudeInitialPrompt(testLaunchConfig(drivercontract.LaunchConfig{}, testLaunchSurfaces{Prompt: "legacy prompt"})))
}

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

	driver := &ClaudeDriver{}
	tests := []struct {
		name string
		line string
		want []drivercontract.ParsedEvent
	}{
		{
			name: "ignores non json",
			line: "debug log",
		},
		{
			name: "ignores invalid json",
			line: "{",
		},
		{
			name: "ignores non init system event",
			line: `{"type":"system","subtype":"ready","session_id":"sess-1"}`,
		},
		{
			name: "system init",
			line: `{"type":"system","subtype":"init","session_id":"sess-1"}`,
			want: []drivercontract.ParsedEvent{{
				Kind:      consts.ParsedEventKindSessionInit,
				SessionID: "sess-1",
			}},
		},
		{
			name: "assistant content blocks",
			line: `{"type":"assistant","message":{"content":[{"type":"thinking","thinking":"plan"},{"type":"text","text":"hello"},{"type":"tool_use","name":"Read","input":{"file_path":"README.md"}}]}}`,
			want: []drivercontract.ParsedEvent{
				{
					Kind: consts.ParsedEventKindThinking,
					Text: "plan",
				},
				{
					Kind: consts.ParsedEventKindText,
					Text: "hello",
				},
				{
					Kind:      consts.ParsedEventKindToolCall,
					ToolName:  "Read",
					ToolInput: `{"file_path":"README.md"}`,
				},
			},
		},
		{
			name: "user replay correlates runtime input acknowledgement",
			line: `{"type":"user","session_id":"sess-1","uuid":"msg-ack","isReplay":true,"parent_tool_use_id":null,"message":{"role":"user","content":[{"type":"text","text":"hello"}]}}`,
			want: []drivercontract.ParsedEvent{{
				Kind:           consts.ParsedEventKindRuntimeInputAcked,
				RuntimeInputID: "msg-ack",
				SessionID:      "sess-1",
			}},
		},
		{
			name: "user replay without correlation id ignored",
			line: `{"type":"user","session_id":"sess-1","isReplay":true,"message":{"role":"user","content":"hello"}}`,
		},
		{
			name: "user replay with blank correlation id ignored",
			line: `{"type":"user","session_id":"sess-1","uuid":"  ","isReplay":true,"message":{"role":"user","content":"hello"}}`,
		},
		{
			name: "non replay user event ignored",
			line: `{"type":"user","session_id":"sess-1","uuid":"msg-noack","message":{"role":"user","content":"hello"}}`,
		},
		{
			name: "result usage and turn end",
			line: `{"type":"result","subtype":"success","session_id":"sess-1","message":{"stop_reason":"end_turn"},"usage":{"input_tokens":1000,"cache_read_input_tokens":400,"cache_creation_input_tokens":50,"output_tokens":300},"modelUsage":{"anthropic/claude-sonnet-4":{"inputTokens":1000,"cacheReadInputTokens":400,"cacheCreationInputTokens":50,"outputTokens":300}}}`,
			want: []drivercontract.ParsedEvent{
				{
					Kind:              consts.ParsedEventKindUsage,
					Model:             "anthropic/claude-sonnet-4",
					InputTokens:       1450,
					CachedInputTokens: 400,
					OutputTokens:      300,
				},
				{
					Kind:       consts.ParsedEventKindTurnEnd,
					SessionID:  "sess-1",
					StopReason: consts.ParsedEventStopReasonEndTurn,
				},
			},
		},
		{
			// Top-level usage is a single request: input must fold in cache_creation and
			// cache_read so total = input + output matches the provider total, and cached
			// is a subset of input. Here input = 200 + 30 + 70 = 300.
			name: "result usage folds cache creation and read into input",
			line: `{"type":"result","subtype":"success","session_id":"sess-1","message":{"stop_reason":"end_turn"},"usage":{"input_tokens":200,"cache_read_input_tokens":70,"cache_creation_input_tokens":30,"output_tokens":120},"modelUsage":{"anthropic/claude-haiku-4":{"inputTokens":200,"cacheReadInputTokens":70,"cacheCreationInputTokens":30,"outputTokens":120}}}`,
			want: []drivercontract.ParsedEvent{
				{
					Kind:              consts.ParsedEventKindUsage,
					Model:             "anthropic/claude-haiku-4",
					InputTokens:       300,
					CachedInputTokens: 70,
					OutputTokens:      120,
				},
				{
					Kind:       consts.ParsedEventKindTurnEnd,
					SessionID:  "sess-1",
					StopReason: consts.ParsedEventStopReasonEndTurn,
				},
			},
		},
		{
			// When modelUsage lists several models, do not mis-attribute the whole
			// request to an arbitrary model; leave model empty for the fallback.
			name: "result usage with multiple models leaves model unknown",
			line: `{"type":"result","subtype":"success","session_id":"sess-1","message":{"stop_reason":"end_turn"},"usage":{"input_tokens":500,"cache_read_input_tokens":0,"cache_creation_input_tokens":0,"output_tokens":80},"modelUsage":{"anthropic/claude-sonnet-4":{"inputTokens":400,"outputTokens":60},"anthropic/claude-haiku-4":{"inputTokens":100,"outputTokens":20}}}`,
			want: []drivercontract.ParsedEvent{
				{
					Kind:              consts.ParsedEventKindUsage,
					Model:             "",
					InputTokens:       500,
					CachedInputTokens: 0,
					OutputTokens:      80,
				},
				{
					Kind:       consts.ParsedEventKindTurnEnd,
					SessionID:  "sess-1",
					StopReason: consts.ParsedEventStopReasonEndTurn,
				},
			},
		},
		{
			name: "normal result text does not report an error",
			line: `{"type":"result","subtype":"success","session_id":"sess-result-text","result":"completed normally"}`,
			want: []drivercontract.ParsedEvent{{
				Kind:      consts.ParsedEventKindTurnEnd,
				SessionID: "sess-result-text",
			}},
		},
		{
			name: "abnormal result reports error before turn end",
			line: `{"type":"result","subtype":"error_max_turns","is_error":true,"session_id":"sess-2","message":{"stop_reason":"max_tokens"},"errors":["too many turns"]}`,
			want: []drivercontract.ParsedEvent{
				{
					Kind:       consts.ParsedEventKindError,
					Message:    "Claude could not complete the request: maximum turn limit reached; too many turns",
					SessionID:  "sess-2",
					StopReason: consts.ParsedEventStopReasonMaxTokens,
				},
				{
					Kind:       consts.ParsedEventKindTurnEnd,
					SessionID:  "sess-2",
					StopReason: consts.ParsedEventStopReasonMaxTokens,
					Metadata:   map[string]any{"claude_result_abnormal": true},
				},
			},
		},
		{
			name: "non string error result retains generic fallback",
			line: `{"type":"result","subtype":"success","is_error":true,"session_id":"sess-result-object","result":{"error":"context payload unavailable"}}`,
			want: []drivercontract.ParsedEvent{
				{
					Kind:      consts.ParsedEventKindError,
					Message:   "Claude could not complete the request",
					SessionID: "sess-result-object",
				},
				{
					Kind:      consts.ParsedEventKindTurnEnd,
					SessionID: "sess-result-object",
					Metadata:  map[string]any{"claude_result_abnormal": true},
				},
			},
		},
		{
			name: "success result with errors reports friendly error before turn end",
			line: `{"type":"result","subtype":"success","session_id":"sess-3","errors":["API Error: 502 {\"error\":\"send upstream request: EOF\"}"]}`,
			want: []drivercontract.ParsedEvent{
				{
					Kind:      consts.ParsedEventKindError,
					Message:   "Claude API request failed (HTTP 502): send upstream request: EOF",
					SessionID: "sess-3",
					Metadata: map[string]any{
						"http_status":                    502,
						"transient_runtime_error":        true,
						"transient_runtime_error_reason": "upstream_5xx",
					},
				},
				{
					Kind:      consts.ParsedEventKindTurnEnd,
					SessionID: "sess-3",
					Metadata:  map[string]any{"claude_result_abnormal": true},
				},
			},
		},
		{
			name: "error result detail reports context window limit before turn end",
			line: `{"type":"result","subtype":"success","is_error":true,"session_id":"sess-context-result","result":"API Error: The model has reached its context window limit."}`,
			want: []drivercontract.ParsedEvent{
				{
					Kind:      consts.ParsedEventKindError,
					Message:   "Claude API request failed: The model has reached its context window limit.",
					SessionID: "sess-context-result",
				},
				{
					Kind:      consts.ParsedEventKindTurnEnd,
					SessionID: "sess-context-result",
					Metadata:  map[string]any{"claude_result_abnormal": true},
				},
			},
		},
		{
			name: "result detail and errors are deduplicated",
			line: `{"type":"result","subtype":"success","is_error":true,"session_id":"sess-result-dedup","result":"API Error: The model has reached its context window limit.","errors":["The model has reached its context window limit"]}`,
			want: []drivercontract.ParsedEvent{
				{
					Kind:      consts.ParsedEventKindError,
					Message:   "Claude could not complete the request: The model has reached its context window limit",
					SessionID: "sess-result-dedup",
				},
				{
					Kind:      consts.ParsedEventKindTurnEnd,
					SessionID: "sess-result-dedup",
					Metadata:  map[string]any{"claude_result_abnormal": true},
				},
			},
		},
		{
			name: "overloaded 529 result is marked retryable",
			line: `{"type":"result","subtype":"success","session_id":"sess-529","errors":["API Error: 529 [1305][该模型当前访问量过大，请您稍后再试][trace-id]"]}`,
			want: []drivercontract.ParsedEvent{
				{
					Kind:      consts.ParsedEventKindError,
					Message:   "Claude API request failed (HTTP 529): [1305][该模型当前访问量过大，请您稍后再试][trace-id]",
					SessionID: "sess-529",
					Metadata: map[string]any{
						"http_status":                    529,
						"transient_runtime_error":        true,
						"transient_runtime_error_reason": "overloaded",
					},
				},
				{
					Kind:      consts.ParsedEventKindTurnEnd,
					SessionID: "sess-529",
					Metadata:  map[string]any{"claude_result_abnormal": true},
				},
			},
		},
		{
			name: "unknown event type",
			line: `{"type":"unknown"}`,
		},
	}

	for _, tc := range tests {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			require.Equal(t, tc.want, driver.ParseLine(tc.line))
		})
	}
}

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

	tests := []struct {
		name    string
		message string
		want    bool
	}{
		{
			name:    "provider reached context window limit",
			message: "Claude API request failed: The model has reached its context window limit.",
			want:    true,
		},
		{
			name:    "context window limit reached with normalized whitespace",
			message: "API Error: context   window\nlimit reached",
			want:    true,
		},
		{
			name:    "prompt too long",
			message: "Prompt is too long for this model",
			want:    true,
		},
		{
			name:    "rate limit is not context overflow",
			message: "API Error: rate limit reached",
		},
		{
			name:    "context limit configuration is not overflow",
			message: "Unable to read the configured context window limit",
		},
		{
			name:    "maximum turn limit is not context overflow",
			message: "Claude could not complete the request: maximum turn limit reached",
		},
		{
			name:    "output truncation is not context overflow",
			message: "Response truncated (finish_reason='length') - model hit max output tokens.",
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			require.Equal(t, tt.want, isClaudeContextError(tt.message))
		})
	}
}

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

	driver := &ClaudeDriver{}
	require.True(t, driver.IsOutputTruncated(drivercontract.ParsedEvent{
		Kind:       consts.ParsedEventKindTurnEnd,
		StopReason: consts.ParsedEventStopReasonMaxTokens,
	}))
	require.False(t, driver.IsOutputTruncated(drivercontract.ParsedEvent{
		Kind:       consts.ParsedEventKindTurnEnd,
		StopReason: consts.ParsedEventStopReasonMaxTokens,
		Metadata:   map[string]any{"claude_result_abnormal": true},
	}))
	require.True(t, driver.IsOutputTruncated(drivercontract.ParsedEvent{
		Kind:    consts.ParsedEventKindError,
		Message: "Response truncated (finish_reason='length') - model hit max output tokens.",
	}))
	require.False(t, driver.IsOutputTruncated(drivercontract.ParsedEvent{
		Kind:    consts.ParsedEventKindError,
		Message: "The model has reached its context window limit.",
	}))
	require.False(t, driver.IsOutputTruncated(drivercontract.ParsedEvent{
		Kind:    consts.ParsedEventKindError,
		Message: "Rate limit reached.",
	}))
}

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

	driver := &ClaudeDriver{}
	driver.ParseLine(`{"type":"user","session_id":"sess-1","uuid":"user-1","message":{"role":"user","content":[{"type":"text","text":"hello"}]}}`)
	driver.ParseLine(`{"type":"assistant","session_id":"sess-1","uuid":"assistant-1","message":{"content":[{"type":"text","text":"hi"}]}}`)
	driver.ParseLine(`{"type":"system","subtype":"hook_progress","session_id":"sess-1","uuid":"system-hook-1"}`)

	driver.mu.Lock()
	defer driver.mu.Unlock()
	require.Equal(t, "sess-1", driver.sessionID)
	require.Equal(t, "assistant-1", driver.lastTranscriptUUID)
}

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

	driver := &ClaudeDriver{}
	driver.rememberClaudeTranscriptBoundary("sess-1", "assistant-1")

	strategy, recoveryRef, supported, err := driver.PrepareRecoveryPointForMessage("agentteam-msg-1")

	require.NoError(t, err)
	require.True(t, supported)
	require.Equal(t, rewindcaps.StrategyClaudeSessionResumeAt, strategy)
	ref, err := rewindcaps.DecodeClaudeSessionResumeAtRecoveryRef(recoveryRef)
	require.NoError(t, err)
	require.Equal(t, "sess-1", ref.SessionID)
	require.Equal(t, "assistant-1", ref.BoundaryUUID)
	require.Equal(t, rewindcaps.RecoveryBoundaryResumeAt, ref.BoundaryKind)
}

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

	driver := &ClaudeDriver{}
	driver.ParseLine(`{"type":"user","session_id":"sess-1","uuid":"agentteam-msg-a","isReplay":true,"message":{"role":"user","content":[{"type":"text","text":"A"}]}}`)
	driver.ParseLine(`{"type":"assistant","session_id":"sess-1","uuid":"assistant-after-a","message":{"content":[{"type":"text","text":"A_DONE"}]}}`)
	events := driver.ParseLine(`{"type":"user","session_id":"sess-1","uuid":"agentteam-msg-b","isReplay":true,"message":{"role":"user","content":[{"type":"text","text":"B"}]}}`)

	require.Equal(t, []drivercontract.ParsedEvent{{
		Kind:           consts.ParsedEventKindRuntimeInputAcked,
		RuntimeInputID: "agentteam-msg-b",
		SessionID:      "sess-1",
	}}, events)
	driver.mu.Lock()
	require.Equal(t, "agentteam-msg-b", driver.lastTranscriptUUID)
	driver.mu.Unlock()

	strategy, recoveryRef, supported, err := driver.PrepareRecoveryPointForMessage("agentteam-msg-b")

	require.NoError(t, err)
	require.True(t, supported)
	require.Equal(t, rewindcaps.StrategyClaudeSessionResumeAt, strategy)
	ref, err := rewindcaps.DecodeClaudeSessionResumeAtRecoveryRef(recoveryRef)
	require.NoError(t, err)
	require.Equal(t, "sess-1", ref.SessionID)
	require.Equal(t, "assistant-after-a", ref.BoundaryUUID)
	require.Equal(t, rewindcaps.RecoveryBoundaryResumeAt, ref.BoundaryKind)
}

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

	driver := &ClaudeDriver{}
	driver.rememberSession("sess-1")

	strategy, recoveryRef, supported, err := driver.PrepareRecoveryPointForMessage("agentteam-msg-1")

	require.NoError(t, err)
	require.True(t, supported)
	require.Equal(t, rewindcaps.StrategyClaudeSessionResumeAt, strategy)
	ref, err := rewindcaps.DecodeClaudeSessionResumeAtRecoveryRef(recoveryRef)
	require.NoError(t, err)
	require.Equal(t, "sess-1", ref.SessionID)
	require.Empty(t, ref.BoundaryUUID)
	require.Equal(t, rewindcaps.RecoveryBoundaryBeforeFirstInput, ref.BoundaryKind)
}

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

	driver := &ClaudeDriver{}

	toolCall := driver.ParseLine(`{"type":"assistant","message":{"content":[{"type":"tool_use","id":"toolu_send","name":"mcp__agentteam__message_send","input":{"target":"#claude","content":"hi"}}]}}`)
	require.Equal(t, []drivercontract.ParsedEvent{{
		Kind:       consts.ParsedEventKindToolCall,
		ToolName:   "mcp__agentteam__message_send",
		ToolCallID: "toolu_send",
		ToolInput:  `{"target":"#claude","content":"hi"}`,
	}}, toolCall)

	toolResult := driver.ParseLine(`{"type":"user","session_id":"sess-1","parent_tool_use_id":"toolu_send","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_send","content":"Target context is outdated: new message acked below, retry message_send if needed."}]}}`)
	require.Equal(t, []drivercontract.ParsedEvent{{
		Kind:       consts.ParsedEventKindToolOutput,
		ToolName:   "mcp__agentteam__message_send",
		ToolCallID: "toolu_send",
		Text:       "Target context is outdated: new message acked below, retry message_send if needed.",
	}}, toolResult)
}

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

	driver := &ClaudeDriver{}
	toolResult := driver.ParseLine(`{"type":"user","session_id":"sess-1","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_1","content":[{"type":"text","text":"line 1"},{"type":"text","text":"line 2"}]}]}}`)

	require.Equal(t, []drivercontract.ParsedEvent{{
		Kind:       consts.ParsedEventKindToolOutput,
		ToolCallID: "toolu_1",
		Text:       "line 1\nline 2",
	}}, toolResult)
}

func TestClaudeDeliverAckedBusyRuntimeInputEncodesCorrelation(t *testing.T) {
	stdin := &recordingWriteCloser{}
	driver := &ClaudeDriver{stdinPipe: stdin}

	err := driver.DeliverAckedBusyRuntimeInput(context.Background(), testRuntimeInputWithConfig(drivercontract.RuntimeInput{ID: "msg-ack"}, "queued follow-up"), "sess-1")
	require.NoError(t, err)

	var msg map[string]any
	require.NoError(t, json.Unmarshal([]byte(strings.TrimSpace(stdin.String())), &msg))
	require.Equal(t, "user", msg["type"])
	require.Equal(t, "msg-ack", msg["uuid"])
	require.Equal(t, "next", msg["priority"])
	require.Equal(t, "sess-1", msg["session_id"])
	_, hasParent := msg["parent_tool_use_id"]
	require.True(t, hasParent)
	require.Nil(t, msg["parent_tool_use_id"])
	message, ok := msg["message"].(map[string]any)
	require.True(t, ok)
	content, ok := message["content"].([]any)
	require.True(t, ok)
	require.Len(t, content, 1)
	block, ok := content[0].(map[string]any)
	require.True(t, ok)
	require.Equal(t, "queued follow-up", block["text"])
}

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

	workDir := t.TempDir()
	argsPath := filepath.Join(workDir, "args.json")
	stdinPath := filepath.Join(workDir, "stdin.txt")
	binary := writeClaudeCaptureBinary(t, workDir)

	driver := &ClaudeDriver{}
	spawned, err := driver.Spawn(context.Background(), testLaunchConfig(drivercontract.LaunchConfig{AgentID: "agent-1", Binary: binary, WorkDir: workDir, Model: "claude-sonnet-4-5", InitialDeliveryMessageID: "agentteam-msg-1", Env: map[string]string{
		"CLAUDE_TEST_ARGS_FILE":  argsPath,
		"CLAUDE_TEST_STDIN_FILE": stdinPath,
	}}, testLaunchSurfaces{FreshPrompt: "fresh prompt", BaseInstructions: "base instructions", DeveloperInstructions: "developer instructions"}))
	require.NoError(t, err)
	if spawned.Cleanup != nil {
		defer spawned.Cleanup()
	}

	require.NoError(t, spawned.Stdin.Close())
	require.NoError(t, spawned.Cmd.Wait())

	args := readCapturedClaudeArgs(t, argsPath)
	require.Contains(t, args, "--allow-dangerously-skip-permissions")
	require.Contains(t, args, "--dangerously-skip-permissions")
	require.Contains(t, args, "--replay-user-messages")
	modelIndex := indexOfArg(t, args, "--model")
	require.Equal(t, "claude-sonnet-4-5", args[modelIndex+1])
	require.Contains(t, args, "--append-system-prompt")
	appendIndex := indexOfArg(t, args, "--append-system-prompt")
	require.Equal(t, "base instructions\n\ndeveloper instructions", args[appendIndex+1])
	require.NotContains(t, args, "--resume")
	sessionIndex := indexOfArg(t, args, "--session-id")
	require.NotEmpty(t, args[sessionIndex+1])

	msg := readCapturedClaudeStdinMessage(t, stdinPath)
	require.Equal(t, "user", msg["type"])
	message, ok := msg["message"].(map[string]any)
	require.True(t, ok)
	content, ok := message["content"].([]any)
	require.True(t, ok)
	require.Len(t, content, 1)
	block, ok := content[0].(map[string]any)
	require.True(t, ok)
	require.Equal(t, "fresh prompt", block["text"])
	_, hasSessionID := msg["session_id"]
	require.False(t, hasSessionID)
	require.Equal(t, "agentteam-msg-1", msg["uuid"])
}

// TestClaudeSpawn_NoPromptWritesZeroFrames verifies the authoritative no-prompt
// gate: a true no-text/no-attachment Claude launch writes zero stream-json
// frames to stdin, does not create an empty user turn, and leaves stdin open
// for the daemon's first durable handoff.
func TestClaudeSpawn_NoPromptWritesZeroFrames(t *testing.T) {
	t.Parallel()

	workDir := t.TempDir()
	framesPath := filepath.Join(workDir, "frames.json")
	binary := writeClaudeFramesBinary(t, workDir)

	driver := &ClaudeDriver{}
	spawned, err := driver.Spawn(context.Background(), drivercontract.LaunchConfig{
		AgentID: "agent-noprompt",
		Binary:  binary,
		WorkDir: workDir,
		Env:     map[string]string{"CLAUDE_TEST_FRAMES_FILE": framesPath},
		Model:   "claude-sonnet-4-5",
	})
	require.NoError(t, err)
	if spawned.Cleanup != nil {
		defer spawned.Cleanup()
	}

	// stdin must still be open (the daemon writes the first durable frame later).
	require.NotNil(t, spawned.Stdin)

	require.NoError(t, spawned.Stdin.Close())
	require.NoError(t, spawned.Cmd.Wait())

	frames := readCapturedClaudeStdinFrames(t, framesPath)
	require.Empty(t, frames, "a no-prompt/no-attachment launch must write zero frames")
}

// TestClaudeSpawn_TextPromptWritesExactlyOneFrame verifies a text prompt
// produces exactly one stream-json frame carrying the correct identity
// (uuid = initial delivery message id, no session_id on a fresh launch).
func TestClaudeSpawn_TextPromptWritesExactlyOneFrame(t *testing.T) {
	t.Parallel()

	workDir := t.TempDir()
	framesPath := filepath.Join(workDir, "frames.json")
	binary := writeClaudeFramesBinary(t, workDir)

	driver := &ClaudeDriver{}
	spawned, err := driver.Spawn(context.Background(), testLaunchConfig(drivercontract.LaunchConfig{AgentID: "agent-text", Binary: binary, WorkDir: workDir, Env: map[string]string{"CLAUDE_TEST_FRAMES_FILE": framesPath}, Model: "claude-sonnet-4-5", InitialDeliveryMessageID: "agentteam-msg-text"}, testLaunchSurfaces{Prompt: "hello claude"}))
	require.NoError(t, err)
	if spawned.Cleanup != nil {
		defer spawned.Cleanup()
	}
	require.NoError(t, spawned.Stdin.Close())
	require.NoError(t, spawned.Cmd.Wait())

	frames := readCapturedClaudeStdinFrames(t, framesPath)
	require.Len(t, frames, 1, "a text prompt must write exactly one frame")
	var msg map[string]any
	require.NoError(t, json.Unmarshal([]byte(frames[0]), &msg))
	require.Equal(t, "user", msg["type"])
	require.Equal(t, "agentteam-msg-text", msg["uuid"])
	message, ok := msg["message"].(map[string]any)
	require.True(t, ok)
	content, ok := message["content"].([]any)
	require.True(t, ok)
	require.Len(t, content, 1)
	block, ok := content[0].(map[string]any)
	require.True(t, ok)
	require.Equal(t, "hello claude", block["text"])
}

// TestClaudeSpawn_AttachmentOnlyWritesExactlyOneFrame verifies an attachment-only
// launch (no text) still writes exactly one stream-json frame with correct
// identity and a non-empty manifest text, so no empty user turn is created.
func TestClaudeSpawn_AttachmentOnlyWritesExactlyOneFrame(t *testing.T) {
	t.Parallel()

	workDir := t.TempDir()
	framesPath := filepath.Join(workDir, "frames.json")
	binary := writeClaudeFramesBinary(t, workDir)

	driver := &ClaudeDriver{}
	spawned, err := driver.Spawn(context.Background(), testLaunchConfig(drivercontract.LaunchConfig{
		AgentID: "agent-attachment",
		Binary:  binary,
		WorkDir: workDir,
		Env:     map[string]string{"CLAUDE_TEST_FRAMES_FILE": framesPath},
		Model:   "claude-sonnet-4-5",
		PromptAttachments: []types.RuntimeAttachment{
			{ID: "att-1", Filename: "spec.pdf", MIME: "application/pdf", URI: "file:///tmp/spec.pdf", Size: 1024},
		},
		InitialDeliveryMessageID: "agentteam-msg-att",
	}, testLaunchSurfaces{FreshPrompt: "Attached files have been prepared locally.\n- spec.pdf"}))
	require.NoError(t, err)
	if spawned.Cleanup != nil {
		defer spawned.Cleanup()
	}
	require.NoError(t, spawned.Stdin.Close())
	require.NoError(t, spawned.Cmd.Wait())

	frames := readCapturedClaudeStdinFrames(t, framesPath)
	require.Len(t, frames, 1, "an attachment-only launch must write exactly one frame")
	var msg map[string]any
	require.NoError(t, json.Unmarshal([]byte(frames[0]), &msg))
	require.Equal(t, "user", msg["type"])
	require.Equal(t, "agentteam-msg-att", msg["uuid"])
	message, ok := msg["message"].(map[string]any)
	require.True(t, ok)
	content, ok := message["content"].([]any)
	require.True(t, ok)
	require.Len(t, content, 1)
	block, ok := content[0].(map[string]any)
	require.True(t, ok)
	text, ok := block["text"].(string)
	require.True(t, ok)
	require.NotEmpty(t, text, "attachment-only frame text must carry the attachment manifest")
	require.Contains(t, text, "spec.pdf")
}

// TestClaudeSpawn_NoPromptDoesNotCreateEmptyUserTurn verifies the gate at the
// message-builder level: a no-prompt/no-attachment launch never calls the stdin
// message builder, so no empty text block is written to stdin.
func TestClaudeSpawn_NoPromptDoesNotCreateEmptyUserTurn(t *testing.T) {
	t.Parallel()

	workDir := t.TempDir()
	stdinPath := filepath.Join(workDir, "stdin.txt")
	framesPath := filepath.Join(workDir, "frames.json")
	binary := writeClaudeFramesBinary(t, workDir)

	driver := &ClaudeDriver{}
	spawned, err := driver.Spawn(context.Background(), drivercontract.LaunchConfig{
		AgentID: "agent-noempty",
		Binary:  binary,
		WorkDir: workDir,
		Env: map[string]string{
			"CLAUDE_TEST_STDIN_FILE":  stdinPath,
			"CLAUDE_TEST_FRAMES_FILE": framesPath,
		},
		Model: "claude-sonnet-4-5",
	})
	require.NoError(t, err)
	if spawned.Cleanup != nil {
		defer spawned.Cleanup()
	}
	require.NoError(t, spawned.Stdin.Close())
	require.NoError(t, spawned.Cmd.Wait())

	raw, err := os.ReadFile(stdinPath)
	require.NoError(t, err)
	require.Empty(t, strings.TrimSpace(string(raw)), "no-prompt launch must not write any stdin bytes")
	frames := readCapturedClaudeStdinFrames(t, framesPath)
	require.Empty(t, frames)
}

// TestClaudeSpawn_ResumeWritesExactlyOneFrame verifies a resumed Claude session
// with a resume prompt writes exactly one frame carrying the resume session id
// and the resume prompt text.
func TestClaudeSpawn_ResumeWritesExactlyOneFrame(t *testing.T) {
	homeDir := t.TempDir()
	t.Setenv("HOME", homeDir)
	workDir := t.TempDir()
	framesPath := filepath.Join(workDir, "frames.json")
	binary := writeClaudeFramesBinary(t, workDir)
	// No local transcript is created. The runtime owns session validity.

	driver := &ClaudeDriver{}
	spawned, err := driver.Spawn(context.Background(), testLaunchConfig(drivercontract.LaunchConfig{AgentID: "agent-resume", Binary: binary, WorkDir: workDir, Model: "claude-sonnet-4-5", SessionID: "session-123", InitialDeliveryMessageID: "agentteam-msg-resume", Env: map[string]string{"CLAUDE_TEST_FRAMES_FILE": framesPath}}, testLaunchSurfaces{ResumePrompt: "resume prompt"}))
	require.NoError(t, err)
	if spawned.Cleanup != nil {
		defer spawned.Cleanup()
	}
	require.NoError(t, spawned.Stdin.Close())
	require.NoError(t, spawned.Cmd.Wait())

	frames := readCapturedClaudeStdinFrames(t, framesPath)
	require.Len(t, frames, 1, "a resumed launch with a resume prompt must write exactly one frame")
	var msg map[string]any
	require.NoError(t, json.Unmarshal([]byte(frames[0]), &msg))
	require.Equal(t, "session-123", msg["session_id"])
	require.Equal(t, "agentteam-msg-resume", msg["uuid"])
	message, ok := msg["message"].(map[string]any)
	require.True(t, ok)
	content, ok := message["content"].([]any)
	require.True(t, ok)
	block, ok := content[0].(map[string]any)
	require.True(t, ok)
	require.Equal(t, "resume prompt", block["text"])
}

// TestClaudeSpawn_PendingRecoveryRewindWritesExactlyOneFrame verifies a pending
// before-first-input recovery rewinds to a fresh session and writes exactly one
// frame with the correct identity (no session_id, fresh prompt text, correct
// uuid), and clears the pending recovery file.
func TestClaudeSpawn_PendingRecoveryRewindWritesExactlyOneFrame(t *testing.T) {
	workDir := t.TempDir()
	framesPath := filepath.Join(workDir, "frames.json")
	binary := writeClaudeFramesBinary(t, workDir)
	ref, err := rewindcaps.EncodeClaudeSessionResumeAtRecoveryRef("old-session", "", rewindcaps.RecoveryBoundaryBeforeFirstInput)
	require.NoError(t, err)
	require.NoError(t, writeClaudePendingRecovery(workDir, rewindcaps.StrategyClaudeSessionResumeAt, ref))

	driver := &ClaudeDriver{}
	spawned, err := driver.Spawn(context.Background(), testLaunchConfig(drivercontract.LaunchConfig{AgentID: "agent-rewind", Binary: binary, WorkDir: workDir, SessionID: "old-session", InitialDeliveryMessageID: "agentteam-msg-rewind", Env: map[string]string{"CLAUDE_TEST_FRAMES_FILE": framesPath}}, testLaunchSurfaces{FreshPrompt: "fresh after first rewind", ResumePrompt: "must not use resume"}))
	require.NoError(t, err)
	if spawned.Cleanup != nil {
		defer spawned.Cleanup()
	}
	require.NoError(t, spawned.Stdin.Close())
	require.NoError(t, spawned.Cmd.Wait())

	require.NoFileExists(t, claudePendingRecoveryPath(workDir), "pending recovery must be cleared after a rewind launch")
	frames := readCapturedClaudeStdinFrames(t, framesPath)
	require.Len(t, frames, 1, "a pending-recovery rewind launch must write exactly one frame")
	var msg map[string]any
	require.NoError(t, json.Unmarshal([]byte(frames[0]), &msg))
	_, hasSessionID := msg["session_id"]
	require.False(t, hasSessionID, "before-first-input rewind launches a fresh session with no session_id")
	require.Equal(t, "agentteam-msg-rewind", msg["uuid"])
	message, ok := msg["message"].(map[string]any)
	require.True(t, ok)
	content, ok := message["content"].([]any)
	require.True(t, ok)
	block, ok := content[0].(map[string]any)
	require.True(t, ok)
	require.Equal(t, "fresh after first rewind", block["text"])
}

// TestClaudeSpawn_NoPromptStdinLifetimeAndCleanup verifies a no-prompt launch
// keeps stdin open for the daemon's first durable handoff and that Cleanup
// closes/releases it exactly once without error.
func TestClaudeSpawn_NoPromptStdinLifetimeAndCleanup(t *testing.T) {
	t.Parallel()

	workDir := t.TempDir()
	binary := writeClaudeFramesBinary(t, workDir)

	driver := &ClaudeDriver{}
	spawned, err := driver.Spawn(context.Background(), drivercontract.LaunchConfig{
		AgentID: "agent-lifetime",
		Binary:  binary,
		WorkDir: workDir,
		Model:   "claude-sonnet-4-5",
	})
	require.NoError(t, err)

	// stdin is still open: a no-prompt launch leaves it available for the first
	// durable handoff, so writing the first real frame must succeed.
	require.NotNil(t, spawned.Stdin)
	first := buildClaudeStdinMsgWithOptions("first durable message", spawned.SessionID, claudeStdinOptions{UUID: "agentteam-msg-first"})
	n, writeErr := spawned.Stdin.Write(append(first, '\n'))
	require.NoError(t, writeErr)
	require.Greater(t, n, 0)

	require.NoError(t, spawned.Stdin.Close())
	require.NoError(t, spawned.Cmd.Wait())
	require.NotNil(t, spawned.Cleanup, "Cleanup must be provided so stdin/process resources are released")
	spawned.Cleanup()
}

// TestLaunchConfigStartsTurn_ClaudeNoPromptIsIdle verifies the Process idle
// resume-safety contract: a Claude launch with no prompt and no attachments is
// idle (does not start a turn), while any prompt or attachment starts a turn.
func TestLaunchConfigStartsTurn_ClaudeNoPromptIsIdle(t *testing.T) {
	t.Parallel()

	claudeCfg := func(prompt, fresh, resume string, attachments int) drivercontract.LaunchConfig {
		cfg := testLaunchConfig(drivercontract.LaunchConfig{RuntimeType: runtimeconfig.TypeClaudeCode}, testLaunchSurfaces{Prompt: prompt, FreshPrompt: fresh, ResumePrompt: resume})
		for index := 0; index < attachments; index++ {
			cfg.PromptAttachments = append(cfg.PromptAttachments, types.RuntimeAttachment{
				ID: "att", Filename: "f.pdf", MIME: "application/pdf", URI: "file:///f.pdf",
			})
		}
		return cfg
	}

	require.False(t, launchConfigStartsTurn(claudeCfg("", "", "", 0)),
		"Claude no-text/no-attachment launch is idle and must not start a turn")
	require.True(t, launchConfigStartsTurn(claudeCfg("prompt", "", "", 0)),
		"Claude text prompt starts a turn")
	require.True(t, launchConfigStartsTurn(claudeCfg("", "fresh", "", 0)),
		"Claude fresh prompt starts a turn")
	require.True(t, launchConfigStartsTurn(claudeCfg("", "", "resume", 0)),
		"Claude resume prompt starts a turn")
	require.True(t, launchConfigStartsTurn(claudeCfg("", "", "", 1)),
		"Claude attachment-only launch starts a turn")
}

// TestLaunchConfigStartsTurn_CrossRuntimeNoPromptGuardrail verifies every
// runtime treats a no-text/no-attachment launch as idle (does not start a
// turn), matching the Claude no-prompt gate contract. A text prompt or
// attachments start a turn on every runtime.
func TestLaunchConfigStartsTurn_CrossRuntimeNoPromptGuardrail(t *testing.T) {
	t.Parallel()

	for _, runtimeType := range runtimeconfig.AllRuntimeTypes {
		cfg := drivercontract.LaunchConfig{RuntimeType: runtimeType}
		require.False(t, launchConfigStartsTurn(cfg),
			"runtime %q no-text/no-attachment launch must be idle", runtimeType)

		setTestLaunchSurface(&cfg, "Prompt", "hello")
		require.True(t, launchConfigStartsTurn(cfg),
			"runtime %q text prompt must start a turn", runtimeType)

		setTestLaunchSurface(&cfg, "Prompt", "")
		cfg.PromptAttachments = []types.RuntimeAttachment{{ID: "att", Filename: "f.pdf", MIME: "application/pdf", URI: "file:///f.pdf"}}
		require.True(t, launchConfigStartsTurn(cfg),
			"runtime %q attachment-only launch must start a turn", runtimeType)
	}
}

// TestClaudeSpawn_TwoStageHandoffWritesExactlyOneFrame verifies the
// publish→ownership-store→DeliverForHandoff seam: a no-prompt launch writes zero
// frames (stage 1), and the first durable message delivered afterward writes
// exactly one frame with the correct identity (stage 2), so the first durable
// message becomes exactly one real Claude turn with no startup/handoff
// duplication.
func TestClaudeSpawn_TwoStageHandoffWritesExactlyOneFrame(t *testing.T) {
	t.Parallel()

	workDir := t.TempDir()
	framesPath := filepath.Join(workDir, "frames.json")
	binary := writeClaudeFramesBinary(t, workDir)

	driver := &ClaudeDriver{}
	spawned, err := driver.Spawn(context.Background(), drivercontract.LaunchConfig{
		AgentID: "agent-two-stage",
		Binary:  binary,
		WorkDir: workDir,
		Env:     map[string]string{"CLAUDE_TEST_FRAMES_FILE": framesPath},
		Model:   "claude-sonnet-4-5",
	})
	require.NoError(t, err)
	if spawned.Cleanup != nil {
		defer spawned.Cleanup()
	}

	// Stage 1: the no-prompt launch must not have written anything yet.
	require.NotNil(t, spawned.Stdin)

	// Stage 2: the first durable message is written through the same
	// EncodeStdinMessage path the idle delivery seam uses. EncodeStdinMessage
	// already appends the newline terminator.
	data := driver.EncodeStdinMessage("first durable message", spawned.SessionID)
	require.NotNil(t, data)
	require.NotEmpty(t, string(data))
	require.NotEqual(t, "{}", strings.TrimSpace(string(data)))
	n, writeErr := spawned.Stdin.Write(data)
	require.NoError(t, writeErr)
	require.Greater(t, n, 0)

	require.NoError(t, spawned.Stdin.Close())
	require.NoError(t, spawned.Cmd.Wait())

	frames := readCapturedClaudeStdinFrames(t, framesPath)
	require.Len(t, frames, 1, "spawn + first durable handoff must yield exactly one real Claude turn")
	var msg map[string]any
	require.NoError(t, json.Unmarshal([]byte(frames[0]), &msg))
	require.Equal(t, "user", msg["type"])
	message, ok := msg["message"].(map[string]any)
	require.True(t, ok)
	content, ok := message["content"].([]any)
	require.True(t, ok)
	block, ok := content[0].(map[string]any)
	require.True(t, ok)
	require.Equal(t, "first durable message", block["text"])
}

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

	workDir := t.TempDir()
	argsPath := filepath.Join(workDir, "args.json")
	stdinPath := filepath.Join(workDir, "stdin.txt")
	binary := writeClaudeCaptureBinary(t, workDir)

	driver := &ClaudeDriver{}
	spawned, err := driver.Spawn(context.Background(), testLaunchConfig(drivercontract.LaunchConfig{AgentID: "knowledge-session", Binary: binary, WorkDir: workDir, DisableNativeTools: true, MCPs: []types.NamedMCPConfig{{
		Name:      "knowledge",
		Transport: types.MCPTransportHTTP,
		URL:       "http://127.0.0.1/knowledge",
	}}, Env: map[string]string{
		"CLAUDE_TEST_ARGS_FILE":  argsPath,
		"CLAUDE_TEST_STDIN_FILE": stdinPath,
	}}, testLaunchSurfaces{FreshPrompt: "question"}))
	require.NoError(t, err)
	if spawned.Cleanup != nil {
		defer spawned.Cleanup()
	}
	require.NoError(t, spawned.Stdin.Close())
	require.NoError(t, spawned.Cmd.Wait())

	args := readCapturedClaudeArgs(t, argsPath)
	toolsIndex := indexOfArg(t, args, "--tools")
	require.Equal(t, "", args[toolsIndex+1])
	require.Contains(t, args, "--strict-mcp-config")
	require.Contains(t, args, "--disable-slash-commands")
}

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

	workDir := t.TempDir()
	binary := writeClaudeCaptureBinary(t, workDir)

	driver := &ClaudeDriver{}
	_, err := driver.Spawn(context.Background(), testLaunchConfig(drivercontract.LaunchConfig{AgentID: "agent-1", Binary: binary, WorkDir: workDir, PermissionMode: types.AgentPermissionModeWorkspaceWrite}, testLaunchSurfaces{Prompt: "fresh prompt"}))
	require.NoError(t, err)
}

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

	var cleanup func()
	args, err := appendClaudeSystemPromptArgs(nil, "base instructions\n\ndeveloper instructions", "windows", func(fn func()) {
		cleanup = fn
	})
	require.NoError(t, err)
	require.Contains(t, args, "--append-system-prompt-file")
	require.NotContains(t, args, "--append-system-prompt")
	appendIndex := indexOfArg(t, args, "--append-system-prompt-file")
	promptFilePath := args[appendIndex+1]

	promptFile, err := os.ReadFile(promptFilePath)
	require.NoError(t, err)
	require.Equal(t, "base instructions\n\ndeveloper instructions", string(promptFile))

	require.NotNil(t, cleanup)
	cleanup()
	_, err = os.Stat(promptFilePath)
	require.ErrorIs(t, err, os.ErrNotExist)
}

func TestClaudeSpawn_UsesResumePromptWhenSessionPresent(t *testing.T) {
	homeDir := t.TempDir()
	t.Setenv("HOME", homeDir)
	workDir := t.TempDir()
	argsPath := filepath.Join(workDir, "args.json")
	stdinPath := filepath.Join(workDir, "stdin.txt")
	binary := writeClaudeCaptureBinary(t, workDir)
	// Ordinary resume does not probe or require a local transcript.

	driver := &ClaudeDriver{}
	spawned, err := driver.Spawn(context.Background(), testLaunchConfig(drivercontract.LaunchConfig{AgentID: "agent-1", Binary: binary, WorkDir: workDir, Model: "claude-sonnet-4-5", SessionID: "session-123", InitialDeliveryMessageID: "agentteam-msg-resume", Env: map[string]string{
		"CLAUDE_TEST_ARGS_FILE":  argsPath,
		"CLAUDE_TEST_STDIN_FILE": stdinPath,
	}}, testLaunchSurfaces{ResumePrompt: "resume prompt", BaseInstructions: "base instructions"}))
	require.NoError(t, err)
	if spawned.Cleanup != nil {
		defer spawned.Cleanup()
	}

	require.NoError(t, spawned.Stdin.Close())
	require.NoError(t, spawned.Cmd.Wait())

	args := readCapturedClaudeArgs(t, argsPath)
	require.Contains(t, args, "--resume")
	resumeIndex := indexOfArg(t, args, "--resume")
	require.Equal(t, "session-123", args[resumeIndex+1])
	require.NotContains(t, args, "--session-id")
	require.Contains(t, args, "--append-system-prompt")
	appendIndex := indexOfArg(t, args, "--append-system-prompt")
	require.Equal(t, "base instructions", args[appendIndex+1])

	msg := readCapturedClaudeStdinMessage(t, stdinPath)
	require.Equal(t, "session-123", msg["session_id"])
	message, ok := msg["message"].(map[string]any)
	require.True(t, ok)
	content, ok := message["content"].([]any)
	require.True(t, ok)
	require.Len(t, content, 1)
	block, ok := content[0].(map[string]any)
	require.True(t, ok)
	require.Equal(t, "resume prompt", block["text"])
	require.Equal(t, "agentteam-msg-resume", msg["uuid"])

	driver.mu.Lock()
	require.Equal(t, "session-123", driver.sessionID)
	require.Empty(t, driver.lastTranscriptUUID)
	driver.mu.Unlock()
}

func TestClaudeDetachedRestoreStagesResumeAtForNextLaunch(t *testing.T) {
	homeDir := t.TempDir()
	t.Setenv("HOME", homeDir)
	workDir := t.TempDir()
	argsPath := filepath.Join(workDir, "args.json")
	stdinPath := filepath.Join(workDir, "stdin.txt")
	binary := writeClaudeCaptureBinary(t, workDir)
	writeClaudeTranscript(t, homeDir, "session-123")
	ref, err := rewindcaps.EncodeClaudeSessionResumeAtRecoveryRef("session-123", "assistant-boundary", rewindcaps.RecoveryBoundaryResumeAt)
	require.NoError(t, err)

	driver := &ClaudeDriver{}
	require.NoError(t, driver.RestoreRecoveryPointDetached(context.Background(), drivercontract.LaunchConfig{
		SessionID: "session-123",
		WorkDir:   workDir,
	}, rewindcaps.StrategyClaudeSessionResumeAt, ref))

	spawned, err := driver.Spawn(context.Background(), testLaunchConfig(drivercontract.LaunchConfig{AgentID: "agent-1", Binary: binary, WorkDir: workDir, SessionID: "session-123", InitialDeliveryMessageID: "agentteam-msg-after-rewind", Env: map[string]string{
		"CLAUDE_TEST_ARGS_FILE":  argsPath,
		"CLAUDE_TEST_STDIN_FILE": stdinPath,
	}}, testLaunchSurfaces{ResumePrompt: "after rewind"}))
	require.NoError(t, err)
	if spawned.Cleanup != nil {
		defer spawned.Cleanup()
	}
	require.NoError(t, spawned.Stdin.Close())
	require.NoError(t, spawned.Cmd.Wait())

	args := readCapturedClaudeArgs(t, argsPath)
	resumeIndex := indexOfArg(t, args, "--resume")
	require.Equal(t, "session-123", args[resumeIndex+1])
	resumeAtIndex := indexOfArg(t, args, "--resume-session-at")
	require.Equal(t, "assistant-boundary", args[resumeAtIndex+1])
	require.NoFileExists(t, claudePendingRecoveryPath(workDir))
	msg := readCapturedClaudeStdinMessage(t, stdinPath)
	require.Equal(t, "session-123", msg["session_id"])
	require.Equal(t, "agentteam-msg-after-rewind", msg["uuid"])
}

func TestClaudeDetachedRestoreBeforeFirstInputLaunchesFreshSession(t *testing.T) {
	workDir := t.TempDir()
	argsPath := filepath.Join(workDir, "args.json")
	stdinPath := filepath.Join(workDir, "stdin.txt")
	binary := writeClaudeCaptureBinary(t, workDir)
	ref, err := rewindcaps.EncodeClaudeSessionResumeAtRecoveryRef("old-session", "", rewindcaps.RecoveryBoundaryBeforeFirstInput)
	require.NoError(t, err)

	driver := &ClaudeDriver{}
	require.NoError(t, driver.RestoreRecoveryPointDetached(context.Background(), drivercontract.LaunchConfig{
		SessionID: "old-session",
		WorkDir:   workDir,
	}, rewindcaps.StrategyClaudeSessionResumeAt, ref))

	spawned, err := driver.Spawn(context.Background(), testLaunchConfig(drivercontract.LaunchConfig{AgentID: "agent-1", Binary: binary, WorkDir: workDir, SessionID: "old-session", InitialDeliveryMessageID: "agentteam-msg-after-first-rewind", Env: map[string]string{
		"CLAUDE_TEST_ARGS_FILE":  argsPath,
		"CLAUDE_TEST_STDIN_FILE": stdinPath,
	}}, testLaunchSurfaces{FreshPrompt: "fresh after first rewind", ResumePrompt: "must not use resume"}))
	require.NoError(t, err)
	if spawned.Cleanup != nil {
		defer spawned.Cleanup()
	}
	require.NoError(t, spawned.Stdin.Close())
	require.NoError(t, spawned.Cmd.Wait())

	args := readCapturedClaudeArgs(t, argsPath)
	require.NotContains(t, args, "--resume")
	require.NotContains(t, args, "--resume-session-at")
	sessionIndex := indexOfArg(t, args, "--session-id")
	require.NotEmpty(t, args[sessionIndex+1])
	require.NotEqual(t, "old-session", args[sessionIndex+1])
	require.NoFileExists(t, claudePendingRecoveryPath(workDir))
	msg := readCapturedClaudeStdinMessage(t, stdinPath)
	_, hasSessionID := msg["session_id"]
	require.False(t, hasSessionID)
	message, ok := msg["message"].(map[string]any)
	require.True(t, ok)
	content, ok := message["content"].([]any)
	require.True(t, ok)
	require.Equal(t, "fresh after first rewind", content[0].(map[string]any)["text"])
	require.Equal(t, "agentteam-msg-after-first-rewind", msg["uuid"])
}

func TestClaudeSpawn_PassesResumeToRuntimeWhenLocalTranscriptMissing(t *testing.T) {
	homeDir := t.TempDir()
	t.Setenv("HOME", homeDir)
	workDir := t.TempDir()
	argsPath := filepath.Join(workDir, "args.json")
	stdinPath := filepath.Join(workDir, "stdin.txt")
	binary := writeClaudeCaptureBinary(t, workDir)

	driver := &ClaudeDriver{}
	spawned, err := driver.Spawn(context.Background(), testLaunchConfig(drivercontract.LaunchConfig{AgentID: "agent-1", Binary: binary, WorkDir: workDir, SessionID: "missing-session", Env: map[string]string{
		"CLAUDE_TEST_ARGS_FILE":  argsPath,
		"CLAUDE_TEST_STDIN_FILE": stdinPath,
	}}, testLaunchSurfaces{FreshPrompt: "fresh prompt", ResumePrompt: "resume prompt"}))
	require.NoError(t, err)
	if spawned.Cleanup != nil {
		defer spawned.Cleanup()
	}

	require.NoError(t, spawned.Stdin.Close())
	require.NoError(t, spawned.Cmd.Wait())

	args := readCapturedClaudeArgs(t, argsPath)
	require.Contains(t, args, "--resume")
	require.NotContains(t, args, "--session-id")
	msg := readCapturedClaudeStdinMessage(t, stdinPath)
	require.Equal(t, "missing-session", msg["session_id"])
	message, ok := msg["message"].(map[string]any)
	require.True(t, ok)
	content, ok := message["content"].([]any)
	require.True(t, ok)
	require.Len(t, content, 1)
	block, ok := content[0].(map[string]any)
	require.True(t, ok)
	require.Equal(t, "resume prompt", block["text"])
}

func TestClaudeSpawnResumeInvalidExitRemainsFailClosedWithoutFreshReplay(t *testing.T) {
	workDir := t.TempDir()
	binary := writeClaudeCaptureBinary(t, workDir)

	driver := &ClaudeDriver{}
	spawned, err := driver.Spawn(context.Background(), testLaunchConfig(drivercontract.LaunchConfig{AgentID: "agent-resume-fail-closed", Binary: binary, WorkDir: workDir, SessionID: "missing-session", Env: map[string]string{
		"CLAUDE_TEST_STDERR_TEXT":      "Invalid session: missing-session",
		"CLAUDE_TEST_EXIT_AFTER_FIRST": "1",
		"CLAUDE_TEST_EXIT_CODE":        "42",
	}}, testLaunchSurfaces{ResumePrompt: "resume prompt"}))
	require.NoError(t, err)
	require.Nil(t, spawned.FreshSessionFallback, "Claude has no deterministic pre-input authority for a fresh cutover")
	require.NoError(t, spawned.Stdin.Close())
	require.Error(t, spawned.Cmd.Wait())
	require.Contains(t, spawned.StderrTail(), "Invalid session")
}

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

	workDir := t.TempDir()
	argsPath := filepath.Join(workDir, "args.json")
	stdinPath := filepath.Join(workDir, "stdin.txt")
	binary := writeClaudeCaptureBinary(t, workDir)

	driver := &ClaudeDriver{}
	spawned, err := driver.Spawn(context.Background(), testLaunchConfig(drivercontract.LaunchConfig{AgentID: "agent-1", Binary: binary, WorkDir: workDir, Env: map[string]string{
		"CLAUDE_TEST_ARGS_FILE":   argsPath,
		"CLAUDE_TEST_STDIN_FILE":  stdinPath,
		"CLAUDE_TEST_STDERR_TEXT": "stderr diagnostic",
	}}, testLaunchSurfaces{Prompt: "fresh prompt"}))
	require.NoError(t, err)

	require.NoError(t, spawned.Stdin.Close())
	require.NoError(t, spawned.Cmd.Wait())
	require.NotNil(t, spawned.StderrTail)
	require.Contains(t, spawned.StderrTail(), "stderr diagnostic")
}

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

	workDir := t.TempDir()
	argsPath := filepath.Join(workDir, "args.json")
	stdinPath := filepath.Join(workDir, "stdin.txt")
	binary := writeClaudeCaptureBinary(t, workDir)

	driver := &ClaudeDriver{}
	spawned, err := driver.Spawn(context.Background(), testLaunchConfig(drivercontract.LaunchConfig{AgentID: "agent-1", Binary: binary, WorkDir: workDir, Env: map[string]string{
		"CLAUDE_TEST_ARGS_FILE":  argsPath,
		"CLAUDE_TEST_STDIN_FILE": stdinPath,
	}}, testLaunchSurfaces{Prompt: "fresh prompt"}))
	require.NoError(t, err)

	require.NoError(t, spawned.Stdin.Close())
	require.NoError(t, spawned.Cmd.Wait())

	args := readCapturedClaudeArgs(t, argsPath)
	require.NotContains(t, args, "--model")
}

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

	workDir := t.TempDir()
	argsPath := filepath.Join(workDir, "args.json")
	stdinPath := filepath.Join(workDir, "stdin.txt")
	binary := writeClaudeCaptureBinary(t, workDir)

	driver := &ClaudeDriver{}
	spawned, err := driver.Spawn(context.Background(), testLaunchConfig(drivercontract.LaunchConfig{AgentID: "agent-1", Binary: binary, WorkDir: workDir, MCPs: []types.NamedMCPConfig{{
		Name:      "zap_http",
		Transport: types.MCPTransportHTTP,
		URL:       "https://mcp.example.com",
		Headers:   map[string]string{"Authorization": "Bearer secret-token"},
	}}, Env: map[string]string{
		"CLAUDE_TEST_ARGS_FILE":  argsPath,
		"CLAUDE_TEST_STDIN_FILE": stdinPath,
	}}, testLaunchSurfaces{Prompt: "fresh prompt"}))
	require.NoError(t, err)

	require.NoError(t, spawned.Stdin.Close())
	require.NoError(t, spawned.Cmd.Wait())
	require.NotNil(t, spawned.Cleanup)
	defer spawned.Cleanup()

	args := readCapturedClaudeArgs(t, argsPath)
	mcpIndex := indexOfArg(t, args, "--mcp-config")
	mcpConfigPath := args[mcpIndex+1]
	require.NotContains(t, mcpConfigPath, "secret-token")
	info, err := os.Stat(mcpConfigPath)
	require.NoError(t, err)
	require.Equal(t, os.FileMode(0o600), info.Mode().Perm())
	configBytes, err := os.ReadFile(mcpConfigPath)
	require.NoError(t, err)
	require.Contains(t, string(configBytes), "Bearer secret-token")
}

func TestBuildClaudeEnvStripsAmbientVendorEnvWhenProviderUpstreamIsConfigured(t *testing.T) {
	t.Setenv("GLM_API_KEY", "sk-upstream")
	t.Setenv("ANTHROPIC_API_KEY", "sk-host")
	t.Setenv("ANTHROPIC_BASE_URL", "https://host.example.com")

	env := buildClaudeEnv(map[string]string{
		"EXTRA_FLAG": "1",
	}, &drivercontract.ProviderConfig{
		BaseURL: "https://anthropic.example.com/v1/messages",
		APIKey:  "sk-direct",
	}, "")

	joined := strings.Join(env, "\n")
	require.NotContains(t, joined, "GLM_API_KEY=sk-upstream")
	require.NotContains(t, joined, "ANTHROPIC_API_KEY=sk-host")
	require.NotContains(t, joined, "ANTHROPIC_BASE_URL=https://host.example.com")
	require.Contains(t, joined, "ANTHROPIC_API_KEY=sk-direct")
	require.Contains(t, joined, "ANTHROPIC_BASE_URL=https://anthropic.example.com")
	require.Contains(t, joined, "EXTRA_FLAG=1")
}

func TestBuildClaudeEnvReplacesAmbientExecCredential(t *testing.T) {
	t.Setenv("AGENTTEAM_EXEC_BASE_URL", "http://127.0.0.1:2")
	t.Setenv("AGENTTEAM_EXEC_TOKEN", "ambient-bearer")
	t.Setenv("AGENTTEAM_EXEC_TOKEN_FILE", "/private/ambient-token")
	t.Setenv("AGENTTEAM_EXTENSION_BASE_URL", "http://127.0.0.1:3")
	t.Setenv("AGENTTEAM_EXTENSION_TOKEN_FILE", "/private/ambient-extension-token")
	t.Setenv("AGENTTEAM_EXTENSION_TOKEN", "ambient-extension-bearer")

	env := buildClaudeEnv(map[string]string{
		"AGENTTEAM_EXEC_BASE_URL":        "http://127.0.0.1:43123",
		"AGENTTEAM_EXEC_TOKEN":           "stale-runtime-bearer",
		"AGENTTEAM_EXEC_TOKEN_FILE":      "/runtime/current-token",
		"AGENTTEAM_EXTENSION_BASE_URL":   "http://127.0.0.1:4",
		"AGENTTEAM_EXTENSION_TOKEN_FILE": "/private/stale-extension-token",
	}, nil, "")

	require.Equal(t, []string{"http://127.0.0.1:43123"}, runtimeEnvValues(env, "AGENTTEAM_EXEC_BASE_URL"))
	require.Empty(t, runtimeEnvValues(env, "AGENTTEAM_EXEC_TOKEN"))
	require.Equal(t, []string{"/runtime/current-token"}, runtimeEnvValues(env, "AGENTTEAM_EXEC_TOKEN_FILE"))
	require.Empty(t, runtimeEnvValues(env, "AGENTTEAM_EXTENSION_BASE_URL"))
	require.Empty(t, runtimeEnvValues(env, "AGENTTEAM_EXTENSION_TOKEN_FILE"))
	require.Empty(t, runtimeEnvValues(env, "AGENTTEAM_EXTENSION_TOKEN"))
}

func TestBuildClaudeEnvUsesAuthTokenForDeepSeekAnthropicEndpoint(t *testing.T) {
	t.Setenv("ANTHROPIC_API_KEY", "sk-host")
	t.Setenv("ANTHROPIC_AUTH_TOKEN", "sk-host-token")

	env := buildClaudeEnv(nil, &drivercontract.ProviderConfig{
		BaseURL: "https://api.deepseek.com/anthropic/v1/messages",
		APIKey:  "sk-direct",
	}, "")

	joined := strings.Join(env, "\n")
	require.Contains(t, joined, "ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic")
	require.Contains(t, joined, "ANTHROPIC_AUTH_TOKEN=sk-direct")
	require.NotContains(t, joined, "ANTHROPIC_API_KEY=sk-direct")
	require.NotContains(t, joined, "ANTHROPIC_API_KEY=sk-host")
	require.NotContains(t, joined, "ANTHROPIC_AUTH_TOKEN=sk-host-token")
}

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

	settings := buildClaudeSettingsOverrides(drivercontract.LaunchConfig{
		Model:      "deepseek-v4-flash",
		SmallModel: "deepseek-v4-lite",
		Provider: &drivercontract.ProviderConfig{
			BaseURL: "https://anthropic.example.com",
			APIKey:  "sk-direct",
		},
	})
	require.NotEmpty(t, settings)

	var payload map[string]map[string]string
	require.NoError(t, json.Unmarshal([]byte(settings), &payload))
	env := payload["env"]
	require.Equal(t, "https://anthropic.example.com", env["ANTHROPIC_BASE_URL"])
	require.Equal(t, "sk-direct", env["ANTHROPIC_API_KEY"])
	require.NotContains(t, env, "ANTHROPIC_AUTH_TOKEN")
	require.Equal(t, "deepseek-v4-flash", env["ANTHROPIC_MODEL"])
	require.Equal(t, "deepseek-v4-lite", env["ANTHROPIC_SMALL_FAST_MODEL"])
}

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

	settings := buildClaudeSettingsOverrides(drivercontract.LaunchConfig{
		Provider: &drivercontract.ProviderConfig{
			BaseURL: "https://api.deepseek.com/anthropic",
			APIKey:  "sk-direct",
		},
	})
	require.NotEmpty(t, settings)

	var payload map[string]map[string]string
	require.NoError(t, json.Unmarshal([]byte(settings), &payload))
	env := payload["env"]
	require.Equal(t, "https://api.deepseek.com/anthropic", env["ANTHROPIC_BASE_URL"])
	require.Equal(t, "sk-direct", env["ANTHROPIC_AUTH_TOKEN"])
	require.NotContains(t, env, "ANTHROPIC_API_KEY")
}

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

	settings := buildClaudeSettingsOverrides(drivercontract.LaunchConfig{
		Provider: &drivercontract.ProviderConfig{
			BaseURL: "https://dendro.codgician.me/anthropic",
			APIKey:  "Bearer dendro-token",
			EnvKey:  "ANTHROPIC_AUTH_TOKEN",
		},
	})
	require.NotEmpty(t, settings)

	var payload map[string]map[string]string
	require.NoError(t, json.Unmarshal([]byte(settings), &payload))
	env := payload["env"]
	require.Equal(t, "https://dendro.codgician.me/anthropic", env["ANTHROPIC_BASE_URL"])
	require.Equal(t, "Bearer dendro-token", env["ANTHROPIC_AUTH_TOKEN"])
	require.NotContains(t, env, "ANTHROPIC_API_KEY")
}

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

	require.Equal(t, "https://anthropic.example.com", claudeAnthropicBaseURL(&drivercontract.ProviderConfig{
		BaseURL: "https://anthropic.example.com/v1",
	}))
	require.Equal(t, "https://anthropic.example.com/custom", claudeAnthropicBaseURL(&drivercontract.ProviderConfig{
		BaseURL: "https://anthropic.example.com/custom/messages",
	}))
	require.Equal(t, "https://api.anthropic.com", claudeAnthropicBaseURL(&drivercontract.ProviderConfig{
		BaseURL: "https://api.anthropic.com",
	}))
}

func writeClaudeCaptureBinary(t *testing.T, workDir string) string {
	t.Helper()

	srcPath := filepath.Join(workDir, "claude-capture.go")
	binaryPath := filepath.Join(workDir, "claude-capture")
	source := `package main

import (
	"bufio"
	"encoding/json"
	"os"
	"strconv"
	"strings"
)

func main() {
	if path := os.Getenv("CLAUDE_TEST_ARGS_FILE"); path != "" {
		file, err := os.Create(path)
		if err != nil {
			panic(err)
		}
		if err := json.NewEncoder(file).Encode(os.Args[1:]); err != nil {
			_ = file.Close()
			panic(err)
		}
		if err := file.Close(); err != nil {
			panic(err)
		}
	}
	if text := os.Getenv("CLAUDE_TEST_STDERR_TEXT"); text != "" {
		_, _ = os.Stderr.WriteString(text + "\n")
	}

	exitAfterFirst := os.Getenv("CLAUDE_TEST_EXIT_AFTER_FIRST") != ""
	exitCode := 1
	if value := os.Getenv("CLAUDE_TEST_EXIT_CODE"); value != "" {
		if parsed, err := strconv.Atoi(value); err == nil {
			exitCode = parsed
		}
	}
	scanner := bufio.NewScanner(os.Stdin)
	var frames []string
	for scanner.Scan() {
		frames = append(frames, scanner.Text())
		if exitAfterFirst {
			os.Exit(exitCode)
		}
	}
	if err := scanner.Err(); err != nil {
		panic(err)
	}
	if path := os.Getenv("CLAUDE_TEST_STDIN_FILE"); path != "" {
		if err := os.WriteFile(path, []byte(strings.Join(frames, "\n")), 0o644); err != nil {
			panic(err)
		}
	}
	if path := os.Getenv("CLAUDE_TEST_FRAMES_FILE"); path != "" {
		file, err := os.Create(path)
		if err != nil {
			panic(err)
		}
		if err := json.NewEncoder(file).Encode(frames); err != nil {
			_ = file.Close()
			panic(err)
		}
		if err := file.Close(); err != nil {
			panic(err)
		}
	}
}
`
	require.NoError(t, os.WriteFile(srcPath, []byte(source), 0o644))

	cmd := exec.Command("go", "build", "-o", binaryPath, srcPath)
	cmd.Dir = workDir
	output, err := cmd.CombinedOutput()
	require.NoError(t, err, string(output))
	return binaryPath
}

// readCapturedClaudeStdinFrames reads every stream-json frame the fake Claude
// received on stdin. An empty slice means zero frames were written.
func readCapturedClaudeStdinFrames(t *testing.T, path string) []string {
	t.Helper()

	data, err := os.ReadFile(path)
	require.NoError(t, err)
	var frames []string
	if len(data) == 0 {
		return frames
	}
	require.NoError(t, json.Unmarshal(data, &frames))
	return frames
}

// writeClaudeFramesBinary is the same fake Claude but always records every
// stdin frame into CLAUDE_TEST_FRAMES_FILE, independent of the stdin file env.
func writeClaudeFramesBinary(t *testing.T, workDir string) string {
	t.Helper()

	srcPath := filepath.Join(workDir, "claude-frames.go")
	binaryPath := filepath.Join(workDir, "claude-frames")
	source := `package main

import (
	"bufio"
	"encoding/json"
	"os"
	"strings"
)

func main() {
	if path := os.Getenv("CLAUDE_TEST_ARGS_FILE"); path != "" {
		file, err := os.Create(path)
		if err != nil {
			panic(err)
		}
		if err := json.NewEncoder(file).Encode(os.Args[1:]); err != nil {
			_ = file.Close()
			panic(err)
		}
		if err := file.Close(); err != nil {
			panic(err)
		}
	}

	scanner := bufio.NewScanner(os.Stdin)
	var frames []string
	for scanner.Scan() {
		frames = append(frames, scanner.Text())
	}
	if path := os.Getenv("CLAUDE_TEST_FRAMES_FILE"); path != "" {
		file, err := os.Create(path)
		if err != nil {
			panic(err)
		}
		if err := json.NewEncoder(file).Encode(frames); err != nil {
			_ = file.Close()
			panic(err)
		}
		if err := file.Close(); err != nil {
			panic(err)
		}
	}
	if path := os.Getenv("CLAUDE_TEST_STDIN_FILE"); path != "" {
		_ = os.WriteFile(path, []byte(strings.Join(frames, "\n")), 0o644)
	}
}
`
	require.NoError(t, os.WriteFile(srcPath, []byte(source), 0o644))

	cmd := exec.Command("go", "build", "-o", binaryPath, srcPath)
	cmd.Dir = workDir
	output, err := cmd.CombinedOutput()
	require.NoError(t, err, string(output))
	return binaryPath
}

func writeClaudeTranscript(t *testing.T, homeDir, sessionID string) {
	t.Helper()
	transcriptDir := filepath.Join(homeDir, ".claude", "projects", "project-1")
	require.NoError(t, os.MkdirAll(transcriptDir, 0o755))
	transcript := strings.Join([]string{
		`{"type":"user","uuid":"user-boundary"}`,
		`{"type":"assistant","uuid":"assistant-boundary"}`,
		`{"type":"system","uuid":"system-tail"}`,
		"",
	}, "\n")
	require.NoError(t, os.WriteFile(filepath.Join(transcriptDir, sessionID+".jsonl"), []byte(transcript), 0o644))
}

func readCapturedClaudeArgs(t *testing.T, path string) []string {
	t.Helper()

	data, err := os.ReadFile(path)
	require.NoError(t, err)
	var args []string
	require.NoError(t, json.Unmarshal(data, &args))
	return args
}

func readCapturedClaudeStdinMessage(t *testing.T, path string) map[string]any {
	t.Helper()

	data, err := os.ReadFile(path)
	require.NoError(t, err)
	var msg map[string]any
	require.NoError(t, json.Unmarshal(data, &msg))
	return msg
}

func indexOfArg(t *testing.T, args []string, want string) int {
	t.Helper()

	for i, arg := range args {
		if arg == want {
			return i
		}
	}
	t.Fatalf("arg %q not found in %v", want, args)
	return -1
}

func TestBuildClaudeMCPConfigSupportsHTTPAndStdioServers(t *testing.T) {
	payload := buildClaudeMCPConfig(drivercontract.LaunchConfig{MCPs: []types.NamedMCPConfig{
		{Name: "zap_http", Transport: types.MCPTransportHTTP, URL: "https://mcp.example.com", Headers: map[string]string{"Authorization": "Bearer token"}},
		{Name: "zap_stdio", Transport: types.MCPTransportStdio, Command: "/bin/zap", Args: []string{"--flag"}, Env: map[string]string{"API_KEY": "secret"}, CWD: "/tmp"},
	}})

	var decoded struct {
		MCPServers map[string]map[string]any `json:"mcpServers"`
	}
	require.NoError(t, json.Unmarshal([]byte(payload), &decoded))
	require.Equal(t, "http", decoded.MCPServers["zap_http"]["type"])
	require.Equal(t, "https://mcp.example.com", decoded.MCPServers["zap_http"]["url"])
	require.Equal(t, "stdio", decoded.MCPServers["zap_stdio"]["type"])
	require.Equal(t, "/bin/zap", decoded.MCPServers["zap_stdio"]["command"])
	require.Equal(t, []any{"--flag"}, decoded.MCPServers["zap_stdio"]["args"])
	require.Equal(t, map[string]any{"API_KEY": "secret"}, decoded.MCPServers["zap_stdio"]["env"])
	require.Equal(t, "/tmp", decoded.MCPServers["zap_stdio"]["cwd"])
}

// Claude connects directly to the configured MCP server; AgentTeam does not parse
// or transform the server's tools/list schemas on this path.
func TestBuildClaudeMCPConfigWiresGenericApprovalServer(t *testing.T) {
	payload := buildClaudeMCPConfig(drivercontract.LaunchConfig{MCPs: []types.NamedMCPConfig{{
		Name: "agentteam", Transport: types.MCPTransportHTTP, URL: "https://agentteam.example.com/mcp",
	}}})
	require.JSONEq(t, `{"mcpServers":{"agentteam":{"type":"http","url":"https://agentteam.example.com/mcp"}}}`, payload)
}
