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

import { act } from 'react';
import { renderHookWithProviders } from '../../test-utils/render.js';
import { useKeypress } from './useKeypress.js';
import { useStdin } from 'ink';
import { EventEmitter } from 'node:events';
import type { Mock } from 'vitest';

// Mock the 'ink' module to control stdin
vi.mock('ink', async (importOriginal) => {
  const original = await importOriginal<typeof import('ink')>();
  return {
    ...original,
    useStdin: vi.fn(),
  };
});

const PASTE_START = '\x1B[200~';
const PASTE_END = '\x1B[201~';

class MockStdin extends EventEmitter {
  isTTY = true;
  isRaw = false;
  setRawMode = vi.fn();
  override on = this.addListener;
  override removeListener = super.removeListener;
  resume = vi.fn();
  pause = vi.fn();

  write(text: string) {
    this.emit('data', text);
  }
}

describe(`useKeypress`, () => {
  let stdin: MockStdin;
  const mockSetRawMode = vi.fn();
  const onKeypress = vi.fn();
  let originalNodeVersion: string;

  const renderKeypressHook = async (isActive = true) =>
    renderHookWithProviders(() => useKeypress(onKeypress, { isActive }));

  beforeEach(() => {
    vi.clearAllMocks();
    vi.useFakeTimers();
    stdin = new MockStdin();
    (useStdin as Mock).mockReturnValue({
      stdin,
      setRawMode: mockSetRawMode,
    });

    originalNodeVersion = process.versions.node;
    vi.unstubAllEnvs();
  });

  afterEach(() => {
    Object.defineProperty(process.versions, 'node', {
      value: originalNodeVersion,
      configurable: true,
    });
  });

  it('should not listen if isActive is false', async () => {
    await renderKeypressHook(false);
    act(() => stdin.write('a'));
    expect(onKeypress).not.toHaveBeenCalled();
  });

  it.each([
    { key: { name: 'a', sequence: 'a' } },
    { key: { name: 'left', sequence: '\x1b[D' } },
    { key: { name: 'right', sequence: '\x1b[C' } },
    { key: { name: 'up', sequence: '\x1b[A' } },
    { key: { name: 'down', sequence: '\x1b[B' } },
    { key: { name: 'tab', sequence: '\x1b[Z', shift: true } },
  ])(
    'should listen for keypress when active for key $key.name',
    async ({ key }) => {
      await renderKeypressHook(true);
      act(() => stdin.write(key.sequence));
      expect(onKeypress).toHaveBeenCalledWith(expect.objectContaining(key));
    },
  );

  it('should set and release raw mode', async () => {
    const { unmount } = await renderKeypressHook(true);
    expect(mockSetRawMode).toHaveBeenCalledWith(true);
    unmount();
    expect(mockSetRawMode).toHaveBeenCalledWith(false);
  });

  it('should stop listening after being unmounted', async () => {
    const { unmount } = await renderKeypressHook(true);
    unmount();
    act(() => stdin.write('a'));
    expect(onKeypress).not.toHaveBeenCalled();
  });

  it('should correctly identify alt+enter (meta key)', async () => {
    await renderKeypressHook(true);
    const key = { name: 'enter', sequence: '\x1B\r' };
    act(() => stdin.write(key.sequence));
    expect(onKeypress).toHaveBeenCalledWith(
      expect.objectContaining({
        ...key,
        shift: false,
        alt: true,
        ctrl: false,
        cmd: false,
      }),
    );
  });

  describe.each([
    {
      description: 'PASTE_WORKAROUND true',
      setup: () => vi.stubEnv('PASTE_WORKAROUND', 'true'),
    },
    {
      description: 'PASTE_WORKAROUND false',
      setup: () => vi.stubEnv('PASTE_WORKAROUND', 'false'),
    },
  ])('in $description', ({ setup }) => {
    beforeEach(() => {
      setup();
    });

    it('should process a paste as a single event', async () => {
      await renderKeypressHook(true);
      const pasteText = 'hello world';
      act(() => stdin.write(PASTE_START + pasteText + PASTE_END));

      expect(onKeypress).toHaveBeenCalledTimes(1);
      expect(onKeypress).toHaveBeenCalledWith({
        name: 'paste',
        shift: false,
        alt: false,
        ctrl: false,
        cmd: false,
        insertable: true,
        sequence: pasteText,
      });
    });

    it('should handle keypress interspersed with pastes', async () => {
      await renderKeypressHook(true);

      const keyA = { name: 'a', sequence: 'a' };
      act(() => stdin.write('a'));
      expect(onKeypress).toHaveBeenCalledWith(
        expect.objectContaining({ ...keyA }),
      );

      const pasteText = 'pasted';
      act(() => stdin.write(PASTE_START + pasteText + PASTE_END));
      expect(onKeypress).toHaveBeenCalledWith(
        expect.objectContaining({ name: 'paste', sequence: pasteText }),
      );

      const keyB = { name: 'b', sequence: 'b' };
      act(() => stdin.write('b'));
      expect(onKeypress).toHaveBeenCalledWith(
        expect.objectContaining({ ...keyB }),
      );

      expect(onKeypress).toHaveBeenCalledTimes(3);
    });

    it('should handle lone pastes', async () => {
      await renderKeypressHook(true);

      const pasteText = 'pasted';
      act(() => {
        stdin.write(PASTE_START);
        stdin.write(pasteText);
        stdin.write(PASTE_END);
      });
      expect(onKeypress).toHaveBeenCalledWith(
        expect.objectContaining({ name: 'paste', sequence: pasteText }),
      );
    });

    it('should handle paste false alarm', async () => {
      await renderKeypressHook(true);

      act(() => {
        stdin.write(PASTE_START.slice(0, 5));
        stdin.write('do');
      });

      expect(onKeypress).toHaveBeenCalledWith(
        expect.objectContaining({ sequence: '\x1B[200d' }),
      );
      expect(onKeypress).toHaveBeenCalledWith(
        expect.objectContaining({ sequence: 'o' }),
      );
      expect(onKeypress).toHaveBeenCalledTimes(2);
    });

    it('should handle back to back pastes', async () => {
      await renderKeypressHook(true);

      const pasteText1 = 'herp';
      const pasteText2 = 'derp';
      act(() => {
        stdin.write(
          PASTE_START +
            pasteText1 +
            PASTE_END +
            PASTE_START +
            pasteText2 +
            PASTE_END,
        );
      });
      expect(onKeypress).toHaveBeenCalledWith(
        expect.objectContaining({ name: 'paste', sequence: pasteText1 }),
      );
      expect(onKeypress).toHaveBeenCalledWith(
        expect.objectContaining({ name: 'paste', sequence: pasteText2 }),
      );

      expect(onKeypress).toHaveBeenCalledTimes(2);
    });

    it('should handle pastes split across writes', async () => {
      await renderKeypressHook(true);

      const keyA = { name: 'a', sequence: 'a' };
      act(() => stdin.write('a'));
      expect(onKeypress).toHaveBeenCalledWith(
        expect.objectContaining({ ...keyA }),
      );

      const pasteText = 'pasted';
      await act(async () => {
        stdin.write(PASTE_START.slice(0, 3));
        vi.advanceTimersByTime(40);
        stdin.write(PASTE_START.slice(3) + pasteText.slice(0, 3));
        vi.advanceTimersByTime(40);
        stdin.write(pasteText.slice(3) + PASTE_END.slice(0, 3));
        vi.advanceTimersByTime(40);
        stdin.write(PASTE_END.slice(3));
      });
      expect(onKeypress).toHaveBeenCalledWith(
        expect.objectContaining({ name: 'paste', sequence: pasteText }),
      );

      const keyB = { name: 'b', sequence: 'b' };
      act(() => stdin.write('b'));
      expect(onKeypress).toHaveBeenCalledWith(
        expect.objectContaining({ ...keyB }),
      );

      expect(onKeypress).toHaveBeenCalledTimes(3);
    });
  });
});
