import { describe, expect, it } from "vitest";

import {
  act,
  defineToolcraft,
  fireEvent,
  renderControlsPanelWithSchema,
  screen,
  vi,
} from "../testing/controls-panel-test-utils";

function createNavigationSchema() {
  return defineToolcraft({
    canvas: { enabled: false },
    panels: {
      controls: {
        sections: [
          {
            controls: {
              density: {
                defaultValue: 50,
                label: "Density",
                max: 100,
                min: 0,
                target: "field.density",
                type: "slider",
              },
            },
            title: "Field",
          },
          {
            controls: {
              enabled: {
                defaultValue: true,
                label: "Enabled",
                target: "wind.enabled",
                type: "switch",
              },
            },
            title: "Wind",
          },
          {
            actionGroup: "secondary",
            controls: {
              export: {
                actions: [
                  {
                    command: "controls.apply",
                    label: "Export",
                    value: "export",
                    variant: "default",
                  },
                ],
                target: "panel.actions",
                type: "panelActions",
              },
            },
            title: "Export",
          },
        ],
        title: "Grass Studio",
      },
    },
  });
}

function createRect({
  height,
  left = 0,
  top,
  width = 300,
}: {
  height: number;
  left?: number;
  top: number;
  width?: number;
}): DOMRect {
  return {
    bottom: top + height,
    height,
    left,
    right: left + width,
    toJSON: () => ({}),
    top,
    width,
    x: left,
    y: top,
  };
}

function prepareScrollablePanel(container: HTMLElement) {
  const root = container.querySelector<HTMLElement>(
    "[data-toolcraft-controls-panel-shell]",
  );
  const viewport = container.querySelector<HTMLElement>(
    '[data-slot="toolcraft-panel-content"]',
  );
  const panel = container.querySelector<HTMLElement>(
    '[data-panel-id="properties"]',
  );
  const sectionAnchors = Array.from(
    container.querySelectorAll<HTMLElement>(
      "[data-toolcraft-controls-section-anchor]",
    ),
  );

  expect(root).toBeTruthy();
  expect(panel).toBeTruthy();
  expect(viewport).toBeTruthy();
  expect(sectionAnchors).toHaveLength(4);

  Object.defineProperties(viewport!, {
    clientHeight: { configurable: true, value: 300 },
    scrollHeight: { configurable: true, value: 900 },
    scrollTop: { configurable: true, value: 0, writable: true },
  });
  vi.spyOn(root!, "getBoundingClientRect").mockReturnValue(
    createRect({ height: 360, left: 400, top: 10 }),
  );
  vi.spyOn(panel!, "getBoundingClientRect").mockReturnValue(
    createRect({ height: 360, left: 400, top: 10 }),
  );
  vi.spyOn(viewport!, "getBoundingClientRect").mockReturnValue(
    createRect({ height: 300, left: 400, top: 60 }),
  );

  const sectionTops = [-200, 40, 240, 380];
  sectionAnchors.forEach((section, index) => {
    vi.spyOn(section, "getBoundingClientRect").mockImplementation(() =>
      createRect({
        height: 180,
        left: 400,
        top: sectionTops[index] ?? 0,
      }),
    );
  });

  const scrollTo = vi.fn();
  Object.defineProperty(viewport!, "scrollTo", {
    configurable: true,
    value: scrollTo,
  });

  return {
    panel: panel!,
    root: root!,
    scrollTo,
    sectionAnchors,
    viewport: viewport!,
  };
}

describe("ControlsPanel section navigation", () => {
  it("requires a 300ms dwell within the 12px left padding and cancels a fast crossing", () => {
    vi.useFakeTimers();

    try {
      const { container } = renderControlsPanelWithSchema(
        createNavigationSchema(),
      );
      const { panel } = prepareScrollablePanel(container);

      act(() => vi.advanceTimersByTime(0));
      fireEvent.pointerMove(panel, { clientX: 413, clientY: 120 });
      act(() => vi.advanceTimersByTime(300));
      expect(
        screen.queryByRole("navigation", { name: "Panel sections" }),
      ).toBeNull();

      fireEvent.pointerMove(panel, { clientX: 408, clientY: 120 });
      act(() => vi.advanceTimersByTime(299));
      expect(
        screen.queryByRole("navigation", { name: "Panel sections" }),
      ).toBeNull();

      fireEvent.pointerLeave(panel, { clientX: 399, clientY: 120 });
      act(() => vi.advanceTimersByTime(300));
      expect(
        screen.queryByRole("navigation", { name: "Panel sections" }),
      ).toBeNull();

      fireEvent.pointerMove(panel, { clientX: 408, clientY: 120 });
      act(() => vi.advanceTimersByTime(300));
      expect(
        screen.getByRole("navigation", { name: "Panel sections" }),
      ).toBeTruthy();
    } finally {
      vi.useRealTimers();
    }
  });

  it("preserves panel-to-popup transfer and requires a fresh dwell after exit", () => {
    vi.useFakeTimers();

    try {
      const { container } = renderControlsPanelWithSchema(
        createNavigationSchema(),
      );
      const { panel } = prepareScrollablePanel(container);

      act(() => vi.advanceTimersByTime(0));
      fireEvent.pointerMove(panel, { clientX: 408, clientY: 120 });
      act(() => vi.advanceTimersByTime(300));

      const navigation = screen.getByRole("navigation", {
        name: "Panel sections",
      });
      const surface = navigation.closest<HTMLElement>(
        "[data-toolcraft-controls-section-navigation-surface]",
      );
      expect(surface).toBeTruthy();
      expect(surface?.dataset.state).toBe("open");
      expect(surface?.className).toContain("duration-[120ms]");
      expect(surface?.className).toContain("data-[state=open]:animate-in");
      expect(surface?.className).toContain("data-[state=closed]:animate-out");
      fireEvent.pointerLeave(panel, { clientX: 399, clientY: 120 });
      act(() => vi.advanceTimersByTime(49));
      expect(
        screen.getByRole("navigation", { name: "Panel sections" }),
      ).toBeTruthy();

      fireEvent.pointerEnter(surface!);
      act(() => vi.advanceTimersByTime(1));
      expect(
        screen.getByRole("navigation", { name: "Panel sections" }),
      ).toBeTruthy();

      fireEvent.pointerLeave(surface!);
      act(() => vi.advanceTimersByTime(49));
      expect(
        screen.getByRole("navigation", { name: "Panel sections" }),
      ).toBeTruthy();
      act(() => vi.advanceTimersByTime(1));
      expect(surface?.dataset.state).toBe("closed");
      expect(surface?.className).toContain("pointer-events-none");
      act(() => vi.advanceTimersByTime(119));
      expect(
        container.querySelector(
          "[data-toolcraft-controls-section-navigation-surface]",
        ),
      ).toBeTruthy();
      act(() => vi.advanceTimersByTime(1));
      expect(
        screen.queryByRole("navigation", { name: "Panel sections" }),
      ).toBeNull();

      fireEvent.pointerMove(panel, { clientX: 408, clientY: 120 });
      act(() => vi.advanceTimersByTime(299));
      expect(
        screen.queryByRole("navigation", { name: "Panel sections" }),
      ).toBeNull();
      act(() => vi.advanceTimersByTime(1));
      expect(
        screen.getByRole("navigation", { name: "Panel sections" }),
      ).toBeTruthy();
    } finally {
      vi.useRealTimers();
    }
  });

  it("keeps the popup open during a slow gap crossing and movement inside it", () => {
    vi.useFakeTimers();

    try {
      const { container } = renderControlsPanelWithSchema(
        createNavigationSchema(),
      );
      const { panel } = prepareScrollablePanel(container);

      act(() => vi.advanceTimersByTime(0));
      fireEvent.pointerMove(panel, { clientX: 408, clientY: 120 });
      act(() => vi.advanceTimersByTime(300));

      const navigation = screen.getByRole("navigation", {
        name: "Panel sections",
      });
      const surface = navigation.closest<HTMLElement>(
        "[data-toolcraft-controls-section-navigation-surface]",
      );

      expect(surface).toBeTruthy();
      vi.spyOn(surface!, "getBoundingClientRect").mockReturnValue(
        createRect({ height: 220, left: 220, top: 90, width: 168 }),
      );

      fireEvent.pointerLeave(panel, { clientX: 399, clientY: 180 });
      act(() => vi.advanceTimersByTime(40));
      fireEvent.pointerMove(document, { clientX: 394, clientY: 180 });
      act(() => vi.advanceTimersByTime(100));
      expect(
        screen.getByRole("navigation", { name: "Panel sections" }),
      ).toBeTruthy();

      fireEvent.pointerMove(document, { clientX: 300, clientY: 180 });
      act(() => vi.advanceTimersByTime(200));
      expect(
        screen.getByRole("navigation", { name: "Panel sections" }),
      ).toBeTruthy();

      fireEvent.pointerMove(document, { clientX: 100, clientY: 400 });
      act(() => vi.advanceTimersByTime(49));
      expect(
        screen.getByRole("navigation", { name: "Panel sections" }),
      ).toBeTruthy();
      act(() => vi.advanceTimersByTime(1));
      expect(surface?.dataset.state).toBe("closed");
      act(() => vi.advanceTimersByTime(120));
      expect(
        screen.queryByRole("navigation", { name: "Panel sections" }),
      ).toBeNull();
    } finally {
      vi.useRealTimers();
    }
  });

  it("keeps the shared layout and jumps to the current section immediately", () => {
    vi.useFakeTimers();

    try {
      const { container } = renderControlsPanelWithSchema(
        createNavigationSchema(),
      );
      const { panel, scrollTo, viewport } = prepareScrollablePanel(container);

      viewport.scrollTop = 120;
      fireEvent.scroll(viewport);
      act(() => vi.advanceTimersByTime(0));
      fireEvent.pointerMove(panel, { clientX: 408, clientY: 120 });
      act(() => vi.advanceTimersByTime(300));

      const navigation = screen.getByRole("navigation", {
        name: "Panel sections",
      });
      const surface = navigation.closest<HTMLElement>(
        "[data-toolcraft-controls-section-navigation-surface]",
      );
      const positioner = surface?.parentElement;
      const list = navigation.querySelector<HTMLElement>(
        "[data-toolcraft-controls-section-navigation-list]",
      );
      const scrollFade = navigation.closest<HTMLElement>(
        '[data-slot="scroll-fade"]',
      );
      const scrollViewport = scrollFade?.querySelector<HTMLElement>(
        "[data-scroll-fade-viewport]",
      );
      const fieldButton = screen.getByRole("button", { name: "Field" });
      const windButton = screen.getByRole("button", { name: "Wind" });
      const navigationLabels = Array.from(
        navigation.querySelectorAll("button"),
        (button) => button.textContent,
      );

      expect(navigationLabels).toEqual(["Setup", "Field", "Wind"]);
      expect(positioner?.style.right).toBe("calc(100% + 8px)");
      expect(positioner?.style.top).toBe("200px");
      expect(surface?.style.maxHeight).toBe("300px");
      expect(surface?.className).toContain("py-2");
      expect(surface?.className).not.toContain("px-3");
      expect(scrollFade).toBeTruthy();
      expect(scrollViewport?.style.maxHeight).toBe("284px");
      expect(scrollViewport?.className).toContain("overflow-y-auto");
      expect(navigation.className).not.toContain("overflow-y-auto");
      expect(list?.className).toContain("gap-2");
      expect(list?.className).toContain("px-3");
      expect(list?.className).not.toContain("px-2");
      expect(fieldButton.className).toContain("text-[13px]");
      expect(fieldButton.className).toContain("px-0");
      expect(fieldButton.className).not.toContain("pl-3");
      expect(fieldButton.className).toContain(
        "data-[current=true]:text-[color:var(--foreground)]",
      );
      expect(fieldButton.className).not.toContain("before:");
      expect(windButton.className).not.toContain("before:");
      expect(fieldButton.className).toContain("focus-visible:ring-2");
      expect(windButton.className).toContain("var(--foreground)_50%");
      expect(windButton.className).toContain(
        "hover:text-[color:color-mix(in_oklab,var(--foreground)_80%,transparent)]",
      );
      expect(fieldButton.getAttribute("aria-current")).toBe("location");
      expect(fieldButton.getAttribute("data-current")).toBe("true");

      fireEvent.click(windButton);

      expect(scrollTo).toHaveBeenCalledWith({ behavior: "auto", top: 300 });
      expect(windButton.getAttribute("aria-current")).toBe("location");
    } finally {
      vi.useRealTimers();
    }
  });

  it("switches sections with wrapping Arrow Up and Arrow Down navigation", () => {
    vi.useFakeTimers();

    try {
      const { container } = renderControlsPanelWithSchema(
        createNavigationSchema(),
      );
      const { panel, scrollTo } = prepareScrollablePanel(container);

      act(() => vi.advanceTimersByTime(0));
      fireEvent.pointerMove(panel, { clientX: 408, clientY: 120 });
      act(() => vi.advanceTimersByTime(300));

      const setupButton = screen.getByRole("button", { name: "Setup" });
      const fieldButton = screen.getByRole("button", { name: "Field" });
      const windButton = screen.getByRole("button", { name: "Wind" });

      fieldButton.focus();
      fireEvent.keyDown(fieldButton, { key: "ArrowDown" });
      expect(document.activeElement).toBe(windButton);
      expect(windButton.getAttribute("aria-current")).toBe("location");
      expect(scrollTo).toHaveBeenLastCalledWith({
        behavior: "auto",
        top: 180,
      });

      fireEvent.keyDown(windButton, { key: "ArrowDown" });
      expect(document.activeElement).toBe(setupButton);
      expect(setupButton.getAttribute("aria-current")).toBe("location");

      fireEvent.keyDown(setupButton, { key: "ArrowUp" });
      expect(document.activeElement).toBe(windButton);
      expect(windButton.getAttribute("aria-current")).toBe("location");
    } finally {
      vi.useRealTimers();
    }
  });

  it("shows bottom and top fades from the navigation scroll position", () => {
    vi.useFakeTimers();

    try {
      const { container } = renderControlsPanelWithSchema(
        createNavigationSchema(),
      );
      const { panel } = prepareScrollablePanel(container);

      act(() => vi.advanceTimersByTime(0));
      fireEvent.pointerMove(panel, { clientX: 408, clientY: 120 });
      act(() => vi.advanceTimersByTime(300));

      const navigation = screen.getByRole("navigation", {
        name: "Panel sections",
      });
      const scrollViewport = navigation.closest<HTMLElement>(
        "[data-scroll-fade-viewport]",
      );

      expect(scrollViewport).toBeTruthy();
      Object.defineProperties(scrollViewport!, {
        clientHeight: { configurable: true, value: 40 },
        scrollHeight: { configurable: true, value: 120 },
        scrollTop: { configurable: true, value: 0, writable: true },
      });

      fireEvent.scroll(scrollViewport!);
      expect(scrollViewport?.style.getPropertyValue("--scroll-fade-leading-fade")).toBe(
        "0px",
      );
      expect(scrollViewport?.style.getPropertyValue("--scroll-fade-trailing-fade")).toBe(
        "20px",
      );

      scrollViewport!.scrollTop = 40;
      fireEvent.scroll(scrollViewport!);
      expect(scrollViewport?.style.getPropertyValue("--scroll-fade-leading-fade")).toBe(
        "20px",
      );
      expect(scrollViewport?.style.getPropertyValue("--scroll-fade-trailing-fade")).toBe(
        "20px",
      );

      scrollViewport!.scrollTop = 80;
      fireEvent.scroll(scrollViewport!);
      expect(scrollViewport?.style.getPropertyValue("--scroll-fade-leading-fade")).toBe(
        "20px",
      );
      expect(scrollViewport?.style.getPropertyValue("--scroll-fade-trailing-fade")).toBe(
        "0px",
      );
    } finally {
      vi.useRealTimers();
    }
  });
});
