/**
 * Headless checks for the asset runtime's geometry/material logic. Run:
 *   node --import ./scripts/ts-resolve.mjs --experimental-strip-types src/assets/registry.test.ts
 * No renderer is created, so this runs without WebGL — three's BufferGeometry
 * and material classes are pure data.
 *
 * The load-bearing test here is EMPTY-MANIFEST PARITY: with no manifest, the
 * blob hook must hand back the caller's own mesh object, not a rebuild of it.
 */
import * as THREE from 'three'
import type { World } from '../contracts'
import { AssetRegistry, setAssets, paintableInfo, normalizeToRadius, makeFitNode, ensureStandardMaterials } from './registry'
import { resolveBlobBody } from './blobBody'

let passed = 0
function ok(cond: boolean, msg: string): void {
  if (!cond) throw new Error('FAIL: ' + msg)
  passed++
}
function near(a: number, b: number, msg: string, eps = 1e-6): void {
  ok(Math.abs(a - b) <= eps, `${msg} (got ${a}, want ${b})`)
}

// ---- EMPTY-MANIFEST PARITY --------------------------------------------------
{
  setAssets(new AssetRegistry({}))
  const systems: unknown[] = []
  const fakeWorld = { addSystem: (s: unknown) => systems.push(s) } as unknown as World

  const built = new THREE.Mesh(
    new THREE.SphereGeometry(0.5, 48, 36),
    new THREE.MeshStandardMaterial({ color: '#F5F5F7' }),
  )
  const out = resolveBlobBody(fakeWorld, 0.5, () => built)
  ok(out === built, 'empty manifest returns the caller\'s own mesh object (identity, not a rebuild)')
  ok(out.geometry === built.geometry, 'geometry untouched')
  ok(out.material === built.material, 'material untouched')
  ok(systems.length === 0, 'no system registered when no asset is in play')

  // and the slot helpers are pass-throughs too
  const fb = new THREE.Object3D()
  ok(new AssetRegistry({}).slotObject('machine.boot', () => fb) === fb,
    'slotObject with an empty manifest returns the fallback itself (no wrapper Group)')

  const host = new THREE.Object3D()
  new AssetRegistry({}).attachSlot('machine.boot', host)
  ok(host.children.length === 0, 'attachSlot with an empty manifest adds nothing')
}

// ---- paintableInfo ----------------------------------------------------------
{
  const good = new THREE.Mesh(new THREE.SphereGeometry(0.5, 24, 18), new THREE.MeshStandardMaterial())
  const info = paintableInfo(good)
  ok(info.ok, 'a plain UV sphere is paintable')
  // three offsets pole UVs slightly outside 0..1; the stock blob must pass.
  ok(info.uvOk, 'the stock blob sphere passes the UV test despite its pole offset')
  ok(info.materialCount === 1, 'one material')
  ok(info.meshCount === 1, 'one mesh')
  ok(info.triCount > 0, `tri count reported (${info.triCount})`)
  ok(info.problems.length === 0, 'no problems reported')
}
{
  const m = new THREE.Mesh(new THREE.SphereGeometry(0.5, 8, 6), [
    new THREE.MeshStandardMaterial(), new THREE.MeshStandardMaterial(),
  ])
  const info = paintableInfo(m)
  ok(!info.ok, 'a two-material mesh is rejected (paint would be silently invisible)')
  ok(info.materialCount === 2, 'material count reported')
  ok(info.problems.some((p) => p.includes('materials')), 'the reason names the materials')
}
{
  const geo = new THREE.BufferGeometry()
  geo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(9), 3))
  const info = paintableInfo(new THREE.Mesh(geo, new THREE.MeshStandardMaterial()))
  ok(!info.ok && !info.uvOk, 'a mesh with no UVs is rejected')
  ok(info.problems.some((p) => p.includes('UV')), 'the reason names UVs')
}
{
  const geo = new THREE.PlaneGeometry(1, 1)
  const uv = geo.getAttribute('uv') as THREE.BufferAttribute
  ;(uv.array as Float32Array)[0] = 3.5 // an atlas/tiled layout
  const info = paintableInfo(new THREE.Mesh(geo, new THREE.MeshStandardMaterial()))
  ok(!info.uvOk, 'UVs outside 0..1 are rejected')
}
{
  const info = paintableInfo(new THREE.Group())
  ok(!info.ok && info.meshCount === 0, 'an empty file is rejected without throwing')
}

// ---- normalizeToRadius ------------------------------------------------------
{
  // A model authored 7x too big with its origin at the feet.
  const geo = new THREE.SphereGeometry(3.5, 16, 12)
  geo.translate(0, 3.5, 0)
  normalizeToRadius(geo, 0.5)
  geo.computeBoundingSphere()
  const bs = geo.boundingSphere!
  near(bs.radius, 0.5, 'normalised to the collider radius')
  near(bs.center.length(), 0, 'recentred on the origin', 1e-5)
}
{
  const degenerate = new THREE.BufferGeometry()
  degenerate.setAttribute('position', new THREE.BufferAttribute(new Float32Array(3), 3))
  normalizeToRadius(degenerate, 0.5) // must not divide by zero / throw
  ok(true, 'a zero-size geometry does not crash the normaliser')
}

// ---- makeFitNode ------------------------------------------------------------
{
  const fit = makeFitNode({ url: 'x', offset: [1, 2, 3], rotationDeg: [0, 90, 0], scale: 2 })
  ok(fit.position.x === 1 && fit.position.z === 3, 'offset applied')
  near(fit.rotation.y, Math.PI / 2, 'degrees converted to radians')
  ok(fit.scale.x === 2 && fit.scale.y === 2, 'uniform scale applied')
  const fit2 = makeFitNode({ url: 'x', scale: [1, 2, 3] }, new THREE.Vector3(2, 2, 2))
  ok(fit2.scale.x === 2 && fit2.scale.y === 4 && fit2.scale.z === 6, 'per-axis scale times the caller fit')
  const id = makeFitNode({ url: 'x' })
  ok(id.position.length() === 0 && id.scale.x === 1, 'an entry with no transform is identity')
}

// ---- ensureStandardMaterials ------------------------------------------------
{
  // telegraph.ts only flashes MeshStandardMaterial; anything else silently
  // breaks the mandatory danger telegraph, so imports are converted.
  const m = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshBasicMaterial({ color: '#ff0000' }))
  const n = ensureStandardMaterials(m)
  ok(n === 1, 'one material converted')
  const mat = m.material as unknown as THREE.MeshStandardMaterial
  ok(mat.isMeshStandardMaterial === true, 'basic material became standard')
  ok(mat.color.getHexString() === 'ff0000', 'colour preserved through the conversion')
  ok(ensureStandardMaterials(m) === 0, 'an already-standard material is left alone')
}

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