// Custom node:test reporter that emits SonarQube's Generic Test Execution XML.
//
// Node's built-in reporters give us coverage (`lcov`) and pass/fail output
// (`spec`/`tap`/`junit`), but SonarQube's "Unit Tests" measure is fed by a
// SEPARATE report in *its own* format via `sonar.testExecutionReportPaths` — the
// lcov report only populates Coverage, which is why the dashboard shows coverage
// while the Unit Tests tile stays "-". This reporter produces that missing report.
//
// Format: https://docs.sonarsource.com/sonarqube/latest/analyzing-source-code/test-coverage/generic-test-data/
//   <testExecutions version="1">
//     <file path="server/test/foo.test.js">
//       <testCase name="..." duration="12"/>      <!-- duration = integer ms -->
//     </file>
//   </testExecutions>
//
// Paths are emitted repo-root-relative (POSIX separators) so they match the
// `sonar.tests` roots; the workflow runs `node --test` from the repo root, so the
// absolute `file` on each event strips cleanly against process.cwd().
import path from 'node:path'

function xmlEscape(s) {
  return String(s).replace(/[<>&"']/g, (c) => ({
    '<': '&lt;',
    '>': '&gt;',
    '&': '&amp;',
    '"': '&quot;',
    "'": '&apos;',
  })[c])
}

export default async function* sonarTestReporter(source) {
  const byFile = new Map()
  const cwd = process.cwd()

  for await (const event of source) {
    if (event.type !== 'test:pass' && event.type !== 'test:fail') continue
    const d = event.data
    // Skip the container events (a `describe` suite) and anything without a file
    // — only real test cases go in the report, so the count matches the runner's.
    if (!d.file || (d.details && d.details.type === 'suite')) continue

    const rel = path.relative(cwd, d.file).split(path.sep).join('/')
    if (!byFile.has(rel)) byFile.set(rel, [])
    byFile.get(rel).push({
      name: d.name,
      duration: Math.max(0, Math.round(d.details?.duration_ms ?? 0)),
      failed: event.type === 'test:fail',
      skipped: Boolean(d.skip || d.todo),
    })
  }

  yield '<?xml version="1.0" encoding="UTF-8"?>\n<testExecutions version="1">\n'
  for (const [file, cases] of byFile) {
    yield `  <file path="${xmlEscape(file)}">\n`
    for (const c of cases) {
      const attrs = `name="${xmlEscape(c.name)}" duration="${c.duration}"`
      if (c.failed) yield `    <testCase ${attrs}><failure message="test failed"/></testCase>\n`
      else if (c.skipped) yield `    <testCase ${attrs}><skipped/></testCase>\n`
      else yield `    <testCase ${attrs}/>\n`
    }
    yield '  </file>\n'
  }
  yield '</testExecutions>\n'
}
