/**
 * @license
 * Copyright 2026 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
import { act } from 'react';
import { renderWithProviders } from '../../test-utils/render.js';
import { createMockSettings } from '../../test-utils/settings.js';
import { makeFakeConfig } from '@google/gemini-cli-core';
import { waitFor } from '../../test-utils/async.js';
import { AskUserDialog } from './AskUserDialog.js';
import { QuestionType, type Question } from '@google/gemini-cli-core';
import { UIStateContext, type UIState } from '../contexts/UIStateContext.js';

// Helper to write to stdin with proper act() wrapping
const writeKey = (stdin: { write: (data: string) => void }, key: string) => {
  act(() => {
    stdin.write(key);
  });
};

describe('AskUserDialog', () => {
  // Ensure keystrokes appear spaced in time to avoid bufferFastReturn
  // converting Enter into Shift+Enter during synchronous test execution.
  let mockTime: number;
  beforeEach(() => {
    mockTime = 0;
    vi.spyOn(Date, 'now').mockImplementation(() => (mockTime += 50));
  });

  afterEach(() => {
    vi.restoreAllMocks();
  });

  const authQuestion: Question[] = [
    {
      question: 'Which authentication method should we use?',
      header: 'Auth',
      type: QuestionType.CHOICE,
      options: [
        { label: 'OAuth 2.0', description: 'Industry standard, supports SSO' },
        { label: 'JWT tokens', description: 'Stateless, good for APIs' },
      ],
      multiSelect: false,
    },
  ];

  it('renders question and options', async () => {
    const { lastFrame } = await renderWithProviders(
      <AskUserDialog
        questions={authQuestion}
        onSubmit={vi.fn()}
        onCancel={vi.fn()}
        width={120}
      />,
      { width: 120 },
    );

    expect(lastFrame()).toMatchSnapshot();
  });

  describe.each([
    {
      name: 'Single Select',
      questions: authQuestion,
      actions: (stdin: { write: (data: string) => void }) => {
        writeKey(stdin, '\r');
      },
      expectedSubmit: { '0': 'OAuth 2.0' },
    },
    {
      name: 'Multi-select',
      questions: [
        {
          question: 'Which features?',
          header: 'Features',
          type: QuestionType.CHOICE,
          options: [
            { label: 'TypeScript', description: '' },
            { label: 'ESLint', description: '' },
          ],
          multiSelect: true,
        },
      ] as Question[],
      actions: (stdin: { write: (data: string) => void }) => {
        writeKey(stdin, '\r'); // Toggle TS
        writeKey(stdin, '\x1b[B'); // Down
        writeKey(stdin, '\r'); // Toggle ESLint
        writeKey(stdin, '\x1b[B'); // Down to All of the above
        writeKey(stdin, '\x1b[B'); // Down to Other
        writeKey(stdin, '\x1b[B'); // Down to Done
        writeKey(stdin, '\r'); // Done
      },
      expectedSubmit: { '0': 'TypeScript, ESLint' },
    },
    {
      name: 'All of the above',
      questions: [
        {
          question: 'Which features?',
          header: 'Features',
          type: QuestionType.CHOICE,
          options: [
            { label: 'TypeScript', description: '' },
            { label: 'ESLint', description: '' },
          ],
          multiSelect: true,
        },
      ] as Question[],
      actions: (stdin: { write: (data: string) => void }) => {
        writeKey(stdin, '\x1b[B'); // Down to ESLint
        writeKey(stdin, '\x1b[B'); // Down to All of the above
        writeKey(stdin, '\r'); // Toggle All of the above
        writeKey(stdin, '\x1b[B'); // Down to Other
        writeKey(stdin, '\x1b[B'); // Down to Done
        writeKey(stdin, '\r'); // Done
      },
      expectedSubmit: { '0': 'TypeScript, ESLint' },
    },
    {
      name: 'Text Input',
      questions: [
        {
          question: 'Name?',
          header: 'Name',
          type: QuestionType.TEXT,
        },
      ] as Question[],
      actions: (stdin: { write: (data: string) => void }) => {
        for (const char of 'test-app') {
          writeKey(stdin, char);
        }
        writeKey(stdin, '\r');
      },
      expectedSubmit: { '0': 'test-app' },
    },
  ])('Submission: $name', ({ name, questions, actions, expectedSubmit }) => {
    it(`submits correct values for ${name}`, async () => {
      const onSubmit = vi.fn();
      const { stdin } = await renderWithProviders(
        <AskUserDialog
          questions={questions}
          onSubmit={onSubmit}
          onCancel={vi.fn()}
          width={120}
        />,
        { width: 120 },
      );

      actions(stdin);

      await waitFor(async () => {
        expect(onSubmit).toHaveBeenCalledWith(expectedSubmit);
      });
    });
  });

  it('verifies "All of the above" visual state with snapshot', async () => {
    const questions = [
      {
        question: 'Which features?',
        header: 'Features',
        type: QuestionType.CHOICE,
        options: [
          { label: 'TypeScript', description: '' },
          { label: 'ESLint', description: '' },
        ],
        multiSelect: true,
      },
    ] as Question[];

    const { stdin, lastFrame, waitUntilReady } = await renderWithProviders(
      <AskUserDialog
        questions={questions}
        onSubmit={vi.fn()}
        onCancel={vi.fn()}
        width={120}
      />,
      { width: 120 },
    );

    // Navigate to "All of the above" and toggle it
    writeKey(stdin, '\x1b[B'); // Down to ESLint
    writeKey(stdin, '\x1b[B'); // Down to All of the above
    writeKey(stdin, '\r'); // Toggle All of the above

    await waitFor(async () => {
      await waitUntilReady();
      // Verify visual state (checkmarks on all options)
      expect(lastFrame()).toMatchSnapshot();
    });
  });

  it('handles custom option in single select with inline typing', async () => {
    const onSubmit = vi.fn();
    const { stdin, lastFrame, waitUntilReady } = await renderWithProviders(
      <AskUserDialog
        questions={authQuestion}
        onSubmit={onSubmit}
        onCancel={vi.fn()}
        width={120}
      />,
      { width: 120 },
    );

    // Move down to custom option
    writeKey(stdin, '\x1b[B');
    writeKey(stdin, '\x1b[B');

    await waitFor(async () => {
      await waitUntilReady();
      expect(lastFrame()).toContain('Enter a custom value');
    });

    // Type directly (inline)
    for (const char of 'API Key') {
      writeKey(stdin, char);
    }

    await waitFor(async () => {
      await waitUntilReady();
      expect(lastFrame()).toContain('API Key');
    });

    // Press Enter to submit the custom value
    writeKey(stdin, '\r');

    await waitFor(async () => {
      expect(onSubmit).toHaveBeenCalledWith({ '0': 'API Key' });
    });
  });

  it('supports multi-line input for "Other" option in choice questions', async () => {
    const authQuestionWithOther: Question[] = [
      {
        question: 'Which authentication method?',
        header: 'Auth',
        type: QuestionType.CHOICE,
        options: [{ label: 'OAuth 2.0', description: '' }],
        multiSelect: false,
      },
    ];

    const onSubmit = vi.fn();
    const { stdin, lastFrame, waitUntilReady } = await renderWithProviders(
      <AskUserDialog
        questions={authQuestionWithOther}
        onSubmit={onSubmit}
        onCancel={vi.fn()}
        width={120}
      />,
      { width: 120 },
    );

    // Navigate to "Other" option
    writeKey(stdin, '\x1b[B'); // Down to "Other"

    // Type first line
    for (const char of 'Line 1') {
      writeKey(stdin, char);
    }

    // Insert newline using \ + Enter (handled by bufferBackslashEnter)
    writeKey(stdin, '\\');
    writeKey(stdin, '\r');

    // Type second line
    for (const char of 'Line 2') {
      writeKey(stdin, char);
    }

    await waitFor(async () => {
      await waitUntilReady();
      expect(lastFrame()).toContain('Line 1');
      await waitUntilReady();
      expect(lastFrame()).toContain('Line 2');
    });

    // Press Enter to submit
    writeKey(stdin, '\r');

    await waitFor(async () => {
      expect(onSubmit).toHaveBeenCalledWith({ '0': 'Line 1\nLine 2' });
    });
  });

  describe.each([
    { useAlternateBuffer: true, expectedArrows: true },
    { useAlternateBuffer: false, expectedArrows: true },
  ])(
    'Scroll Arrows (useAlternateBuffer: $useAlternateBuffer)',
    ({ useAlternateBuffer, expectedArrows }) => {
      it(`shows scroll arrows correctly when useAlternateBuffer is ${useAlternateBuffer}`, async () => {
        const questions: Question[] = [
          {
            question: 'Choose an option',
            header: 'Scroll Test',
            type: QuestionType.CHOICE,
            options: Array.from({ length: 15 }, (_, i) => ({
              label: `Option ${i + 1}`,
              description: `Description ${i + 1}`,
            })),
            multiSelect: false,
          },
        ];

        const { lastFrame, waitUntilReady } = await renderWithProviders(
          <AskUserDialog
            questions={questions}
            onSubmit={vi.fn()}
            onCancel={vi.fn()}
            width={80}
            availableHeight={10} // Small height to force scrolling
          />,
          {
            config: makeFakeConfig({ useAlternateBuffer }),
            settings: createMockSettings({ ui: { useAlternateBuffer } }),
          },
        );

        await waitFor(async () => {
          if (expectedArrows) {
            await waitUntilReady();
            expect(lastFrame()).toContain('▲');
            await waitUntilReady();
            expect(lastFrame()).toContain('▼');
          } else {
            await waitUntilReady();
            expect(lastFrame()).not.toContain('▲');
            await waitUntilReady();
            expect(lastFrame()).not.toContain('▼');
          }
          await waitUntilReady();
          expect(lastFrame()).toMatchSnapshot();
        });
      });
    },
  );

  it('navigates to custom option when typing unbound characters (Type-to-Jump)', async () => {
    const { stdin, lastFrame, waitUntilReady } = await renderWithProviders(
      <AskUserDialog
        questions={authQuestion}
        onSubmit={vi.fn()}
        onCancel={vi.fn()}
        width={120}
      />,
      { width: 120 },
    );

    // Type a character without navigating down
    writeKey(stdin, 'A');

    await waitFor(async () => {
      // Should show the custom input with 'A'
      // Placeholder is hidden when text is present
      await waitUntilReady();
      expect(lastFrame()).toContain('A');
      await waitUntilReady();
      expect(lastFrame()).toContain('3.  A');
    });

    // Continue typing
    writeKey(stdin, 'P');
    writeKey(stdin, 'I');

    await waitFor(async () => {
      await waitUntilReady();
      expect(lastFrame()).toContain('API');
    });
  });

  it('shows progress header for multiple questions', async () => {
    const multiQuestions: Question[] = [
      {
        question: 'Which database should we use?',
        header: 'Database',
        type: QuestionType.CHOICE,
        options: [
          { label: 'PostgreSQL', description: 'Relational database' },
          { label: 'MongoDB', description: 'Document database' },
        ],
        multiSelect: false,
      },
      {
        question: 'Which ORM do you prefer?',
        header: 'ORM',
        type: QuestionType.CHOICE,
        options: [
          { label: 'Prisma', description: 'Type-safe ORM' },
          { label: 'Drizzle', description: 'Lightweight ORM' },
        ],
        multiSelect: false,
      },
    ];

    const { lastFrame } = await renderWithProviders(
      <AskUserDialog
        questions={multiQuestions}
        onSubmit={vi.fn()}
        onCancel={vi.fn()}
        width={120}
      />,
      { width: 120 },
    );

    expect(lastFrame()).toMatchSnapshot();
  });

  it('hides progress header for single question', async () => {
    const { lastFrame } = await renderWithProviders(
      <AskUserDialog
        questions={authQuestion}
        onSubmit={vi.fn()}
        onCancel={vi.fn()}
        width={120}
      />,
      { width: 120 },
    );

    expect(lastFrame()).toMatchSnapshot();
  });

  it('shows keyboard hints', async () => {
    const { lastFrame } = await renderWithProviders(
      <AskUserDialog
        questions={authQuestion}
        onSubmit={vi.fn()}
        onCancel={vi.fn()}
        width={120}
      />,
      { width: 120 },
    );

    expect(lastFrame()).toMatchSnapshot();
  });

  it('navigates between questions with arrow keys', async () => {
    const multiQuestions: Question[] = [
      {
        question: 'Which testing framework?',
        header: 'Testing',
        type: QuestionType.CHOICE,
        options: [{ label: 'Vitest', description: 'Fast unit testing' }],
        multiSelect: false,
      },
      {
        question: 'Which CI provider?',
        header: 'CI',
        type: QuestionType.CHOICE,
        options: [
          { label: 'GitHub Actions', description: 'Built into GitHub' },
        ],
        multiSelect: false,
      },
    ];

    const { stdin, lastFrame, waitUntilReady } = await renderWithProviders(
      <AskUserDialog
        questions={multiQuestions}
        onSubmit={vi.fn()}
        onCancel={vi.fn()}
        width={120}
      />,
      { width: 120 },
    );

    expect(lastFrame()).toContain('Which testing framework?');

    writeKey(stdin, '\x1b[C'); // Right arrow

    await waitFor(async () => {
      await waitUntilReady();
      expect(lastFrame()).toContain('Which CI provider?');
    });

    writeKey(stdin, '\x1b[D'); // Left arrow

    await waitFor(async () => {
      await waitUntilReady();
      expect(lastFrame()).toContain('Which testing framework?');
    });
  });

  it('preserves answers when navigating back', async () => {
    const multiQuestions: Question[] = [
      {
        question: 'Which package manager?',
        header: 'Package',
        type: QuestionType.CHOICE,
        options: [{ label: 'pnpm', description: 'Fast, disk efficient' }],
        multiSelect: false,
      },
      {
        question: 'Which bundler?',
        header: 'Bundler',
        type: QuestionType.CHOICE,
        options: [{ label: 'Vite', description: 'Next generation bundler' }],
        multiSelect: false,
      },
    ];

    const onSubmit = vi.fn();
    const { stdin, lastFrame, waitUntilReady } = await renderWithProviders(
      <AskUserDialog
        questions={multiQuestions}
        onSubmit={onSubmit}
        onCancel={vi.fn()}
        width={120}
      />,
      { width: 120 },
    );

    // Answer first question (should auto-advance)
    writeKey(stdin, '\r');

    await waitFor(async () => {
      await waitUntilReady();
      expect(lastFrame()).toContain('Which bundler?');
    });

    // Navigate back
    writeKey(stdin, '\x1b[D');

    await waitFor(async () => {
      await waitUntilReady();
      expect(lastFrame()).toContain('Which package manager?');
    });

    // Navigate forward
    writeKey(stdin, '\x1b[C');

    await waitFor(async () => {
      await waitUntilReady();
      expect(lastFrame()).toContain('Which bundler?');
    });

    // Answer second question
    writeKey(stdin, '\r');

    await waitFor(async () => {
      await waitUntilReady();
      expect(lastFrame()).toContain('Review your answers:');
    });

    // Submit from Review
    writeKey(stdin, '\r');

    await waitFor(async () => {
      expect(onSubmit).toHaveBeenCalledWith({ '0': 'pnpm', '1': 'Vite' });
    });
  });

  it('shows Review tab in progress header for multiple questions', async () => {
    const multiQuestions: Question[] = [
      {
        question: 'Which framework?',
        header: 'Framework',
        type: QuestionType.CHOICE,
        options: [
          { label: 'React', description: 'Component library' },
          { label: 'Vue', description: 'Progressive framework' },
        ],
        multiSelect: false,
      },
      {
        question: 'Which styling?',
        header: 'Styling',
        type: QuestionType.CHOICE,
        options: [
          { label: 'Tailwind', description: 'Utility-first CSS' },
          { label: 'CSS Modules', description: 'Scoped styles' },
        ],
        multiSelect: false,
      },
    ];

    const { lastFrame } = await renderWithProviders(
      <AskUserDialog
        questions={multiQuestions}
        onSubmit={vi.fn()}
        onCancel={vi.fn()}
        width={120}
      />,
      { width: 120 },
    );

    expect(lastFrame()).toMatchSnapshot();
  });

  it('allows navigating to Review tab and back', async () => {
    const multiQuestions: Question[] = [
      {
        question: 'Create tests?',
        header: 'Tests',
        type: QuestionType.CHOICE,
        options: [{ label: 'Yes', description: 'Generate test files' }],
        multiSelect: false,
      },
      {
        question: 'Add documentation?',
        header: 'Docs',
        type: QuestionType.CHOICE,
        options: [{ label: 'Yes', description: 'Generate JSDoc comments' }],
        multiSelect: false,
      },
    ];

    const { stdin, lastFrame, waitUntilReady } = await renderWithProviders(
      <AskUserDialog
        questions={multiQuestions}
        onSubmit={vi.fn()}
        onCancel={vi.fn()}
        width={120}
      />,
      { width: 120 },
    );

    writeKey(stdin, '\x1b[C'); // Right arrow

    await waitFor(async () => {
      await waitUntilReady();
      expect(lastFrame()).toContain('Add documentation?');
    });

    writeKey(stdin, '\x1b[C'); // Right arrow to Review

    await waitFor(async () => {
      await waitUntilReady();
      expect(lastFrame()).toMatchSnapshot();
    });

    writeKey(stdin, '\x1b[D'); // Left arrow back

    await waitFor(async () => {
      await waitUntilReady();
      expect(lastFrame()).toContain('Add documentation?');
    });
  });

  it('shows warning for unanswered questions on Review tab', async () => {
    const multiQuestions: Question[] = [
      {
        question: 'Which license?',
        header: 'License',
        type: QuestionType.CHOICE,
        options: [{ label: 'MIT', description: 'Permissive license' }],
        multiSelect: false,
      },
      {
        question: 'Include README?',
        header: 'README',
        type: QuestionType.CHOICE,
        options: [{ label: 'Yes', description: 'Generate README.md' }],
        multiSelect: false,
      },
    ];

    const { stdin, lastFrame, waitUntilReady } = await renderWithProviders(
      <AskUserDialog
        questions={multiQuestions}
        onSubmit={vi.fn()}
        onCancel={vi.fn()}
        width={120}
      />,
      { width: 120 },
    );

    // Navigate directly to Review tab without answering
    writeKey(stdin, '\x1b[C');
    writeKey(stdin, '\x1b[C');

    await waitFor(async () => {
      await waitUntilReady();
      expect(lastFrame()).toMatchSnapshot();
    });
  });

  it('submits with unanswered questions when user confirms on Review', async () => {
    const multiQuestions: Question[] = [
      {
        question: 'Target Node version?',
        header: 'Node',
        type: QuestionType.CHOICE,
        options: [{ label: 'Node 20', description: 'LTS version' }],
        multiSelect: false,
      },
      {
        question: 'Enable strict mode?',
        header: 'Strict',
        type: QuestionType.CHOICE,
        options: [{ label: 'Yes', description: 'Strict TypeScript' }],
        multiSelect: false,
      },
    ];

    const onSubmit = vi.fn();
    const { stdin } = await renderWithProviders(
      <AskUserDialog
        questions={multiQuestions}
        onSubmit={onSubmit}
        onCancel={vi.fn()}
        width={120}
      />,
      { width: 120 },
    );

    // Answer only first question
    writeKey(stdin, '\r');
    // Navigate to Review tab
    writeKey(stdin, '\x1b[C');
    // Submit
    writeKey(stdin, '\r');

    await waitFor(async () => {
      expect(onSubmit).toHaveBeenCalledWith({ '0': 'Node 20' });
    });
  });

  describe('Text type questions', () => {
    it('renders text input for type: "text"', async () => {
      const textQuestion: Question[] = [
        {
          question: 'What should we name this component?',
          header: 'Name',
          type: QuestionType.TEXT,
          placeholder: 'e.g., UserProfileCard',
        },
      ];

      const { lastFrame } = await renderWithProviders(
        <AskUserDialog
          questions={textQuestion}
          onSubmit={vi.fn()}
          onCancel={vi.fn()}
          width={120}
        />,
        { width: 120 },
      );

      expect(lastFrame()).toMatchSnapshot();
    });

    it('shows default placeholder when none provided', async () => {
      const textQuestion: Question[] = [
        {
          question: 'Enter the database connection string:',
          header: 'Database',
          type: QuestionType.TEXT,
        },
      ];

      const { lastFrame } = await renderWithProviders(
        <AskUserDialog
          questions={textQuestion}
          onSubmit={vi.fn()}
          onCancel={vi.fn()}
          width={120}
        />,
        { width: 120 },
      );

      expect(lastFrame()).toMatchSnapshot();
    });

    it('supports backspace in text mode', async () => {
      const textQuestion: Question[] = [
        {
          question: 'Enter the function name:',
          header: 'Function',
          type: QuestionType.TEXT,
        },
      ];

      const { stdin, lastFrame, waitUntilReady } = await renderWithProviders(
        <AskUserDialog
          questions={textQuestion}
          onSubmit={vi.fn()}
          onCancel={vi.fn()}
          width={120}
        />,
        { width: 120 },
      );

      for (const char of 'abc') {
        writeKey(stdin, char);
      }

      await waitFor(async () => {
        await waitUntilReady();
        expect(lastFrame()).toContain('abc');
      });

      writeKey(stdin, '\x7f'); // Backspace

      await waitFor(async () => {
        await waitUntilReady();
        expect(lastFrame()).toContain('ab');
        await waitUntilReady();
        expect(lastFrame()).not.toContain('abc');
      });
    });

    it('shows correct keyboard hints for text type', async () => {
      const textQuestion: Question[] = [
        {
          question: 'Enter the variable name:',
          header: 'Variable',
          type: QuestionType.TEXT,
        },
      ];

      const { lastFrame } = await renderWithProviders(
        <AskUserDialog
          questions={textQuestion}
          onSubmit={vi.fn()}
          onCancel={vi.fn()}
          width={120}
        />,
        { width: 120 },
      );

      expect(lastFrame()).toMatchSnapshot();
    });

    it('preserves text answer when navigating between questions', async () => {
      const mixedQuestions: Question[] = [
        {
          question: 'What should we name this hook?',
          header: 'Hook',
          type: QuestionType.TEXT,
        },
        {
          question: 'Should it be async?',
          header: 'Async',
          type: QuestionType.CHOICE,
          options: [
            { label: 'Yes', description: 'Use async/await' },
            { label: 'No', description: 'Synchronous hook' },
          ],
          multiSelect: false,
        },
      ];

      const { stdin, lastFrame, waitUntilReady } = await renderWithProviders(
        <AskUserDialog
          questions={mixedQuestions}
          onSubmit={vi.fn()}
          onCancel={vi.fn()}
          width={120}
        />,
        { width: 120 },
      );

      for (const char of 'useAuth') {
        writeKey(stdin, char);
      }

      writeKey(stdin, '\t'); // Use Tab instead of Right arrow when text input is active

      await waitFor(async () => {
        await waitUntilReady();
        expect(lastFrame()).toContain('Should it be async?');
      });

      writeKey(stdin, '\x1b[D'); // Left arrow should work when NOT focusing a text input

      await waitFor(async () => {
        await waitUntilReady();
        expect(lastFrame()).toContain('useAuth');
      });
    });

    it('handles mixed text and choice questions', async () => {
      const mixedQuestions: Question[] = [
        {
          question: 'What should we name this component?',
          header: 'Name',
          type: QuestionType.TEXT,
          placeholder: 'Enter component name',
        },
        {
          question: 'Which styling approach?',
          header: 'Style',
          type: QuestionType.CHOICE,
          options: [
            { label: 'CSS Modules', description: 'Scoped CSS' },
            { label: 'Tailwind', description: 'Utility classes' },
          ],
          multiSelect: false,
        },
      ];

      const onSubmit = vi.fn();
      const { stdin, lastFrame, waitUntilReady } = await renderWithProviders(
        <AskUserDialog
          questions={mixedQuestions}
          onSubmit={onSubmit}
          onCancel={vi.fn()}
          width={120}
        />,
        { width: 120 },
      );

      for (const char of 'DataTable') {
        writeKey(stdin, char);
      }

      writeKey(stdin, '\r');

      await waitFor(async () => {
        await waitUntilReady();
        expect(lastFrame()).toContain('Which styling approach?');
      });

      writeKey(stdin, '\r');

      await waitFor(async () => {
        await waitUntilReady();
        expect(lastFrame()).toContain('Review your answers:');
        await waitUntilReady();
        expect(lastFrame()).toContain('Name');
        await waitUntilReady();
        expect(lastFrame()).toContain('DataTable');
        await waitUntilReady();
        expect(lastFrame()).toContain('Style');
        await waitUntilReady();
        expect(lastFrame()).toContain('CSS Modules');
      });

      writeKey(stdin, '\r');

      await waitFor(async () => {
        expect(onSubmit).toHaveBeenCalledWith({
          '0': 'DataTable',
          '1': 'CSS Modules',
        });
      });
    });

    it('submits empty text as unanswered', async () => {
      const textQuestion: Question[] = [
        {
          question: 'Enter the class name:',
          header: 'Class',
          type: QuestionType.TEXT,
        },
      ];

      const onSubmit = vi.fn();
      const { stdin } = await renderWithProviders(
        <AskUserDialog
          questions={textQuestion}
          onSubmit={onSubmit}
          onCancel={vi.fn()}
          width={120}
        />,
        { width: 120 },
      );

      writeKey(stdin, '\r');

      await waitFor(async () => {
        expect(onSubmit).toHaveBeenCalledWith({});
      });
    });

    it('clears text on Ctrl+C', async () => {
      const textQuestion: Question[] = [
        {
          question: 'Enter the class name:',
          header: 'Class',
          type: QuestionType.TEXT,
        },
      ];

      const onCancel = vi.fn();
      const { stdin, lastFrame, waitUntilReady } = await renderWithProviders(
        <AskUserDialog
          questions={textQuestion}
          onSubmit={vi.fn()}
          onCancel={onCancel}
          width={120}
        />,
        { width: 120 },
      );

      for (const char of 'SomeText') {
        writeKey(stdin, char);
      }

      await waitFor(async () => {
        await waitUntilReady();
        expect(lastFrame()).toContain('SomeText');
      });

      // Send Ctrl+C
      writeKey(stdin, '\x03'); // Ctrl+C

      await waitFor(async () => {
        // Text should be cleared
        await waitUntilReady();
        expect(lastFrame()).not.toContain('SomeText');
        await waitUntilReady();
        expect(lastFrame()).toContain('>');
      });

      // Should NOT call onCancel (dialog should stay open)
      expect(onCancel).not.toHaveBeenCalled();
    });

    it('allows immediate arrow navigation after switching away from text input', async () => {
      const multiQuestions: Question[] = [
        {
          question: 'Choice Q?',
          header: 'Choice',
          type: QuestionType.CHOICE,
          options: [{ label: 'Option 1', description: '' }],
          multiSelect: false,
        },
        {
          question: 'Text Q?',
          header: 'Text',
          type: QuestionType.TEXT,
        },
      ];

      const { stdin, lastFrame, waitUntilReady } = await renderWithProviders(
        <AskUserDialog
          questions={multiQuestions}
          onSubmit={vi.fn()}
          onCancel={vi.fn()}
          width={120}
        />,
        { width: 120 },
      );

      // 1. Move to Text Q (Right arrow works for Choice Q)
      writeKey(stdin, '\x1b[C');
      await waitFor(async () => {
        await waitUntilReady();
        expect(lastFrame()).toContain('Text Q?');
      });

      // 2. Type something in Text Q to make isEditingCustomOption true
      writeKey(stdin, 'a');
      await waitFor(async () => {
        await waitUntilReady();
        expect(lastFrame()).toContain('a');
      });

      // 3. Move back to Choice Q (Left arrow works because cursor is at left edge)
      // When typing 'a', cursor is at index 1.
      // We need to move cursor to index 0 first for Left arrow to work for navigation.
      writeKey(stdin, '\x1b[D'); // Left arrow moves cursor to index 0
      await waitFor(async () => {
        await waitUntilReady();
        expect(lastFrame()).toContain('Text Q?');
      });

      writeKey(stdin, '\x1b[D'); // Second Left arrow should now trigger navigation
      await waitFor(async () => {
        await waitUntilReady();
        expect(lastFrame()).toContain('Choice Q?');
      });

      // 4. Immediately try Right arrow to go back to Text Q
      writeKey(stdin, '\x1b[C');
      await waitFor(async () => {
        await waitUntilReady();
        expect(lastFrame()).toContain('Text Q?');
      });
    });

    it('handles rapid sequential answers correctly (stale closure protection)', async () => {
      const multiQuestions: Question[] = [
        {
          question: 'Question 1?',
          header: 'Q1',
          type: QuestionType.CHOICE,
          options: [{ label: 'A1', description: '' }],
          multiSelect: false,
        },
        {
          question: 'Question 2?',
          header: 'Q2',
          type: QuestionType.CHOICE,
          options: [{ label: 'A2', description: '' }],
          multiSelect: false,
        },
      ];

      const onSubmit = vi.fn();
      const { stdin, lastFrame, waitUntilReady } = await renderWithProviders(
        <AskUserDialog
          questions={multiQuestions}
          onSubmit={onSubmit}
          onCancel={vi.fn()}
          width={120}
        />,
        { width: 120 },
      );

      // Answer Q1 and Q2 sequentialy
      act(() => {
        stdin.write('\r'); // Select A1 for Q1 -> triggers autoAdvance
      });
      await waitFor(async () => {
        await waitUntilReady();
        expect(lastFrame()).toContain('Question 2?');
      });

      act(() => {
        stdin.write('\r'); // Select A2 for Q2 -> triggers autoAdvance to Review
      });
      await waitFor(async () => {
        await waitUntilReady();
        expect(lastFrame()).toContain('Review your answers:');
      });

      act(() => {
        stdin.write('\r'); // Submit from Review
      });

      await waitFor(async () => {
        expect(onSubmit).toHaveBeenCalledWith({
          '0': 'A1',
          '1': 'A2',
        });
      });
    });
  });

  describe('Markdown rendering', () => {
    it('auto-bolds plain single-line questions', async () => {
      const questions: Question[] = [
        {
          question: 'Which option do you prefer?',
          header: 'Test',
          type: QuestionType.CHOICE,
          options: [{ label: 'Yes', description: '' }],
          multiSelect: false,
        },
      ];

      const { lastFrame, waitUntilReady } = await renderWithProviders(
        <AskUserDialog
          questions={questions}
          onSubmit={vi.fn()}
          onCancel={vi.fn()}
          width={120}
          availableHeight={40}
        />,
        { width: 120 },
      );

      await waitFor(async () => {
        await waitUntilReady();
        const frame = lastFrame();
        // Plain text should be rendered as bold
        expect(frame).toContain('Which option do you prefer?');
      });
    });

    it('does not auto-bold questions that already have markdown', async () => {
      const questions: Question[] = [
        {
          question: 'Is **this** working?',
          header: 'Test',
          type: QuestionType.CHOICE,
          options: [{ label: 'Yes', description: '' }],
          multiSelect: false,
        },
      ];

      const { lastFrame, waitUntilReady } = await renderWithProviders(
        <AskUserDialog
          questions={questions}
          onSubmit={vi.fn()}
          onCancel={vi.fn()}
          width={120}
          availableHeight={40}
        />,
        { width: 120 },
      );

      await waitFor(async () => {
        await waitUntilReady();
        const frame = lastFrame();
        // Should NOT have double-bold (the whole question bolded AND "this" bolded)
        // "Is " should not be bold, only "this" should be bold
        expect(frame).toContain('Is ');
        expect(frame).toContain('this');
        expect(frame).not.toContain('**this**');
      });
    });

    it('renders bold markdown in question', async () => {
      const questions: Question[] = [
        {
          question: 'Is **this** working?',
          header: 'Test',
          type: QuestionType.CHOICE,
          options: [{ label: 'Yes', description: '' }],
          multiSelect: false,
        },
      ];

      const { lastFrame, waitUntilReady } = await renderWithProviders(
        <AskUserDialog
          questions={questions}
          onSubmit={vi.fn()}
          onCancel={vi.fn()}
          width={120}
          availableHeight={40}
        />,
        { width: 120 },
      );

      await waitFor(async () => {
        await waitUntilReady();
        const frame = lastFrame();
        // Check for 'this' - asterisks should be gone
        expect(frame).toContain('this');
        expect(frame).not.toContain('**this**');
      });
    });

    it('renders inline code markdown in question', async () => {
      const questions: Question[] = [
        {
          question: 'Run `npm start`?',
          header: 'Test',
          type: QuestionType.CHOICE,
          options: [{ label: 'Yes', description: '' }],
          multiSelect: false,
        },
      ];

      const { lastFrame, waitUntilReady } = await renderWithProviders(
        <AskUserDialog
          questions={questions}
          onSubmit={vi.fn()}
          onCancel={vi.fn()}
          width={120}
          availableHeight={40}
        />,
        { width: 120 },
      );

      await waitFor(async () => {
        await waitUntilReady();
        const frame = lastFrame();
        // Backticks should be removed
        expect(frame).toContain('Run npm start?');
        expect(frame).not.toContain('`');
      });
    });
  });

  it('uses availableTerminalHeight from UIStateContext if availableHeight prop is missing', async () => {
    const questions: Question[] = [
      {
        question: 'Choose an option',
        header: 'Context Test',
        type: QuestionType.CHOICE,
        options: Array.from({ length: 10 }, (_, i) => ({
          label: `Option ${i + 1}`,
          description: `Description ${i + 1}`,
        })),
        multiSelect: false,
      },
    ];

    const mockUIState = {
      availableTerminalHeight: 5, // Small height to force scroll arrows
    } as UIState;

    const { lastFrame, waitUntilReady } = await renderWithProviders(
      <UIStateContext.Provider value={mockUIState}>
        <AskUserDialog
          questions={questions}
          onSubmit={vi.fn()}
          onCancel={vi.fn()}
          width={80}
        />
      </UIStateContext.Provider>,
      {
        config: makeFakeConfig({ useAlternateBuffer: false }),
        settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
      },
    );

    // With height 5 and alternate buffer disabled, it should show scroll arrows (▲)
    await waitUntilReady();
    expect(lastFrame()).toContain('▲');
    await waitUntilReady();
    expect(lastFrame()).toContain('▼');
  });

  it('does NOT truncate the question when in alternate buffer mode even with small height', async () => {
    const longQuestion =
      'This is a very long question ' + 'with many words '.repeat(10);
    const questions: Question[] = [
      {
        question: longQuestion,
        header: 'Alternate Buffer Test',
        type: QuestionType.CHOICE,
        options: [{ label: 'Option 1', description: 'Desc 1' }],
        multiSelect: false,
      },
    ];

    const mockUIState = {
      availableTerminalHeight: 5,
    } as UIState;

    const { lastFrame, waitUntilReady } = await renderWithProviders(
      <UIStateContext.Provider value={mockUIState}>
        <AskUserDialog
          questions={questions}
          onSubmit={vi.fn()}
          onCancel={vi.fn()}
          width={40} // Small width to force wrapping
        />
      </UIStateContext.Provider>,
      {
        config: makeFakeConfig({ useAlternateBuffer: true }),
        settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
      },
    );

    // Should NOT contain the truncation message
    await waitUntilReady();
    expect(lastFrame()).not.toContain('hidden ...');
    // Should contain the full long question (or at least its parts)
    await waitUntilReady();
    expect(lastFrame()).toContain('This is a very long question');
  });

  describe('Choice question placeholder', () => {
    it('uses placeholder for "Other" option when provided', async () => {
      const questions: Question[] = [
        {
          question: 'Select your preferred language:',
          header: 'Language',
          type: QuestionType.CHOICE,
          options: [
            { label: 'TypeScript', description: '' },
            { label: 'JavaScript', description: '' },
          ],
          placeholder: 'Type another language...',
          multiSelect: false,
        },
      ];

      const { stdin, lastFrame, waitUntilReady } = await renderWithProviders(
        <AskUserDialog
          questions={questions}
          onSubmit={vi.fn()}
          onCancel={vi.fn()}
          width={80}
        />,
        { width: 80 },
      );

      // Navigate to the "Other" option
      writeKey(stdin, '\x1b[B'); // Down
      writeKey(stdin, '\x1b[B'); // Down to Other

      await waitFor(async () => {
        await waitUntilReady();
        expect(lastFrame()).toMatchSnapshot();
      });
    });

    it('uses default placeholder when not provided', async () => {
      const questions: Question[] = [
        {
          question: 'Select your preferred language:',
          header: 'Language',
          type: QuestionType.CHOICE,
          options: [
            { label: 'TypeScript', description: '' },
            { label: 'JavaScript', description: '' },
          ],
          multiSelect: false,
        },
      ];

      const { stdin, lastFrame, waitUntilReady } = await renderWithProviders(
        <AskUserDialog
          questions={questions}
          onSubmit={vi.fn()}
          onCancel={vi.fn()}
          width={80}
        />,
        { width: 80 },
      );

      // Navigate to the "Other" option
      writeKey(stdin, '\x1b[B'); // Down
      writeKey(stdin, '\x1b[B'); // Down to Other

      await waitFor(async () => {
        await waitUntilReady();
        expect(lastFrame()).toMatchSnapshot();
      });
    });

    it('supports "Other" option for yesno questions', async () => {
      const questions: Question[] = [
        {
          question: 'Is this correct?',
          header: 'Confirm',
          type: QuestionType.YESNO,
        },
      ];

      const onSubmit = vi.fn();
      const { stdin, lastFrame, waitUntilReady } = await renderWithProviders(
        <AskUserDialog
          questions={questions}
          onSubmit={onSubmit}
          onCancel={vi.fn()}
          width={80}
        />,
        { width: 80 },
      );

      // Navigate to "Other" (3rd option: 1. Yes, 2. No, 3. Other)
      writeKey(stdin, '\x1b[B'); // Down to No
      writeKey(stdin, '\x1b[B'); // Down to Other

      await waitFor(async () => {
        await waitUntilReady();
        expect(lastFrame()).toContain('Enter a custom value');
      });

      // Type feedback
      for (const char of 'Yes, but with caveats') {
        writeKey(stdin, char);
      }

      await waitFor(async () => {
        await waitUntilReady();
        expect(lastFrame()).toContain('Yes, but with caveats');
      });

      // Submit
      writeKey(stdin, '\r');

      await waitFor(async () => {
        expect(onSubmit).toHaveBeenCalledWith({ '0': 'Yes, but with caveats' });
      });
    });
  });

  it('expands paste placeholders in multi-select custom option via Done', async () => {
    const questions: Question[] = [
      {
        question: 'Which features?',
        header: 'Features',
        type: QuestionType.CHOICE,
        options: [{ label: 'TypeScript', description: '' }],
        multiSelect: true,
      },
    ];

    const onSubmit = vi.fn();
    const { stdin } = await renderWithProviders(
      <AskUserDialog
        questions={questions}
        onSubmit={onSubmit}
        onCancel={vi.fn()}
        width={120}
      />,
      { width: 120 },
    );

    // Select TypeScript
    writeKey(stdin, '\r');
    // Down to Other
    writeKey(stdin, '\x1b[B');

    // Simulate bracketed paste of multi-line text into the custom option
    const pastedText = 'Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6';
    const ESC = '\x1b';
    writeKey(stdin, `${ESC}[200~${pastedText}${ESC}[201~`);

    // Down to Done and submit
    writeKey(stdin, '\x1b[B');
    writeKey(stdin, '\r');

    await waitFor(() => {
      expect(onSubmit).toHaveBeenCalledWith({
        '0': `TypeScript, ${pastedText}`,
      });
    });
  });

  it('shows at least 3 selection options even in small terminal heights', async () => {
    const questions: Question[] = [
      {
        question:
          'A very long question that would normally take up most of the space and squeeze the list if we did not have a heuristic to prevent it. This line is just to make it longer. And another one. Imagine this is a plan.',
        header: 'Test',
        type: QuestionType.CHOICE,
        options: [
          { label: 'Option 1', description: 'Description 1' },
          { label: 'Option 2', description: 'Description 2' },
          { label: 'Option 3', description: 'Description 3' },
          { label: 'Option 4', description: 'Description 4' },
        ],
        multiSelect: false,
      },
    ];

    const { lastFrame, waitUntilReady } = await renderWithProviders(
      <AskUserDialog
        questions={questions}
        onSubmit={vi.fn()}
        onCancel={vi.fn()}
        width={80}
        availableHeight={12} // Very small height
      />,
      { width: 80 },
    );

    await waitFor(async () => {
      await waitUntilReady();
      const frame = lastFrame();
      // Should show at least 3 options
      expect(frame).toContain('1.  Option 1');
      expect(frame).toContain('2.  Option 2');
      expect(frame).toContain('3.  Option 3');
    });
  });

  it('allows the question to exceed 15 lines in a tall terminal', async () => {
    const longQuestion = Array.from(
      { length: 25 },
      (_, i) => `Line ${i + 1}`,
    ).join('\n');
    const questions: Question[] = [
      {
        question: longQuestion,
        header: 'Tall Test',
        type: QuestionType.CHOICE,
        options: [
          { label: 'Option 1', description: 'D1' },
          { label: 'Option 2', description: 'D2' },
          { label: 'Option 3', description: 'D3' },
        ],
        multiSelect: false,
        unconstrainedHeight: false,
      },
    ];

    const { lastFrame, waitUntilReady } = await renderWithProviders(
      <AskUserDialog
        questions={questions}
        onSubmit={vi.fn()}
        onCancel={vi.fn()}
        width={80}
        availableHeight={40} // Tall terminal
      />,
      { width: 80 },
    );

    await waitFor(async () => {
      await waitUntilReady();
      const frame = lastFrame();
      // Should show more than 15 lines of the question
      // (The limit was previously 15, so showing Line 20 proves it's working)
      expect(frame).toContain('Line 20');
      expect(frame).toContain('Line 25');
      // Should still show the options
      expect(frame).toContain('1.  Option 1');
    });
  });

  it('indents multi-line descriptions correctly', async () => {
    const questions: Question[] = [
      {
        question: 'Single choice?',
        header: 'Indent Test',
        type: QuestionType.CHOICE,
        options: [
          {
            label: 'Option 1',
            description:
              'This is a very long description that is expected to wrap onto multiple lines in a narrow terminal. We want to ensure that all lines are correctly indented.',
          },
        ],
        multiSelect: false,
      },
    ];

    const { lastFrame, waitUntilReady } = await renderWithProviders(
      <AskUserDialog
        questions={questions}
        onSubmit={vi.fn()}
        onCancel={vi.fn()}
        width={40} // Narrow width to force wrapping
      />,
      { width: 40 },
    );

    await waitFor(async () => {
      await waitUntilReady();
      // Snapshot will capture the visual alignment
      expect(lastFrame()).toMatchSnapshot();
    });
  });

  it('indents multi-line descriptions correctly in multi-select mode', async () => {
    const questions: Question[] = [
      {
        question: 'Multi-select?',
        header: 'Indent Test',
        type: QuestionType.CHOICE,
        options: [
          {
            label: 'Option 1',
            description:
              'This is a very long description that is expected to wrap onto multiple lines in a narrow terminal. We want to ensure that all lines are correctly indented even with checkboxes.',
          },
        ],
        multiSelect: true,
      },
    ];

    const { lastFrame, waitUntilReady } = await renderWithProviders(
      <AskUserDialog
        questions={questions}
        onSubmit={vi.fn()}
        onCancel={vi.fn()}
        width={40} // Narrow width to force wrapping
      />,
      { width: 40 },
    );

    await waitFor(async () => {
      await waitUntilReady();
      expect(lastFrame()).toMatchSnapshot();
    });
  });
});
