/**
 * End-to-end smoke for the boundary lab.
 *
 * Same discipline as judge-smoke: a green `tsc -b` proved nothing there — the
 * transport was dead because `peaks` + `url` made wavesurfer think the clip was
 * pre-decoded. So every assertion here is POSITIVE and phrased as what a human
 * would see: markers drawn, the clock moving, the call changing.
 *
 * The two failures specific to THIS tool would both pass a compiler:
 *   - the /audio/bounds mount resolving to the wrong directory (a clip plays,
 *     but it is the wrong 90 seconds — inaudible to a test that only checks
 *     "audio played"), so we assert the clip's DURATION matches the ±pad window;
 *   - clip-time vs master-time confusion, which would silently shift every
 *     candidate — so we assert a marker click yields a call in MASTER seconds
 *     inside the boundary's own clip range.
 *
 *   npx vite --port 5199 --strictPort &
 *   node scripts/bounds-smoke.mjs
 */
import { chromium } from 'playwright'
import { existsSync, readFileSync } from 'node:fs'

// NB: do not name this `URL` — it shadows the global used just below
const PAGE = process.env.BOUNDS_URL ?? 'http://localhost:5199/bounds.html'
const DATA = new URL('../public/bounds-opal-festival-2026.json', import.meta.url)
const fail = (m) => { console.error(`✗ ${m}`); process.exitCode = 1 }
const ok = (m) => console.log(`✓ ${m}`)

const doc = JSON.parse(readFileSync(DATA, 'utf8'))

const SYSTEM_CHROME = ['/usr/bin/chromium', '/usr/bin/chromium-browser', '/usr/bin/google-chrome-stable']
  .find((p) => existsSync(p))
const b = await chromium.launch(SYSTEM_CHROME ? { executablePath: SYSTEM_CHROME } : {})
const p = await b.newPage({ viewport: { width: 1280, height: 1000 } })

const bad = []
p.on('console', (m) => m.type() === 'error' && bad.push(m.text()))
p.on('pageerror', (e) => bad.push(String(e)))

await p.goto(PAGE, { waitUntil: 'networkidle' })

// 1 — the boundaries loaded and every one is navigable
await p.waitForSelector('nav button', { timeout: 15000 })
const navs = await p.$$eval('nav button', (n) => n.map((x) => x.textContent))
navs.length === doc.boundaries.length
  ? ok(`${navs.length} boundaries in the nav (${navs.join(' ')})`)
  : fail(`expected ${doc.boundaries.length} nav buttons, got ${navs.length}`)

// 2 — the waveform painted (canvas lives in wavesurfer's shadow root;
//     Playwright selectors pierce it, document.querySelector does NOT)
await p.waitForSelector('canvas', { timeout: 20000 })
ok('waveform canvas rendered')

// 3 — the clip is the RIGHT clip. Two independent checks, because either alone
//     lies: the mount could 404 (caught by the fetch) or fall through to the
//     wrong directory and serve *a* file (caught by the displayed duration).
// Pick the subject by CONTENT, not by position: as boundaries get settled their
// candidate buttons disappear, and a test pinned to boundaries[0] starts failing
// on data changes rather than on regressions. Prefer one that still has a
// machine candidate; fall back to the nominal marker every boundary carries.
const firstIdx = Math.max(0, doc.boundaries.findIndex((x) => x.marks.length > 0))
const first = doc.boundaries[firstIdx]
const probe = first.marks[0] ?? { key: 'nominal', label: 'nominal', t: first.nominal }
await p.$$eval('nav button', (n, i) => n[i].click(), firstIdx)
await p.waitForTimeout(600)
const head = await p.request.get(new global.URL(first.url, PAGE).href)
const bytes = Number(head.headers()['content-length'] ?? 0)
head.ok() && bytes > 500_000
  ? ok(`clip served: ${head.status()} ${(bytes / 1e6).toFixed(1)} MB`)
  : fail(`clip fetch ${head.status()} ${bytes} B — /audio/bounds mount wrong?`)

const want = first.clipEnd - first.clipStart
const shownDur = await p.textContent('.tnum span.w-14, span.w-14')
const mm = /(\d+):(\d+)/.exec(shownDur ?? '')
const shownS = mm ? Number(mm[1]) * 60 + Number(mm[2]) : -1
Math.abs(shownS - want) <= 1
  ? ok(`transport shows ${shownDur?.trim()} = the ±${doc.pad}s window`)
  : fail(`transport shows ${shownDur} (${shownS}s), expected ${want}s`)

// 4 — markers are drawn, one per candidate plus the nominal
const wantMarks = first.marks.length + 1  // candidates/ear + the nominal
const marks = await p.$$eval('[part="region-content"], .wavesurfer-region', (n) => n.length)
marks >= wantMarks
  ? ok(`${marks} markers drawn (>= ${wantMarks} expected)`)
  : fail(`expected >= ${wantMarks} markers, saw ${marks}`)

// 5 — clicking a candidate button records a call IN MASTER TIME, inside the clip
await p.getByRole('button', { name: new RegExp('^' + probe.label + ' · ') }).first().click()
await p.waitForTimeout(400)
const callTxt = await p.textContent('section:has(textarea) .tnum')
const secs = Number(/\(([\d.]+)s/.exec(callTxt ?? '')?.[1])
Number.isFinite(secs) && secs >= first.clipStart && secs <= first.clipEnd
  ? ok(`call recorded at ${secs}s master (inside [${first.clipStart}, ${first.clipEnd}])`)
  : fail(`call "${callTxt}" is not a master time inside this clip`)
Math.abs(secs - probe.t) < 0.01
  ? ok(`call equals the ${probe.label} marker exactly — no clip/master drift`)
  : fail(`call ${secs} != ${probe.label} ${probe.t}`)

// 6 — audio actually PLAYS. The media element is owned by wavesurfer and never
//     enters the DOM, so querySelector('audio') is null by construction — that
//     probe would "pass" forever on a dead transport. Assert what a HUMAN sees:
//     the transport clock advances and the button offers Pause.
await p.waitForTimeout(1500)
const clock = async () => {
  const txt = await p.textContent('[data-testid="transport-clock"]')
  const m = /(\d+):(\d+)/.exec(txt ?? '')
  return m ? Number(m[1]) * 60 + Number(m[2]) : -1
}
const t1 = await clock()
await p.waitForTimeout(1600)
const t2 = await clock()
t2 > t1
  ? ok(`clock advanced ${t1}s -> ${t2}s (playing)`)
  : fail(`clock stuck at ${t1}s — transport never started`)
const pauseVisible = await p.getByRole('button', { name: 'Pause' }).count()
pauseVisible > 0 ? ok('button offers Pause') : fail('button never flipped to Pause')

// 7 — a no-candidate boundary tells the user it is theirs, rather than showing nothing
const blindIdx = doc.boundaries.findIndex((x) => x.noCandidate)
if (blindIdx >= 0) {
  await p.$$eval('nav button', (n, i) => n[i].click(), blindIdx)
  await p.waitForTimeout(500)
  const warn = (await p.textContent('body')) ?? ''
  warn.includes('No machine candidate exists')
    ? ok(`#${doc.boundaries[blindIdx].track} shows the ear-only notice`)
    : fail('no-candidate boundary renders without its notice')
}

// 8 — no console errors anywhere in that flow
bad.length === 0 ? ok('no console errors') : fail(`console errors: ${bad.slice(0, 3).join(' | ')}`)

await b.close()
console.log(process.exitCode ? '\nSMOKE FAILED' : '\nsmoke passed')
