// webgame-smoke — load a single-file HTML canvas game into jsdom with the browser APIs a
// game uses (2d context, requestAnimationFrame, Web Audio) SHIMMED, so its JS actually runs
// headless. Lets a `node --test` assert "boots + ticks without throwing" — the smoke a
// single-file game needs, since node --test alone can't give it a DOM/canvas.
const fs = require("fs");
const { JSDOM, VirtualConsole } = require("jsdom");

function noop() {}

function ctx2d() {
  // every draw call is a no-op; measureText/gradients return usable stubs
  const base = {
    measureText: () => ({ width: 0 }),
    getImageData: () => ({ data: new Uint8ClampedArray(4) }),
    createLinearGradient: () => ({ addColorStop: noop }),
    createRadialGradient: () => ({ addColorStop: noop }),
    createPattern: () => ({}),
    canvas: null,
    save: noop, restore: noop, setTransform: noop,
  };
  return new Proxy(base, {
    get(t, p) {
      if (p in t) return t[p];
      return (t[p] = noop); // unknown -> callable no-op (fillRect, beginPath, arc, …)
    },
    set(t, p, v) { t[p] = v; return true; },
  });
}

function audioStub() {
  const node = new Proxy(
    {
      connect: () => node, disconnect: noop, start: noop, stop: noop,
      frequency: { value: 0, setValueAtTime: noop, linearRampToValueAtTime: noop, exponentialRampToValueAtTime: noop },
      gain: { value: 0, setValueAtTime: noop, linearRampToValueAtTime: noop, exponentialRampToValueAtTime: noop },
      type: "sine",
    },
    { get(t, p) { return p in t ? t[p] : (() => node); } }
  );
  return node;
}
function AudioCtx() {
  return {
    createOscillator: audioStub, createGain: audioStub, createBufferSource: audioStub,
    createBuffer: () => ({ getChannelData: () => new Float32Array(1) }),
    createDynamicsCompressor: audioStub, createBiquadFilter: audioStub,
    destination: {}, currentTime: 0, sampleRate: 44100, state: "running",
    resume: () => Promise.resolve(), close: () => Promise.resolve(),
  };
}

function loadHtmlGame(htmlPath, { frames = 0 } = {}) {
  const html = fs.readFileSync(htmlPath, "utf8");
  const errors = [];
  let rafQueue = [];
  const vc = new VirtualConsole();
  vc.on("jsdomError", (e) => errors.push(String((e && (e.detail || e.message)) || e)));

  const dom = new JSDOM(html, {
    runScripts: "dangerously",
    pretendToBeVisual: true,
    virtualConsole: vc,
    beforeParse(window) {
      window.HTMLCanvasElement.prototype.getContext = function () { return ctx2d(); };
      window.requestAnimationFrame = (cb) => { rafQueue.push(cb); return rafQueue.length; };
      window.cancelAnimationFrame = noop;
      window.AudioContext = AudioCtx;
      window.webkitAudioContext = AudioCtx;
      if (!window.performance) window.performance = {};
      let t = 0;
      window.performance.now = () => (t += 16.7);
      window.addEventListener("error", (e) => errors.push(String((e && (e.error || e.message)) || e)));
      window.onerror = (m) => { errors.push(String(m)); return true; };
    },
  });

  const window = dom.window;
  function tick(n = 1) {
    for (let i = 0; i < n; i++) {
      const q = rafQueue; rafQueue = [];
      for (const cb of q) { try { cb(window.performance.now()); } catch (e) { errors.push(String((e && e.stack) || e)); } }
    }
  }
  if (frames) tick(frames);
  return { window, document: window.document, errors, tick };
}

module.exports = { loadHtmlGame };
