/** * Dev-only: boot GameScene and VERIFY the READY-RESEARCH attention cue * end-to-end in a real browser (the node tests pin the model + the deck; * this drives the live scene + the console): * * 1. deck (window closed) — the RESEARCH slot's cue is armed (a tech * can start), and the cycle paints: face band → edge comet → rest; * 2. console (window open) — the deck's slot rests, the ready category * tabs wear the edge shimmer, and the system's "Unlock Jumpgates" * tech wears the ACK pulse (forced chart-complete): on its plate * while the SYSTEM tree is on screen, on the SYSTEM tab otherwise; * 3. in-flight — starting a project rests BOTH halves (maxConcurrent 1). * * The report (window.__CUE_SHOT + the #report
) lists every check;
 * the two screenshots pin the two faces of the cue (each capture freezes
 * its cycle mid-window so the live clock paints the same frame):
 *
 *   node dev/server.mjs 8091
 *   /…/chrome --headless --disable-gpu --remote-debugging-port=9333 about:blank
 *   node dev/cdp-shot.mjs http://127.0.0.1:8091/dev/research-cue-shot.html \
 *     /tmp/cue-deck.png "window.__CUE_SHOT && window.__CUE_SHOT.ready" 90000 \
 *     "window.__CUE_SHOT.captureDeck()"
 *   node dev/cdp-shot.mjs http://127.0.0.1:8091/dev/research-cue-shot.html \
 *     /tmp/cue-console.png "window.__CUE_SHOT && window.__CUE_SHOT.ready" 90000 \
 *     "window.__CUE_SHOT.captureConsole()"
 */
import Phaser from '../js/vendor/phaser.js';
import { config } from '../js/config/Config.js';
import { ConfigLoader } from '../js/config/ConfigLoader.js';
import { createGameConfig } from '../js/config/GameConfig.js';
import { GameScene } from '../js/scenes/GameScene.js';
import { gatesReady } from '../js/research/SystemCategory.js';

const data = await ConfigLoader.load();
config.init(data);

const errors = [];
const origErr = console.error.bind(console);
console.error = (...a) => { errors.push(a.map(String).join(' ')); origErr(...a); };
window.addEventListener('error', (e) => errors.push(String(e.message)));
window.addEventListener('unhandledrejection', (e) => errors.push(`rejection: ${e.reason}`));

const gameConfig = createGameConfig();
gameConfig.scene = [GameScene];
const game = new Phaser.Game(gameConfig);
window.game = game;

const report = document.createElement('pre');
report.id = 'report';
report.style.cssText = 'position:fixed;left:10px;top:10px;z-index:9999;max-width:75%;margin:0;padding:8px 12px;font:13px/1.5 monospace;color:#eaf6ff;background:rgba(6,20,16,0.92);border:1px solid #1b3a5a;white-space:pre-wrap;';
document.body.appendChild(report);
const setReport = (lines) => { report.textContent = (Array.isArray(lines) ? lines : [lines]).join('\n'); };
setReport('booting…');

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function waitScene() {
  for (let i = 0; i < 100; i++) {
    const s = game.scene.getScene('GameScene');
    if (s && s.ship && s.researchWindow) return s;
    await sleep(100);
  }
  throw new Error('GameScene never booted');
}

// Recorder over a Phaser Graphics (counts the cue's own call surface).
const recorder = (g) => {
  const r = { fillRect: 0, strokePoints: 0, fillPoints: 0, clear: 0 };
  const orig = {
    fillRect: g.fillRect.bind(g),
    strokePoints: g.strokePoints.bind(g),
    fillPoints: g.fillPoints.bind(g),
    clear: g.clear.bind(g),
  };
  g.fillRect = (...a) => { r.fillRect++; return orig.fillRect(...a); };
  g.strokePoints = (...a) => { r.strokePoints++; return orig.strokePoints(...a); };
  g.fillPoints = (...a) => { r.fillPoints++; return orig.fillPoints(...a); };
  g.clear = (...a) => { r.clear++; return orig.clear(...a); };
  r.reset = () => { r.fillRect = 0; r.strokePoints = 0; r.fillPoints = 0; r.clear = 0; };
  return r;
};

try {
  const s = await waitScene();
  await sleep(900); // let the boot settle
  const lines = [];
  const check = (label, cond) => lines.push(`${cond ? '✔' : '✘ FAIL'} ${label}`);
  const bar = s.actionBar;
  const win = s.researchWindow;
  const st = s.researchState;
  const slot = bar.slots.find((x) => x.id === 'research');

  // ---- 1. the deck's RESEARCH slot (the window is closed) --------------
  s._applyResearchCue(); // the scene's per-frame beat
  check('deck: cue armed on the RESEARCH slot (fresh run — a tech can start)', bar.shimmerId === 'research');
  const r = recorder(bar.shimmerG);
  const cfg = bar.cfgShimmer;
  const off = slot.phase * 350; // the bar's per-slot stagger
  const P = cfg.periodMs;
  const at = (u) => (((u % P) + P) % P);
  const step = (t) => { r.reset(); bar.update(t, 16); return { ...r }; };

  let a = step(at(cfg.faceMs / 2 - off));
  check(`deck: face window — the band paints (fillRects ${a.fillRect} ≥ 3)`, a.fillRect >= 3 && a.strokePoints === 0);
  a = step(at(cfg.faceMs + cfg.gapMs + cfg.edgeMs / 2 - off));
  check(`deck: edge window — the comet runs (stroke passes ${a.strokePoints} ≥ 4)`, a.strokePoints >= 4 && a.fillRect === 0);
  a = step(at(P - 1 - off));
  check(`deck: rest — the layer idles (cleared=${a.clear})`, a.fillRect === 0 && a.strokePoints === 0 && a.clear >= 1);

  // ---- 2. the console takes the cue over --------------------------------
  s.deckAction('research');
  for (let i = 0; i < 50 && win.openState !== 'open'; i++) await sleep(60);
  await sleep(800); // the boot reveal finishes
  s._applyResearchCue();
  check('deck: the slot rests while the console is open', bar.shimmerId === null);

  // Force the SYSTEM chart complete (the scene's isNavComplete hook) so
  // the "Unlock Jumpgates" tech is ready to be researched.
  s.systemTree.available = () => true;
  s.systemTree.lockNote = () => null; // the readout agrees (AVAILABLE, not LOCKED)
  win.refresh();
  const gid = gatesReady(s.systemTree, st);
  check('console: the gates tech is READY (chart forced complete)', !!gid);

  const rw = recorder(win.cueG);
  const wcfg = win.cfgShimmer;
  const W = (t) => { rw.reset(); win.update(t); return { ...rw }; };
  // A time inside the comet window (the tabs stagger i*420 — the first
  // tab's window is [0, edgeMs); every tab's is within a few hundred ms).
  const tw = Math.min(200, wcfg.edgeMs);

  let w = W(s.time.now + tw);
  check(`console: the ready tabs wear the edge shimmer (strokes ${w.strokePoints} ≥ 6)`, w.strokePoints >= 6);
  w = W(s.time.now + tw);
  check(`console: the gates plate wears the ACK pulse (halo fills ${w.fillPoints} ≥ 1)`, w.fillPoints >= 1);

  // Off the SYSTEM tab → the pulse moves to the SYSTEM tab itself.
  win.switchCategory('exploration');
  await sleep(30);
  w = W(s.time.now + tw);
  check(`console: on another tab the pulse moves to the SYSTEM tab (halo fills ${w.fillPoints} ≥ 1)`, w.fillPoints >= 1);
  win.switchCategory('system');
  await sleep(30);

  // ---- 3. in-flight — nothing can start, the cue rests everywhere ------
  s.beginResearch('exploration', 'tether_l2');
  s._applyResearchCue();
  await sleep(30);
  check('deck: in flight — the slot rests (maxConcurrent 1)', bar.shimmerId === null);
  w = W(s.time.now + tw);
  check(`console: in flight — the cues rest (strokes ${w.strokePoints}, fills ${w.fillPoints})`,
    w.strokePoints === 0 && w.fillPoints === 0);
  st.active = null; // dev poke — put the run back to "a tech can start"
  win.refresh();

  // ---- the capture poses (beforeCaptureExpr) ---------------------------
  // The scene's live clock keeps redrawing, so each capture widens the
  // cycle's window around the live time — every frame paints the same
  // (frozen-mid-window) pose until the restore lands well after the shot.
  window.__CUE_SHOT = {
    ready: true,
    lines,
    errors,
    captureDeck: async () => {
      // The deck's face: the comms quiet (the intro's ACK queue is
      // dev-drained — in play the hold owns the screen until the player
      // presses through, and the cue rests under it by design), the
      // console closed, the face window frozen mid-sweep.
      const h = s.comms;
      if (h) {
        for (const q of h.queue) if (q?.def?.id) h.played.add(q.def.id);
        h.queue.length = 0;
        if (h.current) h._finish(); // the one in play: end it (the pause lifts)
        await sleep(60);
      }
      if (win.isOpen) win.close(); // straight to the window's API (deckAction's input guard may still hold the ACK click)
      for (let i = 0; i < 30 && win.isOpen; i++) await sleep(60);
      const now = s.time.now;
      const saved = { ...cfg };
      Object.assign(cfg, { periodMs: now * 6, faceMs: now * 1.5, gapMs: 40, edgeMs: 500 });
      await sleep(120); // a couple of scene frames at the pose
      window.__CUE_SHOT.dbg = {
        shimmerId: bar.shimmerId,
        enabled: bar.enabled,
        slotDisabled: slot?.disabled,
        highlightId: bar.highlightId,
        anyAvail: s.researchAnyAvailable(),
        commsPaused: s.comms?.paused === true,
        winOpen: win.openState,
        now,
        cfg: { ...cfg },
        layer: typeof bar.shimmerG.geometry?.commands?.length,
      };
      document.getElementById('report').style.display = 'none';
      setTimeout(() => Object.assign(cfg, saved), 2500);
      return 'deck pose set';
    },
    captureConsole: async () => {
      // The console's face: open on SYSTEM, gates ready, comet mid-lap.
      if (!win.isOpen) win.open();
      for (let i = 0; i < 50 && win.openState !== 'open'; i++) await sleep(60);
      await sleep(900); // the reveal
      s.systemTree.available = () => true;
      s.systemTree.lockNote = () => null;
      win.refresh();
      win.switchCategory('system');
      const now = s.time.now;
      const savedW = { ...wcfg };
      // edgeMs = 2·now → u = now/(2·now) = 0.5 (the comet mid-lap, full
      // envelope) for every staggered tab; periodMs > now keeps ct = now.
      Object.assign(wcfg, { periodMs: now * 4, faceMs: 0, gapMs: 0, edgeMs: Math.max(400, now * 2) });
      await sleep(120);
      document.getElementById('report').style.display = 'none';
      setTimeout(() => Object.assign(wcfg, savedW), 2500);
      return 'console pose set';
    },
  };
  setReport(lines);
  console.info('research-cue-shot: report painted');
} catch (err) {
  errors.push(`FATAL: ${err.message}`);
  setReport(['FATAL: ' + err.message, ...errors.slice(0, 3)]);
  window.__CUE_SHOT = { ready: true, fatal: String(err.message), errors };
  console.error('research-cue-shot: fatal', err);
}