// @vitest-environment happy-dom
// Mirror the IS_REACT_ACT_ENVIRONMENT setup from the other hook tests so
// React doesn't log act() warnings about the createRoot render/unmount.
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;

import React, { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { describe, expect, it, vi, beforeEach } from "vitest";
import { useDrawerKeyboard } from "./useDrawerKeyboard";

/**
 * Minimal renderHook shim — same pattern as `useReconciliation.test.ts`
 * and `useClaimDetail.test.ts`. The project doesn't ship
 * `@testing-library/react`, so we wire a Probe component into a real
 * `createRoot` and let it call our hook.
 *
 * Unlike the other hook tests, `useDrawerKeyboard` returns `void` (its
 * side effect is the window listener), so the shim doesn't need to
 * surface a `result` — we just need `unmount` to verify cleanup.
 */
function renderHook(setup: () => void): { unmount: () => void } {
  const container = document.createElement("div");
  document.body.appendChild(container);

  function Probe() {
    setup();
    return null;
  }

  const root: Root = createRoot(container);
  act(() => {
    root.render(React.createElement(Probe));
  });

  return {
    unmount: () => {
      act(() => root.unmount());
      container.remove();
    },
  };
}

/**
 * Dispatch a synthetic keydown. By default the target is `window`
 * (which is what a real keypress bubbles to). Pass an explicit target
 * for the "input has focus" test, since a real keystroke would have
 * the focused element as its `target`. `bubbles: true` + `cancelable:
 * true` so the event behaves like a real keydown — the handler calls
 * `preventDefault` on matched keys.
 */
function pressKey(
  key: string,
  target: EventTarget = window,
  init: KeyboardEventInit = {}
): void {
  const event = new KeyboardEvent("keydown", {
    key,
    bubbles: true,
    cancelable: true,
    ...init,
  });
  target.dispatchEvent(event);
}

describe("useDrawerKeyboard", () => {
  // `vi.fn()` with no args in vitest 4 is typed `Mock<Procedure |
  // Constructable>`, which carries a constructor signature and won't
  // assign to `() => void`. Constraining the generic to `vi.fn<() => void>()`
  // gives a plain function-typed mock that flows directly into the hook's
  // callback params.
  let onNext: () => void;
  let onPrev: () => void;
  let onClose: () => void;
  let onToggleHelp: () => void;

  beforeEach(() => {
    onNext = vi.fn<() => void>();
    onPrev = vi.fn<() => void>();
    onClose = vi.fn<() => void>();
    onToggleHelp = vi.fn<() => void>();
  });

  it("fires onNext for j and ArrowDown", () => {
    const { unmount } = renderHook(() =>
      useDrawerKeyboard({ enabled: true, onNext, onPrev, onClose, onToggleHelp })
    );

    pressKey("j");
    expect(onNext).toHaveBeenCalledTimes(1);
    expect(onPrev).not.toHaveBeenCalled();

    pressKey("ArrowDown");
    expect(onNext).toHaveBeenCalledTimes(2);
    expect(onPrev).not.toHaveBeenCalled();

    unmount();
  });

  it("fires onPrev for k and ArrowUp", () => {
    const { unmount } = renderHook(() =>
      useDrawerKeyboard({ enabled: true, onNext, onPrev, onClose, onToggleHelp })
    );

    pressKey("k");
    expect(onPrev).toHaveBeenCalledTimes(1);
    expect(onNext).not.toHaveBeenCalled();

    pressKey("ArrowUp");
    expect(onPrev).toHaveBeenCalledTimes(2);
    expect(onNext).not.toHaveBeenCalled();

    unmount();
  });

  it("fires onClose when Escape is pressed", () => {
    const { unmount } = renderHook(() =>
      useDrawerKeyboard({ enabled: true, onNext, onPrev, onClose, onToggleHelp })
    );

    pressKey("Escape");
    expect(onClose).toHaveBeenCalledTimes(1);
    expect(onNext).not.toHaveBeenCalled();
    expect(onPrev).not.toHaveBeenCalled();
    expect(onToggleHelp).not.toHaveBeenCalled();

    unmount();
  });

  it('fires onToggleHelp when "?" is pressed', () => {
    const { unmount } = renderHook(() =>
      useDrawerKeyboard({ enabled: true, onNext, onPrev, onClose, onToggleHelp })
    );

    pressKey("?");
    expect(onToggleHelp).toHaveBeenCalledTimes(1);
    expect(onNext).not.toHaveBeenCalled();
    expect(onPrev).not.toHaveBeenCalled();
    expect(onClose).not.toHaveBeenCalled();

    unmount();
  });

  it("ignores keys when an input has focus", () => {
    const { unmount } = renderHook(() =>
      useDrawerKeyboard({ enabled: true, onNext, onPrev, onClose, onToggleHelp })
    );

    // Mount a real input and focus it. A real keystroke while the input
    // is focused would have `event.target === input` and bubble to
    // window — we simulate that by dispatching the keydown on the input
    // itself with `bubbles: true`.
    const input = document.createElement("input");
    document.body.appendChild(input);
    input.focus();
    expect(document.activeElement).toBe(input);

    pressKey("j", input);
    pressKey("Escape", input);
    pressKey("?", input);

    expect(onNext).not.toHaveBeenCalled();
    expect(onClose).not.toHaveBeenCalled();
    expect(onToggleHelp).not.toHaveBeenCalled();

    input.remove();
    unmount();
  });

  it("does nothing when enabled is false", () => {
    const { unmount } = renderHook(() =>
      useDrawerKeyboard({ enabled: false, onNext, onPrev, onClose, onToggleHelp })
    );

    pressKey("j");
    pressKey("Escape");
    pressKey("?");

    expect(onNext).not.toHaveBeenCalled();
    expect(onPrev).not.toHaveBeenCalled();
    expect(onClose).not.toHaveBeenCalled();
    expect(onToggleHelp).not.toHaveBeenCalled();

    unmount();
  });

  it("cleans up the keydown listener on unmount", () => {
    const { unmount } = renderHook(() =>
      useDrawerKeyboard({ enabled: true, onNext, onPrev, onClose, onToggleHelp })
    );

    pressKey("j");
    expect(onNext).toHaveBeenCalledTimes(1);

    unmount();

    // After unmount the effect's cleanup has removed the listener, so a
    // follow-up keydown should be a no-op. We dispatch a few different
    // keys to be thorough.
    pressKey("j");
    pressKey("Escape");
    pressKey("?");

    expect(onNext).toHaveBeenCalledTimes(1);
    expect(onClose).not.toHaveBeenCalled();
    expect(onToggleHelp).not.toHaveBeenCalled();
  });
});
