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

const mockCheckCliStatus = vi.fn();
const mockFetchPrReviewComments = vi.fn();

vi.mock('../features/interactive/index.js', () => ({
  interactiveMode: vi.fn(),
}));

vi.mock('../shared/prompt/index.js', () => ({
  promptInput: vi.fn(),
  confirm: vi.fn(),
}));

vi.mock('../shared/ui/index.js', () => ({
  success: vi.fn(),
  info: vi.fn(),
  blankLine: vi.fn(),
  error: vi.fn(),
  withProgress: vi.fn(async (_start, _done, operation) => operation()),
}));

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

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

vi.mock('../infra/task/index.js', async (importOriginal) => ({
  ...(await importOriginal<Record<string, unknown>>()),
  summarizeTaskName: vi.fn().mockResolvedValue('test-task'),
  getCurrentBranch: vi.fn().mockReturnValue('main'),
}));

vi.mock('../infra/task/clone-base-branch.js', () => ({
  branchExists: vi.fn(),
}));

vi.mock('../infra/git/index.js', () => ({
  getGitProvider: () => ({
    createIssue: vi.fn(),
    checkCliStatus: (...args: unknown[]) => mockCheckCliStatus(...args),
    fetchPrReviewComments: (...args: unknown[]) => mockFetchPrReviewComments(...args),
  }),
}));

const mockIsIssueReference = vi.fn((s: string) => /^#\d+$/.test(s));
const mockResolveIssueTask = vi.fn();
const mockParseIssueNumbers = vi.fn((args: string[]) => {
  const numbers: number[] = [];
  for (const arg of args) {
    const match = arg.match(/^#(\d+)$/);
    if (match?.[1]) {
      numbers.push(Number.parseInt(match[1], 10));
    }
  }
  return numbers;
});
const mockFormatPrReviewAsTask = vi.fn();

vi.mock('../infra/github/index.js', () => ({
  isIssueReference: (...args: unknown[]) => mockIsIssueReference(...args),
  resolveIssueTask: (...args: unknown[]) => mockResolveIssueTask(...args),
  parseIssueNumbers: (...args: unknown[]) => mockParseIssueNumbers(...args),
  formatPrReviewAsTask: (...args: unknown[]) => mockFormatPrReviewAsTask(...args),
}));

import { interactiveMode } from '../features/interactive/index.js';
import { promptInput, confirm } from '../shared/prompt/index.js';
import { error, info } from '../shared/ui/index.js';
import { determinePiece } from '../features/tasks/execute/selectAndExecute.js';
import { addTask } from '../features/tasks/index.js';
import { getCurrentBranch } from '../infra/task/index.js';
import { branchExists } from '../infra/task/clone-base-branch.js';
import type { PrReviewData } from '../infra/git/index.js';

const mockInteractiveMode = vi.mocked(interactiveMode);
const mockPromptInput = vi.mocked(promptInput);
const mockConfirm = vi.mocked(confirm);
const mockInfo = vi.mocked(info);
const mockError = vi.mocked(error);
const mockDeterminePiece = vi.mocked(determinePiece);
const mockGetCurrentBranch = vi.mocked(getCurrentBranch);
const mockBranchExists = vi.mocked(branchExists);

let testDir: string;

function loadTasks(dir: string): { tasks: Array<Record<string, unknown>> } {
  const raw = fs.readFileSync(path.join(dir, '.takt', 'tasks.yaml'), 'utf-8');
  return parseYaml(raw) as { tasks: Array<Record<string, unknown>> };
}

function addTaskWithPrOption(cwd: string, task: string, prNumber: number): Promise<void> {
  return addTask(cwd, task, { prNumber });
}

function createMockPrReview(overrides: Partial<PrReviewData & { baseRefName?: string }> = {}): PrReviewData {
  return {
    number: 456,
    title: 'Fix auth bug',
    body: 'PR description',
    url: 'https://github.com/org/repo/pull/456',
    headRefName: 'feature/fix-auth-bug',
    comments: [{ author: 'commenter', body: 'Please update tests' }],
    reviews: [{ author: 'reviewer', body: 'Fix null check' }],
    files: ['src/auth.ts'],
    ...overrides,
  } as PrReviewData;
}

beforeEach(() => {
  vi.clearAllMocks();
  testDir = fs.mkdtempSync(path.join(tmpdir(), 'takt-test-'));
  mockDeterminePiece.mockResolvedValue('default');
  mockConfirm.mockResolvedValue(false);
  mockGetCurrentBranch.mockReturnValue('main');
  mockBranchExists.mockReturnValue(true);
  mockCheckCliStatus.mockReturnValue({ available: true });
});

afterEach(() => {
  if (testDir && fs.existsSync(testDir)) {
    fs.rmSync(testDir, { recursive: true });
  }
});

describe('addTask', () => {
  function readOrderContent(dir: string, taskDir: unknown): string {
    return fs.readFileSync(path.join(dir, String(taskDir), 'order.md'), 'utf-8');
  }

  it('should show usage and exit when task is missing', async () => {
    await addTask(testDir);

    expect(mockInfo).toHaveBeenCalledWith('Usage: takt add <task>');
    expect(mockDeterminePiece).not.toHaveBeenCalled();
    expect(fs.existsSync(path.join(testDir, '.takt', 'tasks.yaml'))).toBe(false);
  });

  it('should show usage and exit when task is blank', async () => {
    await addTask(testDir, '   ');

    expect(mockInfo).toHaveBeenCalledWith('Usage: takt add <task>');
    expect(mockDeterminePiece).not.toHaveBeenCalled();
    expect(fs.existsSync(path.join(testDir, '.takt', 'tasks.yaml'))).toBe(false);
  });

  it('should save plain text task without interactive mode', async () => {
    await addTask(testDir, '  JWT認証を実装する  ');

    expect(mockInteractiveMode).not.toHaveBeenCalled();
    const task = loadTasks(testDir).tasks[0]!;
    expect(task.content).toBeUndefined();
    expect(task.task_dir).toBeTypeOf('string');
    expect(readOrderContent(testDir, task.task_dir)).toContain('JWT認証を実装する');
    expect(task.piece).toBe('default');
    expect(task.worktree).toBe(true);
  });

  it('should include worktree settings when enabled', async () => {
    mockConfirm.mockResolvedValue(true);
    mockPromptInput.mockResolvedValueOnce('/custom/path').mockResolvedValueOnce('feat/branch');

    await addTask(testDir, 'Task content');

    const task = loadTasks(testDir).tasks[0]!;
    expect(task.worktree).toBe('/custom/path');
    expect(task.branch).toBe('feat/branch');
    expect(task.auto_pr).toBe(true);
  });

  it('should set base_branch when current branch is not main/master and user confirms', async () => {
    mockGetCurrentBranch.mockReturnValue('feat/awesome');
    mockConfirm.mockResolvedValueOnce(true);
    mockPromptInput.mockResolvedValueOnce('').mockResolvedValueOnce('');
    mockConfirm.mockResolvedValueOnce(false);

    await addTask(testDir, 'Task content');

    const task = loadTasks(testDir).tasks[0]!;
    expect(task.base_branch).toBe('feat/awesome');
  });

  it('should not set base_branch when current branch prompt is declined', async () => {
    mockGetCurrentBranch.mockReturnValue('feat/awesome');
    mockConfirm.mockResolvedValueOnce(false);
    mockPromptInput.mockResolvedValueOnce('').mockResolvedValueOnce('');

    await addTask(testDir, 'Task content');

    const task = loadTasks(testDir).tasks[0]!;
    expect(task.base_branch).toBeUndefined();
    expect(mockBranchExists).not.toHaveBeenCalled();
  });

  it('should skip base branch prompt when current branch detection fails', async () => {
    mockGetCurrentBranch.mockImplementationOnce(() => {
      throw new Error('not a git repository');
    });

    await addTask(testDir, 'Task content');

    expect(mockConfirm).toHaveBeenCalledTimes(1);
    expect(mockConfirm).toHaveBeenCalledWith('Auto-create PR?', true);
    expect(mockConfirm).not.toHaveBeenCalledWith(expect.stringContaining('現在のブランチ'));
    const task = loadTasks(testDir).tasks[0]!;
    expect(task.base_branch).toBeUndefined();
  });

  it('should reprompt when base branch does not exist', async () => {
    mockGetCurrentBranch.mockReturnValue('feat/missing');
    mockConfirm.mockResolvedValueOnce(true);
    mockBranchExists.mockReturnValueOnce(false).mockReturnValueOnce(true);
    mockPromptInput
      .mockResolvedValueOnce('develop')
      .mockResolvedValueOnce('')
      .mockResolvedValueOnce('');
    mockConfirm.mockResolvedValueOnce(false);

    await addTask(testDir, 'Task content');

    const task = loadTasks(testDir).tasks[0]!;
    expect(task.base_branch).toBe('develop');
    expect(mockError).toHaveBeenCalledWith('Base branch does not exist: feat/missing');
  });

  it('should create task from issue reference without interactive mode', async () => {
    mockResolveIssueTask.mockReturnValue('Issue #99: Fix login timeout');

    await addTask(testDir, '#99');

    expect(mockInteractiveMode).not.toHaveBeenCalled();
    expect(mockIsIssueReference).toHaveBeenCalledWith('#99');
    expect(mockParseIssueNumbers).toHaveBeenCalledWith(['#99']);
    expect(mockResolveIssueTask).toHaveBeenCalledWith('#99');
    expect(mockCheckCliStatus).not.toHaveBeenCalled();
    const task = loadTasks(testDir).tasks[0]!;
    expect(task.content).toBeUndefined();
    expect(readOrderContent(testDir, task.task_dir)).toContain('Fix login timeout');
    expect(task.issue).toBe(99);
  });

  it('should create task from PR review comments with PR-specific task settings', async () => {
    const prReview = createMockPrReview();
    const formattedTask = '## PR #456 Review Comments: Fix auth bug';
    mockFetchPrReviewComments.mockReturnValue(prReview);
    mockFormatPrReviewAsTask.mockReturnValue(formattedTask);

    await addTaskWithPrOption(testDir, 'placeholder', 456);

    expect(mockCheckCliStatus).toHaveBeenCalled();
    expect(mockCheckCliStatus.mock.invocationCallOrder[0]).toBeLessThan(
      mockFetchPrReviewComments.mock.invocationCallOrder[0],
    );
    expect(mockFetchPrReviewComments).toHaveBeenCalledWith(456);
    expect(mockFormatPrReviewAsTask).toHaveBeenCalledWith(prReview);
    expect(mockIsIssueReference).not.toHaveBeenCalled();
    expect(mockParseIssueNumbers).not.toHaveBeenCalled();
    expect(mockResolveIssueTask).not.toHaveBeenCalled();
    expect(mockPromptInput).not.toHaveBeenCalled();
    expect(mockConfirm).not.toHaveBeenCalled();
    expect(mockDeterminePiece).toHaveBeenCalledTimes(1);
    const task = loadTasks(testDir).tasks[0]!;
    expect(task.content).toBeUndefined();
    expect(task.branch).toBe('feature/fix-auth-bug');
    expect(task.auto_pr).toBe(false);
    expect(task.worktree).toBe(true);
    expect(task.draft_pr).toBeUndefined();
    expect(readOrderContent(testDir, task.task_dir)).toContain(formattedTask);
  });

  it('should store PR base_ref as base_branch when adding with --pr', async () => {
    const prReview = createMockPrReview({ baseRefName: 'release/main' });
    const formattedTask = '## PR #456 Review Comments: Fix auth bug';
    mockFetchPrReviewComments.mockReturnValue(prReview);
    mockFormatPrReviewAsTask.mockReturnValue(formattedTask);

    await addTaskWithPrOption(testDir, 'placeholder', 456);

    const task = loadTasks(testDir).tasks[0]!;
    expect(task.base_branch).toBe('release/main');
  });

  it('should not create a PR task when PR has no review comments', async () => {
    const prReview = createMockPrReview({ comments: [], reviews: [] });
    mockFetchPrReviewComments.mockReturnValue(prReview);

    await addTaskWithPrOption(testDir, 'placeholder', 456);

    expect(mockCheckCliStatus).toHaveBeenCalled();
    expect(mockFetchPrReviewComments).toHaveBeenCalledWith(456);
    expect(mockFormatPrReviewAsTask).not.toHaveBeenCalled();
    expect(mockDeterminePiece).not.toHaveBeenCalled();
    expect(mockError).toHaveBeenCalled();
    expect(fs.existsSync(path.join(testDir, '.takt', 'tasks.yaml'))).toBe(false);
  });

  it('should show error and not create task when fetchPrReviewComments throws', async () => {
    mockFetchPrReviewComments.mockImplementation(() => { throw new Error('network timeout'); });

    await addTaskWithPrOption(testDir, 'placeholder', 456);

    expect(mockCheckCliStatus).toHaveBeenCalled();
    expect(mockFetchPrReviewComments).toHaveBeenCalledWith(456);
    expect(mockFormatPrReviewAsTask).not.toHaveBeenCalled();
    expect(mockDeterminePiece).not.toHaveBeenCalled();
    expect(mockError).toHaveBeenCalledWith(expect.stringContaining('network timeout'));
    expect(fs.existsSync(path.join(testDir, '.takt', 'tasks.yaml'))).toBe(false);
  });

  it('should not create a PR task when CLI is unavailable', async () => {
    mockCheckCliStatus.mockReturnValue({ available: false, error: 'gh CLI is not available' });

    await addTaskWithPrOption(testDir, 'placeholder', 456);

    expect(mockFetchPrReviewComments).not.toHaveBeenCalled();
    expect(mockFormatPrReviewAsTask).not.toHaveBeenCalled();
    expect(mockDeterminePiece).not.toHaveBeenCalled();
    expect(mockError).toHaveBeenCalled();
    expect(fs.existsSync(path.join(testDir, '.takt', 'tasks.yaml'))).toBe(false);
  });

  it('should not perform issue parsing when PR task text looks like issue reference', async () => {
    const prReview = createMockPrReview();
    const formattedTask = '## PR #456 Review Comments: Fix auth bug';
    mockFetchPrReviewComments.mockReturnValue(prReview);
    mockFormatPrReviewAsTask.mockReturnValue(formattedTask);

    await addTaskWithPrOption(testDir, '#99', 456);

    expect(mockIsIssueReference).not.toHaveBeenCalled();

    expect(mockParseIssueNumbers).not.toHaveBeenCalled();
    expect(mockResolveIssueTask).not.toHaveBeenCalled();
    expect(mockCheckCliStatus).toHaveBeenCalled();
    expect(mockFetchPrReviewComments).toHaveBeenCalledWith(456);
    expect(mockFormatPrReviewAsTask).toHaveBeenCalledWith(prReview);
    const task = loadTasks(testDir).tasks[0]!;
    expect(task.content).toBeUndefined();
    expect(task.branch).toBe('feature/fix-auth-bug');
    expect(task.auto_pr).toBe(false);
  });

  it('should not create task when piece selection is cancelled', async () => {
    mockDeterminePiece.mockResolvedValue(null);

    await addTask(testDir, 'Task content');

    expect(fs.existsSync(path.join(testDir, '.takt', 'tasks.yaml'))).toBe(false);
  });

  it('should not save PR task when piece selection is cancelled', async () => {
    const prReview = createMockPrReview();
    const formattedTask = '## PR #456 Review Comments: Fix auth bug';
    mockDeterminePiece.mockResolvedValue(null);
    mockFetchPrReviewComments.mockReturnValue(prReview);
    mockFormatPrReviewAsTask.mockReturnValue(formattedTask);

    await addTaskWithPrOption(testDir, 'placeholder', 456);

    expect(mockCheckCliStatus).toHaveBeenCalled();
    expect(mockFetchPrReviewComments).toHaveBeenCalledWith(456);
    expect(mockFormatPrReviewAsTask).toHaveBeenCalledWith(prReview);
    expect(mockDeterminePiece).toHaveBeenCalledTimes(1);
    expect(fs.existsSync(path.join(testDir, '.takt', 'tasks.yaml'))).toBe(false);
  });
});
