package agent

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

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

// contextWindowExceededStubDriver is a minimal driver that implements
// drivercontract.ContextWindowExceededDriver for testing. It does NOT implement
// drivercontract.IdleCompactionDriver, so the process-level recovery path is used.
type contextWindowExceededStubDriver struct {
	exceeded     bool
	encodePrompt bool
}

func (d *contextWindowExceededStubDriver) Name() string                    { return "test-cw" }
func (d *contextWindowExceededStubDriver) SupportsStdinNotification() bool { return true }
func (d *contextWindowExceededStubDriver) MCPToolPrefix() string           { return "mcp__agentteam__" }
func (d *contextWindowExceededStubDriver) Spawn(context.Context, drivercontract.LaunchConfig) (drivercontract.SpawnResult, error) {
	return drivercontract.SpawnResult{}, nil
}
func (d *contextWindowExceededStubDriver) ParseLine(string) []drivercontract.ParsedEvent { return nil }

func (d *contextWindowExceededStubDriver) EncodeStdinMessage(message, _ string) []byte {
	if d.encodePrompt {
		return []byte(message + "\n")
	}
	return []byte("{}\n")
}
func (d *contextWindowExceededStubDriver) IsContextWindowExceeded(ev drivercontract.ParsedEvent) bool {
	return d.exceeded
}

// compactionAndCWStubDriver implements both drivercontract.IdleCompactionDriver and
// drivercontract.ContextWindowExceededDriver. The recovery path should be skipped because
// the driver has its own compaction mechanism.
type compactionAndCWStubDriver struct {
	contextWindowExceededStubDriver
	compactionStarted bool
}

func (d *compactionAndCWStubDriver) StartIdleCompaction(_ context.Context, _ string) (bool, error) {
	d.compactionStarted = true
	return true, nil
}

type queuedContextCompactionStubDriver struct {
	contextWindowExceededStubDriver
	queued            bool
	compactionStarted bool
}

func (d *queuedContextCompactionStubDriver) QueueContextCompaction(ev drivercontract.ParsedEvent) bool {
	if !d.IsContextWindowExceeded(ev) {
		return false
	}
	d.queued = true
	return true
}

func (d *queuedContextCompactionStubDriver) StartIdleCompaction(_ context.Context, _ string) (bool, error) {
	if !d.queued {
		return false, nil
	}
	d.queued = false
	d.compactionStarted = true
	return true, nil
}

func parsedEventKinds(events []drivercontract.ParsedEvent) []string {
	kinds := make([]string, 0, len(events))
	for _, ev := range events {
		kinds = append(kinds, ev.Kind)
	}
	return kinds
}

// newTestProcessForRecovery creates a minimal Process suitable for testing
// context recovery. It provides a non-nil cancel func so Sleep() does not panic.
func newTestProcessForRecovery(agentID, sessionID string, driver drivercontract.Driver) *Process {
	_, cancel := context.WithCancel(context.Background())
	return &Process{
		AgentID:   agentID,
		SessionID: sessionID,
		processRuntimeBinding: processRuntimeBinding{driver: driver,
			cancel: cancel}}

}

func TestProcessUsageEventEmitsCallback(t *testing.T) {
	driver := &contextWindowExceededStubDriver{}
	ap := newTestProcessForRecovery("agent-usage", "sess-usage", driver)
	var events []drivercontract.ParsedEvent
	ap.onEvent = func(ev drivercontract.ParsedEvent) {
		events = append(events, ev)
	}

	ap.handleParsedEvent(drivercontract.ParsedEvent{
		Kind:              consts.ParsedEventKindUsage,
		InputTokens:       12,
		CachedInputTokens: 3,
		OutputTokens:      7,
		Model:             "model-usage",
	})

	require.Len(t, events, 1)
	require.Equal(t, consts.ParsedEventKindUsage, events[0].Kind)
	require.Equal(t, "sess-usage", events[0].SessionID)
	require.Equal(t, uint64(12), events[0].InputTokens)
	require.Equal(t, uint64(3), events[0].CachedInputTokens)
	require.Equal(t, uint64(7), events[0].OutputTokens)
}

func TestProcessContextUsageSnapshotWithoutRequestDeltaEmitsCallback(t *testing.T) {
	driver := &contextWindowExceededStubDriver{}
	ap := newTestProcessForRecovery("agent-usage", "sess-usage", driver)
	var events []drivercontract.ParsedEvent
	ap.onEvent = func(ev drivercontract.ParsedEvent) {
		events = append(events, ev)
	}

	ap.handleParsedEvent(drivercontract.ParsedEvent{
		Kind:                consts.ParsedEventKindUsage,
		ActiveContextTokens: 48_000,
		ContextWindowTokens: 258_400,
	})

	require.Len(t, events, 1)
	require.Equal(t, "sess-usage", events[0].SessionID)
	require.Equal(t, uint64(48_000), events[0].ActiveContextTokens)
	require.Equal(t, uint64(258_400), events[0].ContextWindowTokens)
}

func TestContextRecovery_TriggersSleepOnTurnEndWithMaxTokens(t *testing.T) {
	driver := &contextWindowExceededStubDriver{exceeded: true}

	var runtimeEvents []RuntimeEvent
	ap := newTestProcessForRecovery("agent-cw", "sess-cw-1", driver)
	ap.onRuntimeEvent = func(ev RuntimeEvent) {
		runtimeEvents = append(runtimeEvents, ev)
	}

	ap.handleParsedEvent(drivercontract.ParsedEvent{
		Kind:       consts.ParsedEventKindTurnEnd,
		StopReason: consts.ParsedEventStopReasonMaxTokens,
	})

	require.True(t, ap.stopping, "process should be stopping after context recovery")
	require.True(t, ap.sleeping, "process should be sleeping after context recovery")
	require.False(t, ap.isIdle, "process should not be idle during recovery")
	require.Len(t, runtimeEvents, 1)
	require.Equal(t, consts.RuntimeEventClassContextWindowExceeded, runtimeEvents[0].EventClass)
}

func TestContextRecoveryPreservesAllWakeTargetsBeforeTerminalization(t *testing.T) {
	driver := &contextWindowExceededStubDriver{exceeded: true}
	ap := newTestProcessForRecovery("agent-cw-targets", "sess-cw-targets", driver)
	ap.pendingWake = &pendingDelivery{Wake: WakeInfo{MessageID: "msg-channel-thread", Target: "#ops:aaa111", MessageType: "channel_message"}, Status: pendingDeliveryAwaitingCheck}
	ap.pendingWakeToken = "wake-channel-thread"
	ap.pendingDeliveryQueue = []pendingDelivery{{
		Wake:  WakeInfo{MessageID: "msg-dm-thread", Target: "dm:@alice:bbb222", MessageType: "direct_message"},
		Token: "wake-dm-thread",
	}}

	var wakeEvents []WakeStatusEvent
	ap.onWakeStatus = func(ev WakeStatusEvent) {
		wakeEvents = append(wakeEvents, ev)
	}

	ap.handleParsedEvent(drivercontract.ParsedEvent{
		Kind:       consts.ParsedEventKindTurnEnd,
		StopReason: consts.ParsedEventStopReasonMaxTokens,
	})

	require.Len(t, wakeEvents, 2)
	require.Equal(t, "msg-channel-thread", wakeEvents[0].MessageID)
	require.Equal(t, "#ops:aaa111", wakeEvents[0].Target)
	require.Equal(t, consts.WakeStatusCompletedWithoutVisibleReply, wakeEvents[0].Status)
	require.Equal(t, "msg-dm-thread", wakeEvents[1].MessageID)
	require.Equal(t, "dm:@alice:bbb222", wakeEvents[1].Target)
	require.Equal(t, consts.WakeStatusCompletedWithoutVisibleReply, wakeEvents[1].Status)

	needed, wakes := ap.ContextRecoveryNeeded()
	require.True(t, needed)
	require.Equal(t, []WakeInfo{
		{MessageID: "msg-channel-thread", Target: "#ops:aaa111", MessageType: "channel_message"},
		{MessageID: "msg-dm-thread", Target: "dm:@alice:bbb222", MessageType: "direct_message"},
	}, wakes)

	wakes[0].Target = "#mutated"
	_, storedWakes := ap.ContextRecoveryNeeded()
	require.Equal(t, "#ops:aaa111", storedWakes[0].Target, "recovery snapshot must not expose mutable process state")
}

func TestContextRecoveryCapturesWakeAfterFinalVisibleReplyCheck(t *testing.T) {
	driver := &contextWindowExceededStubDriver{exceeded: true, encodePrompt: true}
	stdin := &recordingWriteCloser{}
	ap := newTestProcessForRecovery("agent-cw-final-check", "sess-cw-final-check", driver)
	ap.stdinPipe = stdin
	ap.pendingWake = &pendingDelivery{Wake: WakeInfo{MessageID: "msg-final-check", Target: "dm:@alice:final123", MessageType: "direct_message"}, Status: pendingDeliveryAwaitingCheck}
	ap.pendingWakeToken = "wake-final-check"

	turnEnd := drivercontract.ParsedEvent{
		Kind:       consts.ParsedEventKindTurnEnd,
		StopReason: consts.ParsedEventStopReasonMaxTokens,
	}
	ap.handleParsedEvent(turnEnd)

	require.False(t, ap.stopping, "the first boundary only injects the final delivery check")
	require.Contains(t, stdin.String(), "Final delivery check")
	needed, wakes := ap.ContextRecoveryNeeded()
	require.False(t, needed)
	require.Empty(t, wakes)

	ap.handleParsedEvent(turnEnd)

	require.True(t, ap.stopping)
	needed, wakes = ap.ContextRecoveryNeeded()
	require.True(t, needed)
	require.Equal(t, []WakeInfo{{
		MessageID:   "msg-final-check",
		Target:      "dm:@alice:final123",
		MessageType: "direct_message",
	}}, wakes)
}

func TestContextRecovery_TriggersSleepOnErrorEventFollowedByTurnEnd(t *testing.T) {
	driver := &contextWindowExceededStubDriver{exceeded: true}

	var runtimeEvents []RuntimeEvent
	ap := newTestProcessForRecovery("agent-cw", "sess-cw-2", driver)
	ap.pendingWake = &pendingDelivery{Wake: WakeInfo{MessageID: "msg-error-channel", Target: "#ops:error111", MessageType: "channel_message"}, Status: pendingDeliveryAwaitingCheck}
	ap.pendingWakeToken = "wake-error-channel"
	ap.pendingDeliveryQueue = []pendingDelivery{{
		Wake:  WakeInfo{MessageID: "msg-error-dm", Target: "dm:@alice:error222", MessageType: "direct_message"},
		Token: "wake-error-dm",
	}}
	ap.onRuntimeEvent = func(ev RuntimeEvent) {
		runtimeEvents = append(runtimeEvents, ev)
	}

	// First, deliver an error event that sets contextWindowExceeded.
	ap.handleParsedEvent(drivercontract.ParsedEvent{
		Kind:    consts.ParsedEventKindError,
		Message: "prompt is too long",
	})
	require.True(t, ap.contextWindowExceeded)
	ap.handleParsedEvent(drivercontract.ParsedEvent{
		Kind:    consts.ParsedEventKindError,
		Message: "prompt is still too long",
	})

	// Then, turn_end triggers the recovery.
	ap.handleParsedEvent(drivercontract.ParsedEvent{Kind: consts.ParsedEventKindTurnEnd})

	require.True(t, ap.stopping)
	require.True(t, ap.sleeping)
	require.Len(t, runtimeEvents, 2)
	require.Equal(t, consts.RuntimeEventClassWakeFailed, runtimeEvents[0].EventClass)
	require.Equal(t, consts.RuntimeEventClassContextWindowExceeded, runtimeEvents[1].EventClass)
	needed, wakes := ap.ContextRecoveryNeeded()
	require.True(t, needed)
	require.Equal(t, []WakeInfo{
		{MessageID: "msg-error-channel", Target: "#ops:error111", MessageType: "channel_message"},
		{MessageID: "msg-error-dm", Target: "dm:@alice:error222", MessageType: "direct_message"},
	}, wakes)
}

func TestClaudeContextWindowResultTriggersFreshSessionRecovery(t *testing.T) {
	driver := &ClaudeDriver{}
	ap := newTestProcessForRecovery("agent-claude-cw", "sess-claude-cw", driver)
	ap.pendingWake = &pendingDelivery{Wake: WakeInfo{
		MessageID:   "msg-claude-cw",
		Target:      "#ops:context123",
		MessageType: "channel_message",
	}, Status: pendingDeliveryAwaitingCheck}
	ap.pendingWakeToken = "wake-claude-cw"

	var runtimeEvents []RuntimeEvent
	ap.onRuntimeEvent = func(ev RuntimeEvent) {
		runtimeEvents = append(runtimeEvents, ev)
	}

	// This is the real Claude stream-json shape observed in the incident:
	// is_error is true, errors is absent, and the provider detail is carried
	// by the top-level result string.
	parsed := driver.ParseLine(`{"type":"result","subtype":"success","is_error":true,"session_id":"sess-claude-cw","result":"API Error: The model has reached its context window limit."}`)
	require.Len(t, parsed, 2)
	require.Equal(t, consts.ParsedEventKindError, parsed[0].Kind)
	require.Equal(t, consts.ParsedEventKindTurnEnd, parsed[1].Kind)

	for _, ev := range parsed {
		ap.handleParsedEvent(ev)
	}

	require.True(t, ap.stopping)
	require.True(t, ap.sleeping)
	require.Contains(t, runtimeEvents, RuntimeEvent{
		EventClass: consts.RuntimeEventClassContextWindowExceeded,
		Detail:     "Context window exceeded; triggering graceful session restart",
	})
	needed, wakes := ap.ContextRecoveryNeeded()
	require.True(t, needed)
	require.Equal(t, []WakeInfo{{
		MessageID:   "msg-claude-cw",
		Target:      "#ops:context123",
		MessageType: "channel_message",
	}}, wakes)
}

func TestClaudeContextWindowResultCompactsAndContinuesSameSession(t *testing.T) {
	driver := &ClaudeDriver{}
	stdin := &recordingWriteCloser{}
	driver.stdinPipe = stdin
	ap := newTestProcessForRecovery("agent-claude-compact", "sess-claude-compact", driver)
	ap.stdinPipe = stdin
	ap.pendingWake = &pendingDelivery{Wake: WakeInfo{
		MessageID:   "msg-claude-compact",
		Target:      "#ops:c01a2b3c",
		MessageType: "channel_message",
	}, Status: pendingDeliveryAwaitingCheck}
	ap.pendingWakeToken = "wake-claude-compact"
	store := &memoryDeliveryOwnershipStore{records: map[string]DeliveryOwnershipRecord{
		"msg-claude-compact": {
			Message: types.InboxMessage{
				MessageID: "msg-claude-compact",
				Target:    "#ops:c01a2b3c",
			},
			Input:     drivercontract.RuntimeInput{ID: "msg-claude-compact"},
			SessionID: "sess-claude-compact",
			State:     DeliveryOwnershipTerminal,
			Outcome:   DeliveryOutcomeAcked,
		},
	}}
	require.NoError(t, ap.SetDeliveryOwnershipStore(store))

	for _, ev := range driver.ParseLine(
		`{"type":"result","subtype":"success","is_error":true,"session_id":"sess-claude-compact","result":"API Error: The model has reached its context window limit."}`,
	) {
		ap.handleParsedEvent(ev)
	}

	require.Contains(t, stdin.String(), claudeCompactCommand)
	require.False(t, ap.sleeping, "successful compact start must not discard the exhausted session")
	require.NotNil(t, ap.pendingWake)
	require.Equal(t, "msg-claude-compact", ap.pendingWake.Wake.MessageID)
	require.Equal(t, "#ops:c01a2b3c", ap.pendingWake.Wake.Target)

	for _, line := range []string{
		`{"type":"system","subtype":"compact_boundary","session_id":"sess-claude-compact"}`,
		`{"type":"result","subtype":"success","session_id":"sess-claude-compact"}`,
	} {
		for _, ev := range driver.ParseLine(line) {
			ap.handleParsedEvent(ev)
		}
	}

	lines := strings.Split(strings.TrimSpace(stdin.String()), "\n")
	require.Len(t, lines, 2, "compact and continuation must be the only two writes")
	for _, line := range lines {
		var input struct {
			SessionID string `json:"session_id"`
			Message   struct {
				Content []struct {
					Text string `json:"text"`
				} `json:"content"`
			} `json:"message"`
		}
		require.NoError(t, json.Unmarshal([]byte(line), &input))
		require.Equal(t, "sess-claude-compact", input.SessionID)
	}
	require.Contains(t, lines[0], claudeCompactCommand)
	require.Contains(t, lines[1], contextCompactionContinuationPrompt)
	require.False(t, ap.sleeping)
	require.False(t, ap.stopping)
	needed, wakes := ap.ContextRecoveryNeeded()
	require.False(t, needed)
	require.Empty(t, wakes)
	require.NotNil(t, ap.pendingWake, "the same-session continuation retains the original reply obligation")
	require.Equal(t, "msg-claude-compact", ap.pendingWake.Wake.MessageID)
	require.Equal(t, "#ops:c01a2b3c", ap.pendingWake.Wake.Target)
	record, ok := store.record("msg-claude-compact")
	require.True(t, ok)
	require.True(t, record.IsAcked(), "same-session compaction must not resurrect terminal ownership")
}

func TestClaudeOutputTruncationContinuesInSameSession(t *testing.T) {
	driver := &ClaudeDriver{}
	stdin := &recordingWriteCloser{}
	ap := newTestProcessForRecovery("agent-claude-output", "sess-claude-output", driver)
	ap.stdinPipe = stdin
	ap.pendingWake = &pendingDelivery{Wake: WakeInfo{
		MessageID:   "msg-claude-output",
		Target:      "#ops:output123",
		MessageType: "channel_message",
	}, Status: pendingDeliveryAwaitingCheck}
	ap.pendingWakeToken = "wake-claude-output"

	parsed := driver.ParseLine(`{"type":"result","subtype":"success","session_id":"sess-claude-output","message":{"stop_reason":"max_tokens"}}`)
	require.Len(t, parsed, 1)
	require.Equal(t, consts.ParsedEventKindTurnEnd, parsed[0].Kind)
	ap.handleParsedEvent(parsed[0])

	require.False(t, ap.stopping)
	require.False(t, ap.sleeping)
	require.False(t, ap.contextWindowExceeded)
	require.Contains(t, stdin.String(), outputTruncationContinuationPrompt)
	require.Contains(t, stdin.String(), "available tools to retrieve the recent message history for that exact thread")
	require.NotContains(t, stdin.String(), "message_history_read")
	require.Contains(t, stdin.String(), "do not guess")
	require.Contains(t, stdin.String(), `"session_id":"sess-claude-output"`)
	require.NotNil(t, ap.pendingWake, "same-session continuation must preserve the original wake obligation")
}

func TestClaudeTextOutputTruncationContinuesInSameSession(t *testing.T) {
	driver := &ClaudeDriver{}
	stdin := &recordingWriteCloser{}
	ap := newTestProcessForRecovery("agent-claude-output-text", "sess-claude-output-text", driver)
	ap.stdinPipe = stdin

	parsed := driver.ParseLine(`{"type":"result","subtype":"success","is_error":true,"session_id":"sess-claude-output-text","errors":["Response truncated (finish_reason='length') - model hit max output tokens."]}`)
	require.Len(t, parsed, 2)
	for _, ev := range parsed {
		ap.handleParsedEvent(ev)
	}

	require.False(t, ap.stopping)
	require.Empty(t, ap.runtimeErrorDetail)
	require.Contains(t, stdin.String(), outputTruncationContinuationPrompt)
}

func TestContextRecovery_SkippedWhenDriverHasCompaction(t *testing.T) {
	driver := &compactionAndCWStubDriver{}
	driver.exceeded = true

	var runtimeEvents []RuntimeEvent
	ap := newTestProcessForRecovery("agent-comp-cw", "sess-comp-1", driver)
	ap.onRuntimeEvent = func(ev RuntimeEvent) {
		runtimeEvents = append(runtimeEvents, ev)
	}

	ap.handleParsedEvent(drivercontract.ParsedEvent{
		Kind:       consts.ParsedEventKindTurnEnd,
		StopReason: consts.ParsedEventStopReasonMaxTokens,
	})

	// The driver has drivercontract.IdleCompactionDriver, so context recovery should be skipped
	// and idle compaction should be triggered instead.
	require.False(t, ap.stopping, "process should not be stopping when driver has compaction")
	require.True(t, driver.compactionStarted, "idle compaction should be started")
	require.Len(t, runtimeEvents, 0, "no context_window_exceeded event should be emitted")
}

func TestContextCompactionInjectsContinuationAfterRuntimeIdle(t *testing.T) {
	driver := &queuedContextCompactionStubDriver{
		contextWindowExceededStubDriver: contextWindowExceededStubDriver{
			exceeded:     true,
			encodePrompt: true,
		},
	}
	stdin := &recordingWriteCloser{}
	var events []drivercontract.ParsedEvent
	var wakeEvents []WakeStatusEvent
	ap := newTestProcessForRecovery("agent-context-compact", "sess-context-compact", driver)
	ap.stdinPipe = stdin
	ap.pendingWake = &pendingDelivery{Wake: WakeInfo{MessageID: "msg-context", Target: "#agentteam:c49dbc4c", MessageType: "channel_message"}, Status: pendingDeliveryAwaitingCheck}
	ap.pendingWakeToken = "wake-context"
	ap.processRuntimeBinding.onEvent = func(ev drivercontract.ParsedEvent) {
		events = append(events, ev)
	}
	ap.onWakeStatus = func(ev WakeStatusEvent) {
		wakeEvents = append(wakeEvents, ev)
	}

	ap.handleParsedEvent(drivercontract.ParsedEvent{
		Kind:    consts.ParsedEventKindError,
		Message: "API Error: The model has reached its context window limit.",
	})
	ap.handleParsedEvent(drivercontract.ParsedEvent{Kind: consts.ParsedEventKindTurnEnd})

	require.True(t, driver.compactionStarted)
	require.False(t, ap.stopping)
	require.False(t, ap.sleeping)
	require.NotNil(t, ap.pendingWake, "same-session compaction must preserve the original wake obligation")
	require.Empty(t, wakeEvents, "the interrupted wake must not be terminalized before compaction continuation")
	require.Contains(t, parsedEventKinds(events), consts.ParsedEventKindRuntimeCompacting)

	ap.handleParsedEvent(drivercontract.ParsedEvent{
		Kind:      consts.ParsedEventKindRuntimeIdle,
		SessionID: "sess-context-compact",
	})

	require.Contains(t, stdin.String(), contextCompactionContinuationPrompt)
	require.NotNil(t, ap.pendingWake, "continuation turn still owns the original wake")
	require.False(t, ap.isIdle)
}

func TestNonterminalContextDiagnosticWaitsForTerminalBoundaryBeforeRecovery(t *testing.T) {
	newDriver := func() *queuedContextCompactionStubDriver {
		return &queuedContextCompactionStubDriver{
			contextWindowExceededStubDriver: contextWindowExceededStubDriver{
				exceeded:     true,
				encodePrompt: true,
			},
		}
	}
	diagnostic := drivercontract.ParsedEvent{
		Kind:    consts.ParsedEventKindError,
		Message: "Input exceeds context window of this model",
		Metadata: map[string]any{
			runtimeErrorNonterminalMetadataKey: true,
		},
	}

	recoveredDriver := newDriver()
	recovered := newTestProcessForRecovery("agent-context-auto-recovered", "sess-context-auto-recovered", recoveredDriver)
	recovered.handleParsedEvent(diagnostic)
	require.False(t, recovered.contextWindowExceeded)
	require.False(t, recoveredDriver.queued)
	recoveredDriver.exceeded = false
	recovered.handleParsedEvent(drivercontract.ParsedEvent{Kind: consts.ParsedEventKindTurnEnd})
	require.False(t, recoveredDriver.compactionStarted, "a recovered runtime turn must not be compacted a second time")
	require.False(t, recovered.sleeping)

	terminalDriver := newDriver()
	terminal := newTestProcessForRecovery("agent-context-terminal", "sess-context-terminal", terminalDriver)
	terminal.handleParsedEvent(diagnostic)
	terminal.handleParsedEvent(drivercontract.ParsedEvent{
		Kind:    consts.ParsedEventKindError,
		Message: diagnostic.Message,
	})
	require.True(t, terminal.contextWindowExceeded)
	require.True(t, terminalDriver.queued)
	terminalDriver.exceeded = false
	terminal.handleParsedEvent(drivercontract.ParsedEvent{Kind: consts.ParsedEventKindTurnEnd})
	require.True(t, terminalDriver.compactionStarted, "the terminal error at idle must start AgentTeam native compaction")
	require.False(t, terminal.sleeping)
}

func TestContextCompactionInjectsContinuationAfterTurnEnd(t *testing.T) {
	driver := &queuedContextCompactionStubDriver{
		contextWindowExceededStubDriver: contextWindowExceededStubDriver{
			exceeded:     true,
			encodePrompt: true,
		},
	}
	stdin := &recordingWriteCloser{}
	var wakeEvents []WakeStatusEvent
	ap := newTestProcessForRecovery("agent-context-compact-turn-end", "sess-context-compact-turn-end", driver)
	ap.stdinPipe = stdin
	ap.pendingWake = &pendingDelivery{Wake: WakeInfo{MessageID: "msg-context", Target: "#agentteam:c49dbc4c", MessageType: "channel_message"}, Status: pendingDeliveryAwaitingCheck}
	ap.pendingWakeToken = "wake-context"
	ap.onWakeStatus = func(ev WakeStatusEvent) {
		wakeEvents = append(wakeEvents, ev)
	}

	ap.handleParsedEvent(drivercontract.ParsedEvent{
		Kind:    consts.ParsedEventKindError,
		Message: "API Error: The model has reached its context window limit.",
	})
	ap.handleParsedEvent(drivercontract.ParsedEvent{Kind: consts.ParsedEventKindTurnEnd})

	driver.exceeded = false
	ap.handleParsedEvent(drivercontract.ParsedEvent{
		Kind:      consts.ParsedEventKindTurnEnd,
		SessionID: "sess-context-compact-turn-end",
		Metadata:  map[string]any{contextCompactionConfirmedMetadataKey: true},
	})

	require.Contains(t, stdin.String(), contextCompactionContinuationPrompt)
	require.NotNil(t, ap.pendingWake, "continuation turn still owns the original wake")
	require.Empty(t, wakeEvents, "compaction completion must not terminalize the original wake")
	require.False(t, ap.isIdle)
}

func TestContextCompactionFailureFallsBackToFreshSessionRecovery(t *testing.T) {
	driver := &queuedContextCompactionStubDriver{
		contextWindowExceededStubDriver: contextWindowExceededStubDriver{
			exceeded:     true,
			encodePrompt: true,
		},
	}
	var runtimeEvents []RuntimeEvent
	var wakeEvents []WakeStatusEvent
	ap := newTestProcessForRecovery("agent-context-compact-fail", "sess-context-compact-fail", driver)
	ap.pendingWake = &pendingDelivery{Wake: WakeInfo{MessageID: "msg-context", Target: "#agentteam:c49dbc4c", MessageType: "channel_message"}, Status: pendingDeliveryAwaitingCheck}
	ap.pendingWakeToken = "wake-context"
	ap.onRuntimeEvent = func(ev RuntimeEvent) {
		runtimeEvents = append(runtimeEvents, ev)
	}
	ap.onWakeStatus = func(ev WakeStatusEvent) {
		wakeEvents = append(wakeEvents, ev)
	}

	ap.handleParsedEvent(drivercontract.ParsedEvent{
		Kind:    consts.ParsedEventKindError,
		Message: "API Error: The model has reached its context window limit.",
	})
	ap.handleParsedEvent(drivercontract.ParsedEvent{Kind: consts.ParsedEventKindTurnEnd})

	driver.exceeded = false
	openCode := &OpenCodeDriver{translatedLines: make(chan string, 2)}
	openCode.emitSummarizeError(
		"sess-context-compact-fail",
		false,
		fmt.Errorf("summarize: status 500: summarize unavailable"),
	)
	var failureEvents []drivercontract.ParsedEvent
	for range 2 {
		failureEvents = append(failureEvents, openCode.ParseLine(<-openCode.translatedLines)...)
	}
	require.Equal(t, []string{
		consts.ParsedEventKindError,
		consts.ParsedEventKindTurnEnd,
	}, parsedEventKinds(failureEvents), "the driver must publish the complete recovery boundary")
	for _, event := range failureEvents {
		ap.handleParsedEvent(event)
	}

	require.True(t, ap.sleeping, "failed same-session compaction should fall back to fresh-session recovery")
	require.True(t, ap.contextWindowExceeded)
	require.False(t, ap.contextCompactionContinuation)
	require.Len(t, wakeEvents, 1)
	require.Equal(t, consts.WakeStatusCompletedWithoutVisibleReply, wakeEvents[0].Status)
	require.Len(t, runtimeEvents, 1)
	require.Equal(t, consts.RuntimeEventClassContextWindowExceeded, runtimeEvents[0].EventClass)
}

func TestContextCompactionRuntimeIdleContinuationFailureTriggersFreshSessionRecovery(t *testing.T) {
	driver := &queuedContextCompactionStubDriver{
		contextWindowExceededStubDriver: contextWindowExceededStubDriver{
			exceeded:     true,
			encodePrompt: true,
		},
	}
	ap := newTestProcessForRecovery("agent-rt-idle-continue-fail", "sess-rt-idle-continue-fail", driver)
	ap.pendingWake = &pendingDelivery{Wake: WakeInfo{MessageID: "msg-rt-idle-continue-fail", Target: "#agentteam:c49dbc4c", MessageType: "channel_message"}, Status: pendingDeliveryAwaitingCheck}
	ap.pendingWakeToken = "wake-rt-idle-continue-fail"

	// Overflow error + turn_end queues and starts same-session compaction.
	ap.handleParsedEvent(drivercontract.ParsedEvent{
		Kind:    consts.ParsedEventKindError,
		Message: "API Error: The model has reached its context window limit.",
	})
	ap.handleParsedEvent(drivercontract.ParsedEvent{Kind: consts.ParsedEventKindTurnEnd})
	require.True(t, driver.compactionStarted)

	// Compaction succeeds via runtime_idle, but continuation injection fails
	// (no stdin pipe and the stub has no RuntimeInputDeliveryDriver). It must
	// fall back to fresh-session recovery rather than returning to idle.
	ap.handleParsedEvent(drivercontract.ParsedEvent{
		Kind:      consts.ParsedEventKindRuntimeIdle,
		SessionID: "sess-rt-idle-continue-fail",
	})

	require.True(t, ap.sleeping, "continuation injection failure after runtime_idle compaction must trigger fresh-session recovery")
	require.True(t, ap.contextWindowExceeded)
}

func TestContextRecovery_SkippedWhenNotExceeded(t *testing.T) {
	driver := &contextWindowExceededStubDriver{exceeded: false}

	var runtimeEvents []RuntimeEvent
	ap := newTestProcessForRecovery("agent-cw", "sess-cw-3", driver)
	ap.onRuntimeEvent = func(ev RuntimeEvent) {
		runtimeEvents = append(runtimeEvents, ev)
	}

	ap.handleParsedEvent(drivercontract.ParsedEvent{
		Kind:       consts.ParsedEventKindTurnEnd,
		StopReason: consts.ParsedEventStopReasonEndTurn,
	})

	require.False(t, ap.stopping)
	require.False(t, ap.sleeping)
	require.True(t, ap.isIdle, "process should go idle normally")
	require.Len(t, runtimeEvents, 0)
}

func TestContextRecovery_SkippedWhenStopping(t *testing.T) {
	driver := &contextWindowExceededStubDriver{exceeded: true}

	ap := newTestProcessForRecovery("agent-cw", "sess-cw-4", driver)
	ap.stopping = true // already stopping

	ap.handleParsedEvent(drivercontract.ParsedEvent{
		Kind:       consts.ParsedEventKindTurnEnd,
		StopReason: consts.ParsedEventStopReasonMaxTokens,
	})

	// Should not trigger recovery if already stopping.
	require.False(t, ap.sleeping, "should not sleep if already stopping")
}

func TestRecoveryDisabledSkipsContextRecoveryAndIdleCompaction(t *testing.T) {
	driver := &compactionAndCWStubDriver{}
	driver.exceeded = true

	var runtimeEvents []RuntimeEvent
	ap := newTestProcessForRecovery("agent-execution", "sess-execution", driver)
	ap.disableRecovery = true
	ap.onRuntimeEvent = func(ev RuntimeEvent) {
		runtimeEvents = append(runtimeEvents, ev)
	}

	ap.handleParsedEvent(drivercontract.ParsedEvent{
		Kind:       consts.ParsedEventKindTurnEnd,
		StopReason: consts.ParsedEventStopReasonMaxTokens,
	})

	require.False(t, ap.stopping)
	require.False(t, ap.sleeping)
	require.False(t, driver.compactionStarted)
	require.True(t, ap.isIdle)
	require.Empty(t, runtimeEvents)
}

func TestRecoveryDisabledDoesNotQueueContextCompactionFromError(t *testing.T) {
	driver := &queuedContextCompactionStubDriver{}
	driver.exceeded = true
	ap := newTestProcessForRecovery("agent-disabled-error", "sess-disabled", driver)
	ap.disableRecovery = true

	ap.handleParsedEvent(drivercontract.ParsedEvent{
		Kind:    consts.ParsedEventKindError,
		Message: "context window exceeded",
	})

	require.False(t, driver.queued, "disabled recovery must not leave a driver-local compaction gate behind")
	require.False(t, driver.compactionStarted)
}

func TestClaudeDriver_IsContextWindowExceeded(t *testing.T) {
	d := &ClaudeDriver{}

	require.False(t, d.IsContextWindowExceeded(drivercontract.ParsedEvent{
		Kind:       consts.ParsedEventKindTurnEnd,
		StopReason: consts.ParsedEventStopReasonMaxTokens,
	}))
	require.False(t, d.IsContextWindowExceeded(drivercontract.ParsedEvent{
		Kind:       consts.ParsedEventKindTurnEnd,
		StopReason: consts.ParsedEventStopReasonEndTurn,
	}))
	require.False(t, d.IsContextWindowExceeded(drivercontract.ParsedEvent{
		Kind:       consts.ParsedEventKindTurnEnd,
		StopReason: consts.ParsedEventStopReasonToolUse,
	}))
	require.True(t, d.IsContextWindowExceeded(drivercontract.ParsedEvent{
		Kind:    consts.ParsedEventKindError,
		Message: "prompt is too long: exceeded context window",
	}))
	require.True(t, d.IsContextWindowExceeded(drivercontract.ParsedEvent{
		Kind:    consts.ParsedEventKindError,
		Message: "exceeds the context window of this model",
	}))
	require.False(t, d.IsContextWindowExceeded(drivercontract.ParsedEvent{
		Kind:    consts.ParsedEventKindError,
		Message: "tool not found",
	}))
	require.False(t, d.IsContextWindowExceeded(drivercontract.ParsedEvent{
		Kind:    consts.ParsedEventKindText,
		Message: "some text",
	}))
	require.False(t, d.IsContextWindowExceeded(drivercontract.ParsedEvent{
		Kind:    consts.ParsedEventKindError,
		Message: "",
	}))
}

func TestTransientRuntimeErrorRetriesWithoutFailingWake(t *testing.T) {
	t.Cleanup(setTransientRuntimeRetryDelayFuncForTest(func(int) time.Duration { return 0 }))

	driver := &ackedTurnCountRecoveryStubDriver{ackedRuntimeInputStubDriver: ackedRuntimeInputStubDriver{supports: true}}
	delivered := make(chan struct{}, 1)
	driver.onDeliver = func(drivercontract.RuntimeInput, string) {
		delivered <- struct{}{}
	}
	ap := newTestProcessForRecovery("agent-transient", "sess-1", driver)
	ap.workDir = t.TempDir()
	ap.stdinPipe = noopWriteCloser{}
	ap.pendingWake = &pendingDelivery{Wake: WakeInfo{MessageID: "msg-1", Target: "#general"}, Status: pendingDeliveryAwaitingCheck}
	ap.pendingWakeToken = "wake-token"
	ap.rememberTransientRuntimeRetryInput("msg-1", testRuntimeInputWithConfig(drivercontract.RuntimeInput{ID: "msg-1"}, "retry this"), "sess-1")

	var runtimeEvents []RuntimeEvent
	var wakeEvents []WakeStatusEvent
	ap.onRuntimeEvent = func(ev RuntimeEvent) {
		runtimeEvents = append(runtimeEvents, ev)
	}
	ap.onWakeStatus = func(ev WakeStatusEvent) {
		wakeEvents = append(wakeEvents, ev)
	}

	ap.handleParsedEvent(drivercontract.ParsedEvent{
		Kind:    consts.ParsedEventKindError,
		Message: "Claude API request failed (HTTP 529): overloaded",
		Metadata: map[string]any{
			"transient_runtime_error": true,
			"http_status":             529,
		},
	})

	requireTransientRuntimeRetryDelivery(t, delivered)
	require.Empty(t, wakeEvents)
	require.Len(t, runtimeEvents, 1)
	require.Equal(t, consts.RuntimeEventClassRuntimeRetrying, runtimeEvents[0].EventClass)
	require.Equal(t, "msg-1", driver.lastInput.ID)
	require.Equal(t, "retry this", driver.lastInput.PromptText())
}

func TestTransientRuntimeErrorWaitsForTurnEndBeforeRetry(t *testing.T) {
	t.Cleanup(setTransientRuntimeRetryDelayFuncForTest(func(int) time.Duration { return 0 }))

	driver := &ackedTurnCountRecoveryStubDriver{ackedRuntimeInputStubDriver: ackedRuntimeInputStubDriver{supports: true}}
	delivered := make(chan struct{}, 1)
	driver.onDeliver = func(drivercontract.RuntimeInput, string) {
		delivered <- struct{}{}
	}
	ap := newTestProcessForRecovery("agent-transient", "sess-1", driver)
	ap.workDir = t.TempDir()
	ap.exited = make(chan struct{})
	ap.pendingWake = &pendingDelivery{Wake: WakeInfo{MessageID: "msg-1", Target: "#general"}, Status: pendingDeliveryAwaitingCheck}
	ap.rememberTransientRuntimeRetryInput("msg-1", testRuntimeInputWithConfig(drivercontract.RuntimeInput{ID: "msg-1"}, "retry this"), "sess-1")

	ap.handleParsedEvent(drivercontract.ParsedEvent{
		Kind:    consts.ParsedEventKindError,
		Message: "Claude API request failed (HTTP 529): overloaded",
		Metadata: map[string]any{
			"transient_runtime_error": true,
			"http_status":             529,
		},
	})

	requireNoTransientRuntimeRetryDelivery(t, delivered, 100*time.Millisecond)

	ap.handleParsedEvent(drivercontract.ParsedEvent{Kind: consts.ParsedEventKindTurnEnd})

	requireTransientRuntimeRetryDelivery(t, delivered)
}

func TestTransientRuntimeErrorFailsAfterAttemptLimit(t *testing.T) {
	t.Cleanup(setTransientRuntimeRetryDelayFuncForTest(func(int) time.Duration { return 0 }))

	driver := &ackedTurnCountRecoveryStubDriver{ackedRuntimeInputStubDriver: ackedRuntimeInputStubDriver{supports: true}}
	delivered := make(chan struct{}, transientRuntimeRetryMaxAttempts)
	driver.onDeliver = func(drivercontract.RuntimeInput, string) {
		delivered <- struct{}{}
	}
	ap := newTestProcessForRecovery("agent-transient", "sess-1", driver)
	ap.workDir = t.TempDir()
	ap.pendingWake = &pendingDelivery{Wake: WakeInfo{MessageID: "msg-1", Target: "#general"}, Status: pendingDeliveryAwaitingCheck}
	ap.pendingWakeToken = "wake-token"
	ap.rememberTransientRuntimeRetryInput("msg-1", testRuntimeInputWithConfig(drivercontract.RuntimeInput{ID: "msg-1"}, "retry this"), "sess-1")

	var runtimeEvents []RuntimeEvent
	var wakeEvents []WakeStatusEvent
	ap.onRuntimeEvent = func(ev RuntimeEvent) {
		runtimeEvents = append(runtimeEvents, ev)
	}
	ap.onWakeStatus = func(ev WakeStatusEvent) {
		wakeEvents = append(wakeEvents, ev)
	}

	for i := 0; i < transientRuntimeRetryMaxAttempts; i++ {
		ap.handleParsedEvent(transient529Event())
		ap.handleParsedEvent(drivercontract.ParsedEvent{Kind: consts.ParsedEventKindTurnEnd})
		requireTransientRuntimeRetrySettled(t, ap, delivered)
	}

	ap.handleParsedEvent(transient529Event())

	require.Len(t, wakeEvents, 1)
	require.Equal(t, consts.WakeStatusFailed, wakeEvents[0].Status)
	require.Len(t, runtimeEvents, transientRuntimeRetryMaxAttempts+1)
	require.Equal(t, consts.RuntimeEventClassWakeFailed, runtimeEvents[len(runtimeEvents)-1].EventClass)
}

func TestTransientRuntimeErrorDoesNotRetryWhenRuntimeWillRetry(t *testing.T) {
	driver := &ackedTurnCountRecoveryStubDriver{ackedRuntimeInputStubDriver: ackedRuntimeInputStubDriver{supports: true}}
	ap := newTestProcessForRecovery("agent-transient", "sess-1", driver)
	ap.pendingWake = &pendingDelivery{Wake: WakeInfo{MessageID: "msg-1", Target: "#general"}, Status: pendingDeliveryAwaitingCheck}
	ap.rememberTransientRuntimeRetryInput("msg-1", testRuntimeInputWithConfig(drivercontract.RuntimeInput{ID: "msg-1"}, "retry this"), "sess-1")

	var wakeEvents []WakeStatusEvent
	ap.onWakeStatus = func(ev WakeStatusEvent) {
		wakeEvents = append(wakeEvents, ev)
	}

	ev := transient529Event()
	ev.Metadata["will_retry"] = true
	ap.handleParsedEvent(ev)

	require.Zero(t, driver.deliverCalls)
	require.Empty(t, wakeEvents)
}

func TestTransientRuntimeErrorDoesNotRetryAfterToolCall(t *testing.T) {
	driver := &ackedTurnCountRecoveryStubDriver{ackedRuntimeInputStubDriver: ackedRuntimeInputStubDriver{supports: true}}
	ap := newTestProcessForRecovery("agent-transient", "sess-1", driver)
	ap.pendingWake = &pendingDelivery{Wake: WakeInfo{MessageID: "msg-1", Target: "#general"}, Status: pendingDeliveryAwaitingCheck}
	ap.rememberTransientRuntimeRetryInput("msg-1", testRuntimeInputWithConfig(drivercontract.RuntimeInput{ID: "msg-1"}, "retry this"), "sess-1")

	var wakeEvents []WakeStatusEvent
	ap.onWakeStatus = func(ev WakeStatusEvent) {
		wakeEvents = append(wakeEvents, ev)
	}

	ap.handleParsedEvent(drivercontract.ParsedEvent{Kind: consts.ParsedEventKindToolCall, ToolName: "mcp__agentteam__message_history_read"})
	ap.handleParsedEvent(transient529Event())

	require.Zero(t, driver.deliverCalls)
	require.Len(t, wakeEvents, 1)
	require.Equal(t, consts.WakeStatusFailed, wakeEvents[0].Status)
}

func TestTransientRuntimeErrorDoesNotRetryWithoutRecoveryPoint(t *testing.T) {
	driver := &noPreparedRecoveryStubDriver{ackedRuntimeInputStubDriver: ackedRuntimeInputStubDriver{supports: true}}
	ap := newTestProcessForRecovery("agent-transient", "sess-1", driver)
	ap.pendingWake = &pendingDelivery{Wake: WakeInfo{MessageID: "msg-1", Target: "#general"}, Status: pendingDeliveryAwaitingCheck}
	ap.rememberTransientRuntimeRetryInput("msg-1", testRuntimeInputWithConfig(drivercontract.RuntimeInput{ID: "msg-1"}, "retry this"), "sess-1")

	var wakeEvents []WakeStatusEvent
	ap.onWakeStatus = func(ev WakeStatusEvent) {
		wakeEvents = append(wakeEvents, ev)
	}

	ap.handleParsedEvent(transient529Event())

	require.Zero(t, driver.deliverCalls)
	require.Len(t, wakeEvents, 1)
	require.Equal(t, consts.WakeStatusFailed, wakeEvents[0].Status)
}

func TestTransientRuntimeRetryRedeliveryDoesNotOverwriteRecoveryPoint(t *testing.T) {
	t.Cleanup(setTransientRuntimeRetryDelayFuncForTest(func(int) time.Duration { return 0 }))

	driver := &ackedTurnCountRecoveryStubDriver{ackedRuntimeInputStubDriver: ackedRuntimeInputStubDriver{supports: true}}
	delivered := make(chan struct{}, 1)
	driver.onDeliver = func(drivercontract.RuntimeInput, string) {
		delivered <- struct{}{}
	}
	ap := newTestProcessForRecovery("agent-transient", "sess-1", driver)
	ap.workDir = t.TempDir()
	ap.pendingWake = &pendingDelivery{Wake: WakeInfo{MessageID: "msg-1", Target: "#general"}, Status: pendingDeliveryAwaitingCheck}
	ap.rememberTransientRuntimeRetryInput("msg-1", testRuntimeInputWithConfig(drivercontract.RuntimeInput{ID: "msg-1"}, "retry this"), "sess-1")

	ap.handleParsedEvent(transient529Event())
	ap.handleParsedEvent(drivercontract.ParsedEvent{Kind: consts.ParsedEventKindTurnEnd})

	requireTransientRuntimeRetryDelivery(t, delivered)
	require.Eventually(t, func() bool {
		return ap.transientRuntimeRetryRedeliveryMatches("msg-1")
	}, time.Second, 10*time.Millisecond)
	prepareCalls := driver.prepareCalls

	require.True(t, ap.PrepareRuntimeMessageDelivery(types.InboxMessage{MessageID: "msg-1"}, RuntimeDeliveryPathProtocol))
	require.Equal(t, prepareCalls, driver.prepareCalls)
}

func requireTransientRuntimeRetryDelivery(t *testing.T, delivered <-chan struct{}) {
	t.Helper()
	select {
	case <-delivered:
	case <-time.After(time.Second):
		t.Fatal("timed out waiting for transient runtime retry delivery")
	}
}

func requireNoTransientRuntimeRetryDelivery(t *testing.T, delivered <-chan struct{}, timeout time.Duration) {
	t.Helper()
	select {
	case <-delivered:
		t.Fatal("transient runtime retry delivered before it was expected")
	case <-time.After(timeout):
	}
}

// requireTransientRuntimeRetrySettled waits for an async transient-runtime-retry
// attempt to fully drain — not only to deliver, but to release the delivery gate
// and finish its post-delivery cleanup — so the next synchronous event does not
// race the retry goroutine. Retries are served by a background goroutine that
// waits for turn_end on a 50ms ticker and re-acquires the delivery gate before
// redelivering; advancing the test on the delivery signal alone leaves that
// goroutine mid-flight and makes downstream assertions sensitive to scheduler
// timing (rare wake-status / delivery-timeout flakes). Draining the gate before
// each advance serializes the attempts deterministically.
func requireTransientRuntimeRetrySettled(t *testing.T, ap *Process, delivered <-chan struct{}) {
	t.Helper()
	requireTransientRuntimeRetryDelivery(t, delivered)
	deadline := time.Now().Add(3 * time.Second)
	for {
		if transientRuntimeRetryGateFree(ap) {
			return
		}
		if time.Now().After(deadline) {
			t.Fatal("transient runtime retry goroutine did not release the delivery gate in time")
		}
		time.Sleep(time.Millisecond)
	}
}

// transientRuntimeRetryGateFree reports whether the delivery gate is currently
// unheld. The gate is a write-once, capacity-1 channel initialized lazily on the
// first acquisition, so a nil channel means no goroutine can be holding it.
func transientRuntimeRetryGateFree(ap *Process) bool {
	ch := ap.deliveryGate
	if ch == nil {
		return true
	}
	select {
	case <-ch:
		ch <- struct{}{}
		return true
	default:
		return false
	}
}

func transient529Event() drivercontract.ParsedEvent {
	return drivercontract.ParsedEvent{
		Kind:    consts.ParsedEventKindError,
		Message: "Claude API request failed (HTTP 529): overloaded",
		Metadata: map[string]any{
			"transient_runtime_error": true,
			"http_status":             529,
		},
	}
}

type noPreparedRecoveryStubDriver struct {
	ackedRuntimeInputStubDriver
}

func (d *noPreparedRecoveryStubDriver) PrepareRecoveryPoint() (string, string, bool, error) {
	return "", "", false, nil
}

func (d *noPreparedRecoveryStubDriver) RestoreRecoveryPoint(context.Context, string, string) error {
	return fmt.Errorf("unexpected restore")
}

type noopWriteCloser struct{}

func (noopWriteCloser) Write(p []byte) (int, error) { return len(p), nil }
func (noopWriteCloser) Close() error                { return nil }
