package agent

import (
	"context"
	"encoding/json"
	"log/slog"
	"path/filepath"
	"runtime"
	"strings"
	"testing"
	"time"
)

// ── Fixtures from real Copilot CLI v1.0.28 --output-format json output ──

const fixtureAssistantMessageDelta = `{"type":"assistant.message_delta","data":{"messageId":"b5148f3f-d24b-4a5e-a95c-2be7d6493a52","deltaContent":"pong"},"id":"eb6c3ef1-0388-4010-bf8e-4002b62db58c","timestamp":"2026-04-16T08:43:38.401Z","parentId":"417b175a-b303-4378-9c43-d4fcb177c05a","ephemeral":true}`

const fixtureAssistantMessage = `{"type":"assistant.message","data":{"messageId":"b5148f3f-d24b-4a5e-a95c-2be7d6493a52","content":"pong","toolRequests":[],"interactionId":"267266f6-47bc-4f31-8338-4e95961cf900","outputTokens":5,"requestId":"D012:2F8B66:8CB3C8:98A605:69E0A137"},"id":"ddff21bc-5829-4892-822a-06f3f543ea1d","timestamp":"2026-04-16T08:43:38.493Z","parentId":"417b175a-b303-4378-9c43-d4fcb177c05a"}`

const fixtureAssistantMessageWithTools = `{"type":"assistant.message","data":{"messageId":"0c48f3f5-74a2-485b-8969-3ea8ddc4c303","content":"","toolRequests":[{"toolCallId":"toolu_vrtx_01UqgJdCxuteCRZvKpdjUFyL","name":"bash","arguments":{"command":"ls","description":"List files"},"type":"function","intentionSummary":"List files in current directory"}],"interactionId":"b7bede2d-6996-4728-bdfa-33ba546ed511","outputTokens":112,"requestId":"EB94:21B867:8C33D3:983053:69E0A149"},"id":"6c005d04-bf23-4114-8dcb-f2f9bcdd3880","timestamp":"2026-04-16T08:43:59.066Z","parentId":"387c2814-f893-443c-82b8-00db66fef14c"}`

const fixtureToolExecComplete = `{"type":"tool.execution_complete","data":{"toolCallId":"toolu_vrtx_01UqgJdCxuteCRZvKpdjUFyL","model":"claude-opus-4.6","interactionId":"b7bede2d-6996-4728-bdfa-33ba546ed511","success":true,"result":{"content":"file1.go\nfile2.go\n","detailedContent":"file1.go\nfile2.go\n"},"toolTelemetry":{}},"id":"1662b7b1-5160-4c03-bc83-59a9a367f070","timestamp":"2026-04-16T08:43:59.530Z","parentId":"92531882-91ba-442a-9974-3dd8745fffd0"}`

const fixtureToolExecCompleteError = `{"type":"tool.execution_complete","data":{"toolCallId":"toolu_err_01","model":"claude-opus-4.6","interactionId":"int-1","success":false,"error":{"message":"command not found: foobar"}},"id":"err-1","timestamp":"2026-04-16T08:44:00.000Z","parentId":"p-1"}`

const fixtureTurnStart = `{"type":"assistant.turn_start","data":{"turnId":"0","interactionId":"267266f6-47bc-4f31-8338-4e95961cf900"},"id":"417b175a-b303-4378-9c43-d4fcb177c05a","timestamp":"2026-04-16T08:43:36.401Z","parentId":"ed1a637b-c636-4b74-bc82-4ba3f3386aad"}`

const fixtureResult = `{"type":"result","timestamp":"2026-04-16T08:43:38.524Z","sessionId":"35059dc3-d928-4ffb-8616-b78938621d85","exitCode":0,"usage":{"premiumRequests":3,"totalApiDurationMs":1763,"sessionDurationMs":6275,"codeChanges":{"linesAdded":0,"linesRemoved":0,"filesModified":[]}}}`

const fixtureResultNonZero = `{"type":"result","timestamp":"2026-04-16T08:50:00.000Z","sessionId":"dead-beef","exitCode":1,"usage":{"premiumRequests":1,"totalApiDurationMs":500,"sessionDurationMs":1000}}`

const fixtureSessionError = `{"type":"session.error","data":{"errorType":"rate_limit","message":"Rate limit exceeded"},"id":"se-1","timestamp":"2026-04-16T09:00:00.000Z","parentId":"p-1"}`

const fixtureEphemeral = `{"type":"session.mcp_servers_loaded","data":{"servers":[{"name":"github-mcp-server","status":"connected","source":"builtin"}]},"id":"330ac6bb-b2db-435e-8082-686face58a72","timestamp":"2026-04-16T08:43:34.803Z","parentId":"fe20d689-31ec-492c-9eb5-57a0d0834d70","ephemeral":true}`

const fixtureSessionStart = `{"type":"session.start","data":{"sessionId":"35059dc3-d928-4ffb-8616-b78938621d85","selectedModel":"claude-sonnet-4","context":{"cwd":"/tmp"}},"id":"ss-1","timestamp":"2026-04-16T08:43:34.000Z"}`

const fixtureReasoning = `{"type":"assistant.reasoning","data":{"content":"Let me think about this..."},"id":"r-1","timestamp":"2026-04-16T08:43:37.000Z","parentId":"p-1"}`

const fixtureReasoningDelta = `{"type":"assistant.reasoning_delta","data":{"deltaContent":"thinking step"},"id":"rd-1","timestamp":"2026-04-16T08:43:37.100Z","parentId":"p-1","ephemeral":true}`

const fixtureSessionWarning = `{"type":"session.warning","data":{"warningType":"rate_limit_approaching","message":"You are approaching your rate limit"},"id":"sw-1","timestamp":"2026-04-16T09:00:00.000Z","parentId":"p-1"}`

// parseCopilotEvent is a test helper that unmarshals a JSONL line into a copilotEvent.
func parseCopilotEvent(t *testing.T, line string) copilotEvent {
	t.Helper()
	var evt copilotEvent
	if err := json.Unmarshal([]byte(line), &evt); err != nil {
		t.Fatalf("failed to parse fixture: %v", err)
	}
	return evt
}

// ── Parser tests using real JSONL fixtures ──

func TestCopilotParseAssistantMessageDelta(t *testing.T) {
	t.Parallel()
	evt := parseCopilotEvent(t, fixtureAssistantMessageDelta)

	if evt.Type != "assistant.message_delta" {
		t.Fatalf("expected type assistant.message_delta, got %q", evt.Type)
	}
	var delta copilotMessageDelta
	if err := json.Unmarshal(evt.Data, &delta); err != nil {
		t.Fatalf("unmarshal delta data: %v", err)
	}
	if delta.DeltaContent != "pong" {
		t.Fatalf("expected deltaContent 'pong', got %q", delta.DeltaContent)
	}
	if delta.MessageID != "b5148f3f-d24b-4a5e-a95c-2be7d6493a52" {
		t.Fatalf("unexpected messageId: %q", delta.MessageID)
	}
}

func TestCopilotParseAssistantMessage(t *testing.T) {
	t.Parallel()
	evt := parseCopilotEvent(t, fixtureAssistantMessage)

	if evt.Type != "assistant.message" {
		t.Fatalf("expected type assistant.message, got %q", evt.Type)
	}
	var msg copilotAssistantMessage
	if err := json.Unmarshal(evt.Data, &msg); err != nil {
		t.Fatalf("unmarshal message data: %v", err)
	}
	if msg.Content != "pong" {
		t.Fatalf("expected content 'pong', got %q", msg.Content)
	}
	if msg.OutputTokens != 5 {
		t.Fatalf("expected outputTokens 5, got %d", msg.OutputTokens)
	}
	if len(msg.ToolRequests) != 0 {
		t.Fatalf("expected no tool requests, got %d", len(msg.ToolRequests))
	}
}

func TestCopilotParseAssistantMessageWithToolRequests(t *testing.T) {
	t.Parallel()
	evt := parseCopilotEvent(t, fixtureAssistantMessageWithTools)

	var msg copilotAssistantMessage
	if err := json.Unmarshal(evt.Data, &msg); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if len(msg.ToolRequests) != 1 {
		t.Fatalf("expected 1 tool request, got %d", len(msg.ToolRequests))
	}
	tr := msg.ToolRequests[0]
	if tr.Name != "bash" {
		t.Fatalf("expected tool name 'bash', got %q", tr.Name)
	}
	if tr.ToolCallID != "toolu_vrtx_01UqgJdCxuteCRZvKpdjUFyL" {
		t.Fatalf("unexpected toolCallId: %q", tr.ToolCallID)
	}
	var args map[string]any
	if err := json.Unmarshal(tr.Arguments, &args); err != nil {
		t.Fatalf("unmarshal arguments: %v", err)
	}
	if args["command"] != "ls" {
		t.Fatalf("expected command 'ls', got %v", args["command"])
	}
}

func TestCopilotParseToolExecComplete(t *testing.T) {
	t.Parallel()
	evt := parseCopilotEvent(t, fixtureToolExecComplete)

	if evt.Type != "tool.execution_complete" {
		t.Fatalf("expected type tool.execution_complete, got %q", evt.Type)
	}
	var tc copilotToolExecComplete
	if err := json.Unmarshal(evt.Data, &tc); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if !tc.Success {
		t.Fatal("expected success=true")
	}
	if tc.Model != "claude-opus-4.6" {
		t.Fatalf("expected model claude-opus-4.6, got %q", tc.Model)
	}
	if tc.Result == nil || tc.Result.Content != "file1.go\nfile2.go\n" {
		t.Fatalf("unexpected result content: %v", tc.Result)
	}
}

func TestCopilotParseToolExecCompleteError(t *testing.T) {
	t.Parallel()
	evt := parseCopilotEvent(t, fixtureToolExecCompleteError)

	var tc copilotToolExecComplete
	if err := json.Unmarshal(evt.Data, &tc); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if tc.Success {
		t.Fatal("expected success=false")
	}
	if tc.Error == nil || tc.Error.Message != "command not found: foobar" {
		t.Fatalf("unexpected error: %v", tc.Error)
	}
}

func TestCopilotParseResultFractionalPremiumRequests(t *testing.T) {
	t.Parallel()
	// Regression: real Copilot CLI v1.0.32 emits premiumRequests as a float
	// (e.g. 7.5). Decoding into an int field used to fail the entire result
	// line, dropping sessionId and breaking chat-session resume.
	const line = `{"type":"result","timestamp":"2026-04-20T05:34:30.469Z","sessionId":"349793b7-7067-49d4-a807-8788561643bd","exitCode":0,"usage":{"premiumRequests":7.5,"totalApiDurationMs":1500,"sessionDurationMs":5842,"codeChanges":{"linesAdded":0,"linesRemoved":0,"filesModified":[]}}}`
	var evt copilotEvent
	if err := json.Unmarshal([]byte(line), &evt); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if evt.SessionID != "349793b7-7067-49d4-a807-8788561643bd" {
		t.Fatalf("unexpected sessionId: %q", evt.SessionID)
	}
	if evt.Usage == nil || evt.Usage.PremiumRequests != 7.5 {
		t.Fatalf("expected premiumRequests=7.5, got %#v", evt.Usage)
	}
}

func TestCopilotParseResult(t *testing.T) {
	t.Parallel()
	evt := parseCopilotEvent(t, fixtureResult)

	if evt.Type != "result" {
		t.Fatalf("expected type result, got %q", evt.Type)
	}
	if evt.SessionID != "35059dc3-d928-4ffb-8616-b78938621d85" {
		t.Fatalf("unexpected sessionId: %q", evt.SessionID)
	}
	if evt.ExitCode != 0 {
		t.Fatalf("expected exitCode 0, got %d", evt.ExitCode)
	}
	if evt.Usage == nil {
		t.Fatal("expected usage to be present")
	}
	if evt.Usage.PremiumRequests != 3 {
		t.Fatalf("expected 3 premiumRequests, got %v", evt.Usage.PremiumRequests)
	}
	if evt.Usage.TotalAPIDurationMs != 1763 {
		t.Fatalf("expected totalApiDurationMs 1763, got %d", evt.Usage.TotalAPIDurationMs)
	}
}

func TestCopilotParseResultNonZeroExit(t *testing.T) {
	t.Parallel()
	evt := parseCopilotEvent(t, fixtureResultNonZero)

	if evt.ExitCode != 1 {
		t.Fatalf("expected exitCode 1, got %d", evt.ExitCode)
	}
	if evt.SessionID != "dead-beef" {
		t.Fatalf("unexpected sessionId: %q", evt.SessionID)
	}
}

func TestCopilotParseSessionError(t *testing.T) {
	t.Parallel()
	evt := parseCopilotEvent(t, fixtureSessionError)

	if evt.Type != "session.error" {
		t.Fatalf("expected type session.error, got %q", evt.Type)
	}
	var se copilotSessionError
	if err := json.Unmarshal(evt.Data, &se); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if se.ErrorType != "rate_limit" {
		t.Fatalf("expected errorType rate_limit, got %q", se.ErrorType)
	}
	if se.Message != "Rate limit exceeded" {
		t.Fatalf("unexpected message: %q", se.Message)
	}
}

// ── Integration-style tests: feed fixture JSONL through the event loop ──

// simulateCopilotEventLoop feeds JSONL lines through handleCopilotEvent —
// the exact same function used in production — and collects the results.
func simulateCopilotEventLoop(t *testing.T, lines []string) ([]Message, string, string, map[string]TokenUsage) {
	return simulateCopilotEventLoopWithModel(t, lines, "copilot")
}

func simulateCopilotEventLoopWithModel(t *testing.T, lines []string, seedModel string) ([]Message, string, string, map[string]TokenUsage) {
	t.Helper()
	var msgs []Message
	st := newCopilotEventState(seedModel)

	for _, line := range lines {
		var evt copilotEvent
		if err := json.Unmarshal([]byte(line), &evt); err != nil {
			continue
		}
		msgs = append(msgs, handleCopilotEvent(evt, st)...)
	}
	return msgs, st.sessionID, st.finalStatus, st.usage
}

func TestCopilotEventLoopSimpleMessage(t *testing.T) {
	t.Parallel()
	lines := []string{
		fixtureTurnStart,
		fixtureAssistantMessageDelta,
		fixtureAssistantMessage,
		`{"type":"assistant.turn_end","data":{"turnId":"0"},"id":"fc387368","timestamp":"2026-04-16T08:43:38.494Z","parentId":"ddff21bc"}`,
		fixtureResult,
	}

	msgs, sessionID, status, usage := simulateCopilotEventLoop(t, lines)

	if sessionID != "35059dc3-d928-4ffb-8616-b78938621d85" {
		t.Fatalf("unexpected sessionId: %q", sessionID)
	}
	if status != "completed" {
		t.Fatalf("expected completed, got %q", status)
	}

	// Should have: turn_start(status), delta(text:pong), message doesn't re-emit text
	var gotStatus, gotText bool
	for _, m := range msgs {
		if m.Type == MessageStatus && m.Status == "running" {
			gotStatus = true
		}
		if m.Type == MessageText && m.Content == "pong" {
			gotText = true
		}
	}
	if !gotStatus {
		t.Fatal("expected status=running message")
	}
	if !gotText {
		t.Fatal("expected text=pong message")
	}

	u, ok := usage["copilot"]
	if !ok {
		t.Fatal("expected usage entry for 'copilot'")
	}
	if u.OutputTokens != 5 {
		t.Fatalf("expected 5 outputTokens, got %d", u.OutputTokens)
	}
}

func TestCopilotEventLoopToolUseFlow(t *testing.T) {
	t.Parallel()
	lines := []string{
		fixtureTurnStart,
		fixtureAssistantMessageWithTools,
		fixtureToolExecComplete,
		fixtureResult,
	}

	msgs, sessionID, status, usage := simulateCopilotEventLoop(t, lines)

	if sessionID != "35059dc3-d928-4ffb-8616-b78938621d85" {
		t.Fatalf("unexpected sessionId: %q", sessionID)
	}
	if status != "completed" {
		t.Fatalf("expected completed, got %q", status)
	}

	// Find tool use and tool result messages.
	var toolUse, toolResult *Message
	for i := range msgs {
		if msgs[i].Type == MessageToolUse {
			toolUse = &msgs[i]
		}
		if msgs[i].Type == MessageToolResult {
			toolResult = &msgs[i]
		}
	}
	if toolUse == nil {
		t.Fatal("expected MessageToolUse")
	}
	if toolUse.Tool != "bash" {
		t.Fatalf("expected tool 'bash', got %q", toolUse.Tool)
	}
	if toolUse.CallID != "toolu_vrtx_01UqgJdCxuteCRZvKpdjUFyL" {
		t.Fatalf("unexpected callID: %q", toolUse.CallID)
	}
	if toolResult == nil {
		t.Fatal("expected MessageToolResult")
	}
	if toolResult.CallID != toolUse.CallID {
		t.Fatalf("tool result callID %q doesn't match tool use callID %q", toolResult.CallID, toolUse.CallID)
	}
	if !strings.Contains(toolResult.Output, "file1.go") {
		t.Fatalf("expected tool result to contain 'file1.go', got %q", toolResult.Output)
	}

	// After tool.execution_complete with model, activeModel should be updated.
	if _, ok := usage["claude-opus-4.6"]; ok {
		// outputTokens from assistant.message came BEFORE tool.execution_complete,
		// so they should be under "copilot", not "claude-opus-4.6".
		t.Log("model attribution is correct: assistant.message tokens go under initial model")
	}
	u := usage["copilot"]
	if u.OutputTokens != 112 {
		t.Fatalf("expected 112 outputTokens under 'copilot', got %d", u.OutputTokens)
	}
}

func TestCopilotEventLoopToolExecError(t *testing.T) {
	t.Parallel()
	lines := []string{fixtureToolExecCompleteError}

	msgs, _, _, _ := simulateCopilotEventLoop(t, lines)

	var found bool
	for _, m := range msgs {
		if m.Type == MessageToolResult && strings.Contains(m.Output, "command not found: foobar") {
			found = true
		}
	}
	if !found {
		t.Fatal("expected tool result with error message")
	}
}

func TestCopilotEventLoopSessionStartCapturesSessionID(t *testing.T) {
	t.Parallel()
	// session.start arrives but the run is killed (timeout/cancel/crash) before
	// the synthetic "result" line is emitted. We must still report the session
	// id from session.start so the chat-session resume pointer can advance.
	lines := []string{fixtureSessionStart}

	_, sessionID, _, _ := simulateCopilotEventLoop(t, lines)

	if sessionID != "35059dc3-d928-4ffb-8616-b78938621d85" {
		t.Fatalf("expected session id captured from session.start, got %q", sessionID)
	}
}

func TestCopilotEventLoopResultOverridesSessionStart(t *testing.T) {
	t.Parallel()
	// When both session.start and result carry a session id, the result event
	// wins (it is the authoritative end-of-turn record).
	lines := []string{
		fixtureSessionStart,
		// Different sessionId on the result event (defensive: in practice
		// they should match, but the contract is "result wins").
		`{"type":"result","sessionId":"final-id","exitCode":0}`,
	}

	_, sessionID, _, _ := simulateCopilotEventLoop(t, lines)

	if sessionID != "final-id" {
		t.Fatalf("expected result session id to win, got %q", sessionID)
	}
}

func TestCopilotEventLoopResultWithoutSessionIDPreservesSessionStart(t *testing.T) {
	t.Parallel()
	// Defensive: if a result line arrives without a sessionId (older CLI,
	// truncated output), the session.start id must not be wiped.
	lines := []string{
		fixtureSessionStart,
		`{"type":"result","exitCode":0}`,
	}

	_, sessionID, _, _ := simulateCopilotEventLoop(t, lines)

	if sessionID != "35059dc3-d928-4ffb-8616-b78938621d85" {
		t.Fatalf("expected session.start id to be preserved when result has none, got %q", sessionID)
	}
}

func TestCopilotEventLoopNonZeroExit(t *testing.T) {
	t.Parallel()
	lines := []string{fixtureResultNonZero}

	_, sessionID, status, _ := simulateCopilotEventLoop(t, lines)

	if status != "failed" {
		t.Fatalf("expected failed, got %q", status)
	}
	if sessionID != "dead-beef" {
		t.Fatalf("unexpected sessionId: %q", sessionID)
	}
}

func TestCopilotEventLoopSessionErrorSurvivesNonZeroResult(t *testing.T) {
	t.Parallel()
	st := newCopilotEventState("copilot")

	for _, line := range []string{fixtureSessionError, fixtureResultNonZero} {
		var evt copilotEvent
		if err := json.Unmarshal([]byte(line), &evt); err != nil {
			t.Fatal(err)
		}
		handleCopilotEvent(evt, st)
	}

	if st.finalStatus != "failed" {
		t.Fatalf("expected failed, got %q", st.finalStatus)
	}
	if !strings.Contains(st.finalError, "Rate limit exceeded") {
		t.Fatalf("expected session.error detail to survive result event, got %q", st.finalError)
	}
	if !strings.Contains(st.finalError, "copilot exited with code 1") {
		t.Fatalf("expected exit code context, got %q", st.finalError)
	}
}

func TestCopilotEventLoopSessionError(t *testing.T) {
	t.Parallel()
	lines := []string{fixtureSessionError}

	msgs, _, status, _ := simulateCopilotEventLoop(t, lines)

	if status != "failed" {
		t.Fatalf("expected failed, got %q", status)
	}
	var found bool
	for _, m := range msgs {
		if m.Type == MessageLog && m.Level == "error" && m.Content == "Rate limit exceeded" {
			found = true
		}
	}
	if !found {
		t.Fatal("expected error log message")
	}
}

func TestCopilotEventLoopSkipsUnknownTypes(t *testing.T) {
	t.Parallel()
	lines := []string{
		fixtureEphemeral, // session.mcp_servers_loaded — should not produce messages
		`{"type":"session.skills_loaded","data":{},"id":"x","timestamp":"2026-04-16T08:43:34.811Z","parentId":"y","ephemeral":true}`,
		`{"type":"session.tools_updated","data":{"model":"claude-opus-4.6"},"id":"z","timestamp":"2026-04-16T08:43:36.397Z","parentId":"w","ephemeral":true}`,
	}

	msgs, _, _, _ := simulateCopilotEventLoop(t, lines)

	if len(msgs) != 0 {
		t.Fatalf("expected no messages for unknown/ephemeral event types, got %d: %+v", len(msgs), msgs)
	}
}

func TestCopilotEventLoopMultiTurnUsage(t *testing.T) {
	t.Parallel()
	// Simulate: turn 0 has tool use (112 tokens), tool completes with model info,
	// turn 1 has text response (106 tokens) — now under claude-opus-4.6 model.
	lines := []string{
		fixtureTurnStart,
		fixtureAssistantMessageWithTools, // 112 outputTokens, activeModel="copilot"
		fixtureToolExecComplete,          // sets activeModel="claude-opus-4.6"
		`{"type":"assistant.turn_start","data":{"turnId":"1"},"id":"t1","timestamp":"2026-04-16T08:44:01.000Z","parentId":"p1"}`,
		// Turn 1 assistant.message with 106 tokens — should go under "claude-opus-4.6"
		`{"type":"assistant.message","data":{"messageId":"msg-2","content":"Here are the files.","toolRequests":[],"interactionId":"int-1","outputTokens":106},"id":"m2","timestamp":"2026-04-16T08:44:02.000Z","parentId":"t1"}`,
		fixtureResult,
	}

	_, _, _, usage := simulateCopilotEventLoop(t, lines)

	if u := usage["copilot"]; u.OutputTokens != 112 {
		t.Fatalf("expected 112 tokens under 'copilot', got %d", u.OutputTokens)
	}
	if u := usage["claude-opus-4.6"]; u.OutputTokens != 106 {
		t.Fatalf("expected 106 tokens under 'claude-opus-4.6', got %d", u.OutputTokens)
	}
}

func TestCopilotEventLoopSessionStartSetsModel(t *testing.T) {
	t.Parallel()
	lines := []string{
		fixtureSessionStart,
		fixtureTurnStart,
		fixtureAssistantMessage, // 5 outputTokens
		fixtureResult,
	}

	_, _, _, usage := simulateCopilotEventLoop(t, lines)

	// session.start sets selectedModel to "claude-sonnet-4",
	// so tokens should be attributed there, not "copilot".
	if _, ok := usage["copilot"]; ok {
		t.Fatal("expected no tokens under 'copilot' when session.start provides selectedModel")
	}
	u, ok := usage["claude-sonnet-4"]
	if !ok {
		t.Fatal("expected tokens under 'claude-sonnet-4'")
	}
	if u.OutputTokens != 5 {
		t.Fatalf("expected 5 outputTokens, got %d", u.OutputTokens)
	}
}

func TestCopilotEventLoopSeedModelFromOpts(t *testing.T) {
	t.Parallel()
	// No session.start — seed model comes from opts.Model (simulated via seedModel param).
	lines := []string{
		fixtureTurnStart,
		fixtureAssistantMessage, // 5 outputTokens
		fixtureResult,
	}

	_, _, _, usage := simulateCopilotEventLoopWithModel(t, lines, "gpt-4o")

	u, ok := usage["gpt-4o"]
	if !ok {
		t.Fatal("expected tokens under 'gpt-4o'")
	}
	if u.OutputTokens != 5 {
		t.Fatalf("expected 5 outputTokens, got %d", u.OutputTokens)
	}
}

func TestCopilotEventLoopReasoning(t *testing.T) {
	t.Parallel()
	lines := []string{
		fixtureReasoning,
		fixtureReasoningDelta,
	}

	msgs, _, _, _ := simulateCopilotEventLoop(t, lines)

	var thinking []string
	for _, m := range msgs {
		if m.Type == MessageThinking {
			thinking = append(thinking, m.Content)
		}
	}
	if len(thinking) != 2 {
		t.Fatalf("expected 2 thinking messages, got %d: %v", len(thinking), thinking)
	}
	if thinking[0] != "Let me think about this..." {
		t.Fatalf("unexpected reasoning content: %q", thinking[0])
	}
	if thinking[1] != "thinking step" {
		t.Fatalf("unexpected reasoning_delta content: %q", thinking[1])
	}
}

func TestCopilotEventLoopReasoningTextInMessage(t *testing.T) {
	t.Parallel()
	// assistant.message with reasoningText field set.
	lines := []string{
		`{"type":"assistant.message","data":{"messageId":"msg-r","content":"answer","toolRequests":[],"interactionId":"int-r","outputTokens":10,"reasoningText":"I thought carefully"},"id":"mr","timestamp":"2026-04-16T08:44:00.000Z","parentId":"p-1"}`,
	}

	msgs, _, _, _ := simulateCopilotEventLoop(t, lines)

	var gotThinking bool
	for _, m := range msgs {
		if m.Type == MessageThinking && m.Content == "I thought carefully" {
			gotThinking = true
		}
	}
	if !gotThinking {
		t.Fatal("expected MessageThinking from reasoningText in assistant.message")
	}
}

func TestCopilotEventLoopSessionWarning(t *testing.T) {
	t.Parallel()
	lines := []string{fixtureSessionWarning}

	msgs, _, status, _ := simulateCopilotEventLoop(t, lines)

	// Warnings should NOT change finalStatus.
	if status != "completed" {
		t.Fatalf("expected completed, got %q", status)
	}
	var found bool
	for _, m := range msgs {
		if m.Type == MessageLog && m.Level == "warn" && m.Content == "You are approaching your rate limit" {
			found = true
		}
	}
	if !found {
		t.Fatal("expected warn log message for session.warning")
	}
}

func TestCopilotEventLoopDeltaFallbackOutput(t *testing.T) {
	t.Parallel()
	// Only deltas, no assistant.message — simulates process killed mid-stream.
	lines := []string{
		`{"type":"assistant.message_delta","data":{"messageId":"m1","deltaContent":"hello "},"id":"d1","timestamp":"2026-04-16T08:43:38.000Z","parentId":"p1","ephemeral":true}`,
		`{"type":"assistant.message_delta","data":{"messageId":"m1","deltaContent":"world"},"id":"d2","timestamp":"2026-04-16T08:43:38.100Z","parentId":"p1","ephemeral":true}`,
	}

	st := newCopilotEventState("copilot")
	for _, line := range lines {
		var evt copilotEvent
		if err := json.Unmarshal([]byte(line), &evt); err != nil {
			t.Fatal(err)
		}
		handleCopilotEvent(evt, st)
	}

	if st.output.String() != "hello world" {
		t.Fatalf("expected output 'hello world', got %q", st.output.String())
	}
}

func TestCopilotExecuteSurfacesStderrOnNonZeroResult(t *testing.T) {
	t.Parallel()
	if runtime.GOOS == "windows" {
		t.Skip("shell-script fixture is POSIX-only")
	}

	fakePath := filepath.Join(t.TempDir(), "copilot")
	script := "#!/bin/sh\n" +
		"printf '%s\\n' '{\"type\":\"result\",\"sessionId\":\"sess-fail\",\"exitCode\":1}'\n" +
		"echo \"error: authentication failed: refresh token expired\" >&2\n" +
		"exit 1\n"
	writeTestExecutable(t, fakePath, []byte(script))

	backend, err := New("copilot", Config{ExecutablePath: fakePath, Logger: slog.Default()})
	if err != nil {
		t.Fatalf("new copilot backend: %v", err)
	}

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	session, err := backend.Execute(ctx, "prompt-ignored", ExecOptions{Timeout: 5 * time.Second})
	if err != nil {
		t.Fatalf("execute: %v", err)
	}
	go func() {
		for range session.Messages {
		}
	}()

	select {
	case result, ok := <-session.Result:
		if !ok {
			t.Fatal("result channel closed without a value")
		}
		if result.Status != "failed" {
			t.Fatalf("expected status=failed, got %q (error=%q)", result.Status, result.Error)
		}
		if !strings.Contains(result.Error, "copilot exited with code 1") {
			t.Fatalf("expected error to mention exit code, got %q", result.Error)
		}
		if !strings.Contains(result.Error, "refresh token expired") {
			t.Fatalf("expected error to include stderr hint, got %q", result.Error)
		}
		if !strings.Contains(result.Error, "copilot stderr:") {
			t.Fatalf("expected stderr label in error, got %q", result.Error)
		}
	case <-time.After(10 * time.Second):
		t.Fatal("timeout waiting for result")
	}
}

// ── Arg builder tests ──

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

	args := buildCopilotArgs("write a haiku", ExecOptions{}, slog.Default())
	expected := []string{
		"-p", "write a haiku",
		"--output-format", "json",
		"--allow-all",
		"--no-ask-user",
	}

	if len(args) != len(expected) {
		t.Fatalf("expected %d args, got %d: %v", len(expected), len(args), args)
	}
	for i, want := range expected {
		if args[i] != want {
			t.Fatalf("expected args[%d] = %q, got %q", i, want, args[i])
		}
	}
}

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

	args := buildCopilotArgs("hi", ExecOptions{Model: "gpt-4o"}, slog.Default())

	var foundModel bool
	for i, a := range args {
		if a == "--model" {
			if i+1 >= len(args) || args[i+1] != "gpt-4o" {
				t.Fatalf("expected --model followed by gpt-4o, got %v", args)
			}
			foundModel = true
			break
		}
	}
	if !foundModel {
		t.Fatalf("expected --model flag when Model is set, got args=%v", args)
	}
}

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

	args := buildCopilotArgs("hi", ExecOptions{ResumeSessionID: "sess-42"}, slog.Default())

	var foundResume bool
	for i, a := range args {
		if a == "--resume" {
			if i+1 >= len(args) || args[i+1] != "sess-42" {
				t.Fatalf("expected --resume followed by session id, got %v", args)
			}
			foundResume = true
			break
		}
	}
	if !foundResume {
		t.Fatalf("expected --resume flag when ResumeSessionID is set, got args=%v", args)
	}
}

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

	args := buildCopilotArgs("hi", ExecOptions{}, slog.Default())
	for _, a := range args {
		if a == "--model" {
			t.Fatalf("expected no --model flag when Model is empty, got args=%v", args)
		}
		if a == "--resume" {
			t.Fatalf("expected no --resume flag when ResumeSessionID is empty, got args=%v", args)
		}
	}
}

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

	args := buildCopilotArgs("hi", ExecOptions{
		CustomArgs: []string{"--max-turns", "50"},
	}, slog.Default())

	if args[len(args)-2] != "--max-turns" || args[len(args)-1] != "50" {
		t.Fatalf("expected --max-turns 50 at end of args, got %v", args)
	}
}

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

	args := buildCopilotArgs("hi", ExecOptions{
		CustomArgs: []string{"--output-format", "text", "--max-turns", "50"},
	}, slog.Default())

	for i, a := range args {
		if a == "--output-format" && i+1 < len(args) && args[i+1] == "text" {
			t.Fatalf("blocked --output-format text should have been filtered: %v", args)
		}
	}
	found := false
	for i, a := range args {
		if a == "--max-turns" && i+1 < len(args) && args[i+1] == "50" {
			found = true
		}
	}
	if !found {
		t.Fatalf("expected --max-turns 50 to pass through, got %v", args)
	}
}

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

	args := buildCopilotArgs("hi", ExecOptions{
		CustomArgs: []string{"--resume", "bad-session", "--acp", "--yolo"},
	}, slog.Default())

	for _, a := range args {
		if a == "bad-session" {
			t.Fatalf("blocked --resume value should have been filtered: %v", args)
		}
		if a == "--acp" {
			t.Fatalf("blocked --acp should have been filtered: %v", args)
		}
		if a == "--yolo" {
			t.Fatalf("blocked --yolo should have been filtered: %v", args)
		}
	}
}
