#!/usr/bin/env node
/**
 * SC-3 forbidden-words lint runner.
 *
 * Runs 6 prose CLIs in default (humanized) mode against
 * tests/fixtures/marketplace-medium and matches their stderr output against
 * tier1+tier3 (failure) and tier2 (warning) from
 * tests/lint-forbidden-words.json.
 *
 * Why stderr only: stdout for these CLIs carries the JSON envelope (machine
 * data with structural keys like "scanner" / "severity" that are not prose),
 * while stderr carries the terminal-visible prose (banners, scorecards,
 * fix-plan listings, summaries). The humanized prose fields embedded inside
 * the JSON envelope are already covered by humanizer-data tier1/tier3 tests
 * (tests/lib/humanizer-data.test.mjs), so this runner targets the surface
 * users actually read as English text.
 *
 * Code references inside backticks are stripped before matching, so technical
 * identifiers like `CLAUDE.md` and `MCP` may appear when wrapped in
 * backticks.
 *
 * Exit 0 = PASS (no tier1/tier3), exit 1 = FAIL.
 *
 * Usage:
 *   node tests/lint-default-output.mjs [<fixturePath>]
 */
import { readFile, access, mkdir } from 'node:fs/promises';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { homedir } from 'node:os';

const exec = promisify(execFile);
const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO = resolve(__dirname, '..');
const FORBIDDEN_PATH = resolve(REPO, 'tests/lint-forbidden-words.json');
const DEFAULT_FIXTURE = resolve(REPO, 'tests/fixtures/marketplace-medium');
const BASELINE_DIR = resolve(homedir(), '.config-audit/baselines');
const DEFAULT_BASELINE = resolve(BASELINE_DIR, 'default.json');

// 6 prose CLIs. Manifest and whats-active are inventory CLIs (data, not
// diagnostic prose) — excluded per Step 8 spec.
const CLIS = [
  { name: 'scan-orchestrator', script: 'scanners/scan-orchestrator.mjs' },
  { name: 'posture', script: 'scanners/posture.mjs' },
  { name: 'token-hotspots-cli', script: 'scanners/token-hotspots-cli.mjs' },
  { name: 'plugin-health-scanner', script: 'scanners/plugin-health-scanner.mjs' },
  { name: 'drift-cli', script: 'scanners/drift-cli.mjs', requiresBaseline: true },
  { name: 'fix-cli', script: 'scanners/fix-cli.mjs' },
];

function stripBacktickSpans(s) {
  return s.replace(/`[^`]*`/g, '');
}

/**
 * Compile a regex matching a forbidden word.
 * - Multi-character / dotted / hyphenated / slashed phrases → case-insensitive substring.
 * - Single ASCII words → case-insensitive `\bword\b`.
 */
function compileWordRegex(word) {
  const lower = word.toLowerCase();
  const escaped = lower.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  if (/[ \-./]/.test(lower)) {
    return new RegExp(escaped, 'gi');
  }
  return new RegExp(`\\b${escaped}\\b`, 'gi');
}

async function loadForbidden() {
  return JSON.parse(await readFile(FORBIDDEN_PATH, 'utf8'));
}

async function runCli(scriptPath, args) {
  try {
    const { stdout, stderr } = await exec('node', [scriptPath, ...args], {
      timeout: 60000,
      cwd: REPO,
      maxBuffer: 10 * 1024 * 1024,
    });
    return { stdout: stdout || '', stderr: stderr || '' };
  } catch (err) {
    return { stdout: err.stdout || '', stderr: err.stderr || '' };
  }
}

async function ensureDriftBaseline(fixturePath) {
  try {
    await access(DEFAULT_BASELINE);
    return true;
  } catch {
    try {
      await mkdir(BASELINE_DIR, { recursive: true });
      await runCli(resolve(REPO, 'scanners/drift-cli.mjs'), [fixturePath, '--save']);
      await access(DEFAULT_BASELINE);
      return true;
    } catch {
      return false;
    }
  }
}

function findHits(text, entries) {
  const cleaned = stripBacktickSpans(text);
  const hits = [];
  for (const entry of entries) {
    const re = compileWordRegex(entry.word);
    const matches = [...cleaned.matchAll(re)];
    if (matches.length > 0) {
      hits.push({ word: entry.word, count: matches.length });
    }
  }
  return hits;
}

/**
 * Lint default-mode output of all CLIs against forbidden-words list.
 * @returns {{ failures: Array, warnings: Array }}
 */
export async function lint(fixturePath = DEFAULT_FIXTURE) {
  const data = await loadForbidden();

  const failures = [];
  const warnings = [];

  for (const cli of CLIS) {
    if (cli.requiresBaseline) {
      const ok = await ensureDriftBaseline(fixturePath);
      if (!ok) {
        warnings.push({ cli: cli.name, kind: 'skip', message: 'drift baseline unavailable — skipped' });
        continue;
      }
    }

    const scriptPath = resolve(REPO, cli.script);
    const { stderr } = await runCli(scriptPath, [fixturePath]);

    for (const h of findHits(stderr, data.tier1)) {
      failures.push({ cli: cli.name, tier: 1, ...h });
    }
    for (const h of findHits(stderr, data.tier3)) {
      failures.push({ cli: cli.name, tier: 3, ...h });
    }
    for (const h of findHits(stderr, data.tier2)) {
      warnings.push({ cli: cli.name, tier: 2, ...h });
    }
  }

  return { failures, warnings };
}

async function main() {
  const fixture = process.argv[2] || DEFAULT_FIXTURE;
  const { failures, warnings } = await lint(fixture);

  if (warnings.length > 0) {
    process.stderr.write('Tier-2 warnings (non-blocking):\n');
    for (const w of warnings) {
      if (w.kind === 'skip') {
        process.stderr.write(`  [${w.cli}] ${w.message}\n`);
      } else {
        process.stderr.write(`  [${w.cli}] tier2 "${w.word}" × ${w.count}\n`);
      }
    }
  }

  if (failures.length > 0) {
    process.stderr.write(`\nSC-3 FAIL: ${failures.length} violation(s) across ${CLIS.length} CLIs\n`);
    for (const f of failures) {
      process.stderr.write(`  [${f.cli}] tier${f.tier} "${f.word}" × ${f.count}\n`);
    }
    process.exit(1);
  }

  process.stderr.write(`\nSC-3 PASS: 0 tier1/tier3 violations across ${CLIS.length} CLIs\n`);
  process.exit(0);
}

const isDirectRun =
  process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname);
if (isDirectRun) {
  main().catch((err) => {
    process.stderr.write(`Lint runner error: ${err.message}\n`);
    process.exit(2);
  });
}
