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

const {
  mockExistsSync,
  mockSelectPiece,
  mockSelectOptionWithDefault,
  mockResolvePieceConfigValue,
  mockLoadPieceByIdentifier,
  mockGetPieceDescription,
  mockRunRetryMode,
  mockFindRunForTask,
  mockFindPreviousOrderContent,
  mockStartReExecution,
  mockRequeueTask,
  mockExecuteAndCompleteTask,
  mockWarn,
} = vi.hoisted(() => ({
  mockExistsSync: vi.fn(() => true),
  mockSelectPiece: vi.fn(),
  mockSelectOptionWithDefault: vi.fn(),
  mockResolvePieceConfigValue: vi.fn(),
  mockLoadPieceByIdentifier: vi.fn(),
  mockGetPieceDescription: vi.fn(() => ({
    name: 'default',
    description: 'desc',
    pieceStructure: '',
    movementPreviews: [],
  })),
  mockRunRetryMode: vi.fn(),
  mockFindRunForTask: vi.fn(() => null),
  mockFindPreviousOrderContent: vi.fn(() => null),
  mockStartReExecution: vi.fn(),
  mockRequeueTask: vi.fn(),
  mockExecuteAndCompleteTask: vi.fn(),
  mockWarn: vi.fn(),
}));

vi.mock('node:fs', async (importOriginal) => ({
  ...(await importOriginal<Record<string, unknown>>()),
  existsSync: (...args: unknown[]) => mockExistsSync(...args),
}));

vi.mock('../features/pieceSelection/index.js', () => ({
  selectPiece: (...args: unknown[]) => mockSelectPiece(...args),
}));

vi.mock('../shared/prompt/index.js', () => ({
  selectOptionWithDefault: (...args: unknown[]) => mockSelectOptionWithDefault(...args),
}));

vi.mock('../shared/ui/index.js', () => ({
  info: vi.fn(),
  header: vi.fn(),
  blankLine: vi.fn(),
  status: vi.fn(),
  warn: (...args: unknown[]) => mockWarn(...args),
}));

vi.mock('../shared/utils/index.js', async (importOriginal) => ({
  ...(await importOriginal<Record<string, unknown>>()),
  createLogger: () => ({
    info: vi.fn(),
    error: vi.fn(),
  }),
}));

vi.mock('../infra/config/index.js', () => ({
  resolvePieceConfigValue: (...args: unknown[]) => mockResolvePieceConfigValue(...args),
  loadPieceByIdentifier: (...args: unknown[]) => mockLoadPieceByIdentifier(...args),
  getPieceDescription: (...args: unknown[]) => mockGetPieceDescription(...args),
}));

vi.mock('../features/interactive/index.js', () => ({
  findRunForTask: (...args: unknown[]) => mockFindRunForTask(...args),
  loadRunSessionContext: vi.fn(),
  getRunPaths: vi.fn(() => ({ logsDir: '/tmp/logs', reportsDir: '/tmp/reports' })),
  formatRunSessionForPrompt: vi.fn(() => ({
    runTask: '',
    runPiece: 'default',
    runStatus: '',
    runMovementLogs: '',
    runReports: '',
  })),
  runRetryMode: (...args: unknown[]) => mockRunRetryMode(...args),
  findPreviousOrderContent: (...args: unknown[]) => mockFindPreviousOrderContent(...args),
}));

vi.mock('../infra/task/index.js', () => ({
  TaskRunner: class {
    startReExecution(...args: unknown[]) {
      return mockStartReExecution(...args);
    }
    requeueTask(...args: unknown[]) {
      return mockRequeueTask(...args);
    }
  },
}));

vi.mock('../features/tasks/execute/taskExecution.js', () => ({
  executeAndCompleteTask: (...args: unknown[]) => mockExecuteAndCompleteTask(...args),
}));

vi.mock('../shared/i18n/index.js', () => ({
  getLabel: vi.fn((key: string) => {
    const labels: Record<string, string> = {
      'retry.workflowPrompt': 'Select workflow:',
      'retry.usePreviousWorkflow': 'Use previous',
      'retry.changeWorkflow': 'Change workflow',
    };
    return labels[key] ?? key;
  }),
}));

import { retryFailedTask } from '../features/tasks/list/taskRetryActions.js';
import type { TaskListItem } from '../infra/task/types.js';
import type { PieceConfig } from '../core/models/index.js';

const defaultPieceConfig: PieceConfig = {
  name: 'default',
  description: 'Default piece',
  initialMovement: 'plan',
  maxMovements: 30,
  movements: [
    { name: 'plan', persona: 'planner', instruction: '' },
    { name: 'implement', persona: 'coder', instruction: '' },
    { name: 'review', persona: 'reviewer', instruction: '' },
  ],
};

function makeFailedTask(overrides?: Partial<TaskListItem>): TaskListItem {
  return {
    kind: 'failed',
    name: 'my-task',
    createdAt: '2025-01-15T12:02:00.000Z',
    filePath: '/project/.takt/tasks.yaml',
    content: 'Do something',
    branch: 'takt/my-task',
    worktreePath: '/project/.takt/worktrees/my-task',
    data: { task: 'Do something', piece: 'default' },
    failure: { movement: 'review', error: 'Boom' },
    ...overrides,
  };
}

beforeEach(() => {
  vi.clearAllMocks();
  mockExistsSync.mockReturnValue(true);

  mockSelectPiece.mockResolvedValue('default');
  mockResolvePieceConfigValue.mockReturnValue(3);
  mockLoadPieceByIdentifier.mockReturnValue(defaultPieceConfig);
  mockSelectOptionWithDefault.mockResolvedValue('plan');
  mockRunRetryMode.mockResolvedValue({ action: 'execute', task: '追加指示A' });
  mockFindPreviousOrderContent.mockReturnValue(null);
  mockStartReExecution.mockReturnValue({
    name: 'my-task',
    content: 'Do something',
    data: { task: 'Do something', piece: 'default' },
  });
  mockExecuteAndCompleteTask.mockResolvedValue(true);
});

describe('retryFailedTask', () => {
  it('should run retry mode in existing worktree and execute directly', async () => {
    const task = makeFailedTask();

    const result = await retryFailedTask(task, '/project');

    expect(result).toBe(true);
    expect(mockSelectPiece).toHaveBeenCalledWith('/project');
    expect(mockRunRetryMode).toHaveBeenCalledWith(
      '/project/.takt/worktrees/my-task',
      expect.objectContaining({
        failure: expect.objectContaining({ taskName: 'my-task', taskContent: 'Do something' }),
      }),
      null,
    );
    expect(mockStartReExecution).toHaveBeenCalledWith('my-task', ['failed'], undefined, '追加指示A');
    expect(mockExecuteAndCompleteTask).toHaveBeenCalled();
  });

  it('should pass failed movement as default to selectOptionWithDefault', async () => {
    const task = makeFailedTask(); // failure.movement = 'review'

    await retryFailedTask(task, '/project');

    expect(mockSelectOptionWithDefault).toHaveBeenCalledWith(
      'Start from movement:',
      expect.arrayContaining([
        expect.objectContaining({ value: 'plan' }),
        expect.objectContaining({ value: 'implement' }),
        expect.objectContaining({ value: 'review' }),
      ]),
      'review',
    );
  });

  it('should pass non-initial movement as startMovement', async () => {
    const task = makeFailedTask();
    mockSelectOptionWithDefault.mockResolvedValue('implement');

    await retryFailedTask(task, '/project');

    expect(mockStartReExecution).toHaveBeenCalledWith('my-task', ['failed'], 'implement', '追加指示A');
  });

  it('should not pass startMovement when initial movement is selected', async () => {
    const task = makeFailedTask();

    await retryFailedTask(task, '/project');

    expect(mockStartReExecution).toHaveBeenCalledWith('my-task', ['failed'], undefined, '追加指示A');
  });

  it('should append instruction to existing retry note', async () => {
    const task = makeFailedTask({ data: { task: 'Do something', piece: 'default', retry_note: '既存ノート' } });

    await retryFailedTask(task, '/project');

    expect(mockStartReExecution).toHaveBeenCalledWith(
      'my-task', ['failed'], undefined, '既存ノート\n\n追加指示A',
    );
  });

  it('should search runs in worktree, not projectDir', async () => {
    const task = makeFailedTask();

    await retryFailedTask(task, '/project');

    expect(mockFindRunForTask).toHaveBeenCalledWith('/project/.takt/worktrees/my-task', 'Do something');
  });

  it('should show deprecated config warning when selected run order uses legacy provider fields', async () => {
    const task = makeFailedTask();
    mockFindPreviousOrderContent.mockReturnValue([
      'movements:',
      '  - name: review',
      '    provider: codex',
      '    model: gpt-5.3',
      '    provider_options:',
      '      codex:',
      '        network_access: true',
    ].join('\n'));

    await retryFailedTask(task, '/project');

    expect(mockWarn).toHaveBeenCalledTimes(1);
    expect(mockWarn).toHaveBeenCalledWith(expect.stringContaining('deprecated'));
  });

  it('should not warn when selected run order uses provider block format', async () => {
    const task = makeFailedTask();
    mockFindPreviousOrderContent.mockReturnValue([
      'movements:',
      '  - name: review',
      '    provider:',
      '      type: codex',
      '      model: gpt-5.3',
      '      network_access: true',
    ].join('\n'));

    await retryFailedTask(task, '/project');

    expect(mockWarn).not.toHaveBeenCalled();
  });

  it('should throw when worktree path is not set', async () => {
    const task = makeFailedTask({ worktreePath: undefined });

    await expect(retryFailedTask(task, '/project')).rejects.toThrow('Worktree path is not set');
  });

  it('should throw when worktree directory does not exist', async () => {
    mockExistsSync.mockReturnValue(false);
    const task = makeFailedTask();

    await expect(retryFailedTask(task, '/project')).rejects.toThrow('Worktree directory does not exist');
  });

  it('should return false when piece selection is cancelled', async () => {
    const task = makeFailedTask();
    mockSelectPiece.mockResolvedValue(null);

    const result = await retryFailedTask(task, '/project');

    expect(result).toBe(false);
    expect(mockLoadPieceByIdentifier).not.toHaveBeenCalled();
  });

  it('should return false when retry mode is cancelled', async () => {
    const task = makeFailedTask();
    mockRunRetryMode.mockResolvedValue({ action: 'cancel', task: '' });

    const result = await retryFailedTask(task, '/project');

    expect(result).toBe(false);
    expect(mockStartReExecution).not.toHaveBeenCalled();
  });

  it('should requeue task via requeueTask when save_task action', async () => {
    const task = makeFailedTask();
    mockRunRetryMode.mockResolvedValue({ action: 'save_task', task: '追加指示A' });

    const result = await retryFailedTask(task, '/project');

    expect(result).toBe(true);
    expect(mockRequeueTask).toHaveBeenCalledWith('my-task', ['failed'], undefined, '追加指示A');
    expect(mockStartReExecution).not.toHaveBeenCalled();
    expect(mockExecuteAndCompleteTask).not.toHaveBeenCalled();
  });

  it('should requeue task with existing retry note appended when save_task', async () => {
    const task = makeFailedTask({ data: { task: 'Do something', piece: 'default', retry_note: '既存ノート' } });
    mockRunRetryMode.mockResolvedValue({ action: 'save_task', task: '追加指示A' });

    await retryFailedTask(task, '/project');

    expect(mockRequeueTask).toHaveBeenCalledWith('my-task', ['failed'], undefined, '既存ノート\n\n追加指示A');
  });

  describe('when previous workflow exists', () => {
    beforeEach(() => {
      mockFindRunForTask.mockReturnValue('run-123');
    });

    it('should show workflow selection prompt when runInfo.piece exists', async () => {
      const task = makeFailedTask();

      await retryFailedTask(task, '/project');

      expect(mockSelectOptionWithDefault).toHaveBeenCalledWith(
        'Select workflow:',
        expect.arrayContaining([
          expect.objectContaining({ value: 'use_previous' }),
          expect.objectContaining({ value: 'change' }),
        ]),
        'use_previous',
      );
    });

    it('should use previous workflow when use_previous is selected', async () => {
      const task = makeFailedTask();
      mockSelectOptionWithDefault.mockResolvedValue('use_previous');

      await retryFailedTask(task, '/project');

      expect(mockSelectPiece).not.toHaveBeenCalled();
      expect(mockLoadPieceByIdentifier).toHaveBeenCalledWith('default', '/project');
    });

    it('should call selectPiece when change is selected', async () => {
      const task = makeFailedTask();
      mockSelectOptionWithDefault.mockResolvedValue('change');

      await retryFailedTask(task, '/project');

      expect(mockSelectPiece).toHaveBeenCalledWith('/project');
    });

    it('should return false when workflow selection is cancelled', async () => {
      const task = makeFailedTask();
      mockSelectOptionWithDefault.mockResolvedValue(null);

      const result = await retryFailedTask(task, '/project');

      expect(result).toBe(false);
      expect(mockLoadPieceByIdentifier).not.toHaveBeenCalled();
    });
  });
});
