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, unlink } from 'node:fs/promises';

const exec = promisify(execFile);
const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO = resolve(__dirname, '../..');
const CLI = resolve(REPO, 'scanners/scan-orchestrator.mjs');
const FIXTURE = resolve(REPO, 'tests/fixtures/marketplace-medium');
const SNAPSHOT_PATH = resolve(REPO, 'tests/snapshots/v5.0.0/scan-orchestrator.json');

/**
 * Normalize a scan-orchestrator envelope for snapshot comparison by
 * blanking out time-varying fields (timestamp, durations, target path)
 * and ancestor-cascade-derived counts. `claudeMdEstimatedTokens` reflects
 * walkClaudeMdCascade walking upward from the fixture; any docs edit to
 * this plugin's own CLAUDE.md ripples into it even when scanner behavior
 * is unchanged. Returns a NEW object — does not mutate input.
 */
function normalizeEnvelope(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;
      if (s.activeConfig && 'claudeMdEstimatedTokens' in s.activeConfig) {
        s.activeConfig.claudeMdEstimatedTokens = '<ANCESTOR_DERIVED>';
      }
    }
  }
  return out;
}

async function runOrchestrator(flags) {
  const out = `/tmp/scan-orch-humanizer-${process.pid}-${Date.now()}-${Math.random()}.json`;
  try {
    await exec('node', [CLI, FIXTURE, '--output-file', out, ...flags], {
      timeout: 60000,
      cwd: REPO,
    });
    const written = await readFile(out, 'utf-8');
    return JSON.parse(written);
  } finally {
    await unlink(out).catch(() => {});
  }
}

describe('scan-orchestrator humanizer wiring (Step 5)', () => {
  describe('--json mode (SC-6: byte-equal v5.0.0)', () => {
    it('produces envelope structurally equal to v5.0.0 snapshot', async () => {
      const actual = await runOrchestrator(['--json']);
      const expected = JSON.parse(await readFile(SNAPSHOT_PATH, 'utf-8'));
      assert.deepStrictEqual(normalizeEnvelope(actual), normalizeEnvelope(expected));
    });

    it('does NOT add humanizer fields to findings', async () => {
      const actual = await runOrchestrator(['--json']);
      for (const s of actual.scanners) {
        for (const f of s.findings) {
          assert.equal(f.userImpactCategory, undefined,
            `${f.id}: --json findings must not have userImpactCategory`);
          assert.equal(f.userActionLanguage, undefined,
            `${f.id}: --json findings must not have userActionLanguage`);
          assert.equal(f.relevanceContext, undefined,
            `${f.id}: --json findings must not have relevanceContext`);
        }
      }
    });
  });

  describe('--raw mode (SC-7: byte-equal v5.0.0)', () => {
    it('produces envelope structurally equal to v5.0.0 snapshot', async () => {
      const actual = await runOrchestrator(['--raw']);
      const expected = JSON.parse(await readFile(SNAPSHOT_PATH, 'utf-8'));
      assert.deepStrictEqual(normalizeEnvelope(actual), normalizeEnvelope(expected));
    });

    it('does NOT add humanizer fields to findings', async () => {
      const actual = await runOrchestrator(['--raw']);
      for (const s of actual.scanners) {
        for (const f of s.findings) {
          assert.equal(f.userImpactCategory, undefined,
            `${f.id}: --raw findings must not have userImpactCategory`);
        }
      }
    });
  });

  describe('default mode (humanized)', () => {
    it('preserves envelope-level shape', async () => {
      const actual = await runOrchestrator([]);
      assert.ok(actual.meta, 'meta present');
      assert.ok(Array.isArray(actual.scanners), 'scanners array present');
      assert.ok(actual.aggregate, 'aggregate present');
      assert.equal(actual.scanners.length, 12, 'all 12 scanners present');
    });

    it('preserves scanner shape (scanner/status/findings/counts)', async () => {
      const actual = await runOrchestrator([]);
      for (const s of actual.scanners) {
        assert.ok(typeof s.scanner === 'string', 'scanner name string');
        assert.ok(typeof s.status === 'string', 'status string');
        assert.ok(Array.isArray(s.findings), 'findings array');
        assert.ok(s.counts, 'counts object');
      }
    });

    it('adds humanizer fields to every finding', async () => {
      const actual = await runOrchestrator([]);
      let totalFindings = 0;
      for (const s of actual.scanners) {
        for (const f of s.findings) {
          totalFindings++;
          assert.equal(typeof f.userImpactCategory, 'string',
            `${f.id}: userImpactCategory must be string`);
          assert.equal(typeof f.userActionLanguage, 'string',
            `${f.id}: userActionLanguage must be string`);
          assert.equal(typeof f.relevanceContext, 'string',
            `${f.id}: relevanceContext must be string`);
          assert.ok(['test-fixture-no-impact', 'affects-this-machine-only', 'affects-everyone'].includes(f.relevanceContext),
            `${f.id}: relevanceContext must be one of allowed values, got ${f.relevanceContext}`);
        }
      }
      assert.ok(totalFindings > 0, 'expected at least one finding to assert against');
    });

    it('preserves stable identifiers (id, scanner, severity)', async () => {
      const actualHumanized = await runOrchestrator([]);
      const actualRaw = await runOrchestrator(['--raw']);

      const flatHumanized = actualHumanized.scanners.flatMap(s => s.findings);
      const flatRaw = actualRaw.scanners.flatMap(s => s.findings);
      assert.equal(flatHumanized.length, flatRaw.length, 'finding count matches');

      for (let i = 0; i < flatHumanized.length; i++) {
        const h = flatHumanized[i];
        const r = flatRaw[i];
        assert.equal(h.id, r.id, `finding ${i} id matches`);
        assert.equal(h.scanner, r.scanner, `finding ${i} scanner matches`);
        assert.equal(h.severity, r.severity, `finding ${i} severity matches`);
      }
    });
  });
});
