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

import type { ToolcraftExternalStore } from "../../state/toolcraft-external-store";
import type { ToolcraftInitialState } from "../../state/types";
import { ToolcraftRoot } from "../app-shell/toolcraft-root";
import { useToolcraftStore } from "../app-shell/toolcraft-store-context";
import { TimelinePanel } from "./timeline-panel";

import {
  createTimelineSchema,
  installTimelinePanelTestEnvironment,
  renderTimelinePanel,
} from "./timeline-panel-test-utils";

installTimelinePanelTestEnvironment();

function CaptureStore({
  capture,
}: {
  capture: (store: ToolcraftExternalStore) => void;
}): null {
  capture(useToolcraftStore());
  return null;
}

function renderTimelineWithStore(initialState?: ToolcraftInitialState) {
  let store: ToolcraftExternalStore | undefined;
  const view = render(
    <ToolcraftRoot initialState={initialState} schema={createTimelineSchema()}>
      <TimelinePanel framed={false} />
      <CaptureStore capture={(value) => { store = value; }} />
    </ToolcraftRoot>,
  );

  return { ...view, getStore: () => store! };
}

function configureScrubber(scrubber: HTMLElement): void {
  vi.spyOn(scrubber, "getBoundingClientRect").mockReturnValue({
    bottom: 20,
    height: 20,
    left: 0,
    right: 100,
    toJSON: () => ({}),
    top: 0,
    width: 100,
    x: 0,
    y: 0,
  });
  Object.assign(scrubber, {
    hasPointerCapture: () => true,
    releasePointerCapture: vi.fn(),
    setPointerCapture: vi.fn(),
  });
}

describe("TimelinePanel transport", () => {
  it("stays collapsed by default in the runtime contract", () => {
    renderTimelinePanel();

    expect(screen.getByTestId("timeline-expanded").textContent).toBe("false");
    expect(screen.getByText("Dur:")).toBeTruthy();
    expect(screen.queryByText("Duration:")).toBeNull();
    expect(screen.queryByText("Add your first keyframe from the properties panel.")).toBeNull();
  });

  it("renders compact presentation as playback-only transport", () => {
    renderTimelinePanel({ variant: "compact" });

    expect(screen.getByRole("button", { name: "Pause playback" })).toBeTruthy();
    expect(screen.queryByRole("button", { name: "Disable loop" })).toBeNull();
    expect(screen.queryByText("Dur:")).toBeNull();
    expect(screen.queryByText("Duration:")).toBeNull();
    expect(screen.queryByRole("slider", { name: "Playback position" })).toBeNull();

    fireEvent.click(screen.getByRole("button", { name: "Pause playback" }));

    expect(screen.getByTestId("timeline-playing").textContent).toBe("false");
  });

  it("renders timeline state and dispatches playback commands", () => {
    renderTimelinePanel();

    expect(screen.getByText("8s")).toBeTruthy();
    expect(screen.getByText("Dur:")).toBeTruthy();
    expect(screen.queryByText("Duration:")).toBeNull();
    expect(screen.getByTestId("timeline-playing").textContent).toBe("true");
    expect(screen.getByTestId("timeline-looping").textContent).toBe("true");
    expect(screen.getByTestId("timeline-expanded").textContent).toBe("false");

    fireEvent.click(screen.getByRole("button", { name: "Pause playback" }));
    expect(screen.getByTestId("timeline-playing").textContent).toBe("false");

    fireEvent.click(screen.getByRole("button", { name: "Disable loop" }));
    expect(screen.getByTestId("timeline-looping").textContent).toBe("false");
  });

  it("keeps timeline playhead handles easy to grab without changing visual size", () => {
    const { container } = renderTimelinePanel();

    const compactHandle = container.querySelector<HTMLElement>(
      '[data-slot="timeline-playback-handle"]',
    );

    expect(compactHandle?.className).toContain("size-2");
    expect(compactHandle?.className).toContain("before:inset-[-4px]");

    fireEvent.click(screen.getByRole("button", { name: "Expand timeline panel" }));

    const expandedHitArea = container.querySelector<HTMLElement>(
      '[data-slot="timeline-expanded-playhead-hit-area"]',
    );
    const expandedHandle = container.querySelector<HTMLElement>(
      '[data-slot="timeline-expanded-playhead-handle"]',
    );

    expect(expandedHandle?.className).toContain("size-[9px]");
    expect(expandedHitArea?.getAttribute("style")).toContain("width: 15px;");
  });

  it("edits timeline duration from the highlighted duration value in collapsed and expanded states", () => {
    const { container } = renderTimelinePanel();

    const durationDisplay = container.querySelector<HTMLElement>(
      '[data-slot="timeline-duration-display"]',
    );

    expect(durationDisplay?.className).toContain("!cursor-text");
    expect(durationDisplay?.className).toContain("group-hover/timeline-panel-header:bg");
    expect(durationDisplay?.className).toContain("var(--foreground)_8%");
    expect(durationDisplay?.className).toContain("px-1");
    expect(container.querySelector('[data-slot="timeline-duration-edit-trigger"]')).toBeNull();
    expect(screen.queryByRole("textbox", { name: "timeline duration" })).toBeNull();

    fireEvent.click(screen.getByRole("button", { name: "Edit timeline duration" }));

    const editor = screen.getByRole("textbox", { name: "timeline duration" });

    expect(editor.className).toContain("var(--foreground)_8%");
    expect(editor.className).toContain("px-1");
    expect(editor.className).toContain("!cursor-text");
    expect(screen.queryByRole("button", { name: "Edit timeline duration" })).toBeNull();

    editor.textContent = "12s";
    fireEvent.keyDown(editor, { key: "Enter" });

    expect(screen.getByText("12s")).toBeTruthy();

    fireEvent.click(screen.getByRole("button", { name: "Expand timeline panel" }));

    const expandedDurationDisplay = container.querySelector<HTMLElement>(
      '[data-slot="timeline-duration-display"]',
    );

    expect(expandedDurationDisplay?.className).toContain("!cursor-text");
    expect(expandedDurationDisplay?.className).toContain("group-hover/timeline-panel-header:bg");
    expect(expandedDurationDisplay?.className).toContain("var(--foreground)_8%");

    fireEvent.click(screen.getByRole("button", { name: "Edit timeline duration" }));

    const expandedEditor = screen.getByRole("textbox", { name: "timeline duration" });

    expect(expandedEditor.className).toContain("var(--foreground)_8%");
    expect(expandedEditor.className).toContain("!cursor-text");
  });

  it("replays from the beginning when play is pressed at the non-looping end", () => {
    renderTimelinePanel(
      {},
      {
        timeline: {
          currentTimeSeconds: 8,
          isLooping: false,
          isPlaying: false,
        },
      },
      { mode: "playback" },
    );

    expect(screen.getByTestId("timeline-time").textContent).toBe("8");

    fireEvent.click(screen.getByRole("button", { name: "Play playback" }));

    expect(screen.getByTestId("timeline-time").textContent).toBe("0");
    expect(screen.getByTestId("timeline-playing").textContent).toBe("true");
  });

  it("keeps rAF playback transient until pause commits the latest time", () => {
    let frameCallback: FrameRequestCallback | undefined;
    vi.spyOn(window.performance, "now").mockReturnValue(0);
    vi.spyOn(window, "requestAnimationFrame").mockImplementation((callback) => {
      frameCallback = callback;
      return 1;
    });
    vi.spyOn(window, "cancelAnimationFrame").mockImplementation(() => undefined);
    const { getStore } = renderTimelineWithStore({
      timeline: { isLooping: false, isPlaying: true },
    });

    act(() => {
      frameCallback?.(1000);
    });

    expect(getStore().getState().timeline.currentTimeSeconds).toBe(1);
    expect(getStore().getCommittedState().timeline.currentTimeSeconds).toBe(0);

    fireEvent.click(screen.getByRole("button", { name: "Pause playback" }));

    expect(getStore().getCommittedState().timeline).toMatchObject({
      currentTimeSeconds: 1,
      isPlaying: false,
    });
  });

  it("commits pointer scrubbing only when the pointer interaction ends", () => {
    const { getStore } = renderTimelineWithStore({
      timeline: { isPlaying: false },
    });
    const scrubber = screen.getByRole("slider", { name: "Playback position" });
    configureScrubber(scrubber);

    fireEvent.pointerDown(scrubber, { button: 0, clientX: 25, pointerId: 7 });
    fireEvent.pointerMove(scrubber, { clientX: 50, pointerId: 7 });

    expect(getStore().getState().timeline.currentTimeSeconds).toBe(4);
    expect(getStore().getCommittedState().timeline.currentTimeSeconds).toBe(0);

    const commitTransient = vi.spyOn(getStore(), "commitTransient");
    fireEvent.pointerUp(scrubber, { clientX: 50, pointerId: 7 });

    expect(getStore().getCommittedState().timeline.currentTimeSeconds).toBe(4);
    expect(commitTransient).toHaveBeenCalledTimes(1);
    expect(commitTransient).toHaveBeenCalledWith("playback");
  });

  it("commits pointer scrubbing when pointerCancel ends the interaction", () => {
    const { getStore } = renderTimelineWithStore({
      timeline: { isPlaying: false },
    });
    const scrubber = screen.getByRole("slider", { name: "Playback position" });
    configureScrubber(scrubber);

    fireEvent.pointerDown(scrubber, { button: 0, clientX: 25, pointerId: 8 });
    fireEvent.pointerMove(scrubber, { clientX: 75, pointerId: 8 });

    expect(getStore().getState().timeline.currentTimeSeconds).toBe(6);
    expect(getStore().getCommittedState().timeline.currentTimeSeconds).toBe(0);

    fireEvent.pointerCancel(scrubber, { clientX: 75, pointerId: 8 });

    expect(getStore().getCommittedState().timeline.currentTimeSeconds).toBe(6);
  });

  it("commits keyboard seeks at the keyboard interaction boundary", () => {
    const { getStore } = renderTimelineWithStore({
      timeline: { isPlaying: false },
    });
    const scrubber = screen.getByRole("slider", { name: "Playback position" });

    fireEvent.keyDown(scrubber, { key: "ArrowRight" });

    expect(getStore().getCommittedState().timeline.currentTimeSeconds).toBe(
      getStore().getState().timeline.currentTimeSeconds,
    );
    expect(getStore().getCommittedState().timeline.currentTimeSeconds).toBeGreaterThan(0);
  });

  it("writes and commits the final time before stopping natural playback", () => {
    let frameCallback: FrameRequestCallback | undefined;
    vi.spyOn(window.performance, "now").mockReturnValue(0);
    vi.spyOn(window, "requestAnimationFrame").mockImplementation((callback) => {
      frameCallback = callback;
      return 1;
    });
    vi.spyOn(window, "cancelAnimationFrame").mockImplementation(() => undefined);
    const { getStore } = renderTimelineWithStore({
      timeline: {
        durationSeconds: 1,
        isLooping: false,
        isPlaying: true,
        keyframeGroups: [
          {
            controlId: "shape.opacity",
            keyframes: [
              {
                controlId: "shape.opacity",
                controlLabel: "Opacity",
                id: "shape.opacity::0",
                timeSeconds: 0,
                value: 0,
                valueLabel: "0",
              },
            ],
            label: "Opacity",
          },
        ],
      },
    });

    act(() => {
      frameCallback?.(1000);
    });

    expect(getStore().getCommittedState().timeline).toMatchObject({
      currentTimeSeconds: 1,
      durationSeconds: 1,
      isLooping: false,
      isPlaying: false,
      keyframeGroups: [
        expect.objectContaining({ controlId: "shape.opacity" }),
      ],
    });
  });

  it("persists expanded state in the shared runtime timeline", () => {
    renderTimelinePanel();

    fireEvent.click(screen.getByRole("button", { name: "Expand timeline panel" }));

    expect(screen.getByTestId("timeline-expanded").textContent).toBe("true");
    expect(screen.getByText("Duration:")).toBeTruthy();
    expect(screen.getByText("Add your first keyframe from the properties panel.")).toBeTruthy();
    expect(screen.queryByText("Dur:")).toBeNull();
  });

  it("renders playback-only timelines without an expanded keyframe editor", () => {
    const { container } = renderTimelinePanel(
      {},
      {
        timeline: {
          expanded: true,
          keyframeGroups: [
            {
              controlId: "selectedLayer.opacity",
              keyframes: [
                {
                  controlId: "selectedLayer.opacity",
                  controlLabel: "Opacity",
                  id: "selectedLayer.opacity::4",
                  timeSeconds: 4,
                  valueLabel: "75%",
                },
              ],
              label: "Opacity",
            },
          ],
        },
      },
      { mode: "playback" },
    );

    expect(screen.getByText("Dur:")).toBeTruthy();
    expect(screen.queryByRole("button", { name: "Expand timeline panel" })).toBeNull();
    expect(screen.queryByText("Duration:")).toBeNull();
    expect(screen.queryByText("Opacity")).toBeNull();
    expect(screen.queryByText("Add your first keyframe from the properties panel.")).toBeNull();
    expect(screen.getByRole("button", { name: "Pause playback" })).toBeTruthy();
    expect(
      container.querySelector('[data-slot="timeline-panel"] > [data-panel-id="timeline"]')
        ?.className,
    ).toContain("pr-3");
  });

  it("points the expand toggle down when collapsed and up when expanded", () => {
    const { container } = renderTimelinePanel();

    const getArrowIcon = () => container.querySelector('[data-slot="primitive-arrow-icon"]');

    expect(getArrowIcon()?.classList.contains("rotate-180")).toBe(false);

    fireEvent.click(screen.getByRole("button", { name: "Expand timeline panel" }));

    expect(getArrowIcon()?.classList.contains("rotate-180")).toBe(true);
  });
});
