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

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

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

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

const { mockCheckCliStatus, mockFetchIssue, mockFetchPrReviewComments } = vi.hoisted(() => ({
  mockCheckCliStatus: vi.fn(),
  mockFetchIssue: vi.fn(),
  mockFetchPrReviewComments: vi.fn(),
}));

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

vi.mock('../infra/github/issue.js', () => ({
  parseIssueNumbers: vi.fn(() => []),
  formatIssueAsTask: vi.fn(),
  isIssueReference: vi.fn(),
  resolveIssueTask: vi.fn(),
}));

vi.mock('../features/tasks/index.js', () => ({
  selectAndExecuteTask: vi.fn(),
  determinePiece: vi.fn(),
  saveTaskFromInteractive: vi.fn(),
  createIssueAndSaveTask: vi.fn(),
  promptLabelSelection: vi.fn().mockResolvedValue([]),
}));

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

vi.mock('../features/interactive/index.js', () => ({
  interactiveMode: vi.fn(),
  selectInteractiveMode: vi.fn(() => 'assistant'),
  passthroughMode: vi.fn(),
  quietMode: vi.fn(),
  personaMode: vi.fn(),
  resolveLanguage: vi.fn(() => 'en'),
  selectRun: vi.fn(() => null),
  loadRunSessionContext: vi.fn(),
  listRecentRuns: vi.fn(() => []),
  normalizeTaskHistorySummary: vi.fn((items: unknown[]) => items),
  dispatchConversationAction: vi.fn(async (result: { action: string }, handlers: Record<string, (r: unknown) => unknown>) => {
    return handlers[result.action](result);
  }),
}));

const mockListAllTaskItems = vi.fn();
const mockIsStaleRunningTask = vi.fn();
const mockCheckoutBranch = vi.fn();
vi.mock('../infra/task/index.js', () => ({
  TaskRunner: vi.fn(() => ({
    listAllTaskItems: mockListAllTaskItems,
  })),
  isStaleRunningTask: (...args: unknown[]) => mockIsStaleRunningTask(...args),
  checkoutBranch: (...args: unknown[]) => mockCheckoutBranch(...args),
}));

vi.mock('../infra/config/index.js', () => ({
  getPieceDescription: vi.fn(() => ({ name: 'default', description: 'test piece', pieceStructure: '', movementPreviews: [] })),
  resolveConfigValue: vi.fn((_: string, key: string) => (key === 'piece' ? 'default' : false)),
  resolveConfigValues: vi.fn(() => ({ language: 'en', interactivePreviewMovements: 3, provider: 'claude' })),
  loadPersonaSessions: vi.fn(() => ({})),
}));

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

const mockOpts: Record<string, unknown> = {};

vi.mock('../app/cli/program.js', () => {
  const chainable = {
    opts: vi.fn(() => mockOpts),
    argument: vi.fn().mockReturnThis(),
    action: vi.fn().mockReturnThis(),
  };
  return {
    program: chainable,
    resolvedCwd: '/test/cwd',
    pipelineMode: false,
  };
});

vi.mock('../app/cli/helpers.js', () => ({
  resolveAgentOverrides: vi.fn(),
  isDirectTask: vi.fn(() => false),
}));

import { selectAndExecuteTask, determinePiece, saveTaskFromInteractive } from '../features/tasks/index.js';
import { interactiveMode } from '../features/interactive/index.js';
import { executePipeline } from '../features/pipeline/index.js';
import { executeDefaultAction } from '../app/cli/routing.js';
import { error as logError } from '../shared/ui/index.js';
import type { InteractiveModeResult } from '../features/interactive/index.js';
import type { PrReviewData } from '../infra/git/index.js';

const mockSelectAndExecuteTask = vi.mocked(selectAndExecuteTask);
const mockDeterminePiece = vi.mocked(determinePiece);
const mockInteractiveMode = vi.mocked(interactiveMode);
const mockExecutePipeline = vi.mocked(executePipeline);
const mockLogError = vi.mocked(logError);
const mockSaveTaskFromInteractive = vi.mocked(saveTaskFromInteractive);

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: 'fix/auth-bug',
    comments: [{ author: 'commenter1', body: 'Update tests' }],
    reviews: [{ author: 'reviewer1', body: 'Fix null check' }],
    files: ['src/auth.ts'],
    ...overrides,
  };
}

beforeEach(() => {
  vi.clearAllMocks();
  for (const key of Object.keys(mockOpts)) {
    delete mockOpts[key];
  }
  mockDeterminePiece.mockResolvedValue('default');
  mockInteractiveMode.mockResolvedValue({ action: 'execute', task: 'summarized task' });
  mockListAllTaskItems.mockReturnValue([]);
  mockIsStaleRunningTask.mockReturnValue(false);
});

describe('PR resolution in routing', () => {
  describe('--pr option', () => {
    it('should resolve PR review comments and pass to interactive mode', async () => {
      // Given
      mockOpts.pr = 456;
      const prReview = createMockPrReview();
      mockCheckCliStatus.mockReturnValue({ available: true });
      mockFetchPrReviewComments.mockReturnValue(prReview);

      // When
      await executeDefaultAction();

      // Then
      expect(mockFetchPrReviewComments).toHaveBeenCalledWith(456);
      expect(mockInteractiveMode).toHaveBeenCalledWith(
        '/test/cwd',
        expect.stringContaining('## PR #456 Review Comments:'),
        expect.anything(),
        undefined,
        undefined,
        { excludeActions: ['create_issue'] },
      );
    });

    it('should pass PR base branch as baseBranch when interactive save_task is selected', async () => {
      // Given
      mockOpts.pr = 456;
      const actionResult: InteractiveModeResult = {
        action: 'save_task',
        task: 'Saved PR task',
      };
      mockInteractiveMode.mockResolvedValue(actionResult);
      const prReview = createMockPrReview({ baseRefName: 'release/main', headRefName: 'feat/my-pr-branch' });
      mockCheckCliStatus.mockReturnValue({ available: true });
      mockFetchPrReviewComments.mockReturnValue(prReview);

      // When
      await executeDefaultAction();

      // Then
      expect(mockSaveTaskFromInteractive).toHaveBeenCalledWith(
        '/test/cwd',
        'Saved PR task',
        'default',
        expect.objectContaining({
          presetSettings: expect.objectContaining({
            worktree: true,
            branch: 'feat/my-pr-branch',
            autoPr: false,
            baseBranch: 'release/main',
          }),
        }),
      );
    });

    it('should execute task after resolving PR review comments', async () => {
      // Given
      mockOpts.pr = 456;
      const prReview = createMockPrReview({ headRefName: 'feat/my-pr-branch' });
      mockCheckCliStatus.mockReturnValue({ available: true });
      mockFetchPrReviewComments.mockReturnValue(prReview);

      // When
      await executeDefaultAction();

      // Then: selectAndExecuteTask is called
      expect(mockSelectAndExecuteTask).toHaveBeenCalledWith(
        '/test/cwd',
        'summarized task',
        expect.any(Object),
        undefined,
      );
    });

    it('should checkout PR branch before executing task', async () => {
      // Given
      mockOpts.pr = 456;
      const prReview = createMockPrReview({ headRefName: 'feat/my-pr-branch' });
      mockCheckCliStatus.mockReturnValue({ available: true });
      mockFetchPrReviewComments.mockReturnValue(prReview);

      // When
      await executeDefaultAction();

      // Then: checkoutBranch is called with the PR's head branch
      expect(mockCheckoutBranch).toHaveBeenCalledWith('/test/cwd', 'feat/my-pr-branch');
    });

    it('should exit with error when gh CLI is unavailable', async () => {
      // Given
      mockOpts.pr = 456;
      mockCheckCliStatus.mockReturnValue({
        available: false,
        error: 'gh CLI is not installed',
      });

      const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
        throw new Error('process.exit called');
      });

      // When/Then
      await expect(executeDefaultAction()).rejects.toThrow('process.exit called');
      expect(mockExit).toHaveBeenCalledWith(1);
      expect(mockInteractiveMode).not.toHaveBeenCalled();

      mockExit.mockRestore();
    });

    it('should pass to interactive mode even when PR has no review comments', async () => {
      // Given
      mockOpts.pr = 456;
      const emptyPrReview = createMockPrReview({ reviews: [], comments: [] });
      mockCheckCliStatus.mockReturnValue({ available: true });
      mockFetchPrReviewComments.mockReturnValue(emptyPrReview);

      // When
      await executeDefaultAction();

      // Then: PR title/description/files are still passed to interactive mode
      expect(mockInteractiveMode).toHaveBeenCalledWith(
        '/test/cwd',
        expect.stringContaining('## PR #456 Review Comments:'),
        expect.anything(),
        undefined,
        undefined,
        { excludeActions: ['create_issue'] },
      );
    });

    it('should not resolve issues when --pr is specified', async () => {
      // Given
      mockOpts.pr = 456;
      const prReview = createMockPrReview();
      mockCheckCliStatus.mockReturnValue({ available: true });
      mockFetchPrReviewComments.mockReturnValue(prReview);

      // When
      await executeDefaultAction();

      // Then
      expect(mockFetchIssue).not.toHaveBeenCalled();
    });
  });

  describe('--pr and --issue mutual exclusion', () => {
    it('should exit with error when both --pr and --issue are specified', async () => {
      // Given
      mockOpts.pr = 456;
      mockOpts.issue = 123;

      const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
        throw new Error('process.exit called');
      });

      // When/Then
      await expect(executeDefaultAction()).rejects.toThrow('process.exit called');
      expect(mockLogError).toHaveBeenCalledWith('--pr and --issue cannot be used together');
      expect(mockExit).toHaveBeenCalledWith(1);

      mockExit.mockRestore();
    });
  });

  describe('--pr and --task mutual exclusion', () => {
    it('should exit with error when both --pr and --task are specified', async () => {
      // Given
      mockOpts.pr = 456;
      mockOpts.task = 'some task';

      const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
        throw new Error('process.exit called');
      });

      // When/Then
      await expect(executeDefaultAction()).rejects.toThrow('process.exit called');
      expect(mockLogError).toHaveBeenCalledWith('--pr and --task cannot be used together');
      expect(mockExit).toHaveBeenCalledWith(1);

      mockExit.mockRestore();
    });
  });

  describe('--pr in pipeline mode', () => {
    it('should pass prNumber to executePipeline', async () => {
      // Given: override pipelineMode
      const programModule = await import('../app/cli/program.js');
      const originalPipelineMode = programModule.pipelineMode;
      Object.defineProperty(programModule, 'pipelineMode', { value: true, writable: true });

      mockOpts.pr = 456;
      mockExecutePipeline.mockResolvedValue(0);

      // When
      await executeDefaultAction();

      // Then
      expect(mockExecutePipeline).toHaveBeenCalledWith(
        expect.objectContaining({
          prNumber: 456,
        }),
      );

      // Cleanup
      Object.defineProperty(programModule, 'pipelineMode', { value: originalPipelineMode, writable: true });
    });
  });
});
