/**
 * SC-5 — default-output snapshot test (Wave 4 Step 12).
 *
 * Captures the humanized stdout of three representative CLIs running in
 * default mode against tests/fixtures/marketplace-medium and asserts
 * byte-equal output against tests/snapshots/default-output/<cli>.json.
 *
 * Set UPDATE_SNAPSHOT=1 to seed or refresh a snapshot. Subsequent runs
 * assert byte-equal — any drift fails the test, so humanizer prose
 * changes must be intentional and re-approved by re-running with
 * UPDATE_SNAPSHOT=1.
 *
 * Time-varying fields are normalized before comparison (timestamp,
 * target path, duration_ms). Humanizer-added prose fields
 * (titleHumanized / descriptionHumanized / recommendationHumanized,
 * userImpactCategory, userActionLanguage, relevanceContext) are kept —
 * they are the contract being snapshotted.
 */
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { readFile, writeFile } from 'node:fs/promises';

const exec = promisify(execFile);
const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO = resolve(__dirname, '..');
const FIXTURE = resolve(REPO, 'tests/fixtures/marketplace-medium');
const SNAPSHOT_DIR = resolve(REPO, 'tests/snapshots/default-output');

const UPDATE = process.env.UPDATE_SNAPSHOT === '1';

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 || '' };
  }
}

// ---------------------------------------------------------------------------
// Normalizers — same shape per CLI as json-backcompat / cli-humanizer tests.
// ---------------------------------------------------------------------------

function normalizeScanOrchestrator(env) {
  const out = JSON.parse(JSON.stringify(env));
  if (out.meta) {
    out.meta.target = '<TARGET>';
    out.meta.timestamp = '<TIMESTAMP>';
  }
  if (Array.isArray(out.scanners)) {
    for (const s of out.scanners) {
      s.duration_ms = 0;
      // claudeMdEstimatedTokens reflects walkClaudeMdCascade walking up from
      // the fixture into this plugin's own CLAUDE.md; any docs edit ripples
      // into it independently of scanner internals. Strip to keep the
      // default-output snapshot focused on humanizer prose stability.
      if (s.activeConfig && 'claudeMdEstimatedTokens' in s.activeConfig) {
        s.activeConfig.claudeMdEstimatedTokens = '<ANCESTOR_DERIVED>';
      }
    }
  }
  return out;
}

function normalizeTokenHotspots(p) {
  const out = JSON.parse(JSON.stringify(p));
  out.duration_ms = 0;
  return out;
}

const CLIS = [
  {
    name: 'scan-orchestrator',
    script: 'scanners/scan-orchestrator.mjs',
    snapshotName: 'scan-orchestrator.json',
    normalize: normalizeScanOrchestrator,
    captureStream: 'stdout',
  },
  {
    name: 'token-hotspots',
    script: 'scanners/token-hotspots-cli.mjs',
    snapshotName: 'token-hotspots.json',
    normalize: normalizeTokenHotspots,
    captureStream: 'stdout',
  },
  {
    name: 'posture',
    script: 'scanners/posture.mjs',
    snapshotName: 'posture.json',
    // Posture default mode emits the humanized scorecard to stderr; stdout is
    // empty unless --json/--raw. Snapshot the scorecard text.
    normalize: (s) => s.replace(/\(\d+ms\)/g, '(0ms)'),
    captureStream: 'stderr-text',
  },
];

async function captureForCli(cli) {
  const script = resolve(REPO, cli.script);
  const { stdout, stderr } = await runCli(script, [FIXTURE]);

  if (cli.captureStream === 'stdout') {
    const parsed = JSON.parse(stdout);
    return {
      kind: 'json',
      payload: cli.normalize(parsed),
    };
  }

  if (cli.captureStream === 'stderr-text') {
    return {
      kind: 'text',
      payload: cli.normalize(stderr.trim()),
    };
  }

  throw new Error(`unknown captureStream: ${cli.captureStream}`);
}

async function loadSnapshot(snapshotPath) {
  const raw = await readFile(snapshotPath, 'utf8');
  // Snapshot files are stored as JSON envelopes — text snapshots are wrapped
  // as { kind: 'text', payload: '...' } so all snapshots look uniform on disk.
  return JSON.parse(raw);
}

async function writeSnapshot(snapshotPath, captured) {
  const serialized = JSON.stringify(captured, null, 2) + '\n';
  await writeFile(snapshotPath, serialized, 'utf8');
}

describe('SC-5 default-output snapshot test', () => {
  for (const cli of CLIS) {
    it(`${cli.name} default mode matches tests/snapshots/default-output/${cli.snapshotName}`, async () => {
      const captured = await captureForCli(cli);
      const snapshotPath = resolve(SNAPSHOT_DIR, cli.snapshotName);

      if (UPDATE) {
        await writeSnapshot(snapshotPath, captured);
        return;
      }

      let expected;
      try {
        expected = await loadSnapshot(snapshotPath);
      } catch (err) {
        if (err.code === 'ENOENT') {
          assert.fail(
            `Snapshot missing: ${snapshotPath}. ` +
              `Re-run with UPDATE_SNAPSHOT=1 to seed it.`,
          );
        }
        throw err;
      }

      assert.deepStrictEqual(
        captured,
        expected,
        `${cli.name}: default-output drift detected. ` +
          `If intentional, re-run with UPDATE_SNAPSHOT=1.`,
      );
    });
  }
});
