/**
 * 30-year soak test: builds a realistic city, runs the simulation for
 * 360 months and asserts long-run stability, progression, power pressure
 * recovery, save/load mid-run integrity and per-tick performance.
 */
import { City } from '../src/game/city.js';
import { STRUCT, ZONE } from '../src/config.js';

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

const c = new City(90210, 'Soakville');
c.disastersEnabled = false; // economy focus here; tornado behaviour is covered by engine.mjs
const g = c.grid;
c.money = 60000;

// --- infrastructure: avenue + branches ---
for (let x = 12; x < 52; x++) c.placeStruct(STRUCT.ROAD, x, 32);
for (let z = 16; z < 48; z++) c.placeStruct(STRUCT.ROAD, 32, z);
for (let z = 20; z < 30; z++) { c.placeStruct(STRUCT.ROAD, 18, z); c.placeStruct(STRUCT.ROAD, 46, z); }
for (let z = 34; z < 44; z++) { c.placeStruct(STRUCT.ROAD, 18, z); c.placeStruct(STRUCT.ROAD, 46, z); }

// --- districts ---
const rect = (x0, z0, x1, z1, zid) => {
  const t = [];
  for (let z = z0; z <= z1; z++) for (let x = x0; x <= x1; x++) t.push([x, z]);
  c.placeZone(zid, t);
};
// road-hugging bands (mayors zone what roads can serve)
rect(28, 24, 36, 31, ZONE.RES);
rect(13, 28, 17, 36, ZONE.RES); rect(19, 28, 23, 36, ZONE.RES);
rect(47, 28, 51, 36, ZONE.RES); rect(41, 28, 45, 36, ZONE.RES);
rect(33, 20, 51, 24, ZONE.COM);
rect(14, 38, 22, 43, ZONE.COM); rect(42, 38, 51, 43, ZONE.IND);
rect(29, 34, 35, 43, ZONE.IND);
rect(24, 16, 28, 22, ZONE.RES); rect(36, 16, 40, 22, ZONE.RES);
rect(13, 38, 17, 46, ZONE.RES);

const freeSpot = (w, h) => {
  for (let r = 2; r < 40; r++) {
    for (let cz = 32 - r; cz <= 32 + r; cz += 2) for (let cx = 32 - r; cx <= 32 + r; cx += 2) {
      let okSpot = true;
      for (let dz = -1; dz <= h && okSpot; dz++) for (let dx = -1; dx <= w; dx++) {
        const x = cx + dx, z = cz + dz;
        if (!g.inB(x, z)) { okSpot = false; break; }
        const i = g.idx(x, z);
        if (g.terrain[i] !== 0 || g.struct[i] || g.zone[i]) { okSpot = false; break; }
      }
      if (okSpot) return [cx, cz];
    }
  }
  return null;
};

const put = (sid, w = 1, h = 1) => { const p = freeSpot(w, h); return p ? (c.placeStruct(sid, p[0], p[1]).ok ? p : null) : null; };

put(STRUCT.WIND);          // humble wind turbine start
// services right inside the neighborhoods (legal since buildings claim empty zones)
c.placeStruct(STRUCT.POLICE, 26, 32);   // just off avenue
c.placeStruct(STRUCT.FIRE, 38, 32);
c.placeStruct(STRUCT.HOSPITAL, 24, 33);
c.placeStruct(STRUCT.SCHOOL, 40, 33);
const parkP = [[30, 28], [34, 28], [22, 36], [44, 36], [31, 25]];
for (const [px, pz] of parkP) c.placeStruct(STRUCT.PARK, px, pz);
c.placeStruct(STRUCT.PLAZA, 33, 30);

console.log('— 360-month soak —');
let maxTick = 0, tickSum = 0;
let firesSeen = 0, plantsBuilt = 0;
let minMoney = Infinity, minHappy = 100;

for (let m = 1; m <= 480; m++) {
  const t0 = performance.now();
  c.tick();
  const dt = performance.now() - t0;
  tickSum += dt; if (dt > maxTick) maxTick = dt;

  firesSeen = Math.max(firesSeen, [...g.burning].filter(v => v > 0).length > 0 ? 1 : 0) || firesSeen;
  minMoney = Math.min(minMoney, c.money);
  minHappy = Math.min(minHappy, c.stats.happiness);

  // mayor manages the grid: builds ahead of demand, reacts to strain
  const strained = c.stats.powerUse > c.stats.powerCap * 0.8 || c.stats.brownouts > 0;
  if (strained && m % 2 === 0) {
    const before = [...g.struct].filter(v => v === STRUCT.WIND || v === STRUCT.COAL).length;
    if (put(STRUCT.WIND) || put(STRUCT.COAL, 2, 2)) {}
    const after = [...g.struct].filter(v => v === STRUCT.WIND || v === STRUCT.COAL).length;
    if (after > before) plantsBuilt++;
  }

  // mid-run save/load integrity
  if (m === 180) {
    const snap = JSON.parse(JSON.stringify(c.toJSON()));
    const before = { pop: c.stats.pop, money: Math.round(c.money), month: c.monthIndex };
    const c2 = City.fromJSON(snap);
    ok(c2.stats.pop === before.pop && Math.round(c2.money) === before.money && c2.monthIndex === before.month,
      `mid-run reload preserves state (pop ${before.pop}, ${fmt(before.money)}, m${before.month})`);
    // continue on the reloaded instance to prove it stays healthy
    c.money = c2.money; // keep going with same wallet semantics below
  }
}

function fmt(n) { return '$' + Math.round(n).toLocaleString('en-US'); }

const levels = [1, 2, 3].map(L => [...g.level].filter(v => v === L).length);
console.log(`\nfinal: pop=${c.stats.pop} jobs=${c.stats.jobs} dev=${levels.reduce((a,b)=>a+b,0)} L1/2/3=${levels.join('/')} happy=${c.stats.happiness}`);
console.log(`money=${fmt(c.money)} (min ${fmt(minMoney)}) | avgTick=${(tickSum/360).toFixed(2)}ms maxTick=${maxTick.toFixed(1)}ms`);
console.log(`plantsBuiltDuringRun=${plantsBuilt} | fireMonths=${firesSeen}`);

ok(c.stats.pop > 800, `city grew past 800 citizens (${c.stats.pop})`);
ok(levels[2] >= 2, `land value drove level-3 towers (${levels[2]})`);
ok(levels[1] >= 0, 'density ladder present');
ok(minMoney > -20000, `treasury never spiralled (min ${fmt(minMoney)})`);
ok(c.money > 0, `economy self-sustaining at year 30 (${fmt(c.money)})`);
ok(minHappy > 15, `happiness never collapsed (min ${minHappy})`);
ok(plantsBuilt >= 3, `grid expanded proactively as demand grew (${plantsBuilt} plants added)`);
ok(maxTick < 150, `worst monthly tick fast enough (${maxTick.toFixed(1)}ms)`);
ok(tickSum / 360 < 25, `average monthly tick cheap (${(tickSum / 360).toFixed(2)}ms)`);

const jsonSize = JSON.stringify(c.toJSON()).length;
ok(jsonSize < 2_000_000, `save size sane (${(jsonSize / 1024).toFixed(0)} KB)`);

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