/*
  tests/smoke-test.js — Save Pet
  ---------------------------------------------------------------
  A zero-dependency headless smoke test. It mocks just enough of
  the browser (canvas, DOM, localStorage, rAF) to run the REAL
  js/*.js files under plain Node, so you can catch logic/regression
  bugs in seconds without opening a browser at all.

  Run it after any change to js/game.js, js/entities.js, or
  js/levels.js:

      node tests/smoke-test.js

  Exits with code 0 if everything passes, 1 if anything fails.
  This file ships specifically because of a real bug caught during
  development: the game screen (and its canvas) starts hidden
  (display:none) until the player taps Play/Daily/etc., so the
  canvas's parent reports 0x0 size on first boot. The very first
  render() call used to divide the ground-wobble loop step by that
  0 width, creating an infinite loop that silently froze the game
  on the splash screen for every single player. Test 1 below
  guards against that exact class of bug forever.
*/
"use strict";
let failures = 0;
function ok(label, cond) {
  console.log((cond ? "PASS" : "FAIL") + " - " + label);
  if (!cond) failures++;
}

/* ---------------- minimal browser mocks ---------------- */
global.window = global;
global.performance = { now: () => clock };
try {
  global.navigator = { vibrate: function () { return true; } };
} catch (e) {
  Object.defineProperty(global, "navigator", { value: { vibrate: function () { return true; } }, configurable: true, writable: true });
}
const windowListeners = {};
global.window.addEventListener = function (ev, fn) { (windowListeners[ev] = windowListeners[ev] || []).push(fn); };
global.window.removeEventListener = function () {};
global.fireWindow = function (ev, e) { (windowListeners[ev] || []).forEach(function (fn) { fn(e); }); };
global.devicePixelRatio = 1;

function noop() {}
function fakeCtx() {
  const grad = { addColorStop: noop };
  return {
    save: noop, restore: noop, translate: noop, rotate: noop, scale: noop, setTransform: noop,
    beginPath: noop, moveTo: noop, lineTo: noop, arc: noop, ellipse: noop, fill: noop, stroke: noop,
    clip: noop, closePath: noop, quadraticCurveTo: noop, bezierCurveTo: noop, setLineDash: noop,
    fillRect: noop, clearRect: noop, strokeRect: noop, createLinearGradient: function () { return grad; },
    fillStyle: "", strokeStyle: "", lineWidth: 1, lineCap: "round", lineJoin: "round", globalAlpha: 1
  };
}
function fakeCanvas(getSize) {
  const listeners = {};
  return {
    style: {}, width: 0, height: 0,
    parentElement: { getBoundingClientRect: function () { return getSize(); } },
    getBoundingClientRect: function () { const s = getSize(); return { left: 0, top: 0, width: s.width, height: s.height }; },
    getContext: function () { return fakeCtx(); },
    addEventListener: function (ev, fn) { (listeners[ev] = listeners[ev] || []).push(fn); },
    removeEventListener: noop,
    _fire: function (ev, e) { (listeners[ev] || []).forEach(function (fn) { fn(e); }); }
  };
}
global.document = {
  createElement: function () { return { style: {}, addEventListener: noop, removeEventListener: noop, appendChild: noop, classList: { add: noop, remove: noop, toggle: noop } }; },
  addEventListener: noop, removeEventListener: noop,
  querySelector: function () { return null; }, querySelectorAll: function () { return []; }
};
global.Audio = function () {
  return { play: function () { return Promise.resolve(); }, pause: noop, load: noop, canPlayType: function () { return ""; }, cloneNode: function () { return global.Audio(); }, addEventListener: noop, removeEventListener: noop, currentTime: 0, volume: 1 };
};
global.localStorage = (function () {
  let store = {};
  return { getItem: function (k) { return k in store ? store[k] : null; }, setItem: function (k, v) { store[k] = String(v); }, removeItem: function (k) { delete store[k]; } };
})();
let queued = [];
global.requestAnimationFrame = function (fn) { queued.push(fn); return queued.length; };
global.cancelAnimationFrame = noop;
let clock = 0;
function pump(seconds, stepMs) {
  stepMs = stepMs || 16;
  const steps = Math.round((seconds * 1000) / stepMs);
  for (let i = 0; i < steps; i++) {
    clock += stepMs;
    const fns = queued; queued = [];
    fns.forEach(function (fn) { fn(clock); });
  }
}

/* ---------------- load the real source ---------------- */
const path = require("path").join(__dirname, "..", "js") + require("path").sep;
require(path + "config.js");
require(path + "utils.js");
require(path + "levels.js");
require(path + "entities.js");
require(path + "particles.js");
require(path + "audio.js");
require(path + "achievements.js");
require(path + "game.js");

SP.Storage.load();
SP.Audio.init();

/* ---------------- TEST 1: boot must not hang when the game
   screen (and its canvas) starts hidden, i.e. 0x0 ---------------- */
(function testBootWithHiddenCanvas() {
  let size = { width: 0, height: 0 };
  const canvas = fakeCanvas(function () { return size; });
  const start = Date.now();
  SP.Game.init(canvas);
  const elapsedMs = Date.now() - start;
  ok("boot with a 0x0 (hidden) canvas returns immediately, does not hang", elapsedMs < 500);

  // simulate the game screen becoming visible (ui.js calls onResize() here)
  size = { width: 380, height: 760 };
  SP.Game.onResize();
  SP.Game.startLevel(SP.Levels.getLevel(1), "level");
  const st = SP.Game.getState();
  ok("canvas is measured correctly once visible", st.width === 380 && st.height === 760);
  ok("pet is placed at a real, non-zero position", st.pet.x > 0 && st.pet.y > 0);
  ok("ink budget is non-zero", st.inkBudget > 0);
})();

/* ---------------- TEST 2: level generator produces sane values for all 100 levels ---------------- */
(function testLevelGenerator() {
  let bad = 0;
  for (let id = 1; id <= 100; id++) {
    const lv = SP.Levels.getLevel(id);
    if (!(lv.spawnIntervalMin < lv.spawnIntervalMax)) bad++;
    if (!(lv.inkBudgetRef > 0)) bad++;
    if (lv.hives.length < 1 || lv.hives.length > 4) bad++;
  }
  ok("all 100 levels have sane, valid parameters", bad === 0);
  const a = JSON.stringify(SP.Levels.generateLevel(42));
  const b = JSON.stringify(SP.Levels.generateLevel(42));
  ok("level generation is deterministic (same id -> same level)", a === b);
})();

/* ---------------- TEST 3: a real blocking line stops all bees (true win path) ---------------- */
(function testWinPath() {
  const lvl1 = SP.Levels.getLevel(1);
  SP.Game.startLevel(lvl1, "level");
  const st = SP.Game.getState();
  const hive = st.hives[0], pet = st.pet;
  const dx = pet.x - hive.x, dy = pet.y - hive.y;
  const d = Math.hypot(dx, dy) || 1;
  const px = -dy / d, py = dx / d;
  const midX = hive.x + dx * 0.5, midY = hive.y + dy * 0.5;
  const half = Math.max(st.width, st.height) * 0.6;
  st.lines.push({ points: [{ x: midX - px * half, y: midY - py * half }, { x: midX + px * half, y: midY + py * half }] });
  pump(lvl1.surviveTime + 2);
  ok("a real blocking wall prevents all hits and wins the level", SP.Game.getState().status === "won");
})();

/* ---------------- TEST 4: taking 3 hits loses, with no false progress ---------------- */
(function testLossPath() {
  SP.Game.startLevel(SP.Levels.getLevel(5), "level");
  const st = SP.Game.getState();
  for (let i = 0; i < 3; i++) st.bees.push(SP.Entities.createBee(st.pet.x, st.pet.y, { type: "normal", speed: 0, scale: 1 }));
  pump(1.0);
  ok("3 direct hits ends the run as a loss", SP.Game.getState().status === "lost");
})();

/* ---------------- TEST 5: all 100 levels run start-to-finish without throwing ---------------- */
(function testAllLevelsRun() {
  let errors = 0;
  for (let id = 1; id <= 100; id++) {
    try { SP.Game.startLevel(SP.Levels.getLevel(id), "level"); pump(1.5); }
    catch (e) { errors++; console.error("  level " + id + " threw:", e.message); }
  }
  ok("all 100 levels run without throwing an error", errors === 0);
})();

/* ---------------- summary ---------------- */
console.log("");
if (failures === 0) {
  console.log("ALL SMOKE TESTS PASSED");
  process.exitCode = 0;
} else {
  console.log(failures + " TEST(S) FAILED");
  process.exitCode = 1;
}
