/**
 * Pure-engine test suite — runs in bare Node, no browser/DOM needed.
 * Covers terrain, placement economy, power network, growth, fires,
 * brownouts, save/load integrity and milestones.
 */
import { City } from '../src/game/city.js';
import { STRUCT, ZONE, SIM } from '../src/config.js';

function findSpot(city, w = 1, h = 1, startX = 8, startZ = 34) {
  const g = city.grid;
  for (let r = 0; r < 30; r++) {
    for (let z = startZ - r; z <= startZ + r; z++) {
      for (let x = startX - r; x <= startX + r; x++) {
        if (!g.inB(x, z) || !g.inB(x + w - 1, z + h - 1)) continue;
        let free = true;
        for (let dz = 0; dz < h && free; dz++) for (let dx = 0; dx < w; dx++) {
          const i = g.idx(x + dx, z + dz);
          if (g.terrain[i] !== 0 || g.struct[i] !== 0 || g.zone[i] !== 0) { free = false; break; }
        }
        if (free) return [x, z];
      }
    }
  }
  return null;
}

let pass = 0, fail = 0;
function ok(cond, name) {
  if (cond) { pass++; console.log('  ✔', name); }
  else { fail++; console.log('  ✘ FAIL:', name); }
}

console.log('— terrain generation —');
{
  const c = new City(12345);
  let water = 0, grass = 0, trees = 0;
  for (let i = 0; i < c.grid.n; i++) {
    if (c.grid.terrain[i] === 1) water++; else grass++;
    if (c.grid.scenery[i]) trees++;
  }
  ok(water > 200, `island has ocean (${water} water tiles)`);
  ok(grass > 1500, `buildable land exists (${grass})`);
  ok(trees > 100, `scenery scattered (${trees})`);
}

console.log('— placement & economy —');
{
  const c = new City(777);
  const S = c.grid.size;
  // road line through the middle
  for (let x = 10; x < 40; x++) {
    const r = c.placeStruct(STRUCT.ROAD, x, 32);
    if (!r.ok && x === 10) throw new Error('road placement failed: ' + r.reason);
  }
  ok(c.money < 20000, 'roads cost money');
  const m0 = c.money;
  const bad = c.placeStruct(STRUCT.ROAD, 10, 32);
  ok(!bad.ok, 'cannot overlap existing road');
  ok(c.money === m0, 'failed placement refunds nothing');

  // zones next to the road
  const tiles = [];
  for (let x = 12; x < 24; x++) for (let z = 29; z <= 31; z++) tiles.push([x, z]);
  const placed = c.placeZone(ZONE.RES, tiles);
  ok(placed > 30, `residential zone painted (${placed})`);
  const dup = c.placeZone(ZONE.RES, tiles);
  ok(dup === 0, 're-zoning same land is a no-op');

  // coal plant beside road
  const r = c.placeStruct(STRUCT.COAL, 14, 34);
  ok(r.ok, 'coal plant placed on 2x2 land');
  ok(c.grid.anchor[c.grid.idx(14, 34)] === c.grid.idx(14, 34), 'anchor set');
  ok(c.grid.struct[c.grid.idx(15, 35)] === STRUCT.COAL, 'footprint filled');

  c.recomputePower();
  ok(c.stats.powerCap >= 6000, `plant generates (${c.stats.powerCap})`);
  ok(c.stats.powerUse >= 0, 'meter tracks demand');

  // growth over months

  // growth over months
  for (let i = 0; i < 36; i++) c.tick();
  let dev = 0, unpowered = 0;
  for (let i = 0; i < c.grid.n; i++) {
    if (c.grid.isDeveloped(i)) { dev++; if (!c.grid.powered[i]) unpowered++; }
  }
  const pop = c.stats.pop;
  ok(dev > 3, `zones developed (${dev})`);
  ok(pop > 10, `citizens arrived (pop ${pop})`);
  ok(dev > 0 && unpowered === 0, 'developed buildings enjoy ample power');
  ok(c.monthIndex === 36, 'month counter advances');
  ok(c.history.length > 30, 'history recorded');

  // bulldoze
  const before = c.money;
  const d = c.demolish(14, 34);
  ok(d.ok, 'bulldoze works');
  ok(c.grid.struct[c.grid.idx(15, 35)] === 0, 'footprint cleared');
  ok(c.money < before, 'bulldoze costs money');

  // insufficient funds
  c.money = 5;
  const nope = c.placeStruct(STRUCT.COAL, 20, 34);
  ok(!nope.ok && nope.reason === 'Not enough funds', 'poverty blocked');
}

console.log('— brownouts & capacity —');
{
  const c = new City(42);
  for (let x = 8; x < 50; x++) c.placeStruct(STRUCT.ROAD, x, 30);
  const tiles = [];
  for (let x = 9; x < 49; x++) for (let z = 26; z <= 28; z++) tiles.push([x, z]);
  c.placeZone(ZONE.RES, tiles);
  c.placeZone(ZONE.COM, Array.from({ length: 30 }, (_, k) => [10 + k, 29]));
  c.placeZone(ZONE.IND, Array.from({ length: 30 }, (_, k) => [10 + k, 31]));
  const wspot = findSpot(c, 1, 1, 12, 33);
  ok(!!wspot, 'found turbine spot');
  c.placeStruct(STRUCT.WIND, wspot[0], wspot[1]); // tiny 750-unit supply
  for (let i = 0; i < 64; i++) c.tick();
  ok(c.stats.powerCap === 750, 'wind capacity counted');
  ok(c.stats.pop > 100, `big suburb grew (pop ${c.stats.pop})`);
  ok(c.stats.brownouts > 0 || c.stats.powerUse <= c.stats.powerCap,
    `power pressure visible (use ${c.stats.powerUse}/cap ${c.stats.powerCap}, brownouts ${c.stats.brownouts})`);
}

console.log('— fires & fire stations —');
{
  const c = new City(99);
  for (let x = 8; x < 30; x++) c.placeStruct(STRUCT.ROAD, x, 30);
  const tiles = [];
  for (let x = 9; x < 28; x++) for (let z = 28; z <= 32; z++) tiles.push([x, z]);
  c.placeZone(ZONE.RES, tiles);
  const csp = findSpot(c, 2, 2, 12, 35);
  c.placeStruct(STRUCT.COAL, csp[0], csp[1]);
  for (let i = 0; i < 30; i++) c.tick();

  // light one on fire manually, no station
  const g = c.grid;
  let target = -1;
  for (let i = 0; i < g.n; i++) if (g.isDeveloped(i)) { target = i; break; }
  ok(target >= 0, 'developed tile exists to burn');
  g.burning[target] = 1;
  c.mapFire[target] = 0;
  c.tick();
  ok(g.rubble[target] === 1, 'unprotected fire → rubble');
  ok(g.zone[target] !== 0, 'zone designation survives fire');

  // with a fire station covering, fires die fast
  const fsp = findSpot(c, 1, 1, 18, 26);
  c.placeStruct(STRUCT.FIRE, fsp[0], fsp[1]);
  c.computeServiceMaps();
  let t2 = -1;
  for (let i = 0; i < g.n; i++) if (g.isDeveloped(i)) { t2 = i; break; }
  g.burning[t2] = 2;
  c.tick();
  ok(g.burning[t2] <= 0 || !g.isDeveloped(t2) ? g.rubble[t2] === 1 || g.burning[t2] <= 0 : true,
    'fire station resolves fires');
}

console.log('— taxes & happiness —');
{
  const c = new City(5);
  for (let x = 8; x < 40; x++) c.placeStruct(STRUCT.ROAD, x, 30);
  const tiles = [];
  for (let x = 9; x < 38; x++) for (let z = 27; z <= 33; z++) tiles.push([x, z]);
  c.placeZone(ZONE.RES, tiles);
  c.placeZone(ZONE.COM, Array.from({ length: 20 }, (_, k) => [9 + k, 34]));
  c.placeZone(ZONE.IND, Array.from({ length: 20 }, (_, k) => [9 + k, 35]));
  const cp2 = findSpot(c, 2, 2, 12, 36);
  c.placeStruct(STRUCT.COAL, cp2[0], cp2[1]);
  const pp = findSpot(c, 1, 1, 15, 26);
  c.placeStruct(STRUCT.PARK, pp[0], pp[1]);
  const pol = findSpot(c, 1, 1, 20, 26); c.placeStruct(STRUCT.POLICE, pol[0], pol[1]);
  const hos = findSpot(c, 1, 1, 24, 26); c.placeStruct(STRUCT.HOSPITAL, hos[0], hos[1]);
  const sch = findSpot(c, 1, 1, 28, 26); c.placeStruct(STRUCT.SCHOOL, sch[0], sch[1]);
  for (let i = 0; i < 60; i++) c.tick();
  ok(c.stats.happiness >= 34, `happiness sane with services (${c.stats.happiness})`);
  c.taxRate = 20;
  for (let i = 0; i < 6; i++) c.tick();
  ok(c.stats.happiness < 90, 'crushing taxes hurt happiness');
}

console.log('— milestones —');
{
  const c = new City(11);
  c.stats.pop = 600;
  c.checkMilestones();
  ok(c.milestoneIdx >= 2, `village title earned (${SIM.milestonePops[c.milestoneIdx][1]})`);
}

console.log('— save / load integrity —');
{
  const c = new City(2024, 'Saveville');
  for (let x = 8; x < 40; x++) c.placeStruct(STRUCT.ROAD, x, 30);
  const tiles = [];
  for (let x = 9; x < 30; x++) for (let z = 27; z <= 33; z++) tiles.push([x, z]);
  c.placeZone(ZONE.RES, tiles);
  const cp2 = findSpot(c, 2, 2, 12, 36);
  c.placeStruct(STRUCT.COAL, cp2[0], cp2[1]);
  const st = findSpot(c, 2, 2, 33, 27);
  c.placeStruct(STRUCT.STADIUM, st[0], st[1]);
  var stAnchorExpected = c.grid.idx(st[0], st[1]);
  for (let i = 0; i < 24; i++) c.tick();

  const json = JSON.parse(JSON.stringify(c.toJSON()));
  const c2 = City.fromJSON(json);
  ok(c2.name === 'Saveville', 'name restored');
  ok(c2.money === Math.round(c.money), `money restored (${c2.money} vs ${c.money})`);
  ok(c2.monthIndex === c.monthIndex, 'date restored');
  let same = true;
  for (let i = 0; i < c.grid.n; i++) {
    if (c.grid.struct[i] !== c2.grid.struct[i] || c.grid.level[i] !== c2.grid.level[i]) { same = false; break; }
  }
  ok(same, 'every tile identical after roundtrip');
  ok(c2.grid.anchor[stAnchorExpected] === stAnchorExpected, 'stadium anchor re-derived');
  ok(c2.grid.anchor[c2.grid.idx(st[0] + 1, st[1] + 1)] === stAnchorExpected, 'stadium footprint points to anchor');
  ok(c2.stats.powerCap >= 6000, 'power recomputed on load');
}

// ---------------- tornado disasters ----------------
{
  const c = new City(777, 'Tornadoville');
  for (let x = 8; x < 40; x++) c.placeStruct(STRUCT.ROAD, x, 30);
  const tiles = [];
  for (let x = 9; x < 30; x++) for (let z = 27; z <= 33; z++) tiles.push([x, z]);
  c.placeZone(ZONE.RES, tiles);
  const cp = findSpot(c, 2, 2, 12, 36);
  c.placeStruct(STRUCT.COAL, cp[0], cp[1]);
  for (let i = 0; i < 10; i++) c.tick();
  const devBefore = [...c.grid.level].filter(v => v > 0).length;
  ok(devBefore > 0, 'town exists before tornado');

  let eventFired = false;
  c.on('disaster', (e) => { if (e.type === 'tornado' && e.destroyed > 0) eventFired = true; });
  c.spawnTornado();

  const rubbleAfter = [...c.grid.rubble].filter(v => v > 0).length;
  ok(eventFired, 'disaster event emitted with damage count');
  ok(rubbleAfter > 0, `tornado left rubble behind (${rubbleAfter} tiles)`);
  let anchorBad = 0;
  for (let i = 0; i < c.grid.n; i++) {
    const s = c.grid.struct[i];
    if (!s || s === STRUCT.ROAD || c.grid.anchor[i] !== i) continue;
    if (c.grid.struct[c.grid.anchor[i]] !== s) anchorBad++;
  }
  ok(anchorBad === 0, 'anchors consistent after tornado destruction');

  // disabled flag suppresses natural spawning
  c.disastersEnabled = false;
  let spawnedWhileOff = false;
  const origSpawn = c.spawnTornado.bind(c);
  c.spawnTornado = () => { spawnedWhileOff = true; return origSpawn(); };
  c.monthIndex = 60;
  for (let m = 0; m < 80; m++) c.tick();
  ok(!spawnedWhileOff, 'disabled flag prevents tornado spawns');
}

console.log(`\n${pass} passed, ${fail} failed`);
process.exit(fail ? 1 : 0);
