// SKITTERDOME e2e: gameplay state, editor place/delete, save->load roundtrip, play-from-here, localStorage.
import { chromium } from 'playwright';
import { readFileSync } from 'node:fs';

const URL = process.env.URL ?? 'http://localhost:4517/';
let failures = 0;
const check = (name, ok, extra = '') => {
  console.log(`${ok ? 'PASS' : 'FAIL'}  ${name}${extra ? '  (' + extra + ')' : ''}`);
  if (!ok) failures++;
};
const deepEq = (a, b) => JSON.stringify(a) === JSON.stringify(b);

const browser = await chromium.launch();
const ctx = await browser.newContext({ viewport: { width: 1280, height: 720 } });
const page = await ctx.newPage();
page.on('pageerror', (e) => check('no page errors', false, e.message));

await page.goto(URL);
await page.evaluate(() => localStorage.clear());
await page.reload();
await page.waitForFunction(() => window.__game !== undefined);
await page.waitForTimeout(600);

// --- 1. baseline play state ---
let s = await page.evaluate(() => ({ phase: window.__game.phase, mode: window.__game.mode, hp: window.__game.hp, enemies: window.__game.enemies }));
check('boots into play mode', s.mode === 'play' && s.phase === 'playing');
check('player hp 3', s.hp === 3);
check('default level has 5 enemies', s.enemies === 5);
check('default arena is 28x28', await page.evaluate(() => {
  const sz = window.__game.levelJson.size;
  return sz[0] === 28 && sz[1] === 28;
}));

// --- rocket launcher (F) ---
await page.mouse.move(800, 300); // aim somewhere
await page.keyboard.press('f');
const rocketSpawned = await page.waitForFunction(() => window.__game.rockets === 1, null, { timeout: 4000 })
  .then(() => true).catch(() => false);
check('F fires a rocket', rocketSpawned);
const rocketGone = await page.waitForFunction(() => window.__game.rockets === 0, null, { timeout: 30000 })
  .then(() => true).catch(() => false);
check('rocket explodes (impact or 2s fuse)', rocketGone);

// --- 2. toggle editor ---
await page.keyboard.press('e');
await page.waitForTimeout(300);
check('E toggles editor', await page.evaluate(() => window.__game.mode) === 'editor');
check('palette visible', await page.locator('.palette').isVisible());

// --- 2b. editor camera: zoom + pan ---
const camState = () => page.evaluate(() => {
  const g = window.__gameInstance;
  return { dist: g.edDist, tx: g.edTarget.x, tz: g.edTarget.z };
});
let cam0 = await camState();
await page.mouse.move(700, 400);
await page.mouse.wheel(0, -600); // zoom in
await page.waitForTimeout(120);
let cam1 = await camState();
check('wheel zooms editor camera in', cam1.dist < cam0.dist, `${cam0.dist.toFixed(1)} -> ${cam1.dist.toFixed(1)}`);
for (let i = 0; i < 40; i++) await page.mouse.wheel(0, 600); // zoom way out
await page.waitForTimeout(120);
cam1 = await camState();
check('zoom-out clamps at max', cam1.dist <= 28 * 1.2 + 8 + 0.01, `dist=${cam1.dist.toFixed(1)}`);
// middle-drag pan
cam0 = await camState();
await page.mouse.move(700, 400);
await page.mouse.down({ button: 'middle' });
await page.mouse.move(560, 300, { steps: 6 });
await page.mouse.up({ button: 'middle' });
await page.waitForTimeout(120);
cam1 = await camState();
check('middle-drag pans editor camera', Math.abs(cam1.tx - cam0.tx) > 0.5 || Math.abs(cam1.tz - cam0.tz) > 0.5,
  `target (${cam0.tx.toFixed(1)},${cam0.tz.toFixed(1)}) -> (${cam1.tx.toFixed(1)},${cam1.tz.toFixed(1)})`);
// space + left-drag pan must move the camera and must NOT paint walls
const wallsBeforePan = await page.evaluate(() => window.__game.levelJson.walls.length);
cam0 = await camState();
await page.keyboard.down('Space');
await page.mouse.move(700, 400);
await page.mouse.down();
await page.mouse.move(820, 470, { steps: 6 });
await page.mouse.up();
await page.keyboard.up('Space');
await page.waitForTimeout(120);
cam1 = await camState();
check('space-drag pans editor camera', Math.abs(cam1.tx - cam0.tx) > 0.3 || Math.abs(cam1.tz - cam0.tz) > 0.3);
check('space-drag pan placed no walls', (await page.evaluate(() => window.__game.levelJson.walls.length)) === wallsBeforePan);
// arrow-key pan
cam0 = await camState();
await page.keyboard.down('ArrowRight');
await page.waitForTimeout(450);
await page.keyboard.up('ArrowRight');
cam1 = await camState();
check('arrow keys pan editor camera', cam1.tx > cam0.tx, `${cam0.tx.toFixed(1)} -> ${cam1.tx.toFixed(1)}`);
// re-frame for the placement tests below
await page.evaluate(() => window.__gameInstance.fitEditorCamera());
await page.waitForTimeout(120);

const cellPoint = (cx, cz) => page.evaluate(([x, z]) => {
  const g = window.__gameInstance;
  const v = g.cellToWorld([x, z]);
  v.y = 0;
  v.project(g.camera);
  return { x: ((v.x + 1) / 2) * innerWidth, y: ((-v.y + 1) / 2) * innerHeight };
}, [cx, cz]);
const walls = () => page.evaluate(() => window.__game.levelJson.walls.length);
const hasWall = (cx, cz) => page.evaluate(([x, z]) =>
  window.__game.levelJson.walls.some((c) => c[0] === x && c[1] === z), [cx, cz]);

// --- 3. place + delete wall ---
const before = await walls();
await page.locator('.tool', { hasText: 'Wall' }).click();
let pt = await cellPoint(15, 8);
await page.mouse.click(pt.x, pt.y);
await page.waitForTimeout(150);
check('left-click places wall', (await hasWall(15, 8)) && (await walls()) === before + 1);
await page.mouse.click(pt.x, pt.y, { button: 'right' });
await page.waitForTimeout(150);
check('right-click deletes wall', !(await hasWall(15, 8)) && (await walls()) === before);

// --- 4. place enemy + spawn move ---
await page.locator('.tool', { hasText: 'Enemy bot' }).click();
pt = await cellPoint(13, 13);
await page.mouse.click(pt.x, pt.y);
await page.waitForTimeout(150);
check('places enemy', await page.evaluate(() => window.__game.levelJson.enemies.some((c) => c[0] === 13 && c[1] === 13)));
await page.locator('.tool', { hasText: 'Player spawn' }).click();
pt = await cellPoint(10, 10);
await page.mouse.click(pt.x, pt.y);
await page.waitForTimeout(150);
check('moves player spawn (single)', await page.evaluate(() => {
  const l = window.__game.levelJson;
  return l.playerSpawn[0] === 10 && l.playerSpawn[1] === 10;
}));

// --- 5. drag-and-drop turret from palette ---
const turretsBefore = await page.evaluate(() => window.__game.levelJson.turrets.length);
pt = await cellPoint(12, 15);
await page.locator('.tool', { hasText: 'Turret' }).scrollIntoViewIfNeeded();
const toolBox = await page.locator('.tool', { hasText: 'Turret' }).boundingBox();
await page.mouse.move(toolBox.x + 20, toolBox.y + 10);
await page.mouse.down();
await page.mouse.move(pt.x, pt.y, { steps: 8 });
await page.mouse.up();
await page.waitForTimeout(150);
const dndOk = await page.evaluate(() => window.__game.levelJson.turrets.some((c) => c[0] === 12 && c[1] === 15));
check('drag-and-drop places turret', dndOk || (await page.evaluate(() => window.__game.levelJson.turrets.length)) > turretsBefore, dndOk ? '' : 'native dnd may not fire headless');

// --- 6. save level -> download (generic schema v2) ---
const savedState = await page.evaluate(() => window.__game.levelDoc);
const dlPromise = page.waitForEvent('download');
await page.locator('[data-act="save"]').click();
const dl = await dlPromise;
const path = '/tmp/skitter-e2e-level.json';
await dl.saveAs(path);
const fileJson = JSON.parse(readFileSync(path, 'utf8'));
check('downloaded JSON matches editor state', deepEq(fileJson, savedState));
check('JSON has required v2 fields', fileJson.version === 2 && fileJson.gameType === 'skitterdome' && Array.isArray(fileJson.size) && Array.isArray(fileJson.entities) && fileJson.entities.some((e) => e.type === 'spawn'));

// --- 7. mutate, then load file back -> identical state ---
await page.locator('.tool', { hasText: 'Wall' }).click();
pt = await cellPoint(15, 15);
await page.mouse.click(pt.x, pt.y);
await page.waitForTimeout(150);
check('state diverged after extra edit', !deepEq(await page.evaluate(() => window.__game.levelDoc), savedState));
const fcPromise = page.waitForEvent('filechooser');
await page.locator('[data-act="load"]').click();
const fc = await fcPromise;
await fc.setFiles(path);
const restored = await page.waitForFunction(
  (want) => JSON.stringify(window.__game.levelDoc) === want,
  JSON.stringify(savedState),
  { timeout: 8000 }
).then(() => true).catch(() => false);
check('loading saved file restores identical state', restored);

// --- 7b. v1 -> v2 backward-compat migration ---
const v1Migrated = await page.evaluate(() => {
  window.__game.loadLevel({
    version: 1, size: [12, 12], playerSpawn: [2, 2],
    walls: [[5, 5], [6, 5]], enemies: [[8, 8]], turrets: [], accents: [[3, 3]],
  });
  const d = window.__game.levelDoc;
  const l = window.__game.levelJson;
  return { v: d.version, gt: d.gameType, walls: l.walls.length, enemies: l.enemies.length, spawn: l.playerSpawn };
});
check('v1 JSON migrates to v2 doc', v1Migrated.v === 2 && v1Migrated.gt === 'skitterdome');
check('v1 contents survive migration', v1Migrated.walls === 2 && v1Migrated.enemies === 1 && deepEq(v1Migrated.spawn, [2, 2]));
// restore the pre-migration editor state for the tests below
await page.evaluate((want) => window.__game.loadLevel(want), savedState);
await page.waitForTimeout(150);

// --- 8. play from here ---
await page.locator('[data-act="play"]').click();
await page.waitForTimeout(300);
s = await page.evaluate(() => ({ mode: window.__game.mode, phase: window.__game.phase, enemies: window.__game.enemies }));
check('play-from-here enters play mode', s.mode === 'play' && s.phase === 'playing');
check('enemy count matches level data', s.enemies === savedState.entities.filter((e) => e.type === 'enemy').length);

// --- 9. win condition ---
await page.evaluate(() => {
  const g = window.__gameInstance;
  [...g.enemies].forEach((e) => g.damageEnemy(e, 99));
  [...g.turrets].forEach((t) => g.damageTurret(t, 99));
});
await page.waitForTimeout(300);
check('clearing all enemies -> phase cleared', (await page.evaluate(() => window.__game.phase)) === 'cleared');
check('cleared message shown', await page.locator('.center-msg', { hasText: 'LEVEL CLEARED' }).isVisible());

// --- 10. localStorage survives reload ---
await page.reload();
await page.waitForFunction(() => window.__game !== undefined);
await page.waitForTimeout(400);
check('edit survives page reload (localStorage)', deepEq(await page.evaluate(() => window.__game.levelDoc), savedState));

await browser.close();
console.log(failures === 0 ? '\nALL PASS' : `\n${failures} FAILURE(S)`);
process.exit(failures === 0 ? 0 : 1);
