import { beforeEach, describe, expect, it, vi } from 'vitest';
import { runAgent } from '../agents/runner.js';
import { parseParts } from '../core/piece/engine/task-decomposer.js';
import { detectJudgeIndex } from '../agents/judge-utils.js';
import {
  executeAgent,
  generateReport,
  executePart,
  evaluateCondition,
  judgeStatus,
  decomposeTask,
} from '../core/piece/agent-usecases.js';

vi.mock('../agents/runner.js', () => ({
  runAgent: vi.fn(),
}));

vi.mock('../core/piece/schema-loader.js', () => ({
  loadJudgmentSchema: vi.fn(() => ({ type: 'judgment' })),
  loadEvaluationSchema: vi.fn(() => ({ type: 'evaluation' })),
  loadDecompositionSchema: vi.fn((maxParts: number) => ({ type: 'decomposition', maxParts })),
}));

vi.mock('../core/piece/engine/task-decomposer.js', () => ({
  parseParts: vi.fn(),
}));

vi.mock('../agents/judge-utils.js', () => ({
  buildJudgePrompt: vi.fn(() => 'judge prompt'),
  detectJudgeIndex: vi.fn(() => -1),
}));

function doneResponse(content: string, structuredOutput?: Record<string, unknown>) {
  return {
    persona: 'tester',
    status: 'done' as const,
    content,
    timestamp: new Date('2026-02-12T00:00:00Z'),
    structuredOutput,
  };
}

const judgeOptions = { cwd: '/repo', movementName: 'review' };

describe('agent-usecases', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('executeAgent/generateReport/executePart は runAgent に委譲する', async () => {
    vi.mocked(runAgent).mockResolvedValue(doneResponse('ok'));

    await executeAgent('coder', 'do work', { cwd: '/tmp' });
    await generateReport('coder', 'write report', { cwd: '/tmp' });
    await executePart('coder', 'part work', { cwd: '/tmp' });

    expect(runAgent).toHaveBeenCalledTimes(3);
    expect(runAgent).toHaveBeenNthCalledWith(1, 'coder', 'do work', { cwd: '/tmp' });
    expect(runAgent).toHaveBeenNthCalledWith(2, 'coder', 'write report', { cwd: '/tmp' });
    expect(runAgent).toHaveBeenNthCalledWith(3, 'coder', 'part work', { cwd: '/tmp' });
  });

  it('evaluateCondition は構造化出力の matched_index を優先する', async () => {
    vi.mocked(runAgent).mockResolvedValue(doneResponse('ignored', { matched_index: 2 }));

    const result = await evaluateCondition('agent output', [
      { index: 0, text: 'first' },
      { index: 1, text: 'second' },
    ], { cwd: '/repo' });

    expect(result).toBe(1);
    expect(runAgent).toHaveBeenCalledWith(undefined, 'judge prompt', expect.objectContaining({
      cwd: '/repo',
      outputSchema: { type: 'evaluation' },
    }));
  });

  it('evaluateCondition は構造化出力が使えない場合にタグ検出へフォールバックする', async () => {
    vi.mocked(runAgent).mockResolvedValue(doneResponse('[JUDGE:2]'));
    vi.mocked(detectJudgeIndex).mockReturnValue(1);

    const result = await evaluateCondition('agent output', [
      { index: 0, text: 'first' },
      { index: 1, text: 'second' },
    ], { cwd: '/repo' });

    expect(result).toBe(1);
    expect(detectJudgeIndex).toHaveBeenCalledWith('[JUDGE:2]');
  });

  it('evaluateCondition は runAgent が done 以外なら -1 を返す', async () => {
    vi.mocked(runAgent).mockResolvedValue({
      persona: 'tester',
      status: 'error',
      content: 'failed',
      timestamp: new Date('2026-02-12T00:00:00Z'),
    });

    const result = await evaluateCondition('agent output', [
      { index: 0, text: 'first' },
    ], { cwd: '/repo' });

    expect(result).toBe(-1);
    expect(detectJudgeIndex).not.toHaveBeenCalled();
  });

  // --- judgeStatus: 3-stage fallback ---

  it('judgeStatus は単一ルール時に auto_select を返す', async () => {
    const result = await judgeStatus('structured', 'tag', [{ condition: 'always', next: 'done' }], judgeOptions);

    expect(result).toEqual({ ruleIndex: 0, method: 'auto_select' });
    expect(runAgent).not.toHaveBeenCalled();
  });

  it('judgeStatus はルールが空ならエラー', async () => {
    await expect(judgeStatus('structured', 'tag', [], judgeOptions))
      .rejects.toThrow('judgeStatus requires at least one rule');
  });

  it('judgeStatus は Stage 1 で構造化出力 step を採用する', async () => {
    vi.mocked(runAgent).mockResolvedValueOnce(doneResponse('x', { step: 2 }));

    const result = await judgeStatus('structured', 'tag', [
      { condition: 'a', next: 'one' },
      { condition: 'b', next: 'two' },
    ], judgeOptions);

    expect(result).toEqual({ ruleIndex: 1, method: 'structured_output' });
    expect(runAgent).toHaveBeenCalledTimes(1);
    expect(runAgent).toHaveBeenCalledWith('conductor', 'structured', expect.objectContaining({
      outputSchema: { type: 'judgment' },
    }));
  });

  it('judgeStatus は Stage 2 でタグ検出を使う', async () => {
    // Stage 1: structured output fails (no structuredOutput)
    vi.mocked(runAgent).mockResolvedValueOnce(doneResponse('no match'));
    // Stage 2: tag detection succeeds
    vi.mocked(runAgent).mockResolvedValueOnce(doneResponse('[REVIEW:2]'));

    const result = await judgeStatus('structured', 'tag', [
      { condition: 'a', next: 'one' },
      { condition: 'b', next: 'two' },
    ], judgeOptions);

    expect(result).toEqual({ ruleIndex: 1, method: 'phase3_tag' });
    expect(runAgent).toHaveBeenCalledTimes(2);
    expect(runAgent).toHaveBeenNthCalledWith(1, 'conductor', 'structured', expect.objectContaining({
      outputSchema: { type: 'judgment' },
    }));
    expect(runAgent).toHaveBeenNthCalledWith(2, 'conductor', 'tag', expect.not.objectContaining({
      outputSchema: expect.anything(),
    }));
  });

  it('judgeStatus は Stage 3 で AI Judge を使う', async () => {
    // Stage 1: structured output fails
    vi.mocked(runAgent).mockResolvedValueOnce(doneResponse('no match'));
    // Stage 2: tag detection fails
    vi.mocked(runAgent).mockResolvedValueOnce(doneResponse('no tag'));
    // Stage 3: evaluateCondition succeeds
    vi.mocked(runAgent).mockResolvedValueOnce(doneResponse('ignored', { matched_index: 2 }));

    const result = await judgeStatus('structured', 'tag', [
      { condition: 'a', next: 'one' },
      { condition: 'b', next: 'two' },
    ], judgeOptions);

    expect(result).toEqual({ ruleIndex: 1, method: 'ai_judge' });
    expect(runAgent).toHaveBeenCalledTimes(3);
  });

  it('judgeStatus は全ての判定に失敗したらエラー', async () => {
    // Stage 1: structured output fails
    vi.mocked(runAgent).mockResolvedValueOnce(doneResponse('no match'));
    // Stage 2: tag detection fails
    vi.mocked(runAgent).mockResolvedValueOnce(doneResponse('no tag'));
    // Stage 3: evaluateCondition fails
    vi.mocked(runAgent).mockResolvedValueOnce(doneResponse('still no match'));
    vi.mocked(detectJudgeIndex).mockReturnValue(-1);

    await expect(judgeStatus('structured', 'tag', [
      { condition: 'a', next: 'one' },
      { condition: 'b', next: 'two' },
    ], judgeOptions)).rejects.toThrow('Status not found for movement "review"');
  });

  // --- decomposeTask ---

  it('decomposeTask は構造化出力 parts を返す', async () => {
    vi.mocked(runAgent).mockResolvedValue(doneResponse('x', {
      parts: [
        { id: 'p1', title: 'Part 1', instruction: 'Do 1', timeout_ms: 1000 },
      ],
    }));

    const result = await decomposeTask('instruction', 3, { cwd: '/repo', persona: 'team-leader' });

    expect(result).toEqual([
      { id: 'p1', title: 'Part 1', instruction: 'Do 1', timeoutMs: 1000 },
    ]);
    expect(parseParts).not.toHaveBeenCalled();
  });

  it('decomposeTask は構造化出力がない場合 parseParts にフォールバックする', async () => {
    vi.mocked(runAgent).mockResolvedValue(doneResponse('```json [] ```'));
    vi.mocked(parseParts).mockReturnValue([
      { id: 'p1', title: 'Part 1', instruction: 'fallback', timeoutMs: undefined },
    ]);

    const result = await decomposeTask('instruction', 2, { cwd: '/repo' });

    expect(parseParts).toHaveBeenCalledWith('```json [] ```', 2);
    expect(result).toEqual([
      { id: 'p1', title: 'Part 1', instruction: 'fallback', timeoutMs: undefined },
    ]);
  });

  it('decomposeTask は done 以外をエラーにする', async () => {
    vi.mocked(runAgent).mockResolvedValue({
      persona: 'team-leader',
      status: 'error',
      content: 'failure',
      error: 'bad output',
      timestamp: new Date('2026-02-12T00:00:00Z'),
    });

    await expect(decomposeTask('instruction', 2, { cwd: '/repo' }))
      .rejects.toThrow('Team leader failed: bad output');
  });
});
