#!/usr/bin/env node
// scripts/coverage/mutate.mjs — L3 of the coverage architecture (ADR-020).
//
// Driver for Stryker mutation testing. Discovers every package with a
// stryker.config.json, then runs Stryker per-feature with the feature's
// vitest config + a narrowed mutate scope (entities + use-cases by default,
// per the shared base config).
//
// Usage:
//   pnpm mutate                            # run for every feature with a config
//   pnpm mutate -- --filter @repo/auth     # run for one feature
//   pnpm mutate -- --filter @repo/auth --since main  # incremental mode
//   pnpm mutate -- --json                  # machine-readable summary only
//
// This runs Stryker as a child process, not via dynamic import — Stryker's
// runtime expects to own the test process and our wrapping logic keeps the
// integration simple. Stryker handles concurrency internally.

import fs from "node:fs";
import path from "node:path";
import { spawnSync } from "node:child_process";

const STRYKER_BIN = "node_modules/.bin/stryker";

/**
 * Walk packages/* and apps/* for stryker.config.json files.
 * Returns: [{ packageDir, packageName, configPath }]
 */
export function discoverStrykerConfigs(repoRoot) {
  const out = [];
  for (const root of ["packages", "apps"]) {
    const dir = path.join(repoRoot, root);
    if (!fs.existsSync(dir)) continue;
    for (const pkg of fs.readdirSync(dir)) {
      const configPath = path.join(dir, pkg, "stryker.config.json");
      if (!fs.existsSync(configPath)) continue;
      const pkgJsonPath = path.join(dir, pkg, "package.json");
      let packageName = `${root}/${pkg}`;
      if (fs.existsSync(pkgJsonPath)) {
        try {
          packageName =
            JSON.parse(fs.readFileSync(pkgJsonPath, "utf8")).name ??
            packageName;
        } catch {
          // fall through
        }
      }
      out.push({
        packageDir: path.join(root, pkg),
        packageName,
        configPath,
      });
    }
  }
  return out.sort((a, b) => a.packageDir.localeCompare(b.packageDir));
}

function parseArgs(argv) {
  const out = { filter: null, since: null, json: false };
  for (let i = 2; i < argv.length; i++) {
    const a = argv[i];
    if (a === "--filter") out.filter = argv[++i];
    else if (a === "--since") out.since = argv[++i];
    else if (a === "--json") out.json = true;
    else if (a === "--help" || a === "-h") {
      console.log(
        "Usage: pnpm mutate [-- --filter <name>] [--since <ref>] [--json]",
      );
      process.exit(0);
    }
  }
  return out;
}

function main() {
  const args = parseArgs(process.argv);
  const repoRoot = process.cwd();
  const all = discoverStrykerConfigs(repoRoot);

  const targets = args.filter
    ? all.filter(
        (c) =>
          c.packageName === args.filter || c.packageDir.endsWith(args.filter),
      )
    : all;

  if (targets.length === 0) {
    process.stderr.write(
      `[mutate] No stryker.config.json found${args.filter ? ` matching ${args.filter}` : ""}.\n` +
        `Each feature wanting L3 mutation needs a stryker.config.json. See docs/guides/coverage.md.\n`,
    );
    process.exit(args.filter ? 1 : 0);
  }

  const strykerBinAbs = path.join(repoRoot, STRYKER_BIN);
  if (!fs.existsSync(strykerBinAbs)) {
    process.stderr.write(
      `[mutate] Stryker binary not found at ${STRYKER_BIN}. Run \`pnpm install\`.\n`,
    );
    process.exit(1);
  }

  const results = [];
  for (const target of targets) {
    if (!args.json) {
      process.stderr.write(
        `\n[mutate] === ${target.packageName} (${target.packageDir}) ===\n`,
      );
    }

    const strykerArgs = ["run", target.configPath];
    if (args.since) {
      strykerArgs.push("--since", args.since);
    }

    const result = spawnSync(strykerBinAbs, strykerArgs, {
      cwd: path.join(repoRoot, target.packageDir),
      stdio: args.json ? "pipe" : "inherit",
      env: { ...process.env, FORCE_COLOR: args.json ? "0" : "1" },
    });

    results.push({
      package: target.packageName,
      packageDir: target.packageDir,
      exitCode: result.status,
      status: result.status === 0 ? "pass" : "fail",
    });

    // If a feature fails, continue to next. Surface failures at the end so
    // a single broken feature doesn't mask state for the others.
  }

  const anyFailed = results.some((r) => r.exitCode !== 0);

  if (args.json) {
    process.stdout.write(
      JSON.stringify(
        { status: anyFailed ? "fail" : "pass", results },
        null,
        2,
      ) + "\n",
    );
  } else {
    process.stderr.write("\n[mutate] Summary:\n");
    for (const r of results) {
      process.stderr.write(
        `  ${r.status === "pass" ? "✓" : "✗"} ${r.package}\n`,
      );
    }
  }

  process.exit(anyFailed ? 1 : 0);
}

const invokedDirectly = import.meta.url === `file://${process.argv[1]}`;
if (invokedDirectly) {
  main();
}
