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

import { encodeToolcraftModelDocument } from "../../model-import/canonical/model-document-codec";
import { createValidModelDocument } from "../../model-import/canonical/model-document-test-support";
import { createHydrationModelAsset } from "../../model-import/model-persistence-hydration-test-support";
import { defineToolcraft } from "../../schema/define-toolcraft";
import { ToolcraftRoot } from "../app-shell/toolcraft-root";
import type { ToolcraftModelRenderBinding } from "./model-render-binding";
import {
  ToolcraftModelCanvasLayer,
  ToolcraftModelCanvasLayers,
} from "./model-canvas-layer";
import { ToolcraftModelRenderProvider } from "./model-render-provider";

type TestResource = { id: number };

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

function createBinding(): ToolcraftModelRenderBinding<TestResource> {
  let sequence = 0;
  return {
    create: vi.fn(() => ({ id: ++sequence })),
    dispose: vi.fn(),
    disposePreparedPreview: vi.fn(),
    hitTest: vi.fn(() => true),
    preparePreview: vi.fn(),
    renderExport: vi.fn(),
    renderPreview: vi.fn(),
    update: vi.fn(),
  };
}

function createUploadSchema(
  controls: Record<string, {
    assetKind: "file" | "image" | "model";
    target: string;
    type: "fileDrop";
  }>,
) {
  return defineToolcraft({
    canvas: { enabled: true },
    panels: {
      controls: {
        sections: [{ controls, title: "Source" }],
        title: "Controls",
      },
    },
  });
}

const asset = {
  activeDocumentRef: "document:a",
  fileName: "cube.glb",
  id: "model-1",
  layerId: "layer-1",
  lifecycle: "clean" as const,
  sourceTarget: "source.model",
};

describe("ToolcraftModelCanvasLayer", () => {
  it("renders through the target binding and preserves layer selection semantics", async () => {
    const binding = createBinding();
    const dispatch = vi.fn();
    const interactionHandlers = {
      onLostPointerCapture: vi.fn(),
      onPointerCancel: vi.fn(),
      onPointerDown: vi.fn(),
      onPointerMove: vi.fn(),
      onPointerUp: vi.fn(),
    };
    const bytes = encodeToolcraftModelDocument(createValidModelDocument());
    const resolveResource = vi.fn(async () => bytes);
    const bindings = { "source.model": binding };
    const standardBinding = createBinding();
    const view = render(
      <ToolcraftModelRenderProvider
        bindings={bindings}
        resolveResource={resolveResource}
        standardBinding={standardBinding}
      >
        <ToolcraftModelCanvasLayer
          asset={asset}
          dispatch={dispatch}
          interactionHandlers={interactionHandlers}
          orientation={{ position: [0, 0, 5], up: [0, 1, 0] }}
          phase="analyzing"
          presentationRect={{ height: 200, width: 300, x: 0, y: 0 }}
          selected={false}
          target="source.model"
          viewport={{ height: 200, width: 300 }}
        />
      </ToolcraftModelRenderProvider>,
    );

    await waitFor(() => expect(binding.renderPreview).toHaveBeenCalledTimes(1));
    await waitFor(() =>
      expect(
        screen
          .getByRole("button", { name: "Select cube.glb" })
          .getAttribute("data-canvas-model-render-status"),
      ).toBe("ready"),
    );
    expect(
      screen
        .getByRole("button", { name: "Select cube.glb" })
        .getAttribute("data-canvas-model-phase"),
    ).toBe("analyzing");
    expect(
      screen
        .getByRole("button", { name: "Select cube.glb" })
        .getAttribute("data-canvas-model-target"),
    ).toBe("source.model");
    expect(
      screen
        .getByRole("button", { name: "Select cube.glb" })
        .getAttribute("data-toolcraft-model-orbit-surface"),
    ).toBe("true");
    expect(
      screen
        .getByRole("button", { name: "Select cube.glb" })
        .getAttribute("data-canvas-model-orientation"),
    ).toBe('{"position":[0,0,5],"up":[0,1,0]}');
    expect(binding.create).toHaveBeenCalledWith(
      expect.objectContaining({ presentationOpacity: 0.4 }),
      expect.objectContaining({ acquirePresentation: expect.any(Function) }),
    );

    fireEvent.click(screen.getByRole("button", { name: "Select cube.glb" }));
    expect(dispatch).toHaveBeenCalledWith({
      layerId: "layer-1",
      type: "layers.select",
    });

    view.rerender(
      <ToolcraftModelRenderProvider
        bindings={bindings}
        resolveResource={resolveResource}
        standardBinding={standardBinding}
      >
        <ToolcraftModelCanvasLayer
          asset={asset}
          dispatch={dispatch}
          interactionHandlers={interactionHandlers}
          orientation={{ position: [5, 0, 0], up: [0, 1, 0] }}
          phase="clean"
          presentationRect={{ height: 200, width: 300, x: 0, y: 0 }}
          selected
          target="source.model"
          viewport={{ height: 200, width: 300 }}
        />
      </ToolcraftModelRenderProvider>,
    );

    await waitFor(() => {
      expect(
        screen
          .getByRole("button", { name: "Select cube.glb" })
          .getAttribute("data-selected"),
      ).toBe("true");
      expect(
        screen
          .getByRole("button", { name: "Select cube.glb" })
          .getAttribute("data-canvas-model-phase"),
      ).toBe("clean");
      expect(
        screen
          .getByRole("button", { name: "Select cube.glb" })
          .getAttribute("data-canvas-model-orientation"),
      ).toBe('{"position":[5,0,0],"up":[0,1,0]}');
      expect(binding.update).toHaveBeenCalledWith(
        expect.anything(),
        expect.objectContaining({ presentationOpacity: 1 }),
        expect.anything(),
      );
    });

    act(() => view.unmount());
    expect(binding.dispose).toHaveBeenCalledTimes(1);
  });

  it("never exposes stale render readiness while a changed request is pending", async () => {
    let resolveUpdate: (() => void) | undefined;
    const updatePending = new Promise<void>((resolve) => {
      resolveUpdate = resolve;
    });
    const binding = createBinding();
    binding.update = vi.fn(() => updatePending);
    const bytes = encodeToolcraftModelDocument(createValidModelDocument());
    const bindings = { "source.model": binding };
    const resolveResource = async () => bytes;
    const standardBinding = createBinding();
    const view = render(
      <ToolcraftModelRenderProvider
        bindings={bindings}
        resolveResource={resolveResource}
        standardBinding={standardBinding}
      >
        <ToolcraftModelCanvasLayer
          asset={asset}
          dispatch={vi.fn()}
          phase="clean"
          presentationRect={{ height: 200, width: 300, x: 0, y: 0 }}
          selected={false}
          target="source.model"
          viewport={{ height: 200, width: 300 }}
        />
      </ToolcraftModelRenderProvider>,
    );
    const layer = screen.getByRole("button", { name: "Select cube.glb" });
    await waitFor(() =>
      expect(layer.getAttribute("data-canvas-model-render-status")).toBe(
        "ready",
      ),
    );

    view.rerender(
      <ToolcraftModelRenderProvider
        bindings={bindings}
        resolveResource={resolveResource}
        standardBinding={standardBinding}
      >
        <ToolcraftModelCanvasLayer
          asset={asset}
          dispatch={vi.fn()}
          phase="repairing"
          presentationRect={{ height: 200, width: 300, x: 0, y: 0 }}
          selected={false}
          target="source.model"
          viewport={{ height: 200, width: 300 }}
        />
      </ToolcraftModelRenderProvider>,
    );

    expect(layer.getAttribute("data-canvas-model-render-status")).toBe(
      "pending",
    );
    resolveUpdate?.();
    await waitFor(() =>
      expect(layer.getAttribute("data-canvas-model-render-status")).toBe(
        "ready",
      ),
    );
  });

  it("renders a first staged direct-canvas document before an asset is committed", async () => {
    const binding = createBinding();
    const bytes = encodeToolcraftModelDocument(createValidModelDocument());
    const operation = Object.freeze({
      phase: "analyzing" as const,
      stagedDocumentRef: "document:staged",
      target: "canvas.direct",
    });
    const schema = defineToolcraft({
      canvas: { enabled: true },
      panels: {},
    });

    const view = render(
      <ToolcraftRoot schema={schema}>
        <ToolcraftModelRenderProvider
          bindings={{ "canvas.direct": binding }}
          resolveResource={async () => bytes}
          standardBinding={createBinding()}
        >
          <ToolcraftModelCanvasLayers
            canvasFrame={{
              kind: "finite",
              size: { height: 768, unit: "px", width: 1024 },
            }}
            dispatch={vi.fn()}
            operationSource={{
              getOperation: () => operation,
              subscribe: () => () => {},
            }}
            operationTargets={["canvas.direct"]}
          />
        </ToolcraftModelRenderProvider>
      </ToolcraftRoot>,
    );

    await waitFor(() => expect(binding.renderPreview).toHaveBeenCalledTimes(1));
    expect(binding.create).toHaveBeenCalledWith(
      expect.objectContaining({
        canonicalDocumentRef: "document:staged",
        presentationOpacity: 0.4,
      }),
      expect.anything(),
    );
    const modelLayer = document.querySelector(
      '[data-canvas-model-target="canvas.direct"]',
    );
    expect(modelLayer?.getAttribute("role")).toBeNull();
    expect(modelLayer?.getAttribute("data-canvas-model-phase")).toBe(
      "analyzing",
    );
    await waitFor(() =>
      expect(modelLayer?.getAttribute("data-canvas-model-render-status")).toBe(
        "ready",
      ),
    );
  });

  it("prewarms only configured model controls and ignores generic upload operation targets", async () => {
    const imageBinding = createBinding();
    const modelBinding = createBinding();
    const schema = createUploadSchema({
      image: {
        assetKind: "image",
        target: "source.image",
        type: "fileDrop",
      },
      model: {
        assetKind: "model",
        target: "source.model",
        type: "fileDrop",
      },
    });

    const view = render(
      <ToolcraftRoot schema={schema}>
        <ToolcraftModelRenderProvider
          bindings={{
            "source.image": imageBinding,
            "source.model": modelBinding,
          }}
          resolveResource={async () => null}
          standardBinding={createBinding()}
        >
          <ToolcraftModelCanvasLayers
            canvasFrame={{
              kind: "finite",
              size: { height: 768, unit: "px", width: 1024 },
            }}
            dispatch={vi.fn()}
            operationTargets={["source.image"]}
          />
        </ToolcraftModelRenderProvider>
      </ToolcraftRoot>,
    );

    const modelPrewarm = await waitFor(() => {
      const element = view.container.querySelector(
        '[data-canvas-model-prewarm-target="source.model"]',
      );
      expect(element?.getAttribute("data-canvas-model-render-status")).toBe(
        "ready",
      );
      return element;
    });
    expect(modelPrewarm).not.toBeNull();
    expect(modelPrewarm?.hasAttribute("data-toolcraft-model-orbit-surface"))
      .toBe(false);
    expect(modelBinding.preparePreview).toHaveBeenCalledTimes(1);
    expect(imageBinding.preparePreview).not.toHaveBeenCalled();
    expect(
      view.container.querySelector(
        '[data-canvas-model-prewarm-target="source.image"]',
      ),
    ).toBeNull();
  });

  it("prepares an isolated renderer for every configured model target", async () => {
    const primaryBinding = createBinding();
    const secondaryBinding = createBinding();
    const schema = createUploadSchema({
      primary: {
        assetKind: "model",
        target: "models.primary",
        type: "fileDrop",
      },
      secondary: {
        assetKind: "model",
        target: "models.secondary",
        type: "fileDrop",
      },
    });

    const view = render(
      <ToolcraftRoot schema={schema}>
        <ToolcraftModelRenderProvider
          bindings={{
            "models.primary": primaryBinding,
            "models.secondary": secondaryBinding,
          }}
          resolveResource={async () => null}
          standardBinding={createBinding()}
        >
          <ToolcraftModelCanvasLayers
            canvasFrame={{
              kind: "finite",
              size: { height: 768, unit: "px", width: 1024 },
            }}
            dispatch={vi.fn()}
          />
        </ToolcraftModelRenderProvider>
      </ToolcraftRoot>,
    );

    await waitFor(() => {
      expect(
        view.container.querySelectorAll(
          '[data-canvas-model-prewarm-target][data-canvas-model-render-status="ready"]',
        ),
      ).toHaveLength(2);
    });
    expect(primaryBinding.preparePreview).toHaveBeenCalledTimes(1);
    expect(secondaryBinding.preparePreview).toHaveBeenCalledTimes(1);
  });

  it("uses each model frame as the render and orbit surface in infinite mode", async () => {
    const binding = createBinding();
    const bytes = encodeToolcraftModelDocument(createValidModelDocument());
    const schema = defineToolcraft({ canvas: { enabled: true }, panels: {} });

    const view = render(
      <ToolcraftRoot
        initialState={{
          canvas: {
            mode: "infinite",
            offset: { x: 0, y: 0 },
            size: { height: 768, unit: "px", width: 1024 },
            zoom: 100,
          },
          layers: [
            {
              displayName: "Model",
              id: "layer-1",
              kind: "layer",
              name: "model",
              visible: true,
            },
          ],
          mediaAssets: [
            createHydrationModelAsset("document:a", {
              lifecycle: "clean",
              position: { x: 80, y: -40 },
              size: { height: 200, unit: "px", width: 300 },
              sourceTarget: "source.model",
            }),
          ],
        }}
        schema={schema}
      >
        <ToolcraftModelRenderProvider
          bindings={{ "source.model": binding }}
          resolveResource={async () => bytes}
          standardBinding={createBinding()}
        >
          <ToolcraftModelCanvasLayers
            canvasFrame={{ kind: "infinite" }}
            dispatch={vi.fn()}
          />
        </ToolcraftModelRenderProvider>
      </ToolcraftRoot>,
    );

    const layer = await waitFor(() => {
      const element = view.container.querySelector<HTMLElement>(
        '[data-canvas-model-layer="layer-1"]',
      );
      expect(element).not.toBeNull();
      return element as HTMLElement;
    });

    expect(layer.style.left).toBe("-70px");
    expect(layer.style.top).toBe("-140px");
    expect(layer.style.width).toBe("300px");
    expect(layer.style.height).toBe("200px");
    await waitFor(() =>
      expect(binding.create).toHaveBeenCalledWith(
        expect.objectContaining({
          displayFit: expect.objectContaining({
            viewport: { height: 200, width: 300 },
          }),
        }),
        expect.anything(),
      ),
    );
  });
});
