/**
 * Unit checks for the pure paint math. No test runner / no deps — run directly:
 *   node --experimental-strip-types src/paint/coverage-math.test.ts
 * (Node ≥22; on Node ≥23 the flag is unnecessary.) Uses only console + throw so
 * it also type-checks cleanly under the project's DOM tsconfig and is never
 * bundled (no demo entry imports it).
 */
import type { CoverageReport, PaintColor } from '../contracts'
import {
  ACTIVATE,
  SUPER,
  MEGA_SIZE,
  MINI_SIZE,
  MEGA_MASS,
  MINI_MASS,
  MASS_FLOOR,
  buffStrength,
  computeModifiers,
  countBuckets,
  coverageReportFromCounts,
  colorIndex,
} from './coverage-math'

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-9): void {
  ok(Math.abs(a - b) <= eps, `${msg} (got ${a}, want ${b})`)
}

// helper: build a CoverageReport from a partial byColor map
function cov(part: Partial<Record<PaintColor, number>>): CoverageReport {
  const byColor = {
    red: 0, orange: 0, yellow: 0, green: 0, blue: 0, purple: 0, pink: 0,
    ...part,
  }
  const total = Object.values(byColor).reduce((a, b) => a + b, 0)
  return { total, byColor }
}

// ---- colorIndex ----------------------------------------------------------
ok(colorIndex('red') === 1, 'red index is 1')
ok(colorIndex('pink') === 7, 'pink index is 7')

// ---- countBuckets --------------------------------------------------------
{
  const mask = new Uint8Array(100) // all base (0)
  for (let i = 0; i < 30; i++) mask[i] = 1 // red
  for (let i = 30; i < 50; i++) mask[i] = 4 // green
  const c = countBuckets(mask, 7)
  ok(c[0] === 50, 'base count 50')
  ok(c[1] === 30, 'red count 30')
  ok(c[4] === 20, 'green count 20')
  ok(c[7] === 0, 'pink count 0')
  ok(c.length === 8, 'counts length 8 (base + 7)')
}

// ---- coverageReportFromCounts --------------------------------------------
{
  // counts: [base, red, orange, yellow, green, blue, purple, pink]
  const counts = [50, 30, 0, 0, 20, 0, 0, 0]
  const r = coverageReportFromCounts(counts, 100)
  near(r.byColor.red, 0.3, 'red fraction')
  near(r.byColor.green, 0.2, 'green fraction')
  near(r.total, 0.5, 'total painted fraction')
  // empty texture → all zeros, no NaN
  const z = coverageReportFromCounts([0, 0, 0, 0, 0, 0, 0, 0], 0)
  near(z.total, 0, 'empty total 0')
  near(z.byColor.blue, 0, 'empty blue 0')
}

// ---- buffStrength --------------------------------------------------------
near(buffStrength(0.0), 0, 'strength below threshold is 0')
near(buffStrength(0.199), 0, 'strength just below 20% is 0')
near(buffStrength(ACTIVATE), 0.15, 'strength at 20% is the floor')
near(buffStrength(SUPER), 1, 'strength at 70% is 1')
near(buffStrength(0.9), 1, 'strength above 70% pinned at 1')
near(buffStrength(0.45), 0.15 + 0.85 * 0.5, 'strength ramps linearly at midpoint')

// ---- computeModifiers ----------------------------------------------------
{
  const clean = computeModifiers(cov({}))
  near(clean.speedMul, 1, 'clean speedMul 1')
  near(clean.grip, 0, 'clean grip 0')
  ok(clean.waterproof === false, 'clean not waterproof')
  near(clean.massMul, 1, 'clean massMul 1')
  near(clean.glow, 0, 'clean no glow')
  near(clean.size, 1, 'clean size 1 (no grow/shrink in MVP)')
}
{
  // RED just under activation: no buff yet
  const m = computeModifiers(cov({ red: 0.19 }))
  near(m.speedMul, 1, 'red 19% → no speed buff')
}
{
  // RED at 45% → speedMul = 1 + 0.6 * buffStrength(0.45)
  const s = 0.15 + 0.85 * 0.5
  const m = computeModifiers(cov({ red: 0.45 }))
  near(m.speedMul, 1 + 0.6 * s, 'red 45% speedMul')
  near(m.massMul, 1 + 0.8 * 0.45, 'massMul tracks total 45%')
  near(m.glow, 0, 'red 45% not super')
}
{
  // GREEN super (70%) → grip maxed + glow
  const m = computeModifiers(cov({ green: 0.7 }))
  near(m.grip, 1, 'green 70% grip maxed')
  near(m.glow, 1, 'green 70% is super (glow)')
}
{
  // BLUE ≥20% → waterproof
  const m = computeModifiers(cov({ blue: 0.25 }))
  ok(m.waterproof === true, 'blue 25% waterproof')
}
{
  // massMul clamps at 1.8 when fully painted (no purple/pink → base only)
  const m = computeModifiers(cov({ red: 0.5, blue: 0.5 }))
  near(m.massMul, 1.8, 'full coverage massMul 1.8')
}

// ---- purple MEGA (grow + heavy) ------------------------------------------
{
  // below 20% purple → no size change, no extra mass
  const m = computeModifiers(cov({ purple: 0.19 }))
  near(m.size, 1, 'purple 19% → size unchanged')
  near(m.massMul, 1 + 0.8 * 0.19, 'purple 19% → base mass only')
}
{
  // purple super (70%) → size maxed to 1.45, MEGA mass added, glow
  const m = computeModifiers(cov({ purple: 0.7 }))
  near(m.size, 1 + MEGA_SIZE, 'purple 70% → size 1.45')
  near(m.massMul, 1 + 0.8 * 0.7 + MEGA_MASS, 'purple 70% → base + full MEGA mass')
  near(m.glow, 1, 'purple 70% is super (glow)')
  ok(m.massMul > 1.25, 'MEGA is heavy enough to trip a *1.25 plate')
}
{
  // purple mid (45%) → partial grow via buffStrength
  const s = 0.15 + 0.85 * 0.5
  const m = computeModifiers(cov({ purple: 0.45 }))
  near(m.size, 1 + MEGA_SIZE * s, 'purple 45% → partial grow')
}

// ---- pink MINI (shrink + light) ------------------------------------------
{
  // below 20% pink → no size change
  const m = computeModifiers(cov({ pink: 0.19 }))
  near(m.size, 1, 'pink 19% → size unchanged')
}
{
  // pink super (70%) → size shrunk to 0.62, lighter than clean
  const m = computeModifiers(cov({ pink: 0.7 }))
  near(m.size, 1 - MINI_SIZE, 'pink 70% → size 0.62')
  ok(m.size < 1, 'pink shrinks the blob')
  near(m.massMul, 1 + 0.8 * 0.7 - MINI_MASS, 'pink 70% → base mass minus full MINI reduction')
  ok(m.massMul < 1, 'MINI is lighter than a clean blob at high %')
}
{
  // pink can never drive mass to/below zero (MASS_FLOOR guards it)
  const m = computeModifiers(cov({ pink: 1 }))
  ok(m.massMul >= MASS_FLOOR, 'pink mass floored at MASS_FLOOR')
}

// ---- purple vs pink fight -------------------------------------------------
{
  // equal purple + pink strength cancel → net normal size
  const m = computeModifiers(cov({ purple: 0.5, pink: 0.5 }))
  near(m.size, 1, 'equal purple+pink → size nets to 1 (they fight)')
}
{
  // more purple than pink → still grows, but less than pure purple
  const strong = computeModifiers(cov({ purple: 0.6 }))
  const fought = computeModifiers(cov({ purple: 0.6, pink: 0.3 }))
  ok(fought.size > 1, 'purple-dominant mix still grows')
  ok(fought.size < strong.size, 'pink drags the grow down')
}

console.log(`coverage-math.test: ${passed} assertions passed ✓`)
