import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { renderHook, act } from "@testing-library/react";
import { useRef } from "react";
import { useTimelineKeyboard } from "./useTimelineKeyboard.js";

// ---------------------------------------------------------------------------
// Helper: fire a keyboard event on window
// ---------------------------------------------------------------------------

function fireKeydown(options: KeyboardEventInit) {
  window.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, ...options }));
}

// ---------------------------------------------------------------------------
// Helper: create a scroll container element with controllable scrollLeft
// ---------------------------------------------------------------------------

type ScrollDiv = HTMLDivElement & { _scrollLeft: number };

function makeScrollContainer(): ScrollDiv {
  const el = document.createElement("div") as ScrollDiv;
  el._scrollLeft = 0;
  // jsdom does not actually scroll, but we can track assignments
  Object.defineProperty(el, "scrollLeft", {
    get() {
      return (this as ScrollDiv)._scrollLeft;
    },
    set(v: number) {
      (this as ScrollDiv)._scrollLeft = v;
    },
    configurable: true,
  });
  return el;
}

// ---------------------------------------------------------------------------
// navigator.platform mock
// ---------------------------------------------------------------------------

function setMacPlatform() {
  Object.defineProperty(navigator, "platform", {
    get: () => "MacIntel",
    configurable: true,
  });
}

function setWindowsPlatform() {
  Object.defineProperty(navigator, "platform", {
    get: () => "Win32",
    configurable: true,
  });
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe("useTimelineKeyboard", () => {
  let scrollEl: ScrollDiv;
  let onDeleteSelected: ReturnType<typeof vi.fn>;

  beforeEach(() => {
    scrollEl = makeScrollContainer();
    onDeleteSelected = vi.fn();
    setWindowsPlatform(); // default to non-mac
  });

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

  // -------------------------------------------------------------------------
  // Initial state
  // -------------------------------------------------------------------------

  it("initialises showShortcuts as false", () => {
    const { result } = renderHook(() => {
      const scrollContainerRef = useRef<HTMLElement | null>(scrollEl);
      return useTimelineKeyboard({
        scrollContainerRef,
        cellWidth: 40,
        selectedAllocationIds: [],
        onDeleteSelected,
      });
    });
    expect(result.current.showShortcuts).toBe(false);
  });

  it("exposes a setShowShortcuts setter", () => {
    const { result } = renderHook(() => {
      const scrollContainerRef = useRef<HTMLElement | null>(scrollEl);
      return useTimelineKeyboard({
        scrollContainerRef,
        cellWidth: 40,
        selectedAllocationIds: [],
        onDeleteSelected,
      });
    });
    act(() => {
      result.current.setShowShortcuts(true);
    });
    expect(result.current.showShortcuts).toBe(true);
  });

  // -------------------------------------------------------------------------
  // "?" key — toggle shortcut overlay
  // -------------------------------------------------------------------------

  it("'?' toggles showShortcuts from false to true", () => {
    const { result } = renderHook(() => {
      const scrollContainerRef = useRef<HTMLElement | null>(scrollEl);
      return useTimelineKeyboard({
        scrollContainerRef,
        cellWidth: 40,
        selectedAllocationIds: [],
        onDeleteSelected,
      });
    });
    act(() => {
      fireKeydown({ key: "?" });
    });
    expect(result.current.showShortcuts).toBe(true);
  });

  it("'?' toggles showShortcuts from true back to false", () => {
    const { result } = renderHook(() => {
      const scrollContainerRef = useRef<HTMLElement | null>(scrollEl);
      return useTimelineKeyboard({
        scrollContainerRef,
        cellWidth: 40,
        selectedAllocationIds: [],
        onDeleteSelected,
      });
    });
    act(() => {
      fireKeydown({ key: "?" });
    });
    act(() => {
      fireKeydown({ key: "?" });
    });
    expect(result.current.showShortcuts).toBe(false);
  });

  // -------------------------------------------------------------------------
  // ArrowLeft / ArrowRight — scrolling
  // -------------------------------------------------------------------------

  it("ArrowLeft scrolls left by one cellWidth", () => {
    scrollEl._scrollLeft = 200;
    renderHook(() => {
      const scrollContainerRef = useRef<HTMLElement | null>(scrollEl);
      return useTimelineKeyboard({
        scrollContainerRef,
        cellWidth: 40,
        selectedAllocationIds: [],
        onDeleteSelected,
      });
    });
    act(() => {
      fireKeydown({ key: "ArrowLeft" });
    });
    expect(scrollEl.scrollLeft).toBe(160); // 200 - 40
  });

  it("ArrowRight scrolls right by one cellWidth", () => {
    scrollEl._scrollLeft = 0;
    renderHook(() => {
      const scrollContainerRef = useRef<HTMLElement | null>(scrollEl);
      return useTimelineKeyboard({
        scrollContainerRef,
        cellWidth: 40,
        selectedAllocationIds: [],
        onDeleteSelected,
      });
    });
    act(() => {
      fireKeydown({ key: "ArrowRight" });
    });
    expect(scrollEl.scrollLeft).toBe(40);
  });

  it("Shift+ArrowLeft scrolls by 7 × cellWidth", () => {
    scrollEl._scrollLeft = 400;
    renderHook(() => {
      const scrollContainerRef = useRef<HTMLElement | null>(scrollEl);
      return useTimelineKeyboard({
        scrollContainerRef,
        cellWidth: 40,
        selectedAllocationIds: [],
        onDeleteSelected,
      });
    });
    act(() => {
      fireKeydown({ key: "ArrowLeft", shiftKey: true });
    });
    expect(scrollEl.scrollLeft).toBe(400 - 7 * 40); // 120
  });

  it("Shift+ArrowRight scrolls by 7 × cellWidth", () => {
    scrollEl._scrollLeft = 0;
    renderHook(() => {
      const scrollContainerRef = useRef<HTMLElement | null>(scrollEl);
      return useTimelineKeyboard({
        scrollContainerRef,
        cellWidth: 40,
        selectedAllocationIds: [],
        onDeleteSelected,
      });
    });
    act(() => {
      fireKeydown({ key: "ArrowRight", shiftKey: true });
    });
    expect(scrollEl.scrollLeft).toBe(7 * 40); // 280
  });

  it("uses current cellWidth from ref without re-registering the handler", () => {
    const { rerender } = renderHook(
      ({ cellWidth }) => {
        const scrollContainerRef = useRef<HTMLElement | null>(scrollEl);
        return useTimelineKeyboard({
          scrollContainerRef,
          cellWidth,
          selectedAllocationIds: [],
          onDeleteSelected,
        });
      },
      { initialProps: { cellWidth: 40 } },
    );

    // Update cellWidth without causing a handler re-registration
    rerender({ cellWidth: 60 });

    scrollEl._scrollLeft = 0;
    act(() => {
      fireKeydown({ key: "ArrowRight" });
    });
    expect(scrollEl.scrollLeft).toBe(60);
  });

  it("does not scroll when scrollContainerRef.current is null", () => {
    renderHook(() => {
      const scrollContainerRef = useRef<HTMLElement | null>(null);
      return useTimelineKeyboard({
        scrollContainerRef,
        cellWidth: 40,
        selectedAllocationIds: [],
        onDeleteSelected,
      });
    });
    // Should not throw
    act(() => {
      fireKeydown({ key: "ArrowLeft" });
      fireKeydown({ key: "ArrowRight" });
    });
  });

  // -------------------------------------------------------------------------
  // Delete / Backspace — delete selected allocations
  // -------------------------------------------------------------------------

  it("Delete key calls onDeleteSelected when allocations are selected", () => {
    renderHook(() => {
      const scrollContainerRef = useRef<HTMLElement | null>(scrollEl);
      return useTimelineKeyboard({
        scrollContainerRef,
        cellWidth: 40,
        selectedAllocationIds: ["a1"],
        onDeleteSelected,
      });
    });
    act(() => {
      fireKeydown({ key: "Delete" });
    });
    expect(onDeleteSelected).toHaveBeenCalledOnce();
  });

  it("Backspace key calls onDeleteSelected when allocations are selected", () => {
    renderHook(() => {
      const scrollContainerRef = useRef<HTMLElement | null>(scrollEl);
      return useTimelineKeyboard({
        scrollContainerRef,
        cellWidth: 40,
        selectedAllocationIds: ["a1", "a2"],
        onDeleteSelected,
      });
    });
    act(() => {
      fireKeydown({ key: "Backspace" });
    });
    expect(onDeleteSelected).toHaveBeenCalledOnce();
  });

  it("Delete key does NOT call onDeleteSelected when nothing is selected", () => {
    renderHook(() => {
      const scrollContainerRef = useRef<HTMLElement | null>(scrollEl);
      return useTimelineKeyboard({
        scrollContainerRef,
        cellWidth: 40,
        selectedAllocationIds: [],
        onDeleteSelected,
      });
    });
    act(() => {
      fireKeydown({ key: "Delete" });
    });
    expect(onDeleteSelected).not.toHaveBeenCalled();
  });

  it("Ctrl+Backspace does NOT call onDeleteSelected (browser shortcut reserved)", () => {
    renderHook(() => {
      const scrollContainerRef = useRef<HTMLElement | null>(scrollEl);
      return useTimelineKeyboard({
        scrollContainerRef,
        cellWidth: 40,
        selectedAllocationIds: ["a1"],
        onDeleteSelected,
      });
    });
    act(() => {
      fireKeydown({ key: "Backspace", ctrlKey: true });
    });
    expect(onDeleteSelected).not.toHaveBeenCalled();
  });

  it("Cmd+Backspace (Mac) does NOT call onDeleteSelected", () => {
    setMacPlatform();
    renderHook(() => {
      const scrollContainerRef = useRef<HTMLElement | null>(scrollEl);
      return useTimelineKeyboard({
        scrollContainerRef,
        cellWidth: 40,
        selectedAllocationIds: ["a1"],
        onDeleteSelected,
      });
    });
    act(() => {
      fireKeydown({ key: "Backspace", metaKey: true });
    });
    expect(onDeleteSelected).not.toHaveBeenCalled();
  });

  it("uses latest onDeleteSelected callback without re-registering the handler", () => {
    const firstCallback = vi.fn();
    const secondCallback = vi.fn();

    const { rerender } = renderHook(
      ({ cb }) => {
        const scrollContainerRef = useRef<HTMLElement | null>(scrollEl);
        return useTimelineKeyboard({
          scrollContainerRef,
          cellWidth: 40,
          selectedAllocationIds: ["a1"],
          onDeleteSelected: cb,
        });
      },
      { initialProps: { cb: firstCallback } },
    );

    // Swap to a new callback
    rerender({ cb: secondCallback });

    act(() => {
      fireKeydown({ key: "Delete" });
    });

    // Only the latest callback should be called
    expect(secondCallback).toHaveBeenCalledOnce();
    expect(firstCallback).not.toHaveBeenCalled();
  });

  // -------------------------------------------------------------------------
  // Typing targets — handler is suppressed inside inputs
  // -------------------------------------------------------------------------

  it("ignores '?' when an input element has focus", () => {
    const input = document.createElement("input");
    document.body.appendChild(input);
    input.focus();

    const { result } = renderHook(() => {
      const scrollContainerRef = useRef<HTMLElement | null>(scrollEl);
      return useTimelineKeyboard({
        scrollContainerRef,
        cellWidth: 40,
        selectedAllocationIds: [],
        onDeleteSelected,
      });
    });

    act(() => {
      fireKeydown({ key: "?" });
    });

    expect(result.current.showShortcuts).toBe(false);
    input.remove();
  });

  it("ignores Delete when a textarea has focus", () => {
    const textarea = document.createElement("textarea");
    document.body.appendChild(textarea);
    textarea.focus();

    renderHook(() => {
      const scrollContainerRef = useRef<HTMLElement | null>(scrollEl);
      return useTimelineKeyboard({
        scrollContainerRef,
        cellWidth: 40,
        selectedAllocationIds: ["a1"],
        onDeleteSelected,
      });
    });

    act(() => {
      fireKeydown({ key: "Delete" });
    });

    expect(onDeleteSelected).not.toHaveBeenCalled();
    textarea.remove();
  });

  it("ignores ArrowLeft when a select element has focus", () => {
    scrollEl._scrollLeft = 200;
    const select = document.createElement("select");
    document.body.appendChild(select);
    select.focus();

    renderHook(() => {
      const scrollContainerRef = useRef<HTMLElement | null>(scrollEl);
      return useTimelineKeyboard({
        scrollContainerRef,
        cellWidth: 40,
        selectedAllocationIds: [],
        onDeleteSelected,
      });
    });

    act(() => {
      fireKeydown({ key: "ArrowLeft" });
    });

    expect(scrollEl.scrollLeft).toBe(200); // unchanged
    select.remove();
  });

  it("ignores keys when a contentEditable element has focus", () => {
    // jsdom does not implement isContentEditable, so we create a stub element
    // that reports isContentEditable = true and mock document.activeElement to
    // return it.
    const div = document.createElement("div");
    // jsdom doesn't implement isContentEditable; override it manually
    Object.defineProperty(div, "isContentEditable", {
      get: () => true,
      configurable: true,
    });
    const activeElSpy = vi.spyOn(document, "activeElement", "get").mockReturnValue(div);

    renderHook(() => {
      const scrollContainerRef = useRef<HTMLElement | null>(scrollEl);
      return useTimelineKeyboard({
        scrollContainerRef,
        cellWidth: 40,
        selectedAllocationIds: ["a1"],
        onDeleteSelected,
      });
    });

    act(() => {
      fireKeydown({ key: "Delete" });
    });

    expect(onDeleteSelected).not.toHaveBeenCalled();
    activeElSpy.mockRestore();
  });

  // -------------------------------------------------------------------------
  // Cleanup — event listener is removed on unmount
  // -------------------------------------------------------------------------

  it("removes the keydown listener when the component unmounts", () => {
    const removeSpy = vi.spyOn(window, "removeEventListener");
    const { unmount } = renderHook(() => {
      const scrollContainerRef = useRef<HTMLElement | null>(scrollEl);
      return useTimelineKeyboard({
        scrollContainerRef,
        cellWidth: 40,
        selectedAllocationIds: [],
        onDeleteSelected,
      });
    });
    unmount();
    expect(removeSpy).toHaveBeenCalledWith("keydown", expect.any(Function));
  });
});
