/**
 * Tests for postExecution.ts
 *
 * Verifies branching logic: existing PR → comment, no PR → create.
 */

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

const { mockAutoCommitAndPush, mockPushBranch, mockFindExistingPr, mockCommentOnPr, mockCreatePullRequest, mockBuildPrBody } =
  vi.hoisted(() => ({
    mockAutoCommitAndPush: vi.fn(),
    mockPushBranch: vi.fn(),
    mockFindExistingPr: vi.fn(),
    mockCommentOnPr: vi.fn(),
    mockCreatePullRequest: vi.fn(),
    mockBuildPrBody: vi.fn(() => 'pr-body'),
  }));

vi.mock('../infra/task/index.js', () => ({
  autoCommitAndPush: (...args: unknown[]) => mockAutoCommitAndPush(...args),
  pushBranch: (...args: unknown[]) => mockPushBranch(...args),
}));

vi.mock('../infra/git/index.js', () => ({
  getGitProvider: () => ({
    findExistingPr: (...args: unknown[]) => mockFindExistingPr(...args),
    commentOnPr: (...args: unknown[]) => mockCommentOnPr(...args),
    createPullRequest: (...args: unknown[]) => mockCreatePullRequest(...args),
  }),
}));

vi.mock('../infra/github/index.js', () => ({
  buildPrBody: (...args: unknown[]) => mockBuildPrBody(...args),
}));

vi.mock('../shared/ui/index.js', () => ({
  info: vi.fn(),
  error: vi.fn(),
  success: vi.fn(),
}));

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

import { postExecutionFlow } from '../features/tasks/execute/postExecution.js';

const baseOptions = {
  execCwd: '/clone',
  projectCwd: '/project',
  task: 'Fix the bug',
  branch: 'task/fix-the-bug',
  baseBranch: 'main',
  shouldCreatePr: true,
  draftPr: false,
  pieceIdentifier: 'default',
};

describe('postExecutionFlow', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    mockAutoCommitAndPush.mockReturnValue({ success: true, commitHash: 'abc123' });
    mockPushBranch.mockReturnValue(undefined);
    mockCommentOnPr.mockReturnValue({ success: true });
    mockCreatePullRequest.mockReturnValue({ success: true, url: 'https://github.com/org/repo/pull/1' });
  });

  it('既存PRがない場合は createPullRequest を呼ぶ', async () => {
    mockFindExistingPr.mockReturnValue(undefined);

    await postExecutionFlow(baseOptions);

    expect(mockCreatePullRequest).toHaveBeenCalledTimes(1);
    expect(mockCommentOnPr).not.toHaveBeenCalled();
  });

  it('既存PRがある場合は commentOnPr を呼び createPullRequest は呼ばない', async () => {
    mockFindExistingPr.mockReturnValue({ number: 42, url: 'https://github.com/org/repo/pull/42' });

    await postExecutionFlow(baseOptions);

    expect(mockCommentOnPr).toHaveBeenCalledWith('/project', 42, 'pr-body');
    expect(mockCreatePullRequest).not.toHaveBeenCalled();
  });

  it('shouldCreatePr が false の場合は PR 関連処理をスキップする', async () => {
    await postExecutionFlow({ ...baseOptions, shouldCreatePr: false });

    expect(mockFindExistingPr).not.toHaveBeenCalled();
    expect(mockCommentOnPr).not.toHaveBeenCalled();
    expect(mockCreatePullRequest).not.toHaveBeenCalled();
  });

  it('commit がない場合は PR 関連処理をスキップする', async () => {
    mockAutoCommitAndPush.mockReturnValue({ success: true, commitHash: undefined });

    await postExecutionFlow(baseOptions);

    expect(mockFindExistingPr).not.toHaveBeenCalled();
    expect(mockCreatePullRequest).not.toHaveBeenCalled();
  });

  it('branch がない場合は PR 関連処理をスキップする', async () => {
    await postExecutionFlow({ ...baseOptions, branch: undefined });

    expect(mockFindExistingPr).not.toHaveBeenCalled();
    expect(mockCreatePullRequest).not.toHaveBeenCalled();
  });

  it('draftPr: true の場合、createPullRequest に draft: true が渡される', async () => {
    mockFindExistingPr.mockReturnValue(undefined);

    await postExecutionFlow({ ...baseOptions, draftPr: true });

    expect(mockCreatePullRequest).toHaveBeenCalledWith(
      '/project',
      expect.objectContaining({ draft: true }),
    );
  });

  it('draftPr: false の場合、createPullRequest に draft: false が渡される', async () => {
    mockFindExistingPr.mockReturnValue(undefined);

    await postExecutionFlow({ ...baseOptions, draftPr: false });

    expect(mockCreatePullRequest).toHaveBeenCalledWith(
      '/project',
      expect.objectContaining({ draft: false }),
    );
  });

  it('PR作成失敗時に prFailed: true を返す', async () => {
    mockFindExistingPr.mockReturnValue(undefined);
    mockCreatePullRequest.mockReturnValue({ success: false, error: 'Base ref must be a branch' });

    const result = await postExecutionFlow(baseOptions);

    expect(result.prFailed).toBe(true);
    expect(result.prError).toBe('Base ref must be a branch');
    expect(result.prUrl).toBeUndefined();
  });

  it('PRコメント失敗時に prFailed: true を返す', async () => {
    mockFindExistingPr.mockReturnValue({ number: 42, url: 'https://github.com/org/repo/pull/42' });
    mockCommentOnPr.mockReturnValue({ success: false, error: 'Permission denied' });

    const result = await postExecutionFlow(baseOptions);

    expect(result.prFailed).toBe(true);
    expect(result.prError).toBe('Permission denied');
    expect(result.prUrl).toBeUndefined();
  });

  it('PR作成成功時は prFailed を返さない', async () => {
    mockFindExistingPr.mockReturnValue(undefined);
    mockCreatePullRequest.mockReturnValue({ success: true, url: 'https://github.com/org/repo/pull/1' });

    const result = await postExecutionFlow(baseOptions);

    expect(result.prFailed).toBeUndefined();
    expect(result.prUrl).toBe('https://github.com/org/repo/pull/1');
  });

  it('issues が渡された場合、PRタイトルにIssue番号プレフィックスが付与される', async () => {
    mockFindExistingPr.mockReturnValue(undefined);

    await postExecutionFlow({
      ...baseOptions,
      task: 'Fix the bug',
      issues: [{ number: 123, title: 'This title should not appear in PR', body: '', labels: [], comments: [] }],
    });

    expect(mockCreatePullRequest).toHaveBeenCalledWith(
      '/project',
      expect.objectContaining({ title: '[#123] Fix the bug' }),
    );
  });

  it('issues が空配列の場合、PRタイトルにプレフィックスは付与されない', async () => {
    mockFindExistingPr.mockReturnValue(undefined);

    await postExecutionFlow({
      ...baseOptions,
      issues: [],
    });

    expect(mockCreatePullRequest).toHaveBeenCalledWith(
      '/project',
      expect.objectContaining({ title: 'Fix the bug' }),
    );
  });

  it('issues が undefined の場合、PRタイトルにプレフィックスは付与されない', async () => {
    mockFindExistingPr.mockReturnValue(undefined);

    await postExecutionFlow(baseOptions);

    expect(mockCreatePullRequest).toHaveBeenCalledWith(
      '/project',
      expect.objectContaining({ title: 'Fix the bug' }),
    );
  });

  it('Issueプレフィックス付きタイトルが100文字を超える場合、適切に省略される', async () => {
    mockFindExistingPr.mockReturnValue(undefined);
    const longTask = 'A'.repeat(120);
    const expectedTitle = `[#123] ${'A'.repeat(90)}...`;

    await postExecutionFlow({
      ...baseOptions,
      task: longTask,
      issues: [{ number: 123, title: 'Long issue', body: '', labels: [], comments: [] }],
    });

    expect(mockCreatePullRequest).toHaveBeenCalledWith(
      '/project',
      expect.objectContaining({ title: expectedTitle }),
    );
  });
});

