/**
 * Tests for resolveTaskExecution
 */

import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { describe, it, expect, vi, beforeEach } from 'vitest';

// Mock dependencies before importing the module under test
vi.mock('../infra/config/index.js', () => ({
  loadPieceByIdentifier: vi.fn(),
  isPiecePath: vi.fn(() => false),
  loadGlobalConfig: vi.fn(() => ({})),
}));

import { loadGlobalConfig } from '../infra/config/index.js';
const mockLoadGlobalConfig = vi.mocked(loadGlobalConfig);

vi.mock('../infra/task/index.js', async (importOriginal) => ({
  ...(await importOriginal<Record<string, unknown>>()),
  TaskRunner: vi.fn(),
}));

vi.mock('../infra/task/clone.js', async (importOriginal) => ({
  ...(await importOriginal<Record<string, unknown>>()),
  createSharedClone: vi.fn(),
  removeClone: vi.fn(),
}));

vi.mock('../infra/task/git.js', async (importOriginal) => ({
  ...(await importOriginal<Record<string, unknown>>()),
  getCurrentBranch: vi.fn(() => 'main'),
}));

vi.mock('../infra/task/autoCommit.js', async (importOriginal) => ({
  ...(await importOriginal<Record<string, unknown>>()),
  autoCommitAndPush: vi.fn(),
}));

vi.mock('../infra/task/summarize.js', async (importOriginal) => ({
  ...(await importOriginal<Record<string, unknown>>()),
  summarizeTaskName: vi.fn(),
}));

vi.mock('../shared/ui/index.js', () => {
  const info = vi.fn();
  return {
    header: vi.fn(),
    info,
    error: vi.fn(),
    success: vi.fn(),
    status: vi.fn(),
    blankLine: vi.fn(),
    withProgress: vi.fn(async (start, done, operation) => {
      info(start);
      const result = await operation();
      info(typeof done === 'function' ? done(result) : done);
      return result;
    }),
  };
});

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

vi.mock('../features/tasks/execute/pieceExecution.js', () => ({
  executePiece: vi.fn(),
}));

vi.mock('../shared/context.js', () => ({
  isQuietMode: vi.fn(() => false),
}));

vi.mock('../shared/constants.js', () => ({
  DEFAULT_PIECE_NAME: 'default',
  DEFAULT_LANGUAGE: 'en',
}));

import { createSharedClone } from '../infra/task/clone.js';
import { getCurrentBranch } from '../infra/task/git.js';
import { summarizeTaskName } from '../infra/task/summarize.js';
import { info } from '../shared/ui/index.js';
import { resolveTaskExecution } from '../features/tasks/index.js';
import type { TaskInfo } from '../infra/task/index.js';

const mockCreateSharedClone = vi.mocked(createSharedClone);
const mockGetCurrentBranch = vi.mocked(getCurrentBranch);
const mockSummarizeTaskName = vi.mocked(summarizeTaskName);
const mockInfo = vi.mocked(info);

beforeEach(() => {
  vi.clearAllMocks();
});

describe('resolveTaskExecution', () => {
  it('should return defaults when task has no data', async () => {
    // Given: Task without structured data
    const task: TaskInfo = {
      name: 'simple-task',
      content: 'Simple task content',
      filePath: '/tasks/simple-task.yaml',
      createdAt: '2026-02-09T00:00:00.000Z',
      status: 'pending',
      data: null,
    };

    // When
    const result = await resolveTaskExecution(task, '/project', 'default');

    // Then
    expect(result).toEqual({
      execCwd: '/project',
      execPiece: 'default',
      isWorktree: false,
    });
    expect(mockSummarizeTaskName).not.toHaveBeenCalled();
    expect(mockCreateSharedClone).not.toHaveBeenCalled();
  });

  it('should return defaults when data has no worktree option', async () => {
    // Given: Task with data but no worktree
    const task: TaskInfo = {
      name: 'task-with-data',
      content: 'Task content',
      filePath: '/tasks/task.yaml',
      data: {
        task: 'Task content',
      },
    };

    // When
    const result = await resolveTaskExecution(task, '/project', 'default');

    // Then
    expect(result.isWorktree).toBe(false);
    expect(mockSummarizeTaskName).not.toHaveBeenCalled();
  });

  it('should create shared clone with AI-summarized slug when worktree option is true', async () => {
    // Given: Task with worktree option
    const task: TaskInfo = {
      name: 'japanese-task',
      content: '認証機能を追加する',
      filePath: '/tasks/japanese-task.yaml',
      data: {
        task: '認証機能を追加する',
        worktree: true,
      },
    };

    mockSummarizeTaskName.mockResolvedValue('add-auth');
    mockCreateSharedClone.mockReturnValue({
      path: '/project/../20260128T0504-add-auth',
      branch: 'takt/20260128T0504-add-auth',
    });

    // When
    const result = await resolveTaskExecution(task, '/project', 'default');

    // Then
    expect(mockSummarizeTaskName).toHaveBeenCalledWith('認証機能を追加する', { cwd: '/project' });
    expect(mockCreateSharedClone).toHaveBeenCalledWith('/project', {
      worktree: true,
      branch: undefined,
      taskSlug: 'add-auth',
    });
    expect(mockGetCurrentBranch).toHaveBeenCalledWith('/project');
    expect(result).toEqual({
      execCwd: '/project/../20260128T0504-add-auth',
      execPiece: 'default',
      isWorktree: true,
      branch: 'takt/20260128T0504-add-auth',
      baseBranch: 'main',
    });
  });

  it('should display generating message before AI call', async () => {
    // Given: Task with worktree
    const task: TaskInfo = {
      name: 'test-task',
      content: 'Test task',
      filePath: '/tasks/test.yaml',
      data: {
        task: 'Test task',
        worktree: true,
      },
    };

    mockSummarizeTaskName.mockResolvedValue('test-task');
    mockCreateSharedClone.mockReturnValue({
      path: '/project/../test-task',
      branch: 'takt/test-task',
    });

    // When
    await resolveTaskExecution(task, '/project', 'default');

    // Then
    expect(mockInfo).toHaveBeenCalledWith('Generating branch name...');
    expect(mockInfo).toHaveBeenCalledWith('Branch name generated: test-task');
  });

  it('should use task content (not name) for AI summarization', async () => {
    // Given: Task where name differs from content
    const task: TaskInfo = {
      name: 'old-file-name',
      content: 'New feature implementation details',
      filePath: '/tasks/old-file-name.yaml',
      data: {
        task: 'New feature implementation details',
        worktree: true,
      },
    };

    mockSummarizeTaskName.mockResolvedValue('new-feature');
    mockCreateSharedClone.mockReturnValue({
      path: '/project/../new-feature',
      branch: 'takt/new-feature',
    });

    // When
    await resolveTaskExecution(task, '/project', 'default');

    // Then: Should use content, not file name
    expect(mockSummarizeTaskName).toHaveBeenCalledWith('New feature implementation details', { cwd: '/project' });
  });

  it('should use piece override from task data', async () => {
    // Given: Task with piece override
    const task: TaskInfo = {
      name: 'task-with-piece',
      content: 'Task content',
      filePath: '/tasks/task.yaml',
      data: {
        task: 'Task content',
        piece: 'custom-piece',
      },
    };

    // When
    const result = await resolveTaskExecution(task, '/project', 'default');

    // Then
    expect(result.execPiece).toBe('custom-piece');
  });

  it('should pass branch option to createSharedClone when specified', async () => {
    // Given: Task with custom branch
    const task: TaskInfo = {
      name: 'task-with-branch',
      content: 'Task content',
      filePath: '/tasks/task.yaml',
      data: {
        task: 'Task content',
        worktree: true,
        branch: 'feature/custom-branch',
      },
    };

    mockSummarizeTaskName.mockResolvedValue('custom-task');
    mockCreateSharedClone.mockReturnValue({
      path: '/project/../custom-task',
      branch: 'feature/custom-branch',
    });

    // When
    await resolveTaskExecution(task, '/project', 'default');

    // Then
    expect(mockCreateSharedClone).toHaveBeenCalledWith('/project', {
      worktree: true,
      branch: 'feature/custom-branch',
      taskSlug: 'custom-task',
    });
  });

  it('should display clone creation info', async () => {
    // Given: Task with worktree
    const task: TaskInfo = {
      name: 'info-task',
      content: 'Info task',
      filePath: '/tasks/info.yaml',
      data: {
        task: 'Info task',
        worktree: true,
      },
    };

    mockSummarizeTaskName.mockResolvedValue('info-task');
    mockCreateSharedClone.mockReturnValue({
      path: '/project/../20260128-info-task',
      branch: 'takt/20260128-info-task',
    });

    // When
    await resolveTaskExecution(task, '/project', 'default');

    // Then
    expect(mockInfo).toHaveBeenCalledWith(
      'Clone created: /project/../20260128-info-task (branch: takt/20260128-info-task)'
    );
  });

  it('should return autoPr from task YAML when specified', async () => {
    // Given: Task with auto_pr option
    const task: TaskInfo = {
      name: 'task-with-auto-pr',
      content: 'Task content',
      filePath: '/tasks/task.yaml',
      data: {
        task: 'Task content',
        auto_pr: true,
      },
    };

    // When
    const result = await resolveTaskExecution(task, '/project', 'default');

    // Then
    expect(result.autoPr).toBe(true);
  });

  it('should return autoPr: false from task YAML when specified as false', async () => {
    // Given: Task with auto_pr: false
    const task: TaskInfo = {
      name: 'task-no-auto-pr',
      content: 'Task content',
      filePath: '/tasks/task.yaml',
      data: {
        task: 'Task content',
        auto_pr: false,
      },
    };

    // When
    const result = await resolveTaskExecution(task, '/project', 'default');

    // Then
    expect(result.autoPr).toBe(false);
  });

  it('should fall back to global config autoPr when task YAML does not specify', async () => {
    // Given: Task without auto_pr, global config has autoPr
    mockLoadGlobalConfig.mockReturnValue({
      language: 'en',
      defaultPiece: 'default',
      logLevel: 'info',
      autoPr: true,
    });

    const task: TaskInfo = {
      name: 'task-no-auto-pr-setting',
      content: 'Task content',
      filePath: '/tasks/task.yaml',
      data: {
        task: 'Task content',
      },
    };

    // When
    const result = await resolveTaskExecution(task, '/project', 'default');

    // Then
    expect(result.autoPr).toBe(true);
  });

  it('should return undefined autoPr when neither task nor config specifies', async () => {
    // Given: Neither task nor config has autoPr
    mockLoadGlobalConfig.mockReturnValue({
      language: 'en',
      defaultPiece: 'default',
      logLevel: 'info',
    });

    const task: TaskInfo = {
      name: 'task-default',
      content: 'Task content',
      filePath: '/tasks/task.yaml',
      data: {
        task: 'Task content',
      },
    };

    // When
    const result = await resolveTaskExecution(task, '/project', 'default');

    // Then
    expect(result.autoPr).toBeUndefined();
  });

  it('should prioritize task YAML auto_pr over global config', async () => {
    // Given: Task has auto_pr: false, global config has autoPr: true
    mockLoadGlobalConfig.mockReturnValue({
      language: 'en',
      defaultPiece: 'default',
      logLevel: 'info',
      autoPr: true,
    });

    const task: TaskInfo = {
      name: 'task-override',
      content: 'Task content',
      filePath: '/tasks/task.yaml',
      data: {
        task: 'Task content',
        auto_pr: false,
      },
    };

    // When
    const result = await resolveTaskExecution(task, '/project', 'default');

    // Then
    expect(result.autoPr).toBe(false);
  });

  it('should capture baseBranch from getCurrentBranch when worktree is used', async () => {
    // Given: Task with worktree, on 'develop' branch
    mockGetCurrentBranch.mockReturnValue('develop');
    const task: TaskInfo = {
      name: 'task-on-develop',
      content: 'Task on develop branch',
      filePath: '/tasks/task.yaml',
      data: {
        task: 'Task on develop branch',
        worktree: true,
      },
    };

    mockSummarizeTaskName.mockResolvedValue('task-develop');
    mockCreateSharedClone.mockReturnValue({
      path: '/project/../task-develop',
      branch: 'takt/task-develop',
    });

    // When
    const result = await resolveTaskExecution(task, '/project', 'default');

    // Then
    expect(mockGetCurrentBranch).toHaveBeenCalledWith('/project');
    expect(result.baseBranch).toBe('develop');
  });

  it('should not set baseBranch when worktree is not used', async () => {
    // Given: Task without worktree
    const task: TaskInfo = {
      name: 'task-no-worktree',
      content: 'Task without worktree',
      filePath: '/tasks/task.yaml',
      data: {
        task: 'Task without worktree',
      },
    };

    // When
    const result = await resolveTaskExecution(task, '/project', 'default');

    // Then
    expect(mockGetCurrentBranch).not.toHaveBeenCalled();
    expect(result.baseBranch).toBeUndefined();
  });

  it('should return issueNumber from task data when specified', async () => {
    // Given: Task with issue number
    const task: TaskInfo = {
      name: 'task-with-issue',
      content: 'Fix authentication bug',
      filePath: '/tasks/task.yaml',
      data: {
        task: 'Fix authentication bug',
        issue: 131,
      },
    };

    // When
    const result = await resolveTaskExecution(task, '/project', 'default');

    // Then
    expect(result.issueNumber).toBe(131);
  });

  it('should return undefined issueNumber when task data has no issue', async () => {
    // Given: Task without issue
    const task: TaskInfo = {
      name: 'task-no-issue',
      content: 'Task content',
      filePath: '/tasks/task.yaml',
      data: {
        task: 'Task content',
      },
    };

    // When
    const result = await resolveTaskExecution(task, '/project', 'default');

    // Then
    expect(result.issueNumber).toBeUndefined();
  });

  it('should not start clone creation when abortSignal is already aborted', async () => {
    // Given: Worktree task with pre-aborted signal
    const task: TaskInfo = {
      name: 'aborted-before-clone',
      content: 'Task content',
      filePath: '/tasks/task.yaml',
      data: {
        task: 'Task content',
        worktree: true,
      },
    };
    const controller = new AbortController();
    controller.abort();

    // When / Then
    await expect(resolveTaskExecution(task, '/project', 'default', controller.signal)).rejects.toThrow('Task execution aborted');
    expect(mockSummarizeTaskName).not.toHaveBeenCalled();
    expect(mockCreateSharedClone).not.toHaveBeenCalled();
  });

  it('should stage task_dir spec into run context and return reportDirName', async () => {
    const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'takt-taskdir-normal-'));
    const projectDir = path.join(tmpRoot, 'project');
    fs.mkdirSync(path.join(projectDir, '.takt', 'tasks', '20260201-015714-foptng'), { recursive: true });
    const sourceOrder = path.join(projectDir, '.takt', 'tasks', '20260201-015714-foptng', 'order.md');
    fs.writeFileSync(sourceOrder, '# normal task spec\n', 'utf-8');

    const task: TaskInfo = {
      name: 'task-with-dir',
      content: 'Task content',
      taskDir: '.takt/tasks/20260201-015714-foptng',
      filePath: '/tasks/task.yaml',
      data: {
        task: 'Task content',
      },
    };

    const result = await resolveTaskExecution(task, projectDir, 'default');

    expect(result.reportDirName).toBe('20260201-015714-foptng');
    expect(result.execCwd).toBe(projectDir);
    const stagedOrder = path.join(projectDir, '.takt', 'runs', '20260201-015714-foptng', 'context', 'task', 'order.md');
    expect(fs.existsSync(stagedOrder)).toBe(true);
    expect(fs.readFileSync(stagedOrder, 'utf-8')).toContain('normal task spec');
    expect(result.taskPrompt).toContain('Primary spec: `.takt/runs/20260201-015714-foptng/context/task/order.md`.');
    expect(result.taskPrompt).not.toContain(projectDir);
  });

  it('should throw when taskDir format is invalid', async () => {
    const task: TaskInfo = {
      name: 'task-with-invalid-dir',
      content: 'Task content',
      taskDir: '.takt/reports/20260201-015714-foptng',
      filePath: '/tasks/task.yaml',
      data: {
        task: 'Task content',
      },
    };

    await expect(resolveTaskExecution(task, '/project', 'default')).rejects.toThrow(
      'Invalid task_dir format: .takt/reports/20260201-015714-foptng',
    );
  });

  it('should throw when taskDir contains parent directory segment', async () => {
    const task: TaskInfo = {
      name: 'task-with-parent-dir',
      content: 'Task content',
      taskDir: '.takt/tasks/..',
      filePath: '/tasks/task.yaml',
      data: {
        task: 'Task content',
      },
    };

    await expect(resolveTaskExecution(task, '/project', 'default')).rejects.toThrow(
      'Invalid task_dir format: .takt/tasks/..',
    );
  });

  it('should stage task_dir spec into worktree run context and return run-scoped task prompt', async () => {
    const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'takt-taskdir-'));
    const projectDir = path.join(tmpRoot, 'project');
    const cloneDir = path.join(tmpRoot, 'clone');
    fs.mkdirSync(path.join(projectDir, '.takt', 'tasks', '20260201-015714-foptng'), { recursive: true });
    fs.mkdirSync(cloneDir, { recursive: true });
    const sourceOrder = path.join(projectDir, '.takt', 'tasks', '20260201-015714-foptng', 'order.md');
    fs.writeFileSync(sourceOrder, '# webhook task\n', 'utf-8');

    const task: TaskInfo = {
      name: 'task-with-taskdir-worktree',
      content: 'Task content',
      taskDir: '.takt/tasks/20260201-015714-foptng',
      filePath: '/tasks/task.yaml',
      data: {
        task: 'Task content',
        worktree: true,
      },
    };

    mockSummarizeTaskName.mockResolvedValue('webhook-task');
    mockCreateSharedClone.mockReturnValue({
      path: cloneDir,
      branch: 'takt/webhook-task',
    });

    const result = await resolveTaskExecution(task, projectDir, 'default');

    const stagedOrder = path.join(cloneDir, '.takt', 'runs', '20260201-015714-foptng', 'context', 'task', 'order.md');
    expect(fs.existsSync(stagedOrder)).toBe(true);
    expect(fs.readFileSync(stagedOrder, 'utf-8')).toContain('webhook task');

    expect(result.taskPrompt).toContain('Implement using only the files in `.takt/runs/20260201-015714-foptng/context/task`.');
    expect(result.taskPrompt).toContain('Primary spec: `.takt/runs/20260201-015714-foptng/context/task/order.md`.');
    expect(result.taskPrompt).not.toContain(projectDir);
  });
});
