/**
 * End-to-end smoke for the set-judge.
 *
 * A clean `tsc -b` and a clean `vite build` say nothing about whether the thing
 * works: the two failures this catches — an audio mount that resolves to the
 * wrong file, and an export that writes a shape the release step can't read —
 * are both invisible to the compiler. Assert something POSITIVE at every step.
 *
 *   npx vite --port 5199 --strictPort &
 *   node scripts/judge-smoke.mjs
 */
import { chromium } from 'playwright'

const URL = process.env.JUDGE_URL ?? 'http://localhost:5199/judge.html'
const fail = (m) => { console.error(`✗ ${m}`); process.exitCode = 1 }
const ok = (m) => console.log(`✓ ${m}`)

// Use the system Chromium rather than Playwright's download. The box already
// ships one, and `npx playwright install` pulls ~200 MB per version bump for a
// browser we would only ever point at localhost.
import { existsSync } from 'node:fs'
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(URL, { waitUntil: 'networkidle' })
await p.waitForSelector('li', { timeout: 15000 })

// 1 — the set loaded, and every track in it is present
const rows = await p.$$('ul > li')
rows.length === 15 ? ok(`15 track rows`) : fail(`expected 15 rows, got ${rows.length}`)

// 2 — the focused row shows real evidence, not an empty shell.
// `li canvas` pierces wavesurfer's shadow root (Playwright selectors do).
await p.waitForSelector('li canvas', { timeout: 20000 })
const lanes = await p.$$eval('li [title^="orbit-"]', (n) => n.length)
lanes > 0 ? ok(`orbit rail rendered (${lanes} lanes)`) : fail('no orbit lanes in the focused row')

// 3 — audio actually PLAYS.
//
// Two wrong versions of this check before it worked, both of which PASSED or
// failed for reasons unrelated to the app:
//   a) fetching `document.querySelector('audio').src` and asserting a 206 —
//      there was no <audio> in the light DOM, so it fetched the empty string,
//      got the PAGE back, and read a 206 from somewhere else entirely;
//   b) counting `document.querySelectorAll('li canvas')` — wavesurfer renders
//      into a SHADOW ROOT, which Playwright's own selectors pierce but
//      `querySelector` inside page.evaluate does not.
// So: drive it through Playwright's locators, and assert the playhead moves.
// The media element is detached (`new Audio()`), so it is not queryable at all —
// which is fine, because the honest assertion is the one PLN can see: the
// transport's own clock has to advance, and the button has to flip to Pause.
const clock = p.locator('li [tabindex="0"] span').first()
await p.locator('li [aria-label="Play"]').first().click()
await p.waitForTimeout(2500)
const shown = (await clock.textContent())?.trim() ?? ''
const flipped = await p.locator('li [aria-label="Pause"]').first().isVisible().catch(() => false)
const secs = /^(\d+):(\d+(?:\.\d+)?)$/.exec(shown)
const elapsed = secs ? Number(secs[1]) * 60 + Number(secs[2]) : 0
elapsed > 0.2 && flipped
  ? ok(`audio plays — transport reads ${shown}, button flipped to Pause`)
  : fail(`playback did not advance: clock="${shown}" pauseButton=${flipped}`)
await p.locator('li [aria-label="Pause"]').first().click().catch(() => {})

// 4 — the keyboard pass: g on #1, j then n on #2
await p.keyboard.press('g')
await p.keyboard.press('j')
await p.keyboard.press('n')
await p.waitForTimeout(300)
const tally = await p.$$eval('header + div span', (n) => n.map((x) => x.textContent?.trim()))
tally.some((x) => /\b2\/\d+ judged/.test(x ?? ''))
  ? ok('2 judged after g / j / n')
  : fail(`tally reads ${JSON.stringify(tally)}`)

// 5 — the artifact: a DecisionSet the release step can actually consume
const [dl] = await Promise.all([
  p.waitForEvent('download', { timeout: 10000 }),
  p.click('button:has-text("decisions.json")'),
])
const stream = await dl.createReadStream()
let raw = ''
for await (const c of stream) raw += c
let d
try { d = JSON.parse(raw) } catch (e) { fail(`export is not valid JSON: ${e}`) }
if (d) {
  const checks = [
    [d.gig === 'opal-festival-2026', `gig is ${d.gig}`],
    [Array.isArray(d.decisions) && d.decisions.length === 15, `${d.decisions?.length} decisions`],
    [d.decisions?.[0]?.verdict === 'go', `#1 verdict is ${d.decisions?.[0]?.verdict}`],
    [d.decisions?.[1]?.verdict === 'nogo', `#2 verdict is ${d.decisions?.[1]?.verdict}`],
    [d.decisions?.every((x) => x.trackId && x.title && x.n), 'every decision carries id/title/n'],
    [Boolean(d.decidedAt), 'decidedAt stamped'],
  ]
  for (const [pass, msg] of checks) (pass ? ok : fail)(msg)
}

// 6 — no errors on the console; a UI that throws while looking fine is the worst kind
bad.length === 0 ? ok('no console errors') : fail(`console errors: ${bad.slice(0, 3).join(' | ')}`)

await p.screenshot({ path: '/tmp/judge-set.png', fullPage: true })
await b.close()
console.log(process.exitCode ? '\nSMOKE FAILED' : '\nsmoke passed — /tmp/judge-set.png')
