/**
 * Tests for the Agent tool — spawn a subagent with isolated context
 * using createSubagentSession-based execution.
 */

import { describe, it, expect, vi, beforeEach } from 'vitest';

import type { IAgentDefinition } from '../../agents/agent-definition-types.js';
import type { IResolvedConfig } from '../../config/config-types.js';
import type { ILoadedContext } from '../../context/context-loader.js';
import type { ISubagentManager } from '../../subagents/index.js';
import type { IAgentToolDeps } from '../agent-tool.js';
import type { IAIProvider, IToolWithEventService } from '@robota-sdk/agent-core';
import type { IToolResult } from '@robota-sdk/agent-core';
import type { ITerminalOutput, TPermissionHandler } from '@robota-sdk/agent-session';

const mockProvider = {
  generateResponse: vi.fn(),
} as unknown as IAIProvider;

// Mock createSubagentSession
const mockRun = vi.fn().mockResolvedValue('task completed successfully');
const mockSessionInstance = { run: mockRun };
vi.mock('../../assembly/create-subagent-session.js', () => ({
  createSubagentSession: vi.fn(() => mockSessionInstance),
}));

// Mock getBuiltInAgent
vi.mock('../../agents/built-in-agents.js', () => ({
  getBuiltInAgent: vi.fn((name: string) => {
    const agents: Record<string, IAgentDefinition> = {
      'general-purpose': {
        name: 'general-purpose',
        description: 'General-purpose agent',
        systemPrompt: 'You are a general agent.',
      },
      Explore: {
        name: 'Explore',
        description: 'Exploration agent',
        systemPrompt: 'You are an explorer.',
      },
      Plan: {
        name: 'Plan',
        description: 'Planning agent',
        systemPrompt: 'You are a planner.',
      },
    };
    return agents[name];
  }),
}));

import { createAgentTool, createAgentToolPromptDescription } from '../agent-tool.js';
import { createSubagentSession } from '../../assembly/create-subagent-session.js';
import { getBuiltInAgent } from '../../agents/built-in-agents.js';

/** Extract the JSON-parsed data from an IToolResult */
function parseToolResult(toolResult: IToolResult): Record<string, unknown> {
  return JSON.parse(toolResult.data as string);
}

function makeTool(name: string): IToolWithEventService {
  return {
    getName: () => name,
    getDescription: () => `Mock ${name} tool`,
    schema: {
      name,
      description: `Mock ${name} tool`,
      parameters: { type: 'object', properties: {} },
    },
    execute: vi.fn(),
    validate: vi.fn(() => true),
    validateParameters: vi.fn(() => ({ valid: true, errors: [] })),
    setEventService: vi.fn(),
  } as unknown as IToolWithEventService;
}

function makeTerminal(): ITerminalOutput {
  return {
    write: vi.fn(),
    writeLine: vi.fn(),
    spinner: vi.fn(),
  } as unknown as ITerminalOutput;
}

function makeConfig(overrides?: Partial<IResolvedConfig>): IResolvedConfig {
  return {
    defaultTrustLevel: 'moderate',
    provider: { name: 'anthropic', model: 'claude-sonnet-4-6', apiKey: 'test-key' },
    permissions: { allow: [], deny: [] },
    env: {},
    ...overrides,
  };
}

function makeContext(overrides?: Partial<ILoadedContext>): ILoadedContext {
  return {
    agentsMd: '# AGENTS.md',
    claudeMd: '# CLAUDE.md',
    ...overrides,
  };
}

function makeDeps(overrides?: Partial<IAgentToolDeps>): IAgentToolDeps {
  return {
    config: makeConfig(),
    context: makeContext(),
    tools: [makeTool('Read')],
    terminal: makeTerminal(),
    provider: mockProvider,
    ...overrides,
  };
}

describe('Agent tool', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    mockRun.mockResolvedValue('task completed successfully');
  });

  it('should have correct name and parameter schema', () => {
    const tool = createAgentTool(makeDeps());
    expect(tool.getName()).toBe('Agent');
    const schema = tool.schema;
    expect(schema.name).toBe('Agent');
    expect(schema.description).toContain('subagent');
    expect(schema.description).toContain('one subagent job');
    expect(schema.description).toContain('terminal result');
    expect(schema.description).toContain('terminal result');
    expect(schema.description).toContain('tool call');
    expect(schema.description).toContain('When the user explicitly asks');
    expect(schema.description).toContain('start the requested subagent job immediately');
    expect(schema.description).toContain('Do not ask a follow-up question');
    expect(schema.description).toContain('one Agent tool call with jobs');
    expect(schema.description).toContain('base user-facing claims on returned mode and counts');
    expect(schema.description).not.toContain('<agent');
    expect(schema.description).not.toContain('pseudo-tags');
    // Verify parameters include prompt, subagent_type, model
    const props = schema.parameters.properties;
    expect(props).toHaveProperty('prompt');
    expect(props).toHaveProperty('subagent_type');
    expect(props).toHaveProperty('model');
    expect(props).not.toHaveProperty('parallel');
    expect(props).not.toHaveProperty('background');
    expect(props).not.toHaveProperty('detach');
    expect(props).toHaveProperty('isolation');
  });

  it('should describe the user-facing claim contract in the prompt description', () => {
    const description = createAgentToolPromptDescription([
      { name: 'developer', description: 'Implementation agent' },
    ]);

    expect(description).toContain('one Agent tool call with jobs');
    expect(description).toContain('mode, requestedJobCount, startedJobCount');
    expect(description).toContain('base user-facing claims on returned mode and counts');
    expect(description).toContain('developer (Implementation agent)');
  });

  it('should resolve built-in agent type "Explore"', async () => {
    const tools = [makeTool('Read'), makeTool('Grep')];
    const config = makeConfig();
    const context = makeContext();
    const terminal = makeTerminal();
    const permissionHandler: TPermissionHandler = vi.fn();

    const tool = createAgentTool({
      config,
      context,
      tools,
      terminal,
      provider: mockProvider,
      permissionHandler,
    });

    await tool.execute({
      prompt: 'Find all test files',
      subagent_type: 'Explore',
    });

    expect(getBuiltInAgent).toHaveBeenCalledWith('Explore');
    expect(createSubagentSession).toHaveBeenCalledWith(
      expect.objectContaining({
        agentDefinition: expect.objectContaining({ name: 'Explore' }),
        parentConfig: config,
        parentContext: context,
        parentTools: tools,
        terminal,
        permissionHandler,
      }),
    );
  });

  it('should default to general-purpose when no subagent_type', async () => {
    const tools = [makeTool('Read')];
    const config = makeConfig();
    const context = makeContext();

    const tool = createAgentTool({
      config,
      context,
      tools,
      terminal: makeTerminal(),
      provider: mockProvider,
    });

    await tool.execute({ prompt: 'Do something' });

    expect(getBuiltInAgent).toHaveBeenCalledWith('general-purpose');
  });

  it('should return response with agentId', async () => {
    const tool = createAgentTool(makeDeps());

    const toolResult = await tool.execute({ prompt: 'Do task' });
    const result = parseToolResult(toolResult);

    expect(result['success']).toBe(true);
    expect(result['output']).toBe('task completed successfully');
    expect(result['agentId']).toBeDefined();
    expect(result['agentId']).toMatch(/^agent_/);
  });

  it('should route execution through injected SubagentManager in background mode', async () => {
    const subagentManager = {
      spawn: vi.fn().mockResolvedValue({
        id: 'agent_managed_1',
        type: 'Explore',
        label: 'Explore',
        parentSessionId: 'session_parent',
        status: 'running',
        mode: 'background',
        depth: 1,
        cwd: '/workspace',
        promptPreview: 'Find files',
        updatedAt: '2026-04-30T00:00:00.000Z',
      }),
      wait: vi.fn().mockResolvedValue({
        jobId: 'agent_managed_1',
        output: 'managed output',
      }),
      list: vi.fn(),
      get: vi.fn(),
      cancel: vi.fn(),
      close: vi.fn(),
      send: vi.fn(),
      shutdown: vi.fn(),
    };

    const tool = createAgentTool(
      makeDeps({
        cwd: '/workspace',
        parentSessionId: 'session_parent',
        subagentManager,
      }),
    );

    const toolResult = await tool.execute({
      prompt: 'Find files',
      subagent_type: 'Explore',
    });
    const result = parseToolResult(toolResult);

    expect(subagentManager.spawn).toHaveBeenCalledWith(
      expect.objectContaining({
        type: 'Explore',
        label: 'Explore',
        parentSessionId: 'session_parent',
        mode: 'background',
        depth: 1,
        cwd: '/workspace',
        prompt: 'Find files',
      }),
    );
    expect(subagentManager.wait).toHaveBeenCalledWith('agent_managed_1');
    expect(createSubagentSession).not.toHaveBeenCalled();
    expect(result).toEqual(
      expect.objectContaining({
        success: true,
        output: 'managed output',
        agentId: 'agent_managed_1',
      }),
    );
  });

  it('should forward worktree isolation and return handoff metadata', async () => {
    const subagentManager: ISubagentManager = {
      spawn: vi.fn().mockResolvedValue({
        id: 'agent_worktree_1',
        type: 'Explore',
        label: 'Explore',
        parentSessionId: 'session_parent',
        status: 'running',
        mode: 'background',
        depth: 1,
        cwd: '/workspace',
        isolation: 'worktree',
        promptPreview: 'Change files',
        updatedAt: '2026-04-30T00:00:00.000Z',
      }),
      wait: vi.fn().mockResolvedValue({
        jobId: 'agent_worktree_1',
        output: 'changed files',
        metadata: {
          worktreePath: '/workspace/.robota/worktrees/agent_worktree_1',
          branchName: 'robota/agent_worktree_1',
          worktreeStatus: ' M changed.ts',
          worktreeNextAction: 'Review /workspace/.robota/worktrees/agent_worktree_1.',
        },
      }),
      list: vi.fn(),
      get: vi.fn(),
      cancel: vi.fn(),
      close: vi.fn(),
      send: vi.fn(),
      shutdown: vi.fn(),
    };

    const tool = createAgentTool(
      makeDeps({
        cwd: '/workspace',
        parentSessionId: 'session_parent',
        subagentManager,
      }),
    );

    const toolResult = await tool.execute({
      prompt: 'Change files',
      subagent_type: 'Explore',
      isolation: 'worktree',
    });
    const result = parseToolResult(toolResult);

    expect(subagentManager.spawn).toHaveBeenCalledWith(
      expect.objectContaining({
        type: 'Explore',
        prompt: 'Change files',
        isolation: 'worktree',
      }),
    );
    expect(result).toEqual(
      expect.objectContaining({
        success: true,
        output: 'changed files',
        agentId: 'agent_worktree_1',
        metadata: {
          worktreePath: '/workspace/.robota/worktrees/agent_worktree_1',
          branchName: 'robota/agent_worktree_1',
          worktreeStatus: ' M changed.ts',
          worktreeNextAction: 'Review /workspace/.robota/worktrees/agent_worktree_1.',
        },
        worktreePath: '/workspace/.robota/worktrees/agent_worktree_1',
        branchName: 'robota/agent_worktree_1',
        worktreeStatus: ' M changed.ts',
        worktreeNextAction: 'Review /workspace/.robota/worktrees/agent_worktree_1.',
      }),
    );
  });

  it('should default to background mode and wait when background is omitted', async () => {
    const subagentManager = {
      spawn: vi.fn().mockResolvedValue({
        id: 'agent_background_1',
        type: 'Explore',
        label: 'Explore',
        parentSessionId: 'session_parent',
        status: 'running',
        mode: 'background',
        depth: 1,
        cwd: '/workspace',
        promptPreview: 'Find files',
        updatedAt: '2026-04-30T00:00:00.000Z',
      }),
      wait: vi.fn().mockResolvedValue({
        jobId: 'agent_background_1',
        output: 'background output',
      }),
      list: vi.fn(),
      get: vi.fn(),
      cancel: vi.fn(),
      close: vi.fn(),
      send: vi.fn(),
      shutdown: vi.fn(),
    };

    const tool = createAgentTool(
      makeDeps({
        cwd: '/workspace',
        parentSessionId: 'session_parent',
        subagentManager,
      }),
    );

    const toolResult = await tool.execute({
      prompt: 'Find files',
      subagent_type: 'Explore',
    });
    const result = parseToolResult(toolResult);

    expect(subagentManager.spawn).toHaveBeenCalledWith(
      expect.objectContaining({
        type: 'Explore',
        mode: 'background',
        prompt: 'Find files',
      }),
    );
    expect(subagentManager.wait).toHaveBeenCalledWith('agent_background_1');
    expect(result).toEqual(
      expect.objectContaining({
        success: true,
        output: 'background output',
        agentId: 'agent_background_1',
      }),
    );
  });

  it('should ignore undeclared background mode input and wait for terminal result', async () => {
    const subagentManager = {
      spawn: vi.fn().mockResolvedValue({
        id: 'agent_background_2',
        type: 'Explore',
        label: 'Explore',
        parentSessionId: 'session_parent',
        status: 'running',
        mode: 'background',
        depth: 1,
        cwd: '/workspace',
        promptPreview: 'Find files',
        updatedAt: '2026-04-30T00:00:00.000Z',
      }),
      wait: vi.fn().mockResolvedValue({
        jobId: 'agent_background_2',
        output: 'explicit background output',
      }),
      list: vi.fn(),
      get: vi.fn(),
      cancel: vi.fn(),
      close: vi.fn(),
      send: vi.fn(),
      shutdown: vi.fn(),
    };

    const tool = createAgentTool(
      makeDeps({
        cwd: '/workspace',
        parentSessionId: 'session_parent',
        subagentManager,
      }),
    );

    const toolResult = await tool.execute({
      prompt: 'Find files',
      subagent_type: 'Explore',
      background: true,
    });
    const result = parseToolResult(toolResult);

    expect(subagentManager.spawn).toHaveBeenCalledWith(
      expect.objectContaining({
        type: 'Explore',
        mode: 'background',
        prompt: 'Find files',
      }),
    );
    expect(subagentManager.wait).toHaveBeenCalledWith('agent_background_2');
    expect(result).toEqual(
      expect.objectContaining({
        success: true,
        output: 'explicit background output',
        agentId: 'agent_background_2',
      }),
    );
  });

  it('should ignore undeclared detach input and wait for terminal result', async () => {
    const subagentManager = {
      spawn: vi.fn().mockResolvedValue({
        id: 'agent_detached_1',
        type: 'Explore',
        label: 'Explore',
        parentSessionId: 'session_parent',
        status: 'running',
        mode: 'background',
        depth: 1,
        cwd: '/workspace',
        promptPreview: 'Find files',
        updatedAt: '2026-04-30T00:00:00.000Z',
      }),
      wait: vi.fn().mockResolvedValue({
        jobId: 'agent_detached_1',
        output: 'detached input still waited',
      }),
      list: vi.fn(),
      get: vi.fn(),
      cancel: vi.fn(),
      close: vi.fn(),
      send: vi.fn(),
      shutdown: vi.fn(),
    };

    const tool = createAgentTool(
      makeDeps({
        cwd: '/workspace',
        parentSessionId: 'session_parent',
        subagentManager,
      }),
    );

    const toolResult = await tool.execute({
      prompt: 'Find files',
      subagent_type: 'Explore',
      detach: true,
    });
    const result = parseToolResult(toolResult);

    expect(subagentManager.spawn).toHaveBeenCalledWith(
      expect.objectContaining({
        type: 'Explore',
        mode: 'background',
        prompt: 'Find files',
      }),
    );
    expect(subagentManager.wait).toHaveBeenCalledWith('agent_detached_1');
    expect(result).toEqual(
      expect.objectContaining({
        success: true,
        output: 'detached input still waited',
        agentId: 'agent_detached_1',
      }),
    );
  });

  it('should return a failed terminal result when a background subagent times out', async () => {
    const subagentManager = {
      spawn: vi.fn().mockResolvedValue({
        id: 'agent_timeout_1',
        type: 'Explore',
        label: 'Explore',
        parentSessionId: 'session_parent',
        status: 'running',
        mode: 'background',
        depth: 1,
        cwd: '/workspace',
        promptPreview: 'Find files',
        updatedAt: '2026-04-30T00:00:00.000Z',
      }),
      wait: vi
        .fn()
        .mockRejectedValue(new Error('Background agent produced no activity for 240000ms')),
      list: vi.fn(),
      get: vi.fn(),
      cancel: vi.fn(),
      close: vi.fn(),
      send: vi.fn(),
      shutdown: vi.fn(),
    };

    const tool = createAgentTool(
      makeDeps({
        cwd: '/workspace',
        parentSessionId: 'session_parent',
        subagentManager,
      }),
    );

    const toolResult = await tool.execute({
      prompt: 'Find files',
      subagent_type: 'Explore',
    });
    const result = parseToolResult(toolResult);

    expect(subagentManager.wait).toHaveBeenCalledWith('agent_timeout_1');
    expect(result['success']).toBe(false);
    expect(result['agentId']).toBe('agent_timeout_1');
    expect(result['error']).toContain('Background agent produced no activity');
  });

  it('should return terminal results for parallel direct Agent tool calls including timeouts', async () => {
    const subagentManager = {
      spawn: vi
        .fn()
        .mockResolvedValueOnce({
          id: 'agent_parallel_1',
          type: 'Explore',
          label: 'Explore',
          parentSessionId: 'session_parent',
          status: 'running',
          mode: 'background',
          depth: 1,
          cwd: '/workspace',
          promptPreview: 'Developer analysis',
          updatedAt: '2026-04-30T00:00:00.000Z',
        })
        .mockResolvedValueOnce({
          id: 'agent_parallel_2',
          type: 'Plan',
          label: 'Plan',
          parentSessionId: 'session_parent',
          status: 'running',
          mode: 'background',
          depth: 1,
          cwd: '/workspace',
          promptPreview: 'Designer analysis',
          updatedAt: '2026-04-30T00:00:00.000Z',
        })
        .mockResolvedValueOnce({
          id: 'agent_parallel_3',
          type: 'Explore',
          label: 'Explore',
          parentSessionId: 'session_parent',
          status: 'running',
          mode: 'background',
          depth: 1,
          cwd: '/workspace',
          promptPreview: 'Risk analysis',
          updatedAt: '2026-04-30T00:00:00.000Z',
        }),
      wait: vi
        .fn()
        .mockResolvedValueOnce({
          jobId: 'agent_parallel_1',
          output: 'developer complete',
        })
        .mockResolvedValueOnce({
          jobId: 'agent_parallel_2',
          output: 'designer complete',
        })
        .mockRejectedValueOnce(new Error('Background agent produced no activity for 240000ms')),
      list: vi.fn(),
      get: vi.fn(),
      cancel: vi.fn(),
      close: vi.fn(),
      send: vi.fn(),
      shutdown: vi.fn(),
    };

    const tool = createAgentTool(
      makeDeps({
        cwd: '/workspace',
        parentSessionId: 'session_parent',
        subagentManager,
      }),
    );

    const [developerResult, designerResult, timeoutResult] = await Promise.all([
      tool.execute({
        prompt: 'Developer analysis',
        subagent_type: 'Explore',
      }),
      tool.execute({
        prompt: 'Designer analysis',
        subagent_type: 'Plan',
      }),
      tool.execute({
        prompt: 'Risk analysis',
        subagent_type: 'Explore',
      }),
    ]);

    expect(subagentManager.wait).toHaveBeenCalledTimes(3);
    expect(subagentManager.wait).toHaveBeenNthCalledWith(1, 'agent_parallel_1');
    expect(subagentManager.wait).toHaveBeenNthCalledWith(2, 'agent_parallel_2');
    expect(subagentManager.wait).toHaveBeenNthCalledWith(3, 'agent_parallel_3');
    expect(parseToolResult(developerResult)).toEqual(
      expect.objectContaining({
        success: true,
        output: 'developer complete',
        agentId: 'agent_parallel_1',
      }),
    );
    expect(parseToolResult(designerResult)).toEqual(
      expect.objectContaining({
        success: true,
        output: 'designer complete',
        agentId: 'agent_parallel_2',
      }),
    );
    const timedOut = parseToolResult(timeoutResult);
    expect(timedOut['success']).toBe(false);
    expect(timedOut['agentId']).toBe('agent_parallel_3');
    expect(timedOut['error']).toContain('Background agent produced no activity');
  });

  it('should start all jobs in one batch Agent tool call before waiting', async () => {
    const subagentManager = {
      spawn: vi
        .fn()
        .mockResolvedValueOnce({
          id: 'agent_batch_1',
          type: 'Explore',
          label: 'Explore',
          parentSessionId: 'session_parent',
          status: 'running',
          mode: 'background',
          depth: 1,
          cwd: '/workspace',
          promptPreview: 'Developer analysis',
          updatedAt: '2026-04-30T00:00:00.000Z',
        })
        .mockResolvedValueOnce({
          id: 'agent_batch_2',
          type: 'Plan',
          label: 'Plan',
          parentSessionId: 'session_parent',
          status: 'running',
          mode: 'background',
          depth: 1,
          cwd: '/workspace',
          promptPreview: 'Designer analysis',
          updatedAt: '2026-04-30T00:00:00.000Z',
        }),
      wait: vi
        .fn()
        .mockResolvedValueOnce({ jobId: 'agent_batch_1', output: 'developer complete' })
        .mockResolvedValueOnce({ jobId: 'agent_batch_2', output: 'designer complete' }),
      list: vi.fn(),
      get: vi.fn(),
      cancel: vi.fn(),
      close: vi.fn(),
      send: vi.fn(),
      shutdown: vi.fn(),
    };

    const tool = createAgentTool(
      makeDeps({
        cwd: '/workspace',
        parentSessionId: 'session_parent',
        subagentManager,
      }),
    );

    const toolResult = await tool.execute({
      jobs: [
        { prompt: 'Developer analysis', subagent_type: 'Explore' },
        { prompt: 'Designer analysis', subagent_type: 'Plan' },
      ],
    });
    const result = parseToolResult(toolResult);

    expect(subagentManager.spawn).toHaveBeenCalledTimes(2);
    expect(subagentManager.wait).toHaveBeenCalledTimes(2);
    expect(subagentManager.spawn.mock.invocationCallOrder[1]).toBeLessThan(
      subagentManager.wait.mock.invocationCallOrder[0]!,
    );
    expect(result['success']).toBe(true);
    expect(result['agentIds']).toEqual(['agent_batch_1', 'agent_batch_2']);
    expect(result['jobs']).toEqual([
      expect.objectContaining({
        index: 0,
        success: true,
        agentId: 'agent_batch_1',
        subagent_type: 'Explore',
        output: 'developer complete',
      }),
      expect.objectContaining({
        index: 1,
        success: true,
        agentId: 'agent_batch_2',
        subagent_type: 'Plan',
        output: 'designer complete',
      }),
    ]);
  });

  it('should return provenance and counts for a four-job batch Agent tool call', async () => {
    const subagentManager = {
      spawn: vi
        .fn()
        .mockResolvedValueOnce({
          id: 'agent_batch_1',
          type: 'Explore',
          label: 'developer',
          parentSessionId: 'session_parent',
          status: 'running',
          mode: 'background',
          depth: 1,
          cwd: '/workspace',
          promptPreview: 'Developer analysis',
          updatedAt: '2026-04-30T00:00:00.000Z',
        })
        .mockResolvedValueOnce({
          id: 'agent_batch_2',
          type: 'Plan',
          label: 'designer',
          parentSessionId: 'session_parent',
          status: 'running',
          mode: 'background',
          depth: 1,
          cwd: '/workspace',
          promptPreview: 'Designer analysis',
          updatedAt: '2026-04-30T00:00:00.000Z',
        })
        .mockResolvedValueOnce({
          id: 'agent_batch_3',
          type: 'Explore',
          label: 'reviewer',
          parentSessionId: 'session_parent',
          status: 'running',
          mode: 'background',
          depth: 1,
          cwd: '/workspace',
          promptPreview: 'Review analysis',
          updatedAt: '2026-04-30T00:00:00.000Z',
        })
        .mockResolvedValueOnce({
          id: 'agent_batch_4',
          type: 'general-purpose',
          label: 'tester',
          parentSessionId: 'session_parent',
          status: 'running',
          mode: 'background',
          depth: 1,
          cwd: '/workspace',
          promptPreview: 'Test analysis',
          updatedAt: '2026-04-30T00:00:00.000Z',
        }),
      wait: vi
        .fn()
        .mockResolvedValueOnce({ jobId: 'agent_batch_1', output: 'developer complete' })
        .mockResolvedValueOnce({ jobId: 'agent_batch_2', output: 'designer complete' })
        .mockResolvedValueOnce({ jobId: 'agent_batch_3', output: 'reviewer complete' })
        .mockResolvedValueOnce({ jobId: 'agent_batch_4', output: 'tester complete' }),
      list: vi.fn(),
      get: vi.fn(),
      cancel: vi.fn(),
      close: vi.fn(),
      send: vi.fn(),
      shutdown: vi.fn(),
    };

    const tool = createAgentTool(
      makeDeps({
        cwd: '/workspace',
        parentSessionId: 'session_parent',
        subagentManager,
      }),
    );

    const toolResult = await tool.execute({
      jobs: [
        { label: 'developer', prompt: 'Developer analysis', subagent_type: 'Explore' },
        { label: 'designer', prompt: 'Designer analysis', subagent_type: 'Plan' },
        { label: 'reviewer', prompt: 'Review analysis', subagent_type: 'Explore' },
        { label: 'tester', prompt: 'Test analysis' },
      ],
    });
    const result = parseToolResult(toolResult);

    expect(subagentManager.spawn).toHaveBeenCalledTimes(4);
    expect(subagentManager.wait).toHaveBeenCalledTimes(4);
    expect(subagentManager.spawn.mock.invocationCallOrder[3]).toBeLessThan(
      subagentManager.wait.mock.invocationCallOrder[0]!,
    );
    expect(subagentManager.spawn).toHaveBeenNthCalledWith(
      1,
      expect.objectContaining({ label: 'developer', prompt: 'Developer analysis' }),
    );
    expect(subagentManager.spawn).toHaveBeenNthCalledWith(
      4,
      expect.objectContaining({ label: 'tester', prompt: 'Test analysis' }),
    );
    expect(result).toEqual(
      expect.objectContaining({
        success: true,
        mode: 'batch',
        requestedJobCount: 4,
        startedJobCount: 4,
        failedJobCount: 0,
        agentIds: ['agent_batch_1', 'agent_batch_2', 'agent_batch_3', 'agent_batch_4'],
        provenance: expect.objectContaining({
          source: 'agent-tool-batch',
          requestedJobCount: 4,
          startedJobCount: 4,
        }),
      }),
    );
    expect(result['jobs']).toEqual([
      expect.objectContaining({ index: 0, label: 'developer', groupId: result['groupId'] }),
      expect.objectContaining({ index: 1, label: 'designer', groupId: result['groupId'] }),
      expect.objectContaining({ index: 2, label: 'reviewer', groupId: result['groupId'] }),
      expect.objectContaining({ index: 3, label: 'tester', groupId: result['groupId'] }),
    ]);
  });

  it('should ignore undeclared model-emitted orchestration hints without changing single-call execution', async () => {
    const subagentManager = {
      spawn: vi.fn().mockResolvedValue({
        id: 'agent_parallel_hint_1',
        type: 'Explore',
        label: 'Explore',
        parentSessionId: 'session_parent',
        status: 'running',
        mode: 'background',
        depth: 1,
        cwd: '/workspace',
        promptPreview: 'Explore in parallel',
        updatedAt: '2026-04-30T00:00:00.000Z',
      }),
      wait: vi.fn().mockResolvedValue({
        jobId: 'agent_parallel_hint_1',
        output: 'parallel hint complete',
      }),
      list: vi.fn(),
      get: vi.fn(),
      cancel: vi.fn(),
      close: vi.fn(),
      send: vi.fn(),
      shutdown: vi.fn(),
    };

    const tool = createAgentTool(
      makeDeps({
        cwd: '/workspace',
        parentSessionId: 'session_parent',
        subagentManager,
      }),
    );

    const toolResult = await tool.execute({
      prompt: 'Explore in parallel',
      subagent_type: 'Explore',
      parallel: true,
      background: true,
      detach: true,
    });

    expect(subagentManager.spawn).toHaveBeenCalledWith(
      expect.objectContaining({
        type: 'Explore',
        mode: 'background',
        prompt: 'Explore in parallel',
      }),
    );
    expect(subagentManager.wait).toHaveBeenCalledWith('agent_parallel_hint_1');
    expect(parseToolResult(toolResult)).toEqual(
      expect.objectContaining({
        success: true,
        mode: 'single',
        requestedJobCount: 1,
        startedJobCount: 1,
        output: 'parallel hint complete',
        agentId: 'agent_parallel_hint_1',
      }),
    );
  });

  it('should return error for unknown agent type', async () => {
    const tool = createAgentTool(makeDeps());

    const toolResult = await tool.execute({
      prompt: 'Do task',
      subagent_type: 'nonexistent',
    });
    const result = parseToolResult(toolResult);

    expect(result['success']).toBe(false);
    expect(result['error']).toContain('Unknown agent type');
    expect(result['error']).toContain('nonexistent');
  });

  it('should apply model override from tool args', async () => {
    const tool = createAgentTool(makeDeps());

    await tool.execute({
      prompt: 'Do task',
      subagent_type: 'general-purpose',
      model: 'haiku',
    });

    expect(createSubagentSession).toHaveBeenCalledWith(
      expect.objectContaining({
        agentDefinition: expect.objectContaining({
          name: 'general-purpose',
          model: 'haiku',
        }),
      }),
    );
  });

  it('should pass onTextDelta and onToolExecution callbacks', async () => {
    const onTextDelta = vi.fn();
    const onToolExecution = vi.fn();

    const tool = createAgentTool(
      makeDeps({
        onTextDelta,
        onToolExecution,
      }),
    );

    await tool.execute({ prompt: 'Do task' });

    expect(createSubagentSession).toHaveBeenCalledWith(
      expect.objectContaining({
        onTextDelta: expect.any(Function),
        onToolExecution: expect.any(Function),
      }),
    );

    const sessionOptions = vi.mocked(createSubagentSession).mock.calls[0]?.[0];
    expect(sessionOptions).toBeDefined();
    if (!sessionOptions) throw new Error('Expected subagent session options');

    sessionOptions.onTextDelta?.('streamed chunk');
    sessionOptions.onToolExecution?.({
      type: 'start',
      toolName: 'Read',
      toolArgs: { file_path: 'README.md' },
    });

    expect(onTextDelta).toHaveBeenCalledWith('streamed chunk');
    expect(onToolExecution).toHaveBeenCalledWith({
      type: 'start',
      toolName: 'Read',
      toolArgs: { file_path: 'README.md' },
    });
  });

  it('should handle session.run error gracefully', async () => {
    mockRun.mockRejectedValueOnce(new Error('Provider timeout'));

    const tool = createAgentTool(makeDeps());

    const toolResult = await tool.execute({ prompt: 'Do task' });
    const result = parseToolResult(toolResult);

    expect(result['success']).toBe(false);
    expect(result['error']).toContain('Provider timeout');
    expect(result['agentId']).toBeDefined();
  });

  it('should use custom agent registry for non-built-in types', async () => {
    const customAgent: IAgentDefinition = {
      name: 'CustomWorker',
      description: 'A custom worker',
      systemPrompt: 'You are custom.',
    };
    const customRegistry = vi.fn().mockReturnValue(customAgent);

    const tool = createAgentTool(makeDeps({ customAgentRegistry: customRegistry }));

    await tool.execute({
      prompt: 'Custom task',
      subagent_type: 'CustomWorker',
    });

    expect(customRegistry).toHaveBeenCalledWith('CustomWorker');
    expect(createSubagentSession).toHaveBeenCalledWith(
      expect.objectContaining({
        agentDefinition: expect.objectContaining({ name: 'CustomWorker' }),
      }),
    );
  });

  it('should create independent tool instances per factory call', () => {
    const deps1 = makeDeps({ config: makeConfig({ defaultTrustLevel: 'moderate' }) });
    const deps2 = makeDeps({ config: makeConfig({ defaultTrustLevel: 'full' }) });

    const tool1 = createAgentTool(deps1);
    const tool2 = createAgentTool(deps2);

    // Both tools exist independently
    expect(tool1.getName()).toBe('Agent');
    expect(tool2.getName()).toBe('Agent');
    // They are distinct instances
    expect(tool1).not.toBe(tool2);
  });

  it('should handle session.run throwing a non-Error value', async () => {
    mockRun.mockRejectedValueOnce('string error without Error object');

    const tool = createAgentTool(makeDeps());

    const toolResult = await tool.execute({ prompt: 'Do task' });
    const result = parseToolResult(toolResult);

    expect(result['success']).toBe(false);
    expect(result['error']).toContain('string error without Error object');
    expect(result['agentId']).toBeDefined();
  });

  it('should return error when custom registry also returns undefined', async () => {
    const customRegistry = vi.fn().mockReturnValue(undefined);

    const tool = createAgentTool(makeDeps({ customAgentRegistry: customRegistry }));

    const toolResult = await tool.execute({
      prompt: 'Do task',
      subagent_type: 'totally-unknown',
    });
    const result = parseToolResult(toolResult);

    expect(result['success']).toBe(false);
    expect(result['error']).toContain('Unknown agent type: totally-unknown');
    expect(customRegistry).toHaveBeenCalledWith('totally-unknown');
  });

  it('should not override model when model arg is not provided', async () => {
    const tool = createAgentTool(makeDeps());

    await tool.execute({
      prompt: 'Do task',
      subagent_type: 'Explore',
      // no model arg
    });

    expect(createSubagentSession).toHaveBeenCalledWith(
      expect.objectContaining({
        agentDefinition: expect.objectContaining({
          name: 'Explore',
        }),
      }),
    );
    expect(createSubagentSession).toHaveBeenCalledWith(
      expect.objectContaining({
        agentDefinition: expect.not.objectContaining({
          model: expect.any(String),
        }),
      }),
    );
  });

  it('should prefer custom registry over built-in when both match', async () => {
    const customAgent: IAgentDefinition = {
      name: 'Explore',
      description: 'Custom explore',
      systemPrompt: 'Custom prompt.',
    };
    const customRegistry = vi.fn().mockReturnValue(customAgent);

    const tool = createAgentTool(makeDeps({ customAgentRegistry: customRegistry }));

    await tool.execute({
      prompt: 'Explore task',
      subagent_type: 'Explore',
    });

    expect(customRegistry).toHaveBeenCalledWith('Explore');
    expect(createSubagentSession).toHaveBeenCalledWith(
      expect.objectContaining({
        agentDefinition: expect.objectContaining({
          name: 'Explore',
          description: 'Custom explore',
        }),
      }),
    );
  });

  it('should generate unique agentIds across calls', async () => {
    const tool = createAgentTool(makeDeps());

    const result1 = parseToolResult(await tool.execute({ prompt: 'Task 1' }));
    const result2 = parseToolResult(await tool.execute({ prompt: 'Task 2' }));

    expect(result1['agentId']).not.toBe(result2['agentId']);
  });

  it('should not include agentId in error result for unknown agent type', async () => {
    const tool = createAgentTool(makeDeps());

    const toolResult = await tool.execute({
      prompt: 'Do task',
      subagent_type: 'nonexistent',
    });
    const result = parseToolResult(toolResult);

    // Unknown agent type error should NOT have agentId (happens before session creation)
    expect(result['agentId']).toBeUndefined();
  });

  it('should pass isForkWorker as undefined (not fork) to createSubagentSession', async () => {
    const tool = createAgentTool(makeDeps());

    await tool.execute({ prompt: 'Do task' });

    // Agent tool never sets isForkWorker (only useSubmitHandler fork runner does)
    expect(createSubagentSession).toHaveBeenCalledWith(
      expect.not.objectContaining({ isForkWorker: true }),
    );
  });

  // PRESET-016: runtime gate consulted at dispatch time. A spy SubagentManager lets us assert
  // dispatch is refused (no spawn) when the gate predicate returns false.
  function makeManagerSpy(): ISubagentManager {
    return {
      spawn: vi.fn().mockResolvedValue({
        id: 'agent_gate_1',
        type: 'Explore',
        label: 'Explore',
        parentSessionId: 'session_parent',
        status: 'running',
        mode: 'background',
        depth: 1,
        cwd: '/workspace',
        promptPreview: 'Gate task',
        updatedAt: '2026-06-14T00:00:00.000Z',
      }),
      wait: vi.fn().mockResolvedValue({ jobId: 'agent_gate_1', output: 'gate output' }),
      list: vi.fn(),
      get: vi.fn(),
      cancel: vi.fn(),
      close: vi.fn(),
      send: vi.fn(),
      shutdown: vi.fn(),
    };
  }

  it('TC-03: gate disabled → returns disabled result without dispatching', async () => {
    const subagentManager = makeManagerSpy();
    const tool = createAgentTool(
      makeDeps({
        cwd: '/workspace',
        parentSessionId: 'session_parent',
        subagentManager,
        isParallelSubagentsEnabled: () => false,
      }),
    );

    const toolResult = await tool.execute({ prompt: 'Gate task', subagent_type: 'Explore' });
    const result = parseToolResult(toolResult);

    expect(subagentManager.spawn).not.toHaveBeenCalled();
    expect(createSubagentSession).not.toHaveBeenCalled();
    expect(result['success']).toBe(false);
    expect(result['requestedJobCount']).toBe(0);
    expect(result['startedJobCount']).toBe(0);
    expect(result['error']).toContain('Parallel subagents are disabled');
  });

  it('TC-03b: gate disabled also refuses batch jobs without dispatching', async () => {
    const subagentManager = makeManagerSpy();
    const tool = createAgentTool(
      makeDeps({
        cwd: '/workspace',
        parentSessionId: 'session_parent',
        subagentManager,
        isParallelSubagentsEnabled: () => false,
      }),
    );

    const toolResult = await tool.execute({
      jobs: [{ prompt: 'Developer analysis', subagent_type: 'Explore' }],
    });
    const result = parseToolResult(toolResult);

    expect(subagentManager.spawn).not.toHaveBeenCalled();
    expect(result['error']).toContain('Parallel subagents are disabled');
  });

  it('TC-04: gate enabled → dispatch proceeds normally', async () => {
    const subagentManager = makeManagerSpy();
    const tool = createAgentTool(
      makeDeps({
        cwd: '/workspace',
        parentSessionId: 'session_parent',
        subagentManager,
        isParallelSubagentsEnabled: () => true,
      }),
    );

    const toolResult = await tool.execute({ prompt: 'Gate task', subagent_type: 'Explore' });
    const result = parseToolResult(toolResult);

    expect(subagentManager.spawn).toHaveBeenCalledTimes(1);
    expect(subagentManager.wait).toHaveBeenCalledWith('agent_gate_1');
    expect(result['success']).toBe(true);
    expect(result['output']).toBe('gate output');
  });

  it('TC-04b: no gate predicate → dispatch proceeds (default behavior unchanged)', async () => {
    const subagentManager = makeManagerSpy();
    const tool = createAgentTool(
      makeDeps({
        cwd: '/workspace',
        parentSessionId: 'session_parent',
        subagentManager,
        // isParallelSubagentsEnabled intentionally omitted
      }),
    );

    const toolResult = await tool.execute({ prompt: 'Gate task', subagent_type: 'Explore' });
    const result = parseToolResult(toolResult);

    expect(subagentManager.spawn).toHaveBeenCalledTimes(1);
    expect(result['success']).toBe(true);
  });
});
