/**
 * The smallest `document.createElement("canvas")` that makes `TextureBin` draw.
 *
 * `TextureBin.get` returns `null` when there is no canvas to draw on, which is
 * the right behaviour under Node and is exactly what makes the *material*
 * assertions in this directory impossible without a stub: a `screenContent`
 * material built under `node --test` has a null `map` for a reason that has
 * nothing to do with whether the binding is correct.
 *
 * So this installs a context that records nothing and rasterises nothing. It
 * exists only so that `new THREE.CanvasTexture(canvas)` has a canvas, and the
 * assertions that follow are about *which map is bound to which slot*, never
 * about pixels. `src/test/render/textureMaps.test.ts` is where the drawings
 * themselves are pinned, and duplicating that here would be two files asserting
 * one thing.
 *
 * Deliberately not auto-installing on import: a module with a side effect on
 * `globalThis` that fires on import is the kind of thing that makes one test
 * file's behaviour depend on another's import order.
 */

interface StubCanvas {
  width: number;
  height: number;
  getContext(id: string): unknown;
}

function stubContext(): unknown {
  const noop = (): void => {};
  return {
    set fillStyle(_v: unknown) {},
    get fillStyle(): string {
      return "";
    },
    set strokeStyle(_v: unknown) {},
    set lineWidth(_v: number) {},
    set lineCap(_v: string) {},
    set lineJoin(_v: string) {},
    set globalAlpha(_v: number) {},
    set globalCompositeOperation(_v: string) {},
    set font(_v: string) {},
    set textAlign(_v: string) {},
    set textBaseline(_v: string) {},
    set filter(_v: string) {},
    save: noop,
    restore: noop,
    translate: noop,
    rotate: noop,
    scale: noop,
    clip: noop,
    fillRect: noop,
    clearRect: noop,
    strokeRect: noop,
    beginPath: noop,
    closePath: noop,
    moveTo: noop,
    lineTo: noop,
    arc: noop,
    arcTo: noop,
    ellipse: noop,
    rect: noop,
    quadraticCurveTo: noop,
    bezierCurveTo: noop,
    fill: noop,
    stroke: noop,
    fillText: noop,
    createLinearGradient: () => ({ addColorStop: noop }),
    createRadialGradient: () => ({ addColorStop: noop }),
    getImageData: (_x: number, _y: number, w: number, h: number) => ({
      data: new Uint8ClampedArray(Math.max(1, w * h * 4)).fill(255),
      width: w,
      height: h,
    }),
    putImageData: noop,
  };
}

/**
 * Install the stub and return the undo. Call the undo in a `finally`: leaving a
 * fake `document` on `globalThis` changes how every module loaded afterwards
 * decides whether it is in a browser.
 */
export function withStubCanvas(): () => void {
  const global = globalThis as { document?: unknown };
  const had = "document" in global;
  const previous = global.document;
  global.document = {
    createElement(tag: string): StubCanvas {
      if (tag !== "canvas") throw new Error(`unexpected element <${tag}>`);
      return {
        width: 0,
        height: 0,
        getContext: () => stubContext(),
      };
    },
  };
  return () => {
    if (had) global.document = previous;
    else delete global.document;
  };
}

/** A deterministic PRNG, so "same seed, same geometry" is testable at all. */
export function seeded(seed = 0x12345678): () => number {
  let value = seed >>> 0;
  return () => {
    value = (Math.imul(value, 1664525) + 1013904223) >>> 0;
    return value / 0x1_0000_0000;
  };
}
