import { act, cleanup, fireEvent, render, screen } from "@testing-library/react";
import * as React from "react";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";

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

const persistenceKey = "toolcraft:scrub-lifecycle:state:v1";
const scrubLifecycleSchema = defineToolcraft({
  canvas: { enabled: true },
  panels: { timeline: { mode: "playback" } },
  persistence: {
    include: ["panels", "timeline"],
    key: persistenceKey,
    storage: "localStorage",
    version: 1,
  },
});

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

function ScrubLifecycleHarness({
  capture,
}: {
  capture: (store: ToolcraftExternalStore) => void;
}): React.JSX.Element {
  const [visible, setVisible] = React.useState(true);

  return (
    <>
      <button onClick={() => setVisible(false)} type="button">
        Unmount timeline
      </button>
      {visible ? <TimelinePanel framed={false} /> : null}
      <CaptureStore capture={capture} />
    </>
  );
}

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(),
  });
}

function persistedTime(): number | undefined {
  const persisted = JSON.parse(window.localStorage.getItem(persistenceKey) ?? "{}");
  return persisted.state?.timeline?.currentTimeSeconds;
}

beforeAll(() => {
  Object.defineProperty(window, "matchMedia", {
    configurable: true,
    value: () => ({
      addEventListener: () => undefined,
      addListener: () => undefined,
      matches: false,
      removeEventListener: () => undefined,
      removeListener: () => undefined,
    }),
  });
});

afterEach(() => {
  vi.useRealTimers();
  cleanup();
  window.localStorage.clear();
  vi.restoreAllMocks();
});

function renderLifecycleHarness(): {
  getStore: () => ToolcraftExternalStore;
} {
  let store: ToolcraftExternalStore | undefined;
  render(
    <ToolcraftRoot
      initialState={{ timeline: { isPlaying: false } }}
      schema={scrubLifecycleSchema}
    >
      <ScrubLifecycleHarness capture={(value) => { store = value; }} />
    </ToolcraftRoot>,
  );
  act(() => {
    vi.advanceTimersByTime(120);
  });

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

describe("timeline scrub lifecycle boundaries", () => {
  it("commits and persists an active scrub when the timeline unmounts", () => {
    vi.useFakeTimers();
    const { getStore } = renderLifecycleHarness();
    const scrubber = screen.getByRole("slider", { name: "Playback position" });
    configureScrubber(scrubber);
    fireEvent.pointerDown(scrubber, { clientX: 25, pointerId: 4 });
    fireEvent.pointerMove(scrubber, { clientX: 50, pointerId: 4 });
    const commitTransient = vi.spyOn(getStore(), "commitTransient");

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

    expect(commitTransient).toHaveBeenCalledTimes(1);
    expect(commitTransient).toHaveBeenCalledWith("playback");
    expect(getStore().getCommittedState().timeline.currentTimeSeconds).toBe(4);
    act(() => {
      vi.advanceTimersByTime(120);
    });
    expect(persistedTime()).toBe(4);
  });

  it("finishes lost pointer capture once even when pointerUp follows", () => {
    vi.useFakeTimers();
    const { getStore } = renderLifecycleHarness();
    const scrubber = screen.getByRole("slider", { name: "Playback position" });
    configureScrubber(scrubber);
    fireEvent.pointerDown(scrubber, { clientX: 25, pointerId: 5 });
    fireEvent.pointerMove(scrubber, { clientX: 75, pointerId: 5 });
    const commitTransient = vi.spyOn(getStore(), "commitTransient");

    fireEvent.lostPointerCapture(scrubber, { pointerId: 5 });

    expect(getStore().getCommittedState().timeline.currentTimeSeconds).toBe(6);
    fireEvent.pointerUp(scrubber, { clientX: 75, pointerId: 5 });
    expect(commitTransient).toHaveBeenCalledTimes(1);
    expect(commitTransient).toHaveBeenCalledWith("playback");
    act(() => {
      vi.advanceTimersByTime(120);
    });
    expect(persistedTime()).toBe(6);
  });
});
