import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import type { ToolContext } from './core.js';
import { TOOL_DEFS, executeTool } from './orchestration.js';

let tmp: string;

beforeEach(() => {
  tmp = mkdtempSync(join(tmpdir(), 'orchestration-test-'));
});

afterEach(() => {
  rmSync(tmp, { recursive: true, force: true });
});

const makeCtx = (overrides: Partial<ToolContext> = {}): ToolContext =>
  ({
    workspacePath: tmp,
    spawnSubTask: vi.fn().mockResolvedValue({
      subtaskIndex: 1,
      jobId: 42,
      workspacePath: join(tmp, 'subtasks', '1'),
    }),
    ...overrides,
  }) as unknown as ToolContext;

describe('TOOL_DEFS', () => {
  it('registers SpawnSubTask with title and instruction required', () => {
    const def = TOOL_DEFS['SpawnSubTask'];
    expect(def?.function.name).toBe('SpawnSubTask');
    expect(def?.function.parameters?.['required']).toEqual(['title', 'instruction']);
  });
});

describe('executeTool', () => {
  it('returns null for unknown tool names', async () => {
    expect(await executeTool('NotATool', {}, makeCtx())).toBeNull();
  });

  it('errors when spawnSubTask is unavailable in this context', async () => {
    const res = await executeTool(
      'SpawnSubTask',
      { title: 't', instruction: 'i' },
      makeCtx({ spawnSubTask: undefined }),
    );
    expect(res?.isError).toBe(true);
    expect(res?.output).toContain('使用できません');
  });

  it.each([
    [{ instruction: 'i' }],
    [{ title: 't' }],
    [{ title: '   ', instruction: 'i' }],
    [{ title: 't', instruction: '' }],
    [{ title: 42, instruction: 'i' }],
  ])('rejects missing/blank title or instruction: %j', async input => {
    const res = await executeTool('SpawnSubTask', input as Record<string, unknown>, makeCtx());
    expect(res?.isError).toBe(true);
    expect(res?.output).toContain('必須');
  });

  it('spawns with the default piece and reports job details', async () => {
    const ctx = makeCtx();
    const res = await executeTool('SpawnSubTask', { title: ' t ', instruction: ' do it ' }, ctx);
    expect(res?.isError).toBe(false);
    expect(res?.output).toContain('サブタスク #1');
    expect(res?.output).toContain('ジョブ ID: 42');
    expect(ctx.spawnSubTask).toHaveBeenCalledWith({ title: 't', instruction: 'do it', piece: 'general' });
  });

  it('rejects a piece that exists neither builtin nor custom', async () => {
    const ctx = makeCtx();
    const res = await executeTool(
      'SpawnSubTask',
      { title: 't', instruction: 'i', piece: 'no-such-piece' },
      ctx,
    );
    expect(res?.isError).toBe(true);
    expect(res?.output).toContain('no-such-piece');
    expect(ctx.spawnSubTask).not.toHaveBeenCalled();
  });

  it('accepts a piece found in a custom pieces dir', async () => {
    const customDir = join(tmp, 'custom-pieces');
    mkdirSync(customDir, { recursive: true });
    writeFileSync(join(customDir, 'my-piece.yaml'), 'name: my-piece\n');
    const ctx = makeCtx({ customPiecesDir: customDir } as Partial<ToolContext>);
    const res = await executeTool(
      'SpawnSubTask',
      { title: 't', instruction: 'i', piece: 'my-piece' },
      ctx,
    );
    expect(res?.isError).toBe(false);
    expect(ctx.spawnSubTask).toHaveBeenCalledWith({ title: 't', instruction: 'i', piece: 'my-piece' });
  });

  it('accepts custom pieces dirs given as an array', async () => {
    const customDir = join(tmp, 'pieces-a');
    mkdirSync(customDir, { recursive: true });
    writeFileSync(join(customDir, 'arr-piece.yaml'), 'name: arr-piece\n');
    const ctx = makeCtx({ customPiecesDir: [join(tmp, 'empty'), customDir] } as Partial<ToolContext>);
    const res = await executeTool(
      'SpawnSubTask',
      { title: 't', instruction: 'i', piece: 'arr-piece' },
      ctx,
    );
    expect(res?.isError).toBe(false);
  });

  it('surfaces spawn failures as tool errors', async () => {
    const ctx = makeCtx({ spawnSubTask: vi.fn().mockRejectedValue(new Error('queue full')) });
    const res = await executeTool('SpawnSubTask', { title: 't', instruction: 'i' }, ctx);
    expect(res?.isError).toBe(true);
    expect(res?.output).toContain('queue full');
  });
});

const baseCtx = { workspacePath: '/tmp/x', editAllowed: false } as ToolContext;

describe('delegate tool', () => {
  it('is registered in TOOL_DEFS', () => {
    expect(TOOL_DEFS.delegate).toBeDefined();
    expect(TOOL_DEFS.delegate.function.name).toBe('delegate');
  });

  it('calls ctx.runDelegate and returns its result as output', async () => {
    const runDelegate = vi.fn().mockResolvedValue({ result: 'sub did the thing', status: 'success' });
    const res = await executeTool('delegate', { description: 'analyze', prompt: 'analyze the CSVs' }, { ...baseCtx, runDelegate });
    expect(runDelegate).toHaveBeenCalledWith({ description: 'analyze', prompt: 'analyze the CSVs' });
    expect(res?.output).toContain('sub did the thing');
    expect(res?.isError).toBeFalsy();
  });

  it('returns isError when runDelegate is not wired', async () => {
    const res = await executeTool('delegate', { description: 'x', prompt: 'y' }, baseCtx);
    expect(res?.isError).toBe(true);
  });

  it('surfaces needs_user_input as a non-error bubble-up', async () => {
    const runDelegate = vi.fn().mockResolvedValue({ result: 'need the date', status: 'needs_user_input' });
    const res = await executeTool('delegate', { description: 'x', prompt: 'y' }, { ...baseCtx, runDelegate });
    expect(res?.output).toContain('need the date');
    expect(res?.isError).toBeFalsy();
  });

  it('marks aborted delegation as isError', async () => {
    const runDelegate = vi.fn().mockResolvedValue({ result: 'loop limit', status: 'aborted' });
    const res = await executeTool('delegate', { description: 'x', prompt: 'y' }, { ...baseCtx, runDelegate });
    expect(res?.isError).toBe(true);
  });
});

describe('WaitSubTask tool', () => {
  it('is registered in TOOL_DEFS as an argless tool', () => {
    const def = TOOL_DEFS['WaitSubTask'];
    expect(def?.function.name).toBe('WaitSubTask');
    expect(def?.function.parameters?.['properties']).toEqual({});
    expect(def?.function.parameters?.['required']).toEqual([]);
  });

  it('errors when the context cannot wait for subtasks (no hasPendingSubtasks)', async () => {
    const ctx = makeCtx({ hasPendingSubtasks: undefined } as Partial<ToolContext>);
    const res = await executeTool('WaitSubTask', {}, ctx);
    expect(res?.isError).toBe(true);
    expect(res?.output).toContain('使用できません');
    expect(ctx.pendingSubtaskWait).toBeUndefined();
  });

  it('does not park when there are no pending subtasks', async () => {
    const hasPendingSubtasks = vi.fn().mockResolvedValue(false);
    const ctx = makeCtx({ hasPendingSubtasks } as Partial<ToolContext>);
    const res = await executeTool('WaitSubTask', {}, ctx);
    expect(hasPendingSubtasks).toHaveBeenCalledTimes(1);
    expect(res?.isError).toBe(false);
    expect(res?.output).toContain('未完了サブタスクはありません');
    expect(ctx.pendingSubtaskWait).toBeUndefined();
  });

  it('sets the park flag when subtasks are still pending', async () => {
    const hasPendingSubtasks = vi.fn().mockResolvedValue(true);
    const ctx = makeCtx({ hasPendingSubtasks } as Partial<ToolContext>);
    const res = await executeTool('WaitSubTask', {}, ctx);
    expect(hasPendingSubtasks).toHaveBeenCalledTimes(1);
    expect(res?.isError).toBe(false);
    expect(ctx.pendingSubtaskWait).toBe(true);
  });
});
