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

import { defineToolcraft } from "../../schema/define-toolcraft";
import type { ToolcraftCanvasFrame } from "../../state/canvas-frame";
import { ToolcraftRoot } from "../app-shell/toolcraft-root";
import { useToolcraftDispatch } from "../app-shell/use-toolcraft";
import {
  ToolcraftProductSceneBoundsBoundary,
  ToolcraftProductSceneSurface,
  useToolcraftProductSceneFrame,
} from "./product-scene-surface";

const schema = defineToolcraft({
  canvas: {
    enabled: true,
    size: { height: 200, unit: "px", width: 300 },
    sizing: { mode: "editable-output" },
  },
  panels: {},
  persistence: { storage: "none" },
});

afterEach(() => {
  cleanup();
});

function FrameProbe(): React.JSX.Element {
  const frame = useToolcraftProductSceneFrame();
  return <output data-testid="frame">{JSON.stringify(frame)}</output>;
}

function renderSurface(
  frame: ToolcraftCanvasFrame,
  boundsProvider = vi.fn(() => [
    { height: 50, width: 80, x: -40, y: -25 },
    { height: 20, width: 30, x: 30, y: 15 },
  ]),
) {
  return {
    boundsProvider,
    ...render(
      <ToolcraftRoot
        initialState={{
          canvas: {
            mode: frame.kind,
            offset: { x: 0, y: 0 },
            size: { height: 200, unit: "px", width: 300 },
            zoom: 100,
          },
        }}
        schema={schema}
      >
        <ToolcraftProductSceneBoundsBoundary boundsProvider={boundsProvider}>
          <ToolcraftProductSceneSurface frame={frame}>
            <FrameProbe />
          </ToolcraftProductSceneSurface>
        </ToolcraftProductSceneBoundsBoundary>
      </ToolcraftRoot>,
    ),
  };
}

describe("ToolcraftProductSceneSurface", () => {
  it("keeps the finite frame canonical without invoking product bounds", () => {
    const { boundsProvider, container } = renderSurface({
      kind: "finite",
      size: { height: 200, unit: "px", width: 300 },
    });

    expect(boundsProvider).not.toHaveBeenCalled();
    expect(screen.getByTestId("frame").textContent).toBe(
      JSON.stringify({
        kind: "finite",
        rect: { height: 200, width: 300, x: 0, y: 0 },
      }),
    );
    expect(
      container.querySelector("[data-toolcraft-product-scene]"),
    ).toBeNull();
  });

  it("positions one infinite product surface from the exact provider union", () => {
    const { boundsProvider, container } = renderSurface({ kind: "infinite" });
    const productScene = container.querySelector<HTMLElement>(
      "[data-toolcraft-product-scene]",
    );

    expect(boundsProvider).toHaveBeenCalledTimes(1);
    expect(productScene?.dataset.toolcraftProductSceneStatus).toBe("ready");
    expect(productScene?.style.cssText).toContain("height: 60px");
    expect(productScene?.style.cssText).toContain("left: -40px");
    expect(productScene?.style.cssText).toContain("top: -25px");
    expect(productScene?.style.cssText).toContain("width: 100px");
    expect(screen.getByTestId("frame").textContent).toBe(
      JSON.stringify({
        kind: "infinite",
        rect: { height: 60, width: 100, x: -40, y: -25 },
      }),
    );
  });

  it("keeps hook consumers stable when state changes without changing bounds", () => {
    const renderProbe = vi.fn();

    function StableFrameProbe(): React.JSX.Element {
      renderProbe();
      const dispatch = useToolcraftDispatch();
      useToolcraftProductSceneFrame();
      return (
        <button
          onClick={() =>
            dispatch({ delta: { x: 12, y: -8 }, type: "canvas.panBy" })
          }
          type="button"
        >
          Pan
        </button>
      );
    }

    const boundsProvider = vi.fn(() => [
      { height: 60, width: 100, x: -40, y: -25 },
    ]);
    render(
      <ToolcraftRoot
        initialState={{
          canvas: {
            mode: "infinite",
            offset: { x: 0, y: 0 },
            size: { height: 200, unit: "px", width: 300 },
            zoom: 100,
          },
        }}
        schema={schema}
      >
        <ToolcraftProductSceneBoundsBoundary boundsProvider={boundsProvider}>
          <ToolcraftProductSceneSurface frame={{ kind: "infinite" }}>
            <StableFrameProbe />
          </ToolcraftProductSceneSurface>
        </ToolcraftProductSceneBoundsBoundary>
      </ToolcraftRoot>,
    );

    expect(renderProbe).toHaveBeenCalledTimes(1);
    fireEvent.click(screen.getByRole("button", { name: "Pan" }));
    expect(boundsProvider).toHaveBeenCalledTimes(2);
    expect(renderProbe).toHaveBeenCalledTimes(1);
  });

  it.each([
    {
      expectedKind: "empty",
      provider: () => [],
    },
    {
      expectedKind: "unavailable",
      provider: () => [
        { height: 20, width: Number.NaN, x: 0, y: 0 },
      ],
    },
    {
      expectedKind: "unavailable",
      provider: () => {
        throw new Error("broken provider");
      },
    },
  ])(
    "reports $expectedKind without falling back to finite geometry",
    ({ expectedKind, provider }) => {
      const { container } = renderSurface(
        { kind: "infinite" },
        vi.fn(provider),
      );
      const productScene = container.querySelector<HTMLElement>(
        "[data-toolcraft-product-scene]",
      );

      expect(productScene?.dataset.toolcraftProductSceneStatus).toBe(
        expectedKind,
      );
      expect(productScene?.style.width).toBe("");
      expect(productScene?.style.height).toBe("");
      expect(screen.getByTestId("frame").textContent).toBe(
        JSON.stringify({ kind: expectedKind, rect: null }),
      );
    },
  );

  it("fails directly when the frame hook escapes the product scene", () => {
    expect(() => render(<FrameProbe />)).toThrow(
      "useToolcraftProductSceneFrame must be used inside Toolcraft product scene content",
    );
  });
});
