package agent

import (
	"context"
	"errors"
	"io"
	"path/filepath"
	"strconv"
	"strings"
	"testing"
	"time"

	"reasonix/internal/event"
	"reasonix/internal/evidence"
	"reasonix/internal/jobs"
	"reasonix/internal/provider"
	"reasonix/internal/tool"
)

func testTaskContext() context.Context {
	return WithParentSession(context.Background(), "parent-session")
}

// TestTaskToolReturnsSubAgentFinalAnswer runs a task against a mock provider
// that emits a single text turn, and verifies the tool returns that text with a
// transcript reference — sub-agent intermediate state isn't supposed to leak.
func TestTaskToolReturnsSubAgentFinalAnswer(t *testing.T) {
	sub := &mockProvider{name: "sub", chunks: []provider.Chunk{
		{Type: provider.ChunkText, Text: "found 3 callers of Foo"},
		{Type: provider.ChunkDone},
	}}
	parentReg := tool.NewRegistry()
	task := newTestTaskTool(t, sub, parentReg, "test-sys-prompt", "", "", nil)

	out, err := task.Execute(testTaskContext(), []byte(`{"prompt":"find callers of Foo"}`))
	if err != nil {
		t.Fatalf("Execute: %v", err)
	}
	_ = subagentRefFromOutput(t, out)
	if !strings.Contains(out, "found 3 callers of Foo") {
		t.Errorf("got %q, want sub-agent final answer", out)
	}
	if !strings.Contains(out, "To continue this same subagent transcript in a later call, pass this ref as `continue_from`. Start a fresh subagent when the next task is independent.") {
		t.Errorf("got %q, want continuation guidance", out)
	}

	// The sub-agent must have received the prompt as its user message and
	// the configured system prompt at the top — proving the session was
	// fresh, not the parent's.
	if sys := sub.lastReq.Messages[0]; sys.Role != provider.RoleSystem || sys.Content != "test-sys-prompt" {
		t.Errorf("first message = %+v, want system 'test-sys-prompt'", sys)
	}
	if got := lastUser(sub.lastReq); !strings.Contains(got, `<subagent-context event="SubagentStart">`) || !strings.Contains(got, "find callers of Foo") || !strings.Contains(got, completeSubtaskContract) {
		t.Errorf("sub-agent user = %q, want SubagentStart context plus prompt", got)
	}
}

func TestSubagentResultWarnsOnHostDecisionLanguage(t *testing.T) {
	out := GuardSubagentHostDecisionText("等待用户批准后再执行修改")
	if !strings.Contains(out, "Subagent boundary") {
		t.Fatalf("guarded output missing boundary warning:\n%s", out)
	}
	plain := "found 3 callers of Foo"
	if got := GuardSubagentHostDecisionText(plain); got != plain {
		t.Fatalf("plain output changed: %q", got)
	}
}

func TestTaskToolInjectsWorkspaceContextIntoSubagentPrompt(t *testing.T) {
	sub := &mockProvider{name: "sub", chunks: []provider.Chunk{
		{Type: provider.ChunkText, Text: "answer"},
		{Type: provider.ChunkDone},
	}}
	workspace := t.TempDir()
	task := NewTaskTool(sub, nil, tool.NewRegistry(), 20, 0, 0, 0, 0, 0, 0, 0.0, "", "sys", nil, 0, "", "", nil).
		WithTranscripts(NewSubagentStore(t.TempDir()), workspace, "base-model", "base-effort")

	if _, err := task.Execute(testTaskContext(), []byte(`{"prompt":"inspect project"}`)); err != nil {
		t.Fatalf("Execute: %v", err)
	}
	if sys := sub.lastReq.Messages[0]; sys.Role != provider.RoleSystem || sys.Content != "sys" {
		t.Fatalf("system prompt = %+v, want original prompt", sys)
	}
	got := lastUser(sub.lastReq)
	if !strings.Contains(got, `<workspace-context event="SubagentWorkspace">`) ||
		!strings.Contains(got, "Current workspace: "+strconv.Quote(workspace)) ||
		!strings.Contains(got, `prefer "." or relative paths`) ||
		!strings.Contains(got, "inspect project") || !strings.Contains(got, completeSubtaskContract) {
		t.Fatalf("sub-agent user = %q, want workspace context plus prompt", got)
	}
}

func TestTaskToolCancelDuringStuckProviderReturnsPromptly(t *testing.T) {
	task := newTestTaskTool(t, stuckStreamProvider{}, tool.NewRegistry(), "sys", "", "", nil)

	ctx, cancel := context.WithCancel(testTaskContext())
	done := make(chan error, 1)
	go func() {
		_, err := task.Execute(ctx, []byte(`{"prompt":"wait on stuck provider"}`))
		done <- err
	}()

	time.Sleep(50 * time.Millisecond)
	cancel()

	select {
	case err := <-done:
		if err == nil {
			t.Fatal("Execute returned nil after context cancellation")
		}
		if !errors.Is(err, context.Canceled) {
			t.Fatalf("Execute error = %v, want context cancellation", err)
		}
	case <-time.After(500 * time.Millisecond):
		t.Fatal("TaskTool.Execute did not return promptly after cancellation")
	}
}

func TestTaskToolSchemaExposesOnlyContinueFromForPersistence(t *testing.T) {
	task := NewTaskTool(&mockProvider{name: "sub"}, nil, tool.NewRegistry(), 20, 0, 0, 0, 0, 0, 0, 0.0, "", "sys", nil, 0, "", "", nil)
	schema := string(task.Schema())
	if !strings.Contains(schema, `"continue_from"`) {
		t.Fatalf("task schema = %s, want continue_from", schema)
	}
	if strings.Contains(schema, "fork_from") {
		t.Fatalf("task schema = %s, want no fork_from", schema)
	}
}

func TestParallelTasksSchemaDoesNotExposePersistentContinuation(t *testing.T) {
	task := NewTaskTool(&mockProvider{name: "sub"}, nil, tool.NewRegistry(), 20, 0, 0, 0, 0, 0, 0, 0.0, "", "sys", nil, 0, "", "", nil)
	parallel := NewParallelTasksTool(task, tool.NewRegistry())
	schema := string(parallel.Schema())
	if strings.Contains(schema, "continue_from") || strings.Contains(schema, "fork_from") {
		t.Fatalf("parallel_tasks schema = %s, want no persistent continuation fields", schema)
	}
}

func TestTaskToolInheritsReasoningLanguageFromContext(t *testing.T) {
	sub := &mockProvider{name: "sub", chunks: []provider.Chunk{
		{Type: provider.ChunkText, Text: "done"},
		{Type: provider.ChunkDone},
	}}
	task := newTestTaskTool(t, sub, tool.NewRegistry(), "sys", "", "", nil)

	ctx := WithReasoningLanguagePreference(testTaskContext(), "zh")
	if _, err := task.Execute(ctx, []byte(`{"prompt":"inspect auth"}`)); err != nil {
		t.Fatalf("Execute: %v", err)
	}
	got := lastUser(sub.lastReq)
	if !strings.HasPrefix(got, "<reasoning-language>") || !strings.Contains(got, "简体中文") || !strings.Contains(got, "inspect auth") {
		t.Fatalf("sub-agent user = %q, want reasoning-language-prefixed prompt", got)
	}
}

func TestTaskToolPropagatesSubagentImageCandidates(t *testing.T) {
	sub := &mockProvider{name: "sub", chunks: []provider.Chunk{
		{Type: provider.ChunkText, Text: "image received"},
		{Type: provider.ChunkDone},
	}}
	task := newTestTaskTool(t, sub, tool.NewRegistry(), "sys", "", "", nil)
	ctx := WithSubagentImageCandidates(testTaskContext(), []string{"data:image/png;base64,AAAA"})
	if _, err := task.Execute(ctx, []byte(`{"prompt":"inspect the attached image"}`)); err != nil {
		t.Fatalf("Execute: %v", err)
	}
	var images []string
	for _, msg := range sub.lastReq.Messages {
		if msg.Role == provider.RoleUser {
			images = msg.Images
		}
	}
	if len(images) != 1 || images[0] != "data:image/png;base64,AAAA" {
		t.Fatalf("sub-agent images = %v, want the parent candidate", images)
	}
}

// TestTaskToolFiltersTools verifies the whitelist behaviour: when the caller
// names a subset of tools, the sub-agent's registry contains exactly that set
// with recursive delegation tools available while max_subagent_depth leaves one
// more layer.
func TestTaskToolFiltersTools(t *testing.T) {
	sub := &mockProvider{name: "sub", chunks: []provider.Chunk{
		{Type: provider.ChunkText, Text: "ok"},
		{Type: provider.ChunkDone},
	}}
	parentReg := tool.NewRegistry()
	parentReg.Add(fakeTool{name: "read_file", readOnly: true})
	parentReg.Add(fakeTool{name: "write_file", readOnly: false})
	parentReg.Add(fakeTool{name: "bash", readOnly: false})
	task := newTestTaskTool(t, sub, parentReg, "sys", "", "", nil)
	parentReg.Add(task) // simulate the wiring in cli.setup
	parentReg.Add(fakeTool{name: "run_skill", readOnly: false})
	parentReg.Add(fakeTool{name: "read_only_skill", readOnly: true})
	parentReg.Add(fakeTool{name: "research", readOnly: false})

	args := []byte(`{"prompt":"x","tools":["read_file","task","write_file","run_skill","read_only_skill","research"]}`)
	if _, err := task.Execute(testTaskContext(), args); err != nil {
		t.Fatalf("Execute: %v", err)
	}
	// The sub-agent's tool schemas should reflect the whitelist minus always
	// unavailable background/install tools. Recursive tools stay visible at depth 1.
	got := map[string]bool{}
	for _, s := range sub.lastReq.Tools {
		got[s.Name] = true
	}
	for _, want := range []string{"read_file", "write_file", "task", "run_skill", "read_only_skill", "research"} {
		if !got[want] {
			t.Errorf("sub-agent tools = %v, want %q exposed at depth 1", got, want)
		}
	}
	if got["bash"] {
		t.Errorf("sub-agent tools = %v, want bash omitted when not requested", got)
	}
}

// TestTaskToolDefaultsToParentToolsWithDepthRemaining covers the no-whitelist
// path: the first-layer sub-agent inherits parent tools except always-hidden
// background/install tools because it still has one delegation layer available.
func TestTaskToolDefaultsToParentToolsWithoutMetaTools(t *testing.T) {
	sub := &mockProvider{name: "sub", chunks: []provider.Chunk{
		{Type: provider.ChunkText, Text: "ok"},
		{Type: provider.ChunkDone},
	}}
	parentReg := tool.NewRegistry()
	parentReg.Add(fakeTool{name: "read_file", readOnly: true})
	parentReg.Add(fakeTool{name: "grep", readOnly: true})
	task := newTestTaskTool(t, sub, parentReg, "sys", "", "", nil)
	parentReg.Add(task)
	parentReg.Add(fakeTool{name: "run_skill", readOnly: false})
	parentReg.Add(fakeTool{name: "read_only_skill", readOnly: true})
	parentReg.Add(fakeTool{name: "explore", readOnly: false})
	parentReg.Add(fakeTool{name: "research", readOnly: false})
	parentReg.Add(fakeTool{name: "review", readOnly: false})
	parentReg.Add(fakeTool{name: "security_review", readOnly: false})
	parentReg.Add(fakeTool{name: "remember", readOnly: false})

	if _, err := task.Execute(testTaskContext(), []byte(`{"prompt":"x"}`)); err != nil {
		t.Fatalf("Execute: %v", err)
	}
	got := map[string]bool{}
	for _, s := range sub.lastReq.Tools {
		got[s.Name] = true
	}
	for _, want := range []string{"read_file", "grep", "remember", "task", "run_skill", "read_only_skill", "explore", "research", "review", "security_review"} {
		if !got[want] {
			t.Errorf("default sub-agent tools = %v, want %q inherited at depth 1", got, want)
		}
	}
}

func TestTaskToolAllowsSecondLayerAndStopsThere(t *testing.T) {
	sub := &mockProvider{name: "sub", chunks: []provider.Chunk{
		{Type: provider.ChunkText, Text: "depth two answer"},
		{Type: provider.ChunkDone},
	}}
	parentReg := tool.NewRegistry()
	parentReg.Add(fakeTool{name: "read_file", readOnly: true})
	task := newTestTaskTool(t, sub, parentReg, "sys", "", "", nil).WithMaxSubagentDepth(2)
	parentReg.Add(task)
	parentReg.Add(fakeTool{name: "run_skill", readOnly: false})

	depthOneCtx := WithSubagentDepth(testTaskContext(), 1)
	if _, err := task.Execute(depthOneCtx, []byte(`{"prompt":"spawn second layer"}`)); err != nil {
		t.Fatalf("depth-1 task should be able to spawn depth 2: %v", err)
	}
	got := map[string]bool{}
	for _, s := range sub.lastReq.Tools {
		got[s.Name] = true
	}
	if got["task"] || got["run_skill"] {
		t.Fatalf("depth-2 child should not receive recursive tools; tools=%v", toolSchemaNames(sub.lastReq.Tools))
	}

	depthTwoCtx := WithSubagentDepth(testTaskContext(), 2)
	if _, err := task.Execute(depthTwoCtx, []byte(`{"prompt":"spawn third layer"}`)); err == nil || !strings.Contains(err.Error(), "subagent delegation depth limit reached") {
		t.Fatalf("depth-2 task error = %v, want depth limit", err)
	}
}

func TestTaskToolMaxSubagentDepthOneRestoresSingleLayerBoundary(t *testing.T) {
	sub := &mockProvider{name: "sub", chunks: []provider.Chunk{
		{Type: provider.ChunkText, Text: "single layer answer"},
		{Type: provider.ChunkDone},
	}}
	parentReg := tool.NewRegistry()
	parentReg.Add(fakeTool{name: "read_file", readOnly: true})
	task := newTestTaskTool(t, sub, parentReg, "sys", "", "", nil).WithMaxSubagentDepth(1)
	parentReg.Add(task)
	parentReg.Add(fakeTool{name: "run_skill", readOnly: false})
	parentReg.Add(fakeTool{name: "read_only_task", readOnly: true})

	if _, err := task.Execute(testTaskContext(), []byte(`{"prompt":"single layer"}`)); err != nil {
		t.Fatalf("root task should still spawn first-layer subagent: %v", err)
	}
	got := map[string]bool{}
	for _, s := range sub.lastReq.Tools {
		got[s.Name] = true
	}
	for _, hidden := range []string{"task", "run_skill", "read_only_task"} {
		if got[hidden] {
			t.Fatalf("max_subagent_depth=1 should hide recursive tool %q; tools=%v", hidden, toolSchemaNames(sub.lastReq.Tools))
		}
	}

	depthOneCtx := WithSubagentDepth(testTaskContext(), 1)
	if _, err := task.Execute(depthOneCtx, []byte(`{"prompt":"too deep"}`)); err == nil || !strings.Contains(err.Error(), "max_subagent_depth=1") {
		t.Fatalf("depth-1 task error = %v, want max depth rejection", err)
	}
}

func TestTaskToolUsesConfiguredProfileForExecution(t *testing.T) {
	parent := &mockProvider{name: "parent", chunks: []provider.Chunk{
		{Type: provider.ChunkText, Text: "parent answer"},
		{Type: provider.ChunkDone},
	}}
	resolved := &mockProvider{name: "resolved", chunks: []provider.Chunk{
		{Type: provider.ChunkText, Text: "resolved answer"},
		{Type: provider.ChunkDone},
	}}
	var gotModel, gotEffort string
	resolve := func(model, effort string) (provider.Provider, *provider.Pricing, int, error) {
		gotModel, gotEffort = model, effort
		return resolved, nil, 0, nil
	}
	task := newTestTaskTool(t, parent, tool.NewRegistry(), "sys", "deepseek-pro", "max", resolve)

	out, err := task.Execute(testTaskContext(), []byte(`{"prompt":"x"}`))
	if err != nil {
		t.Fatalf("Execute: %v", err)
	}
	if !strings.Contains(out, "resolved answer") {
		t.Fatalf("sub-agent did not use resolved provider, got %q", out)
	}
	if gotModel != "deepseek-pro" || gotEffort != "max" {
		t.Fatalf("resolved profile = %q/%q, want deepseek-pro/max", gotModel, gotEffort)
	}
}

func TestTaskToolReturnsProfileResolutionErrors(t *testing.T) {
	parent := &mockProvider{name: "parent", chunks: []provider.Chunk{
		{Type: provider.ChunkText, Text: "parent answer"},
		{Type: provider.ChunkDone},
	}}
	resolve := func(string, string) (provider.Provider, *provider.Pricing, int, error) {
		return nil, nil, 0, errors.New("bad effort")
	}
	task := newTestTaskTool(t, parent, tool.NewRegistry(), "sys", "", "", resolve)

	_, err := task.Execute(testTaskContext(), []byte(`{"prompt":"x","effort":"turbo"}`))
	if err == nil || !strings.Contains(err.Error(), "bad effort") {
		t.Fatalf("Execute error = %v, want profile resolution error", err)
	}
}

func TestTaskToolRequiresTranscriptStore(t *testing.T) {
	sub := &mockProvider{name: "sub", chunks: []provider.Chunk{
		{Type: provider.ChunkText, Text: "answer"},
		{Type: provider.ChunkDone},
	}}
	task := NewTaskTool(sub, nil, tool.NewRegistry(), 20, 0, 0, 0, 0, 0, 0, 0.0, "", "sys", nil, 0, "", "", nil)

	_, err := task.Execute(testTaskContext(), []byte(`{"prompt":"x"}`))
	if err == nil || !strings.Contains(err.Error(), "transcript store is required") {
		t.Fatalf("Execute error = %v, want transcript store requirement", err)
	}
}

// TestTaskToolRunsEphemerallyWithoutParentSession mirrors headless `reasonix run`:
// the store is wired but the context carries no parent session, so the sub-agent
// must run without persistence and return its plain answer (no transcript ref).
func TestTaskToolRunsEphemerallyWithoutParentSession(t *testing.T) {
	sub := &mockProvider{name: "sub", chunks: []provider.Chunk{
		{Type: provider.ChunkText, Text: "headless answer"},
		{Type: provider.ChunkDone},
	}}
	task := newTestTaskTool(t, sub, tool.NewRegistry(), "sys", "", "", nil)

	out, err := task.Execute(context.Background(), []byte(`{"prompt":"x"}`))
	if err != nil {
		t.Fatalf("Execute: %v", err)
	}
	if !strings.Contains(out, "headless answer") {
		t.Fatalf("got %q, want sub-agent final answer", out)
	}
	if strings.Contains(out, "Subagent reference") {
		t.Fatalf("ephemeral run should not emit a transcript reference: %q", out)
	}
}

func TestReadOnlyTaskToolRunsEphemerallyWithReadOnlyRegistry(t *testing.T) {
	sub := &mockProvider{name: "sub", chunks: []provider.Chunk{
		{Type: provider.ChunkText, Text: "read-only findings"},
		{Type: provider.ChunkDone},
	}}
	parentReg := tool.NewRegistry()
	parentReg.Add(fakeTool{name: "read_file", readOnly: true})
	parentReg.Add(fakeTool{name: "write_file", readOnly: false})
	parentReg.Add(fakeTool{name: "todo_write", readOnly: true})
	parentReg.Add(fakeTool{name: "complete_step", readOnly: true})
	parentReg.Add(fakeTool{name: "connect_tool_source", readOnly: true})
	parentReg.Add(fakeTool{name: "read_only_skill", readOnly: true})
	parentReg.Add(fakeTool{name: "bash", readOnly: false})
	task := newTestTaskTool(t, sub, parentReg, "writer sys", "", "", nil)
	readonly := NewReadOnlyTaskTool(task)
	parentReg.Add(task)
	parentReg.Add(readonly)

	out, err := readonly.Execute(testTaskContext(), []byte(`{"prompt":"inspect callers"}`))
	if err != nil {
		t.Fatalf("Execute: %v", err)
	}
	if !strings.Contains(out, "read-only findings") {
		t.Fatalf("output = %q, want final answer", out)
	}
	if strings.Contains(out, "Subagent reference") {
		t.Fatalf("read_only_task should not persist transcript refs: %q", out)
	}
	if sys := sub.lastReq.Messages[0]; sys.Role != provider.RoleSystem || sys.Content != DefaultReadOnlyTaskSystemPrompt {
		t.Fatalf("read_only_task system prompt = %+v, want read-only prompt", sys)
	}
	if got := lastUser(sub.lastReq); !strings.Contains(got, "Current workspace: ") || !strings.Contains(got, "inspect callers") {
		t.Fatalf("read_only_task user = %q, want workspace context plus prompt", got)
	}

	got := map[string]bool{}
	for _, s := range sub.lastReq.Tools {
		got[s.Name] = true
	}
	for _, want := range []string{"read_file", "bash"} {
		if !got[want] {
			t.Fatalf("read_only_task sub-agent missing %q; tools=%v", want, toolSchemaNames(sub.lastReq.Tools))
		}
	}
	for _, hidden := range []string{"write_file", "todo_write", "complete_step", "connect_tool_source", "task"} {
		if got[hidden] {
			t.Fatalf("read_only_task sub-agent should hide %q; tools=%v", hidden, toolSchemaNames(sub.lastReq.Tools))
		}
	}
	for _, want := range []string{"read_only_task", "read_only_skill"} {
		if !got[want] {
			t.Fatalf("read_only_task depth-1 sub-agent should expose %q; tools=%v", want, toolSchemaNames(sub.lastReq.Tools))
		}
	}
}

func TestTaskToolRejectsContinuationWithoutParentSession(t *testing.T) {
	sub := &mockProvider{name: "sub", chunks: []provider.Chunk{
		{Type: provider.ChunkText, Text: "answer"},
		{Type: provider.ChunkDone},
	}}
	task := newTestTaskTool(t, sub, tool.NewRegistry(), "sys", "", "", nil)

	_, err := task.Execute(context.Background(), []byte(`{"prompt":"x","continue_from":"sa_whatever"}`))
	if err == nil || !strings.Contains(err.Error(), "persisted session") {
		t.Fatalf("Execute error = %v, want persisted-session requirement", err)
	}
}

func TestTaskToolPersistsAndContinuesTranscript(t *testing.T) {
	sub := &mockProvider{name: "sub", streams: [][]provider.Chunk{
		{
			{Type: provider.ChunkText, Text: "first answer"},
			{Type: provider.ChunkDone},
		},
		{
			{Type: provider.ChunkText, Text: "second answer"},
			{Type: provider.ChunkDone},
		},
	}}
	reg := tool.NewRegistry()
	reg.Add(fakeTool{name: "read_file", readOnly: true})
	store := NewSubagentStore(t.TempDir())
	task := newTestTaskTool(t, sub, reg, "sys", "", "", nil).
		WithTranscripts(store, t.TempDir(), "base-model", "base-effort")

	first, err := task.Execute(testTaskContext(), []byte(`{"prompt":"first task"}`))
	if err != nil {
		t.Fatalf("first Execute: %v", err)
	}
	ref := subagentRefFromOutput(t, first)
	meta, err := store.LoadMeta(ref)
	if err != nil {
		t.Fatalf("LoadMeta: %v", err)
	}
	if meta.ParentSession != "parent-session" {
		t.Fatalf("parent session = %q, want parent-session", meta.ParentSession)
	}
	if !strings.Contains(first, "first answer") {
		t.Fatalf("first output = %q, want answer", first)
	}

	second, err := task.Execute(testTaskContext(), []byte(`{"prompt":"second task","continue_from":"`+ref+`"}`))
	if err != nil {
		t.Fatalf("second Execute: %v", err)
	}
	if !strings.Contains(second, "second answer") {
		t.Fatalf("second output = %q, want answer", second)
	}
	if len(sub.requests) != 2 {
		t.Fatalf("provider requests = %d, want 2", len(sub.requests))
	}
	msgs := sub.requests[1].Messages
	if len(msgs) < 4 {
		t.Fatalf("continued request messages = %+v, want prior transcript plus new task", msgs)
	}
	if !strings.Contains(msgs[1].Content, "first task") || msgs[2].Content != "first answer" || !strings.Contains(lastUser(sub.requests[1]), "second task") {
		t.Fatalf("continued request messages = %+v, want first task/answer then second task", msgs)
	}
}

func TestTaskToolContinueFromAncestorReturnsCopiedReferenceGuidance(t *testing.T) {
	sub := &mockProvider{name: "sub", streams: [][]provider.Chunk{
		{
			{Type: provider.ChunkText, Text: "root answer"},
			{Type: provider.ChunkDone},
		},
		{
			{Type: provider.ChunkText, Text: "child answer"},
			{Type: provider.ChunkDone},
		},
	}}
	sessionDir := t.TempDir()
	store := NewSubagentStore(filepath.Join(sessionDir, "subagents"))
	reg := tool.NewRegistry()
	reg.Add(fakeTool{name: "read_file", readOnly: true})
	task := NewTaskTool(sub, nil, reg, 20, 0, 0, 0, 0, 0, 0, 0.0, "", "sys", nil, 0, "", "", nil).
		WithTranscripts(store, t.TempDir(), "base-model", "base-effort")

	rootCtx := WithParentSession(context.Background(), "root")
	first, err := task.Execute(rootCtx, []byte(`{"prompt":"root task"}`))
	if err != nil {
		t.Fatalf("first Execute: %v", err)
	}
	rootRef := subagentRefFromOutput(t, first)

	if err := SaveBranchMeta(filepath.Join(sessionDir, "root.jsonl"), BranchMeta{}); err != nil {
		t.Fatalf("SaveBranchMeta root: %v", err)
	}
	if err := SaveBranchMeta(filepath.Join(sessionDir, "child.jsonl"), BranchMeta{ParentID: "root"}); err != nil {
		t.Fatalf("SaveBranchMeta child: %v", err)
	}

	childCtx := WithParentSession(context.Background(), "child")
	second, err := task.Execute(childCtx, []byte(`{"prompt":"child task","continue_from":"`+rootRef+`"}`))
	if err != nil {
		t.Fatalf("second Execute: %v", err)
	}
	childRef := subagentRefFromOutput(t, second)
	if childRef == rootRef {
		t.Fatalf("child ref = source ref %q, want copied ref", childRef)
	}
	if !strings.Contains(second, "Forked from: "+rootRef) {
		t.Fatalf("second output = %q, want Forked from source ref", second)
	}
	if !strings.Contains(second, "The requested ref resolves to an ancestor conversation transcript") {
		t.Fatalf("second output = %q, want ancestor-copy guidance", second)
	}
	if !strings.Contains(second, "Final answer:\nchild answer") {
		t.Fatalf("second output = %q, want final answer", second)
	}
}

func TestTaskToolLegacyForkFromAncestorConvertsToCopiedReference(t *testing.T) {
	sub := &mockProvider{name: "sub", streams: [][]provider.Chunk{
		{
			{Type: provider.ChunkText, Text: "root answer"},
			{Type: provider.ChunkDone},
		},
		{
			{Type: provider.ChunkText, Text: "child answer"},
			{Type: provider.ChunkDone},
		},
	}}
	sessionDir := t.TempDir()
	store := NewSubagentStore(filepath.Join(sessionDir, "subagents"))
	reg := tool.NewRegistry()
	reg.Add(fakeTool{name: "read_file", readOnly: true})
	task := NewTaskTool(sub, nil, reg, 20, 0, 0, 0, 0, 0, 0, 0.0, "", "sys", nil, 0, "", "", nil).
		WithTranscripts(store, t.TempDir(), "base-model", "base-effort")

	rootCtx := WithParentSession(context.Background(), "root")
	first, err := task.Execute(rootCtx, []byte(`{"prompt":"root task"}`))
	if err != nil {
		t.Fatalf("first Execute: %v", err)
	}
	rootRef := subagentRefFromOutput(t, first)

	if err := SaveBranchMeta(filepath.Join(sessionDir, "root.jsonl"), BranchMeta{}); err != nil {
		t.Fatalf("SaveBranchMeta root: %v", err)
	}
	if err := SaveBranchMeta(filepath.Join(sessionDir, "child.jsonl"), BranchMeta{ParentID: "root"}); err != nil {
		t.Fatalf("SaveBranchMeta child: %v", err)
	}

	childCtx := WithParentSession(context.Background(), "child")
	second, err := task.Execute(childCtx, []byte(`{"prompt":"child task","fork_from":"`+rootRef+`"}`))
	if err != nil {
		t.Fatalf("second Execute: %v", err)
	}
	childRef := subagentRefFromOutput(t, second)
	if childRef == rootRef {
		t.Fatalf("child ref = source ref %q, want copied ref", childRef)
	}
	if !strings.Contains(second, "Forked from: "+rootRef) ||
		!strings.Contains(second, "Final answer:\nchild answer") {
		t.Fatalf("second output = %q, want copied reference guidance and final answer", second)
	}
}

func TestTaskToolRejectsLegacyForkFromCurrentSession(t *testing.T) {
	sub := &mockProvider{name: "sub", streams: [][]provider.Chunk{
		{
			{Type: provider.ChunkText, Text: "first answer"},
			{Type: provider.ChunkDone},
		},
		{
			{Type: provider.ChunkText, Text: "should not run"},
			{Type: provider.ChunkDone},
		},
	}}
	task := newTestTaskTool(t, sub, tool.NewRegistry(), "sys", "", "", nil).
		WithTranscripts(NewSubagentStore(t.TempDir()), t.TempDir(), "base-model", "base-effort")

	first, err := task.Execute(testTaskContext(), []byte(`{"prompt":"first task"}`))
	if err != nil {
		t.Fatalf("first Execute: %v", err)
	}
	ref := subagentRefFromOutput(t, first)
	_, err = task.Execute(testTaskContext(), []byte(`{"prompt":"second task","fork_from":"`+ref+`"}`))
	if err == nil || !strings.Contains(err.Error(), "cannot be safely converted") {
		t.Fatalf("legacy fork error = %v, want unsafe conversion rejection", err)
	}
	if len(sub.requests) != 1 {
		t.Fatalf("provider requests = %d, want only first run", len(sub.requests))
	}
}

func TestTaskToolFailedForegroundContinuationPersistsAndRejectsReuse(t *testing.T) {
	sub := &mockProvider{name: "sub", streams: [][]provider.Chunk{
		{
			{Type: provider.ChunkText, Text: "first answer"},
			{Type: provider.ChunkDone},
		},
		{
			{Type: provider.ChunkError, Err: errors.New("provider failed")},
		},
	}}
	store := NewSubagentStore(t.TempDir())
	reg := tool.NewRegistry()
	reg.Add(fakeTool{name: "read_file", readOnly: true})
	task := NewTaskTool(sub, nil, reg, 20, 0, 0, 0, 0, 0, 0, 0.0, "", "sys", nil, 0, "", "", nil).
		WithTranscripts(store, t.TempDir(), "base-model", "base-effort")

	first, err := task.Execute(testTaskContext(), []byte(`{"prompt":"first task"}`))
	if err != nil {
		t.Fatalf("first Execute: %v", err)
	}
	ref := subagentRefFromOutput(t, first)

	_, err = task.Execute(testTaskContext(), []byte(`{"prompt":"second task","continue_from":"`+ref+`"}`))
	if err == nil || !strings.Contains(err.Error(), "provider failed") {
		t.Fatalf("second Execute error = %v, want provider failure", err)
	}
	meta, err := store.LoadMeta(ref)
	if err != nil {
		t.Fatalf("LoadMeta: %v", err)
	}
	if meta.Status != SubagentFailed {
		t.Fatalf("status = %q, want failed", meta.Status)
	}
	loaded, err := LoadSession(store.sessionPath(ref))
	if err != nil {
		t.Fatalf("LoadSession: %v", err)
	}
	msgs := loaded.Snapshot()
	if len(msgs) != 5 || !strings.Contains(msgs[1].Content, "first task") || msgs[2].Content != "first answer" || !strings.Contains(msgs[3].Content, "second task") || !msgs[4].LocalOnly {
		t.Fatalf("failed continuation transcript = %+v, want tasks plus provider-excluded failure recovery", msgs)
	}
	if _, err := task.Execute(testTaskContext(), []byte(`{"prompt":"third task","continue_from":"`+ref+`"}`)); err == nil || !strings.Contains(err.Error(), "failed and cannot be continued") {
		t.Fatalf("reuse error = %v, want failed ref rejection", err)
	}
}

func TestTaskToolBackgroundPanicPersistsFailedMetadata(t *testing.T) {
	sub := panicProvider{name: "panic-sub"}
	store := NewSubagentStore(t.TempDir())
	reg := tool.NewRegistry()
	reg.Add(fakeTool{name: "read_file", readOnly: true})
	task := NewTaskTool(sub, nil, reg, 20, 0, 0, 0, 0, 0, 0, 0.0, "", "sys", nil, 0, "", "", nil).
		WithTranscripts(store, t.TempDir(), "base-model", "base-effort")

	jm := jobs.NewManager(event.Discard)
	defer jm.Close()
	ctx := testTaskContext()
	ctx = jobs.WithSession(ctx, "parent-session")
	ctx = jobs.WithManager(ctx, jm)
	out, err := task.Execute(ctx, []byte(`{"prompt":"panic task","run_in_background":true}`))
	if err != nil {
		t.Fatalf("Execute: %v", err)
	}
	ref := subagentRefFromOutput(t, out)
	jobID := extractJobID(out)
	if jobID == "" {
		t.Fatalf("no background job id in output:\n%s", out)
	}
	res := jm.WaitForSession(context.Background(), "parent-session", []string{jobID}, 5)
	if len(res) != 1 || res[0].Status != jobs.Failed {
		t.Fatalf("background job result = %+v, want failed", res)
	}
	if !strings.Contains(res[0].Output, "Subagent reference (failed): "+ref) {
		t.Fatalf("job output = %q, want failed subagent ref %s", res[0].Output, ref)
	}
	meta, err := store.LoadMeta(ref)
	if err != nil {
		t.Fatalf("LoadMeta: %v", err)
	}
	if meta.Status != SubagentFailed {
		t.Fatalf("status = %q, want failed", meta.Status)
	}
	if _, err := task.Execute(testTaskContext(), []byte(`{"prompt":"again","continue_from":"`+ref+`"}`)); err == nil || !strings.Contains(err.Error(), "failed and cannot be continued") {
		t.Fatalf("reuse error = %v, want failed continuation rejection", err)
	}
}

func TestTaskToolBackgroundResultIncludesReferenceGuidance(t *testing.T) {
	sub := &mockProvider{name: "sub", chunks: []provider.Chunk{
		{Type: provider.ChunkText, Text: "background answer"},
		{Type: provider.ChunkDone},
	}}
	store := NewSubagentStore(t.TempDir())
	reg := tool.NewRegistry()
	reg.Add(fakeTool{name: "read_file", readOnly: true})
	task := NewTaskTool(sub, nil, reg, 20, 0, 0, 0, 0, 0, 0, 0.0, "", "sys", nil, 0, "", "", nil).
		WithTranscripts(store, t.TempDir(), "base-model", "base-effort")

	jm := jobs.NewManager(event.Discard)
	defer jm.Close()
	ctx := testTaskContext()
	ctx = jobs.WithSession(ctx, "parent-session")
	ctx = jobs.WithManager(ctx, jm)
	out, err := task.Execute(ctx, []byte(`{"prompt":"background task","run_in_background":true}`))
	if err != nil {
		t.Fatalf("Execute: %v", err)
	}
	ref := subagentRefFromOutput(t, out)
	if !strings.Contains(out, "To continue this same subagent transcript in a later call") {
		t.Fatalf("start output = %q, want reference guidance", out)
	}
	jobID := extractJobID(out)
	if jobID == "" {
		t.Fatalf("no background job id in output:\n%s", out)
	}
	res := jm.WaitForSession(context.Background(), "parent-session", []string{jobID}, 5)
	if len(res) != 1 || res[0].Status != jobs.Done {
		t.Fatalf("background job result = %+v, want succeeded", res)
	}
	if !strings.Contains(res[0].Output, "Subagent reference: "+ref) ||
		!strings.Contains(res[0].Output, "To continue this same subagent transcript in a later call") ||
		!strings.Contains(res[0].Output, "Final answer:\nbackground answer") {
		t.Fatalf("job output = %q, want reference guidance and final answer", res[0].Output)
	}
}

func TestTaskToolBackgroundAncestorContinuationIncludesForkGuidance(t *testing.T) {
	sub := &mockProvider{name: "sub", streams: [][]provider.Chunk{
		{
			{Type: provider.ChunkText, Text: "root answer"},
			{Type: provider.ChunkDone},
		},
		{
			{Type: provider.ChunkText, Text: "child background answer"},
			{Type: provider.ChunkDone},
		},
	}}
	sessionDir := t.TempDir()
	store := NewSubagentStore(filepath.Join(sessionDir, "subagents"))
	reg := tool.NewRegistry()
	reg.Add(fakeTool{name: "read_file", readOnly: true})
	task := NewTaskTool(sub, nil, reg, 20, 0, 0, 0, 0, 0, 0, 0.0, "", "sys", nil, 0, "", "", nil).
		WithTranscripts(store, t.TempDir(), "base-model", "base-effort")

	rootCtx := WithParentSession(context.Background(), "root")
	rootOut, err := task.Execute(rootCtx, []byte(`{"prompt":"root task"}`))
	if err != nil {
		t.Fatalf("root Execute: %v", err)
	}
	rootRef := subagentRefFromOutput(t, rootOut)
	if err := SaveBranchMeta(filepath.Join(sessionDir, "root.jsonl"), BranchMeta{}); err != nil {
		t.Fatalf("SaveBranchMeta root: %v", err)
	}
	if err := SaveBranchMeta(filepath.Join(sessionDir, "child.jsonl"), BranchMeta{ParentID: "root"}); err != nil {
		t.Fatalf("SaveBranchMeta child: %v", err)
	}

	jm := jobs.NewManager(event.Discard)
	defer jm.Close()
	childCtx := WithParentSession(context.Background(), "child")
	childCtx = jobs.WithSession(childCtx, "child")
	childCtx = jobs.WithManager(childCtx, jm)
	startOut, err := task.Execute(childCtx, []byte(`{"prompt":"child task","continue_from":"`+rootRef+`","run_in_background":true}`))
	if err != nil {
		t.Fatalf("child Execute: %v", err)
	}
	childRef := subagentRefFromOutput(t, startOut)
	if childRef == rootRef {
		t.Fatalf("child ref = source ref %q, want copied ref", childRef)
	}
	if !strings.Contains(startOut, "Forked from: "+rootRef) ||
		!strings.Contains(startOut, "The requested ref resolves to an ancestor conversation transcript") ||
		strings.Contains(startOut, "Final answer:") {
		t.Fatalf("start output = %q, want fork guidance without final answer", startOut)
	}
	jobID := extractJobID(startOut)
	if jobID == "" {
		t.Fatalf("no background job id in output:\n%s", startOut)
	}
	res := jm.WaitForSession(context.Background(), "child", []string{jobID}, 5)
	if len(res) != 1 || res[0].Status != jobs.Done {
		t.Fatalf("background job result = %+v, want succeeded", res)
	}
	if !strings.Contains(res[0].Output, "Subagent reference: "+childRef) ||
		!strings.Contains(res[0].Output, "Forked from: "+rootRef) ||
		!strings.Contains(res[0].Output, "The requested ref resolves to an ancestor conversation transcript") ||
		!strings.Contains(res[0].Output, "Final answer:\nchild background answer") {
		t.Fatalf("job output = %q, want copied ref guidance and final answer", res[0].Output)
	}
}

func TestTaskToolBackgroundCapRefusesFanOut(t *testing.T) {
	sub := &mockProvider{name: "sub", chunks: []provider.Chunk{
		{Type: provider.ChunkText, Text: "background answer"},
		{Type: provider.ChunkDone},
	}}
	store := NewSubagentStore(t.TempDir())
	reg := tool.NewRegistry()
	reg.Add(fakeTool{name: "read_file", readOnly: true})
	task := NewTaskTool(sub, nil, reg, 20, 0, 0, 0, 0, 0, 0, 0.0, "", "sys", nil, 0, "", "", nil).
		WithTranscripts(store, t.TempDir(), "base-model", "base-effort")

	jm := jobs.NewManager(event.Discard)
	defer jm.Close()
	ctx := testTaskContext()
	ctx = jobs.WithSession(ctx, "parent-session")
	ctx = jobs.WithManager(ctx, jm)

	// Saturate the cap with still-running task jobs owned by this session.
	release := make(chan struct{})
	var ids []string
	for range maxConcurrentBackgroundTasks {
		j := jm.StartForSession("parent-session", "task", "busy", func(jctx context.Context, _ io.Writer) (string, error) {
			select {
			case <-release:
			case <-jctx.Done():
			}
			return "ok", nil
		})
		ids = append(ids, j.ID)
	}

	if _, err := task.Execute(ctx, []byte(`{"prompt":"one more","run_in_background":true}`)); err == nil ||
		!strings.Contains(err.Error(), "limit") || !strings.Contains(err.Error(), "wait") {
		t.Fatalf("Execute over cap = %v, want background task limit refusal", err)
	}

	// Foreground execution is not capped.
	if out, err := task.Execute(ctx, []byte(`{"prompt":"foreground task"}`)); err != nil || !strings.Contains(out, "background answer") {
		t.Fatalf("foreground Execute = %q, %v; want uncapped foreground run", out, err)
	}

	// Collecting the running jobs frees the cap.
	close(release)
	jm.WaitForSession(context.Background(), "parent-session", ids, 5)
	out, err := task.Execute(ctx, []byte(`{"prompt":"after drain","run_in_background":true}`))
	if err != nil {
		t.Fatalf("Execute after drain: %v", err)
	}
	jobID := extractJobID(out)
	if jobID == "" {
		t.Fatalf("no background job id in output:\n%s", out)
	}
	if res := jm.WaitForSession(context.Background(), "parent-session", []string{jobID}, 5); len(res) != 1 || res[0].Status != jobs.Done {
		t.Fatalf("post-drain job = %+v, want done", res)
	}
}

func TestTaskToolBackgroundRunPublishesEvidenceForCollection(t *testing.T) {
	reg := evidenceRegistry()
	finalText := []provider.Chunk{{Type: provider.ChunkText, Text: "done, explanations added"}, {Type: provider.ChunkDone}}
	sub := &scriptedProvider{name: "sub", turns: [][]provider.Chunk{
		{toolCallChunk("criteria", "todo_write", `{"todos":[{"content":"Add explanations","status":"in_progress"}]}`), {Type: provider.ChunkDone}},
		{toolCallChunk("write", "write_file", `{"path":"qa/bank.md"}`), {Type: provider.ChunkDone}},
		finalText,
		finalText,
		finalText,
	}}
	task := NewTaskTool(sub, nil, reg, 20, 0, 0, 0, 0, 0, 0, 0.0, "", "sys", nil, 0, "", "", nil).
		WithTranscripts(NewSubagentStore(t.TempDir()), t.TempDir(), "base-model", "base-effort")

	jm := jobs.NewManager(event.Discard)
	defer jm.Close()
	parentLedger := evidence.NewLedger()
	ctx := testTaskContext()
	ctx = jobs.WithSession(ctx, "parent-session")
	ctx = jobs.WithManager(ctx, jm)
	ctx = evidence.WithLedger(ctx, parentLedger)

	out, err := task.Execute(ctx, []byte(`{"prompt":"add explanations to the question bank","run_in_background":true}`))
	if err != nil {
		t.Fatalf("Execute: %v", err)
	}
	jobID := extractJobID(out)
	res := jm.WaitForSession(context.Background(), "parent-session", []string{jobID}, 5)
	if len(res) != 1 || res[0].Status != jobs.Done {
		t.Fatalf("background task = %+v, want done", res)
	}
	if parentLedger.Summary().HasMutation() {
		t.Fatal("background goroutine wrote directly into the parent turn ledger")
	}

	summary := jm.LeaseEvidenceForSession("parent-session", jobID)
	if !summary.HasMutation() {
		t.Fatal("terminal background task did not publish its mutation evidence")
	}
	paths := summary.MutationPaths()
	if len(paths) != 1 || filepath.ToSlash(paths[0]) != "qa/bank.md" {
		t.Fatalf("background mutation paths = %v, want qa/bank.md", paths)
	}
	// Lease does not consume: the evidence stays available until the collecting
	// turn commits, so a cancelled/errored turn can re-collect it.
	if again := jm.LeaseEvidenceForSession("parent-session", jobID); !again.HasMutation() {
		t.Fatalf("lease consumed background evidence without a commit: %+v", again)
	}
	jm.CommitEvidenceForSession("parent-session", jobID)
	if after := jm.LeaseEvidenceForSession("parent-session", jobID); len(after.Receipts) != 0 {
		t.Fatalf("committed background evidence still leasable: %+v", after)
	}
}

// startTerminalBackgroundMutation registers a background task job that publishes
// one mutation and returns after it reaches a terminal state, ready to collect.
func startTerminalBackgroundMutation(t *testing.T, jm *jobs.Manager, session, path string) string {
	t.Helper()
	j := jm.StartForSession(session, "task", "bg writer", func(ctx context.Context, _ io.Writer) (string, error) {
		jobs.PublishEvidence(ctx, evidence.ChildEvidenceSummary{Receipts: []evidence.Receipt{{
			ToolName: "write_file", Success: true, Write: true, Mutation: true, Paths: []string{path},
		}}})
		return "background answer", nil
	})
	if res := jm.WaitForSession(context.Background(), session, []string{j.ID}, 5); len(res) != 1 || res[0].Status != jobs.Done {
		t.Fatalf("background job = %+v, want done", res)
	}
	return j.ID
}

func waitBuiltin(t *testing.T, reg *tool.Registry) {
	t.Helper()
	wait, ok := tool.LookupBuiltin("wait")
	if !ok {
		t.Fatal("wait builtin not registered")
	}
	reg.Add(wait)
}

func TestBackgroundEvidenceNotCommittedWhenTurnFails(t *testing.T) {
	// The delivery turn collects a background writer's mutation via wait, then
	// fails to sign it off, exhausting readiness. Because the turn never
	// delivered, the lease must not be committed: the mutation stays collectable
	// so the next turn can review it instead of shipping it unreviewed.
	jm := jobs.NewManager(event.Discard)
	defer jm.Close()
	jobID := startTerminalBackgroundMutation(t, jm, "parent-session", "internal/auth/session.go")

	reg := evidenceRegistry()
	waitBuiltin(t, reg)
	prov := &scriptedProvider{name: "p", turns: [][]provider.Chunk{
		{toolCallChunk("w", "wait", `{"job_ids":["`+jobID+`"]}`), {Type: provider.ChunkDone}},
		{{Type: provider.ChunkText, Text: "all set"}, {Type: provider.ChunkDone}}, // no sign-off
		{{Type: provider.ChunkText, Text: "all set"}, {Type: provider.ChunkDone}},
		{{Type: provider.ChunkText, Text: "all set"}, {Type: provider.ChunkDone}},
	}}
	a := New(prov, reg, NewSession(""), Options{Jobs: jm}, event.Discard)
	ctx := withClosedLoopContext(jobs.WithManager(WithParentSession(context.Background(), "parent-session"), jm))
	ctx = jobs.WithSession(ctx, "parent-session")

	err := a.Run(ctx, "collect and finish the background task")
	var readiness *FinalReadinessError
	if !errors.As(err, &readiness) {
		t.Fatalf("turn = %v, want readiness exhaustion on the uncollected sign-off", err)
	}
	// The failed turn must not have consumed the evidence.
	if leased := jm.LeaseEvidenceForSession("parent-session", jobID); !leased.HasMutation() {
		t.Fatalf("failed delivery turn consumed the background evidence: %+v", leased)
	}
}

func TestBackgroundEvidenceCommittedWhenTurnDelivers(t *testing.T) {
	// A successful turn that collected a background writer's mutation commits the
	// lease, permanently draining the job's evidence so a later re-poll does not
	// re-demand review of work already delivered.
	jm := jobs.NewManager(event.Discard)
	defer jm.Close()
	jobID := startTerminalBackgroundMutation(t, jm, "parent-session", "notes.txt")

	reg := tool.NewRegistry()
	reg.Add(fakeTool{name: "read_file", readOnly: true})
	waitBuiltin(t, reg)
	prov := &scriptedProvider{name: "p", turns: [][]provider.Chunk{
		{toolCallChunk("w", "wait", `{"job_ids":["`+jobID+`"]}`), {Type: provider.ChunkDone}},
		{{Type: provider.ChunkText, Text: "collected the result"}, {Type: provider.ChunkDone}},
	}}
	// No delivery profile: the turn succeeds immediately after collecting, so the
	// commit-on-success hook fires without a full sign-off script.
	a := New(prov, reg, NewSession(""), Options{Jobs: jm}, event.Discard)
	ctx := jobs.WithManager(WithParentSession(context.Background(), "parent-session"), jm)
	ctx = jobs.WithSession(ctx, "parent-session")

	if err := a.Run(ctx, "collect the background task"); err != nil {
		t.Fatalf("delivering turn failed: %v", err)
	}
	if leased := jm.LeaseEvidenceForSession("parent-session", jobID); len(leased.Receipts) != 0 {
		t.Fatalf("delivered turn did not commit the background lease: %+v", leased)
	}
}

// TestFailedTurnBackgroundMutationForcesReadinessOnNextRunWithoutWait extends
// TestBackgroundEvidenceNotCommittedWhenTurnFails: after the first turn collects
// a background mutation via wait but fails to sign it off, Run's Reset wipes the
// per-turn ledger before the second turn starts. Without re-injecting the still
// uncommitted mutation, a second turn that never calls wait/bash_output again
// would sail through final-readiness having never seen it. Run must re-lease it
// automatically so the gate still blocks.
func TestFailedTurnBackgroundMutationForcesReadinessOnNextRunWithoutWait(t *testing.T) {
	jm := jobs.NewManager(event.Discard)
	defer jm.Close()
	jobID := startTerminalBackgroundMutation(t, jm, "parent-session", "internal/auth/session.go")

	reg := evidenceRegistry()
	waitBuiltin(t, reg)
	prov := &scriptedProvider{name: "p", turns: [][]provider.Chunk{
		{toolCallChunk("w", "wait", `{"job_ids":["`+jobID+`"]}`), {Type: provider.ChunkDone}},
		{{Type: provider.ChunkText, Text: "all set"}, {Type: provider.ChunkDone}}, // no sign-off
		{{Type: provider.ChunkText, Text: "all set"}, {Type: provider.ChunkDone}},
		{{Type: provider.ChunkText, Text: "all set"}, {Type: provider.ChunkDone}},
		// Second Run: the model never calls wait/bash_output again.
		{{Type: provider.ChunkText, Text: "sure, here you go"}, {Type: provider.ChunkDone}},
		{{Type: provider.ChunkText, Text: "sure, here you go"}, {Type: provider.ChunkDone}},
		{{Type: provider.ChunkText, Text: "sure, here you go"}, {Type: provider.ChunkDone}},
	}}
	a := New(prov, reg, NewSession(""), Options{Jobs: jm}, event.Discard)
	ctx := withClosedLoopContext(jobs.WithManager(WithParentSession(context.Background(), "parent-session"), jm))
	ctx = jobs.WithSession(ctx, "parent-session")

	var readiness *FinalReadinessError
	if err := a.Run(ctx, "collect and finish the background task"); !errors.As(err, &readiness) {
		t.Fatalf("first turn = %v, want readiness exhaustion on the uncollected sign-off", err)
	}
	if leased := jm.LeaseEvidenceForSession("parent-session", jobID); !leased.HasMutation() {
		t.Fatalf("first failed turn consumed the background evidence: %+v", leased)
	}

	readiness = nil
	if err := a.Run(ctx, "never mind, just answer directly"); !errors.As(err, &readiness) {
		t.Fatalf("second turn (no wait call) = %v, want readiness exhaustion on the still-pending mutation", err)
	}
	if leased := jm.LeaseEvidenceForSession("parent-session", jobID); !leased.HasMutation() {
		t.Fatalf("second failed turn consumed the background evidence: %+v", leased)
	}
}

// TestRestartRecoversPendingBackgroundMutationForcesReadinessWithoutWait mirrors
// the same guarantee across a process restart: a background task mutates and
// finishes while no turn is collecting it, the process exits before any turn
// commits (or even leases) that evidence, and a fresh Manager + Agent pair —
// standing in for the restarted process — must still see it and enforce
// final-readiness on the very first turn, with no wait/bash_output call at all.
func TestRestartRecoversPendingBackgroundMutationForcesReadinessWithoutWait(t *testing.T) {
	sessionPath := filepath.Join(t.TempDir(), "session.jsonl")
	first := jobs.NewManager(event.Discard)
	first.SetActiveSessionPath("parent-session", sessionPath)
	j := first.StartForSession("parent-session", "task", "bg writer", func(ctx context.Context, _ io.Writer) (string, error) {
		jobs.PublishEvidence(ctx, evidence.ChildEvidenceSummary{Receipts: []evidence.Receipt{{
			ToolName: "write_file", Success: true, Write: true, Mutation: true, Paths: []string{"internal/auth/session.go"},
		}}})
		return "background answer", nil
	})
	if res := first.WaitForSession(context.Background(), "parent-session", []string{j.ID}, 5); len(res) != 1 || res[0].Status != jobs.Done {
		t.Fatalf("background job = %+v, want done", res)
	}
	first.Close() // the process exits before any turn ever leased this evidence

	second := jobs.NewManager(event.Discard)
	defer second.Close()
	second.SetActiveSessionPath("parent-session", sessionPath)

	reg := evidenceRegistry()
	prov := &scriptedProvider{name: "p", turns: [][]provider.Chunk{
		{{Type: provider.ChunkText, Text: "all set"}, {Type: provider.ChunkDone}},
		{{Type: provider.ChunkText, Text: "all set"}, {Type: provider.ChunkDone}},
		{{Type: provider.ChunkText, Text: "all set"}, {Type: provider.ChunkDone}},
	}}
	a := New(prov, reg, NewSession(""), Options{Jobs: second}, event.Discard)
	ctx := withClosedLoopContext(jobs.WithManager(WithParentSession(context.Background(), "parent-session"), second))
	ctx = jobs.WithSession(ctx, "parent-session")

	var readiness *FinalReadinessError
	if err := a.Run(ctx, "what's the status?"); !errors.As(err, &readiness) {
		t.Fatalf("post-restart turn = %v, want readiness exhaustion on the recovered mutation", err)
	}
	if leased := second.LeaseEvidenceForSession("parent-session", j.ID); !leased.HasMutation() {
		t.Fatalf("recovered evidence lost after the failed post-restart turn: %+v", leased)
	}
}

func TestTaskToolRejectsMismatchedContinuationProfile(t *testing.T) {
	sub := &mockProvider{name: "sub", chunks: []provider.Chunk{
		{Type: provider.ChunkText, Text: "answer"},
		{Type: provider.ChunkDone},
	}}
	task := newTestTaskTool(t, sub, tool.NewRegistry(), "sys", "", "", nil).
		WithTranscripts(NewSubagentStore(t.TempDir()), t.TempDir(), "base-model", "")

	out, err := task.Execute(testTaskContext(), []byte(`{"prompt":"first task"}`))
	if err != nil {
		t.Fatalf("first Execute: %v", err)
	}
	ref := subagentRefFromOutput(t, out)
	_, err = task.Execute(testTaskContext(), []byte(`{"prompt":"second task","continue_from":"`+ref+`","model":"other-model"}`))
	if err == nil || !strings.Contains(err.Error(), "model/effort") {
		t.Fatalf("mismatched model error = %v, want compatibility failure", err)
	}
}

func extractJobID(msg string) string {
	quote := strings.Index(msg, `"`)
	if quote < 0 {
		return ""
	}
	end := strings.Index(msg[quote+1:], `"`)
	if end < 0 {
		return ""
	}
	return msg[quote+1 : quote+1+end]
}

func subagentRefFromOutput(t *testing.T, out string) string {
	t.Helper()
	for line := range strings.SplitSeq(out, "\n") {
		if after, ok := strings.CutPrefix(line, "Subagent reference: "); ok {
			return strings.TrimSpace(after)
		}
	}
	t.Fatalf("no subagent reference in output:\n%s", out)
	return ""
}

func TestSubSinkForwardsUsageToParent(t *testing.T) {
	var got []event.Event
	parent := event.FuncSink(func(e event.Event) {
		got = append(got, e)
	})
	subSinkFor("task_1", parent).Emit(event.Event{
		Kind:        event.Usage,
		Usage:       &provider.Usage{PromptTokens: 10, CompletionTokens: 2, TotalTokens: 12},
		UsageSource: event.UsageSourceSubagent,
	})
	if len(got) != 1 || got[0].Usage == nil || got[0].UsageSource != event.UsageSourceSubagent {
		t.Fatalf("forwarded events = %+v, want subagent usage", got)
	}
}

func TestTaskToolCarriesRecentKeepIntoSubsessions(t *testing.T) {
	task := NewTaskTool(&mockProvider{name: "sub"}, nil, tool.NewRegistry(), 20, 0, 7, 0, 0, 0, 0, 0.0, "", "sys", nil, 0, "", "", nil)
	if task.recentKeep != 7 {
		t.Fatalf("recentKeep = %d, want 7", task.recentKeep)
	}
}

func newTestTaskTool(t *testing.T, prov provider.Provider, reg *tool.Registry, sysPrompt, subagentModel, subagentEffort string, resolve func(string, string) (provider.Provider, *provider.Pricing, int, error)) *TaskTool {
	t.Helper()
	return NewTaskToolWithOptions(TaskToolOptions{
		Provider:        prov,
		ParentRegistry:  reg,
		MaxSteps:        20,
		SysPrompt:       sysPrompt,
		SubagentModel:   subagentModel,
		SubagentEffort:  subagentEffort,
		ResolveProvider: resolve,
	}).WithTranscripts(NewSubagentStore(t.TempDir()), t.TempDir(), "base-model", "base-effort")
}

type panicProvider struct{ name string }

func (p panicProvider) Name() string { return p.name }

func (p panicProvider) Stream(context.Context, provider.Request) (<-chan provider.Chunk, error) {
	panic("subagent boom")
}
