#!/usr/bin/env node
// Frontend render smoke check (added v0.1.0:82). Dev/build-time only — NOT shipped in
// the image. Catches the v78/v79 blank-screen class that curl/health checks miss:
//
//   Stage 1 (deterministic, no DOM): run the *shipped* Babel over the app's inline JSX
//     and assert the output is a CLASSIC (non-module) script. Babel 8's preset-react
//     defaults to the automatic JSX runtime, which emits `import {jsx} from
//     "react/jsx-runtime"` — illegal in this inline <script> and blanks the whole app
//     (the v79 incident). This stage fails loudly if a re-vendor reintroduces that.
//   Stage 2 (jsdom mount): load index.html with the vendored React/ReactDOM/Babel served
//     from disk, let Babel transform + execute the app exactly as a browser would, and
//     assert React actually mounts the login UI into #root (the v78 blank-screen class).
//
// Run standalone: node render-smoke.mjs   (from start9/0.4/; needs the jsdom devDep)
// Wired into the default `make` goal so every package build is gated on it.
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, resolve, join } from 'node:path'
import vm from 'node:vm'
import { JSDOM, ResourceLoader, VirtualConsole } from 'jsdom'

const here = dirname(fileURLToPath(import.meta.url))
const repoRoot = resolve(here, '..', '..') // start9/0.4 -> repo root
const frontendDir = join(repoRoot, 'frontend')
const vendorDir = join(frontendDir, 'assets', 'vendor')

const fail = (m) => { console.error(`  FAIL ${m}`); process.exitCode = 1 }
const pass = (m) => console.log(`  PASS ${m}`)

const html = readFileSync(join(frontendDir, 'index.html'), 'utf8')

const babelFile = (html.match(/src="\/assets\/vendor\/(babel-standalone-[^"]+)"/) || [])[1]
const inline = (html.match(/<script type="text\/babel">([\s\S]*?)<\/script>/) || [])[1]
if (!babelFile) fail('index.html references a vendored babel-standalone file')
if (!inline) fail('index.html has an inline <script type="text/babel"> block')

// ── Stage 1: transform with the shipped Babel; require a classic, parseable script ──
if (babelFile && inline) {
  const sandbox = { console }
  sandbox.window = sandbox
  sandbox.self = sandbox
  vm.runInNewContext(readFileSync(join(vendorDir, babelFile), 'utf8'), sandbox)
  const Babel = sandbox.Babel || sandbox.window.Babel
  if (!Babel) {
    fail('vendored Babel did not expose a global Babel')
  } else {
    // No data-presets on the tag -> the app relies on the classic 'react' preset.
    const out = Babel.transform(inline, { presets: ['react'] }).code
    pass('inline JSX transforms under the shipped Babel')
    if (/^\s*import[\s{('"*]/m.test(out) || /^\s*export[\s{ ]/m.test(out)) {
      fail('transformed app has a top-level ESM import/export (the v79 blank-screen bug)')
    } else {
      pass('transformed app is a classic (non-module) script — no ESM import/export')
    }
    try { new vm.Script(out); pass('transformed app parses as an executable classic script') }
    catch (e) { fail(`transformed app does not parse: ${e.message}`) }
  }
}

// ── Stage 2: mount in jsdom and assert the login UI rendered ──
{
  class DiskLoader extends ResourceLoader {
    fetch(url) {
      const m = url.match(/\/assets\/vendor\/([^?#]+)$/)
      if (m) { try { return Promise.resolve(readFileSync(join(vendorDir, m[1]))) } catch { return null } }
      return null // ignore fonts, favicon, API, etc.
    }
  }
  const dom = new JSDOM(html, {
    runScripts: 'dangerously',
    resources: new DiskLoader(),
    url: 'http://localhost:8080/',
    pretendToBeVisual: true,
    virtualConsole: new VirtualConsole(), // swallow app console noise; we assert on the DOM
  })
  const { window } = dom
  // Stub the browser APIs jsdom lacks that an app may touch on first render, and
  // neutralize the network so the initial data fetch can't crash the mount.
  const noopMql = () => ({ matches: false, media: '', onchange: null, addListener() {}, removeListener() {}, addEventListener() {}, removeEventListener() {}, dispatchEvent() { return false } })
  window.matchMedia = window.matchMedia || noopMql
  window.scrollTo = window.scrollTo || (() => {})
  class NoopObserver { observe() {} unobserve() {} disconnect() {} takeRecords() { return [] } }
  window.ResizeObserver = window.ResizeObserver || NoopObserver
  window.IntersectionObserver = window.IntersectionObserver || NoopObserver
  window.fetch = () => new Promise(() => {}) // never resolves -> app stays on its initial render

  const deadlineMs = 10000
  const startedAt = Date.now()
  await new Promise((done) => {
    const tick = () => {
      const root = window.document.getElementById('root')
      if ((root && root.children.length > 0) || Date.now() - startedAt > deadlineMs) return done()
      setTimeout(tick, 50)
    }
    if (window.document.readyState === 'complete') tick()
    else window.addEventListener('load', tick)
  })

  const root = window.document.getElementById('root')
  if (root && root.children.length > 0) pass('React mounted content into #root')
  else fail('React did not mount any content into #root (blank screen)')

  const loginEl = window.document.querySelector('.login-card, .login-title, .login-form')
  const txt = window.document.body.textContent || ''
  if (loginEl || /sign in|log in|password/i.test(txt)) pass('login UI rendered')
  else fail('login UI did not render (no .login-* element or login text found)')

  dom.window.close()
}

if (process.exitCode) console.error('\nRENDER SMOKE FAILED')
else console.log('\nALL PASS (render smoke)')
