/**
 * Unit checks for the manifest schema. No test runner / no deps — run directly:
 *   node --experimental-strip-types src/assets/manifest.test.ts
 * (Node ≥22; on Node ≥23 the flag is unnecessary.) Same pattern as
 * paint/coverage-math.test.ts: console + throw only, never bundled.
 *
 * The manifest is hand-edited by a non-programmer, so the property under test
 * is "one bad line never costs you the whole pack".
 */
import { validateManifest, mergeManifests, type Manifest } from './manifest'
import { SLOT_IDS, isSlotId, SLOT_LABELS } from './slots'

let passed = 0
function ok(cond: boolean, msg: string): void {
  if (!cond) throw new Error('FAIL: ' + msg)
  passed++
}

// ---- garbage in, empty manifest out (never throws) --------------------------
for (const junk of [null, undefined, 42, 'nope', [], true]) {
  const r = validateManifest(junk)
  ok(Object.keys(r.manifest).length === 0, `junk input ${JSON.stringify(junk)} -> {}`)
}

// ---- the shipped empty manifest ---------------------------------------------
ok(Object.keys(validateManifest(JSON.parse('{}')).manifest).length === 0, 'empty {} -> {}')
ok(validateManifest({}).problems.length === 0, 'empty {} has no problems')

// ---- a good entry survives intact -------------------------------------------
{
  const r = validateManifest({
    'machine.boot': {
      url: 'assets/live/boot.glb',
      offset: [0, 1, 0],
      rotationDeg: [0, 90, 0],
      scale: 2,
      idleClip: 'idle',
    },
  })
  const e = r.manifest['machine.boot']
  ok(r.problems.length === 0, 'clean entry produces no problems')
  ok(e?.url === 'assets/live/boot.glb', 'url kept')
  ok(e?.offset?.[1] === 1, 'offset kept')
  ok(e?.rotationDeg?.[1] === 90, 'rotationDeg kept')
  ok(e?.scale === 2, 'scale kept')
  ok(e?.idleClip === 'idle', 'idleClip kept')
}

// ---- one bad line does not cost you the pack --------------------------------
{
  const r = validateManifest({
    'machine.boot': { url: 'good.glb' },
    'machine.bucket': { url: 'also-good.glb', offset: 'sideways' },
    'not.a.slot': { url: 'x.glb' },
    'machine.arch': { nourl: true },
    'machine.fan': 'just a string',
  })
  ok(r.manifest['machine.boot'] !== undefined, 'good entry survives a bad neighbour')
  ok(r.manifest['machine.bucket'] !== undefined, 'entry with a bad field still loads')
  ok(r.manifest['machine.bucket']?.offset === undefined, 'bad offset dropped, not kept')
  ok(r.manifest['machine.arch'] === undefined, 'entry without a url is dropped')
  ok((r.manifest as Record<string, unknown>)['not.a.slot'] === undefined, 'unknown slot dropped')
  ok(r.problems.length === 4, `4 problems reported (got ${r.problems.length})`)
  ok(r.problems.some((p) => p.slot === 'not.a.slot'), 'unknown slot is named in problems')
}

// ---- scale accepts a number or a triple, rejects zero -----------------------
ok(validateManifest({ 'machine.fan': { url: 'a', scale: [1, 2, 3] } }).manifest['machine.fan']?.scale !== undefined, 'triple scale ok')
ok(validateManifest({ 'machine.fan': { url: 'a', scale: 0 } }).manifest['machine.fan']?.scale === undefined, 'zero scale rejected')
ok(validateManifest({ 'machine.fan': { url: 'a', offset: [1, 2, NaN] } }).manifest['machine.fan']?.offset === undefined, 'NaN offset rejected')

// ---- merge: later source wins whole entries ---------------------------------
{
  const shipped: Manifest = {
    'machine.boot': { url: 'shipped.glb', scale: 3 },
    'machine.fan': { url: 'fan.glb' },
  }
  const local: Manifest = { 'machine.boot': { url: 'local.glb' } }
  const m = mergeManifests(shipped, local)
  ok(m['machine.boot']?.url === 'local.glb', 'local override wins')
  ok(m['machine.boot']?.scale === undefined, 'override replaces the whole entry, not per-field')
  ok(m['machine.fan']?.url === 'fan.glb', 'untouched shipped entry survives the merge')
  ok(shipped['machine.boot']?.url === 'shipped.glb', 'merge does not mutate its inputs')
}

// ---- slot table is coherent --------------------------------------------------
ok(new Set(SLOT_IDS).size === SLOT_IDS.length, 'no duplicate slot ids')
ok(SLOT_IDS.every((s) => typeof SLOT_LABELS[s] === 'string'), 'every slot has a plain-language label')
ok(SLOT_IDS.every(isSlotId), 'every slot id passes isSlotId')
ok(!isSlotId('blob.bodyy') && !isSlotId(''), 'isSlotId rejects near-misses')

console.log(`manifest.test.ts: ${passed} checks passed`)
