import * as React from "react";
import { cleanup, render } from "@testing-library/react";
import { afterEach, beforeAll, beforeEach, expect, 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 { CanvasShell } from "../canvas/canvas-shell";
import { ControlsPanel } from "../controls-panel/controls-panel";
import {
  DEFAULT_TOOLCRAFT_ORIENTATION_POSE,
  type ToolcraftOrientationPose,
} from "./orientation-gizmo-math";
import { useToolcraftModelOrbitInteraction } from "./use-toolcraft-model-orbit-interaction";

const schema = defineToolcraft({
  canvas: {
    enabled: true,
    size: { height: 200, unit: "px", width: 300 },
  },
  panels: {
    controls: {
      sections: [
        {
          controls: {
            scale: {
              defaultValue: 1,
              label: "Scale",
              max: 2,
              min: 0.5,
              target: "model.scale",
              type: "slider",
            },
            orientation: {
              defaultValue: DEFAULT_TOOLCRAFT_ORIENTATION_POSE,
              keyframeable: false,
              label: false,
              target: "view.orbit",
              type: "orientationGizmo",
            },
          },
          title: "Model",
        },
      ],
      title: "Master Controls",
    },
  },
});

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

function OrbitSurface({ hit }: { hit: boolean }): React.JSX.Element {
  const handlers = useToolcraftModelOrbitInteraction<HTMLDivElement>({
    hitTest: () => hit,
    target: "view.orbit",
  });

  return (
    <div
      data-testid="model-surface"
      {...handlers}
      style={{ height: 200, width: 300 }}
    />
  );
}

function createCanvasContextMock(): CanvasRenderingContext2D {
  return {
    arc: vi.fn(),
    beginPath: vi.fn(),
    clearRect: vi.fn(),
    fill: vi.fn(),
    lineCap: "butt",
    lineTo: vi.fn(),
    moveTo: vi.fn(),
    setTransform: vi.fn(),
    stroke: vi.fn(),
  } as unknown as CanvasRenderingContext2D;
}

beforeAll(() => {
  Object.defineProperty(window, "matchMedia", {
    configurable: true,
    value: () => ({
      addEventListener: () => undefined,
      matches: false,
      removeEventListener: () => undefined,
    }),
  });
  Object.defineProperty(window, "requestAnimationFrame", {
    configurable: true,
    value: (callback: FrameRequestCallback) =>
      window.setTimeout(() => callback(performance.now()), 16),
    writable: true,
  });
  Object.defineProperty(window, "cancelAnimationFrame", {
    configurable: true,
    value: (handle: number) => window.clearTimeout(handle),
    writable: true,
  });
});

beforeEach(() => {
  vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue(
    createCanvasContextMock(),
  );
  Object.defineProperty(window, "requestAnimationFrame", {
    configurable: true,
    value: vi.fn((callback: FrameRequestCallback) =>
      window.setTimeout(() => callback(performance.now()), 16),
    ),
    writable: true,
  });
  Object.defineProperty(window, "cancelAnimationFrame", {
    configurable: true,
    value: vi.fn((handle: number) => window.clearTimeout(handle)),
    writable: true,
  });
});

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

const gizmoBounds: DOMRect = {
  bottom: 70,
  height: 70,
  left: 0,
  right: 70,
  toJSON: () => ({}),
  top: 0,
  width: 70,
  x: 0,
  y: 0,
};

const modelSurfaceBounds: DOMRect = {
  bottom: 200,
  height: 200,
  left: 0,
  right: 300,
  toJSON: () => ({}),
  top: 0,
  width: 300,
  x: 0,
  y: 0,
};

export function expectOrientationPoseClose(
  received: ToolcraftOrientationPose,
  expected: ToolcraftOrientationPose,
): void {
  for (const key of ["position", "up"] as const) {
    expected[key].forEach((value, index) => {
      expect(received[key][index]).toBeCloseTo(value, 12);
    });
  }
}

export function mockOrientationGizmoBounds(element: Element): void {
  vi.spyOn(element, "getBoundingClientRect").mockReturnValue(gizmoBounds);
}

export function mockModelSurfaceBounds(element: Element): void {
  vi.spyOn(element, "getBoundingClientRect").mockReturnValue(
    modelSurfaceBounds,
  );
}

export function renderOrientationGizmoRuntime({
  hit = true,
}: {
  hit?: boolean;
} = {}): ToolcraftExternalStore {
  let store: ToolcraftExternalStore | undefined;
  render(
    <ToolcraftRoot schema={schema}>
      <div style={{ height: 640, position: "relative", width: 640 }}>
        <CanvasShell>
          <OrbitSurface hit={hit} />
        </CanvasShell>
        <ControlsPanel framed={false} />
        <CaptureStore
          capture={(value) => {
            store = value;
          }}
        />
      </div>
    </ToolcraftRoot>,
  );

  if (!store) {
    throw new Error("Orientation gizmo test runtime did not expose its store.");
  }

  return store;
}
