// A reporter whose only job is to answer one question: DID EVERY TEST FILE THIS
// RUN SET OUT TO RUN ACTUALLY REPORT A RESULT?
//
// ---------------------------------------------------------------------------
// WHY A WHOLE REPORTER EXISTS FOR ONE COMPARISON
// ---------------------------------------------------------------------------
//
// A vitest worker is a forked Node process. When one of them dies mid-file, the
// run does not stop and it does not necessarily go red. What it prints is:
//
//     Error: [vitest-pool]: Worker forks emitted error.
//     Caused by: Error: Worker exited unexpectedly
//      Test Files  61 passed (62)
//           Tests  1905 passed | 1 skipped (1936)
//
// Read that quickly and it says "passed". The only honest numbers in it are the
// two parenthesised totals, and the thirty tests that went missing between
// 1905 and 1936 are not attributed to anything. Nothing names the file.
//
// This project cannot live with that. The suite is the entire correctness
// argument (CLAUDE.md §11): it is what proves that every journal entry
// balances and that one company cannot read another's books. A suite that can
// quietly run 61 of its 62 files and print green is not a gate — it is a gate
// that opens on its own when the machine is busy, which is exactly when you
// would want it shut.
//
// So: compare what was scheduled against what came back, say which files are
// missing BY NAME, and make the process exit non-zero. Both halves matter. A
// warning nobody reads is how this bug survived in the first place.
//
// ---------------------------------------------------------------------------
// WHY NOT JUST RELY ON VITEST'S OWN EXIT CODE
// ---------------------------------------------------------------------------
//
// Today vitest does usually exit 1 here, because the dead worker is recorded as
// an unhandled error and unhandled errors set the exit code. That is a
// SIDE EFFECT of how the death happened to be noticed, not a guarantee that the
// results are complete, and it has two known holes:
//
//   - The pool removes its 'exit' listener before it deliberately stops a
//     worker (see ForksPoolWorker.stop()). A worker that dies inside that
//     window is not "unexpected" to vitest, so no error is recorded — and the
//     file's results are gone all the same.
//   - `dangerouslyIgnoreUnhandledErrors` turns the whole mechanism off.
//
// This check does not care how the file went missing. It compares two sets.
//
// ---------------------------------------------------------------------------
// WHY NOT THE LEAKED-DATABASE CHECK IN global-setup.ts
// ---------------------------------------------------------------------------
//
// That one stays, and it is better than this at explaining what happened: it
// names the file AND hands you the database, stamped with its owner, to look
// inside. But it can only see files that got as far as creating a database.
// Roughly a third of this suite — everything under tests/web, tests/tools, the
// localdb unit tests — never touches PostgreSQL at all, and a worker that dies
// while importing one of those leaks nothing and would leave no trace. The two
// checks overlap on purpose; this is the one that covers every file.

import type { Reporter, TestModule, TestSpecification } from "vitest/node";

/** States that mean "this file finished and its results are in the totals".
 *  Anything else — `pending`, `queued` — is a file that was scheduled and never
 *  came back, which is precisely the failure this exists to catch. */
const FINISHED = new Set(["passed", "failed", "skipped"]);

export default class CompletenessReporter implements Reporter {
  /** moduleId (absolute path) of every file this run scheduled. */
  private scheduled = new Set<string>();

  onTestRunStart(specifications: ReadonlyArray<TestSpecification>): void {
    // Cleared rather than appended to: in watch mode this reporter instance
    // survives across runs, and a stale id from the previous run would be
    // reported as missing forever.
    this.scheduled = new Set(specifications.map((s) => s.moduleId));
  }

  onTestRunEnd(
    testModules: ReadonlyArray<TestModule>,
    _unhandledErrors: unknown,
    reason: "passed" | "failed" | "interrupted",
  ): void {
    // Ctrl+C, or a watch run cancelled by an edit. Files are SUPPOSED to be
    // missing then, and shouting about it would train people to ignore this.
    if (reason === "interrupted") return;

    const finished = new Set(
      testModules.filter((m) => FINISHED.has(m.state())).map((m) => m.moduleId),
    );
    const missing = [...this.scheduled].filter((id) => !finished.has(id));
    if (missing.length === 0) return;

    const root = process.cwd().replaceAll("\\", "/");
    const relative = (id: string) => {
      const normalised = id.replaceAll("\\", "/");
      return normalised.startsWith(`${root}/`) ? normalised.slice(root.length + 1) : normalised;
    };

    console.error(
      `\n[completeness] THIS RUN IS INCOMPLETE. ${missing.length} of ${this.scheduled.size} test ` +
      "file(s) were scheduled but never reported a result, so their tests are NOT in the totals " +
      "above — however green those totals look:\n" +
      missing.map((id) => `  - ${relative(id)}`).join("\n") +
      "\n\nThe usual cause is the worker process running that file dying (vitest prints " +
      "\"Worker exited unexpectedly\"). Run those files on their own to see what they do. This run " +
      "is being failed on purpose: a suite that can drop the invariant tests while printing green " +
      "is worse than one that fails.\n",
    );
    process.exitCode = 1;
  }
}
