import { useState } from "react";
import { describe, it, expect, vi } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";

import { FloatingWindow } from "./FloatingWindow";
import { MenuBar } from "./MenuBar";
import { zIndex } from "./zIndex";

describe("FloatingWindow (#16, G1)", () => {
  it("Escape closes only the topmost window when stacked (#17)", () => {
    const onCloseOuter = vi.fn();
    const onCloseInner = vi.fn();
    const { rerender } = render(
      <>
        <FloatingWindow open title="Outer" onClose={onCloseOuter}>
          <p>outer body</p>
        </FloatingWindow>
        <FloatingWindow open title="Inner" onClose={onCloseInner}>
          <p>inner body</p>
        </FloatingWindow>
      </>,
    );

    // The inner (last-mounted) window is topmost: Escape closes only it.
    fireEvent.keyDown(document, { key: "Escape" });
    expect(onCloseInner).toHaveBeenCalledTimes(1);
    expect(onCloseOuter).not.toHaveBeenCalled();

    // Unmount the inner window; the outer becomes topmost and now handles Escape.
    rerender(
      <>
        <FloatingWindow open title="Outer" onClose={onCloseOuter}>
          <p>outer body</p>
        </FloatingWindow>
        <FloatingWindow open={false} title="Inner" onClose={onCloseInner}>
          <p>inner body</p>
        </FloatingWindow>
      </>,
    );
    fireEvent.keyDown(document, { key: "Escape" });
    expect(onCloseOuter).toHaveBeenCalledTimes(1);
    expect(onCloseInner).toHaveBeenCalledTimes(1);
  });

  it("renders nothing when closed", () => {
    const { container } = render(
      <FloatingWindow open={false} title="Panel" onClose={vi.fn()}>
        <button>inside</button>
      </FloatingWindow>,
    );
    expect(container.querySelector('[role="dialog"]')).toBeNull();
  });

  it("renders a titled modal dialog at the floatingWindow z-index", () => {
    render(
      <FloatingWindow open title="My panel" onClose={vi.fn()}>
        <p>body</p>
      </FloatingWindow>,
    );
    const dialog = screen.getByRole("dialog", { name: "My panel" });
    expect(dialog.getAttribute("aria-modal")).toBe("true");
    // The backdrop carries the frozen z-index token.
    const backdrop = dialog.parentElement as HTMLElement;
    expect(backdrop.style.zIndex).toBe(String(zIndex.floatingWindow));
  });

  it("moves focus into the window on open (focus-trap entry)", () => {
    render(
      <FloatingWindow open title="Panel" onClose={vi.fn()}>
        <input aria-label="first field" />
        <button>second</button>
      </FloatingWindow>,
    );
    // The first focusable element (the Close button in the header) receives focus.
    expect(document.activeElement).toBe(
      screen.getByRole("button", { name: "close Panel" }),
    );
  });

  it("wraps Tab from the last focusable back to the first (focus-trap)", () => {
    render(
      <FloatingWindow open title="Panel" onClose={vi.fn()}>
        <button>only</button>
      </FloatingWindow>,
    );
    const closeBtn = screen.getByRole("button", { name: "close Panel" });
    const onlyBtn = screen.getByRole("button", { name: "only" });

    // Focus the last focusable, press Tab ⇒ wraps to the first (close button).
    onlyBtn.focus();
    fireEvent.keyDown(document, { key: "Tab" });
    expect(document.activeElement).toBe(closeBtn);

    // Shift+Tab from the first wraps to the last.
    closeBtn.focus();
    fireEvent.keyDown(document, { key: "Tab", shiftKey: true });
    expect(document.activeElement).toBe(onlyBtn);
  });

  it("closes on Escape, backdrop click, and the Close button", () => {
    const onClose = vi.fn();
    render(
      <FloatingWindow open title="Panel" onClose={onClose}>
        <p>body</p>
      </FloatingWindow>,
    );

    fireEvent.keyDown(document, { key: "Escape" });
    expect(onClose).toHaveBeenCalledTimes(1);

    const dialog = screen.getByRole("dialog");
    fireEvent.mouseDown(dialog.parentElement as HTMLElement); // backdrop
    expect(onClose).toHaveBeenCalledTimes(2);

    fireEvent.click(screen.getByRole("button", { name: "close Panel" }));
    expect(onClose).toHaveBeenCalledTimes(3);
  });

  it("does not close on clicks inside the panel body", () => {
    const onClose = vi.fn();
    render(
      <FloatingWindow open title="Panel" onClose={onClose}>
        <button>inside</button>
      </FloatingWindow>,
    );
    fireEvent.mouseDown(screen.getByRole("button", { name: "inside" }));
    fireEvent.click(screen.getByRole("button", { name: "inside" }));
    expect(onClose).not.toHaveBeenCalled();
  });

  it("keeps focus on a controlled input across renders that change onClose identity (#17 focus-loss bug)", () => {
    // Reproduces the reported bug: a caller re-creates its `onClose` closure on
    // every render (as TicketDetail's `requestClose` does). If the focus-trap
    // effect re-ran on that identity change, focus would jump back to the first
    // focusable after each keystroke — only one character could be typed.
    function Harness() {
      const [value, setValue] = useState("");
      // New closure identity on every render, like an inline/rebuilt handler.
      const onClose = () => {};
      return (
        <FloatingWindow open title="Panel" onClose={onClose}>
          <input
            aria-label="field"
            value={value}
            onChange={(e) => setValue(e.target.value)}
          />
        </FloatingWindow>
      );
    }
    render(<Harness />);
    const field = screen.getByRole("textbox", { name: "field" });
    field.focus();
    expect(document.activeElement).toBe(field);

    fireEvent.change(field, { target: { value: "a" } });
    expect(document.activeElement).toBe(field);
    fireEvent.change(field, { target: { value: "ab" } });
    fireEvent.change(field, { target: { value: "abc" } });
    // Focus never left the field, so multi-character input works.
    expect(document.activeElement).toBe(field);
    expect((field as HTMLInputElement).value).toBe("abc");
  });

  it("restores focus to the opener when closed", () => {
    function Harness() {
      const [open, setOpen] = useState(false);
      return (
        <>
          <button onClick={() => setOpen(true)}>opener</button>
          <FloatingWindow open={open} title="Panel" onClose={() => setOpen(false)}>
            <p>body</p>
          </FloatingWindow>
        </>
      );
    }
    render(<Harness />);
    const opener = screen.getByRole("button", { name: "opener" });
    opener.focus();
    fireEvent.click(opener);
    // Window open, focus moved inside.
    expect(document.activeElement).not.toBe(opener);
    // Close via Escape ⇒ focus returns to the opener.
    fireEvent.keyDown(document, { key: "Escape" });
    expect(document.activeElement).toBe(opener);
  });
});

describe("MenuBar (#16)", () => {
  const menus = [
    {
      id: "view",
      label: "View",
      items: [
        { id: "a", label: "Agents", onSelect: vi.fn() },
        { id: "b", label: "Git", onSelect: vi.fn() },
      ],
    },
  ];

  it("opens a dropdown on click and fires the item's onSelect, then closes", () => {
    const onSelect = vi.fn();
    render(
      <MenuBar
        menus={[
          {
            id: "view",
            label: "View",
            items: [{ id: "a", label: "Agents", onSelect }],
          },
        ]}
      />,
    );
    // Item hidden until the menu opens.
    expect(screen.queryByRole("button", { name: "Agents" })).toBeNull();

    fireEvent.click(screen.getByRole("button", { name: "View" }));
    fireEvent.click(screen.getByRole("button", { name: "Agents" }));

    expect(onSelect).toHaveBeenCalledTimes(1);
    // Menu closes after selection.
    expect(screen.queryByRole("button", { name: "Agents" })).toBeNull();
  });

  it("closes the open menu on Escape", () => {
    render(<MenuBar menus={menus} />);
    fireEvent.click(screen.getByRole("button", { name: "View" }));
    expect(screen.getByRole("button", { name: "Agents" })).toBeTruthy();
    fireEvent.keyDown(document, { key: "Escape" });
    expect(screen.queryByRole("button", { name: "Agents" })).toBeNull();
  });

  it("puts the dropdown at the menuDropdown z-index", () => {
    render(<MenuBar menus={menus} />);
    fireEvent.click(screen.getByRole("button", { name: "View" }));
    const dropdown = screen.getByRole("menu", { name: "View" });
    expect(dropdown.style.zIndex).toBe(String(zIndex.menuDropdown));
  });
});
