/**
 * Tests for issue resolution in routing module.
 *
 * Verifies that issue references (--issue N or #N positional arg)
 * are resolved before interactive mode and passed to selectAndExecuteTask
 * via selectOptions.issues.
 */

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

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

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

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('../infra/github/issue.js', () => ({
  parseIssueNumbers: vi.fn(() => []),
  checkGhCli: vi.fn(),
  fetchIssue: vi.fn(),
  formatIssueAsTask: vi.fn(),
  isIssueReference: vi.fn(),
  resolveIssueTask: vi.fn(),
  createIssue: vi.fn(),
}));

vi.mock('../features/tasks/index.js', () => ({
  selectAndExecuteTask: vi.fn(),
  determinePiece: vi.fn(),
  saveTaskFromInteractive: vi.fn(),
  createIssueFromTask: vi.fn(),
}));

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

vi.mock('../features/interactive/index.js', () => ({
  interactiveMode: vi.fn(),
  selectInteractiveMode: vi.fn(() => 'assistant'),
  selectRecentSession: vi.fn(() => null),
  passthroughMode: vi.fn(),
  quietMode: vi.fn(),
  personaMode: vi.fn(),
  resolveLanguage: vi.fn(() => 'en'),
}));

vi.mock('../infra/config/index.js', () => ({
  getPieceDescription: vi.fn(() => ({ name: 'default', description: 'test piece', pieceStructure: '', movementPreviews: [] })),
  loadGlobalConfig: vi.fn(() => ({ interactivePreviewMovements: 3 })),
}));

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(),
  parseCreateWorktreeOption: vi.fn(),
  isDirectTask: vi.fn(() => false),
}));

import { checkGhCli, fetchIssue, formatIssueAsTask, parseIssueNumbers } from '../infra/github/issue.js';
import { selectAndExecuteTask, determinePiece, createIssueFromTask, saveTaskFromInteractive } from '../features/tasks/index.js';
import { interactiveMode, selectRecentSession } from '../features/interactive/index.js';
import { loadGlobalConfig } from '../infra/config/index.js';
import { confirm } from '../shared/prompt/index.js';
import { isDirectTask } from '../app/cli/helpers.js';
import { executeDefaultAction } from '../app/cli/routing.js';
import type { GitHubIssue } from '../infra/github/types.js';

const mockCheckGhCli = vi.mocked(checkGhCli);
const mockFetchIssue = vi.mocked(fetchIssue);
const mockFormatIssueAsTask = vi.mocked(formatIssueAsTask);
const mockParseIssueNumbers = vi.mocked(parseIssueNumbers);
const mockSelectAndExecuteTask = vi.mocked(selectAndExecuteTask);
const mockDeterminePiece = vi.mocked(determinePiece);
const mockCreateIssueFromTask = vi.mocked(createIssueFromTask);
const mockSaveTaskFromInteractive = vi.mocked(saveTaskFromInteractive);
const mockInteractiveMode = vi.mocked(interactiveMode);
const mockSelectRecentSession = vi.mocked(selectRecentSession);
const mockLoadGlobalConfig = vi.mocked(loadGlobalConfig);
const mockConfirm = vi.mocked(confirm);
const mockIsDirectTask = vi.mocked(isDirectTask);

function createMockIssue(number: number): GitHubIssue {
  return {
    number,
    title: `Issue #${number}`,
    body: `Body of issue #${number}`,
    labels: [],
    comments: [],
  };
}

beforeEach(() => {
  vi.clearAllMocks();
  // Reset opts
  for (const key of Object.keys(mockOpts)) {
    delete mockOpts[key];
  }
  // Default setup
  mockDeterminePiece.mockResolvedValue('default');
  mockInteractiveMode.mockResolvedValue({ action: 'execute', task: 'summarized task' });
  mockConfirm.mockResolvedValue(true);
  mockIsDirectTask.mockReturnValue(false);
  mockParseIssueNumbers.mockReturnValue([]);
});

describe('Issue resolution in routing', () => {
  describe('--issue option', () => {
    it('should resolve issue and pass to interactive mode when --issue is specified', async () => {
      // Given
      mockOpts.issue = 131;
      const issue131 = createMockIssue(131);
      mockCheckGhCli.mockReturnValue({ available: true });
      mockFetchIssue.mockReturnValue(issue131);
      mockFormatIssueAsTask.mockReturnValue('## GitHub Issue #131: Issue #131');

      // When
      await executeDefaultAction();

      // Then: issue should be fetched
      expect(mockFetchIssue).toHaveBeenCalledWith(131);

      // Then: interactive mode should receive the formatted issue as initial input
      expect(mockInteractiveMode).toHaveBeenCalledWith(
        '/test/cwd',
        '## GitHub Issue #131: Issue #131',
        expect.anything(),
        undefined,
      );

      // Then: selectAndExecuteTask should receive issues in options
      expect(mockSelectAndExecuteTask).toHaveBeenCalledWith(
        '/test/cwd',
        'summarized task',
        expect.objectContaining({
          issues: [issue131],
        }),
        undefined,
      );
    });

    it('should exit with error when gh CLI is unavailable for --issue', async () => {
      // Given
      mockOpts.issue = 131;
      mockCheckGhCli.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();
    });
  });

  describe('#N positional argument', () => {
    it('should resolve issue reference and pass to interactive mode', async () => {
      // Given
      const issue131 = createMockIssue(131);
      mockIsDirectTask.mockReturnValue(true);
      mockCheckGhCli.mockReturnValue({ available: true });
      mockFetchIssue.mockReturnValue(issue131);
      mockFormatIssueAsTask.mockReturnValue('## GitHub Issue #131: Issue #131');
      mockParseIssueNumbers.mockReturnValue([131]);

      // When
      await executeDefaultAction('#131');

      // Then: interactive mode should be entered with formatted issue
      expect(mockInteractiveMode).toHaveBeenCalledWith(
        '/test/cwd',
        '## GitHub Issue #131: Issue #131',
        expect.anything(),
        undefined,
      );

      // Then: selectAndExecuteTask should receive issues
      expect(mockSelectAndExecuteTask).toHaveBeenCalledWith(
        '/test/cwd',
        'summarized task',
        expect.objectContaining({
          issues: [issue131],
        }),
        undefined,
      );
    });
  });

  describe('non-issue input', () => {
    it('should pass regular text input to interactive mode without issues', async () => {
      // When
      await executeDefaultAction('refactor the code');

      // Then: interactive mode should receive the original text
      expect(mockInteractiveMode).toHaveBeenCalledWith(
        '/test/cwd',
        'refactor the code',
        expect.anything(),
        undefined,
      );

      // Then: no issue fetching should occur
      expect(mockFetchIssue).not.toHaveBeenCalled();

      // Then: selectAndExecuteTask should be called without issues
      const callArgs = mockSelectAndExecuteTask.mock.calls[0];
      expect(callArgs?.[2]?.issues).toBeUndefined();
    });

    it('should enter interactive mode with no input when no args provided', async () => {
      // When
      await executeDefaultAction();

      // Then: interactive mode should be entered with undefined input
      expect(mockInteractiveMode).toHaveBeenCalledWith(
        '/test/cwd',
        undefined,
        expect.anything(),
        undefined,
      );

      // Then: no issue fetching should occur
      expect(mockFetchIssue).not.toHaveBeenCalled();
    });
  });

  describe('interactive mode cancel', () => {
    it('should not call selectAndExecuteTask when interactive mode is cancelled', async () => {
      // Given
      mockOpts.issue = 131;
      const issue131 = createMockIssue(131);
      mockCheckGhCli.mockReturnValue({ available: true });
      mockFetchIssue.mockReturnValue(issue131);
      mockFormatIssueAsTask.mockReturnValue('## GitHub Issue #131');
      mockInteractiveMode.mockResolvedValue({ action: 'cancel', task: '' });

      // When
      await executeDefaultAction();

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

  describe('create_issue action', () => {
    it('should create issue first, then delegate final confirmation to saveTaskFromInteractive', async () => {
      // Given
      mockInteractiveMode.mockResolvedValue({ action: 'create_issue', task: 'New feature request' });
      mockCreateIssueFromTask.mockReturnValue(226);

      // When
      await executeDefaultAction();

      // Then: issue is created first
      expect(mockCreateIssueFromTask).toHaveBeenCalledWith('New feature request');
      // Then: saveTaskFromInteractive receives final confirmation message
      expect(mockSaveTaskFromInteractive).toHaveBeenCalledWith(
        '/test/cwd',
        'New feature request',
        'default',
        { issue: 226, confirmAtEndMessage: 'Add this issue to tasks?' },
      );
    });

    it('should skip confirmation and task save when issue creation fails', async () => {
      // Given
      mockInteractiveMode.mockResolvedValue({ action: 'create_issue', task: 'New feature request' });
      mockCreateIssueFromTask.mockReturnValue(undefined);

      // When
      await executeDefaultAction();

      // Then
      expect(mockCreateIssueFromTask).toHaveBeenCalledWith('New feature request');
      expect(mockSaveTaskFromInteractive).not.toHaveBeenCalled();
    });

    it('should not call selectAndExecuteTask when create_issue action is chosen', async () => {
      // Given
      mockInteractiveMode.mockResolvedValue({ action: 'create_issue', task: 'New feature request' });

      // When
      await executeDefaultAction();

      // Then: selectAndExecuteTask should NOT be called
      expect(mockSelectAndExecuteTask).not.toHaveBeenCalled();
    });
  });

  describe('session selection with provider=claude', () => {
    it('should pass selected session ID to interactiveMode when provider is claude', async () => {
      // Given
      mockLoadGlobalConfig.mockReturnValue({ interactivePreviewMovements: 3, provider: 'claude' });
      mockConfirm.mockResolvedValue(true);
      mockSelectRecentSession.mockResolvedValue('session-xyz');

      // When
      await executeDefaultAction();

      // Then: selectRecentSession should be called
      expect(mockSelectRecentSession).toHaveBeenCalledWith('/test/cwd', 'en');

      // Then: interactiveMode should receive the session ID as 4th argument
      expect(mockInteractiveMode).toHaveBeenCalledWith(
        '/test/cwd',
        undefined,
        expect.anything(),
        'session-xyz',
      );

      expect(mockConfirm).toHaveBeenCalledWith('Choose a previous session?', false);
    });

    it('should not call selectRecentSession when user selects no in confirmation', async () => {
      // Given
      mockLoadGlobalConfig.mockReturnValue({ interactivePreviewMovements: 3, provider: 'claude' });
      mockConfirm.mockResolvedValue(false);

      // When
      await executeDefaultAction();

      // Then
      expect(mockConfirm).toHaveBeenCalledWith('Choose a previous session?', false);
      expect(mockSelectRecentSession).not.toHaveBeenCalled();
      expect(mockInteractiveMode).toHaveBeenCalledWith(
        '/test/cwd',
        undefined,
        expect.anything(),
        undefined,
      );
    });

    it('should not call selectRecentSession when provider is not claude', async () => {
      // Given
      mockLoadGlobalConfig.mockReturnValue({ interactivePreviewMovements: 3, provider: 'openai' });

      // When
      await executeDefaultAction();

      // Then: selectRecentSession should NOT be called
      expect(mockSelectRecentSession).not.toHaveBeenCalled();

      // Then: interactiveMode should be called with undefined session ID
      expect(mockInteractiveMode).toHaveBeenCalledWith(
        '/test/cwd',
        undefined,
        expect.anything(),
        undefined,
      );
    });
  });
});
