import test from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import process from 'node:process';
import {
  resolveBinary, buildHeader, parseExit,
  parsePest, parseVitest,
  parseViteBuild, parseLarastan, parseGitleaks, parseLychee,
  computeVerdict,
  buildCanonicalLine, formatRow, verdictLine,
  CHECKS,
} from './run.mjs';

test('resolveBinary: win32 → bin\\<name>.exe', () => {
  assert.equal(resolveBinary('gitleaks', 'win32'), 'bin\\gitleaks.exe');
});
test('resolveBinary: non-win32 → bare name on PATH', () => {
  assert.equal(resolveBinary('lychee', 'linux'), 'lychee');
  assert.equal(resolveBinary('lychee', 'darwin'), 'lychee');
});

test('buildHeader: starts with the tier banner', () => {
  assert.ok(buildHeader('quick').startsWith('─ /regression quick '));
  assert.ok(buildHeader('full').startsWith('─ /regression full '));
});
test('buildHeader: is padded with dashes', () => {
  assert.ok(buildHeader('full').length >= 30);
});

test('parseExit: builds "<label> <code>" token', () => {
  assert.equal(parseExit('Pint', 0), 'Pint 0');
  assert.equal(parseExit('ESLint', 1), 'ESLint 1');
});

test('parsePest: passed + skipped, no failures → total derived', () => {
  const out = '  Tests:    3 skipped, 739 passed (2104 assertions)\n  Duration: 71.23s';
  assert.equal(parsePest(out), 'Pest 742/739/3sk/0');
});
test('parsePest: with failures', () => {
  const out = '  Tests:    2 failed, 1 skipped, 736 passed (2090 assertions)';
  assert.equal(parsePest(out), 'Pest 739/736/1sk/2');
});
test('parsePest: passed only → zeros for skipped/failed', () => {
  assert.equal(parsePest('  Tests:    19 passed (44 assertions)'), 'Pest 19/19/0sk/0');
});
test('parsePest: JSON format (pest --parallel) passed + skipped', () => {
  const out = '{"tool":"pest","result":"passed","tests":793,"passed":790,"assertions":2391,"duration_ms":32200,"skipped":3}';
  assert.equal(parsePest(out), 'Pest 793/790/3sk/0');
});
test('parsePest: JSON format with failures', () => {
  const out = '{"tool":"pest","result":"failed","tests":793,"passed":788,"assertions":2380,"duration_ms":31000,"skipped":3}';
  assert.equal(parsePest(out), 'Pest 793/788/3sk/2');
});
test('parsePest: JSON format no skipped', () => {
  const out = '{"tool":"pest","result":"passed","tests":19,"passed":19,"assertions":44,"duration_ms":1711}';
  assert.equal(parsePest(out), 'Pest 19/19/0sk/0');
});

test('parseVitest: files + passed + skipped', () => {
  const out = ' Test Files  92 passed (92)\n      Tests  774 passed | 3 skipped (777)\n   Duration  12.6s';
  assert.equal(parseVitest(out), 'Vitest 92f/774/3sk/0');
});
test('parseVitest: with failures, does not confuse "Test Files" with "Tests"', () => {
  const out = ' Test Files  2 failed | 90 passed (92)\n      Tests  5 failed | 769 passed (774)';
  assert.equal(parseVitest(out), 'Vitest 90f/769/0sk/5');
});

test('parseViteBuild: extracts build time', () => {
  assert.equal(parseViteBuild('✓ 312 modules transformed.\n✓ built in 2.03s'), 'Vite build 2.03s');
});
test('parseViteBuild: no match → "?"', () => {
  assert.equal(parseViteBuild('build crashed'), 'Vite build ?s');
});

test('parseLarastan: clean → 0', () => {
  assert.equal(parseLarastan(' [OK] No errors'), 'Larastan 0');
});
test('parseLarastan: counts errors', () => {
  assert.equal(parseLarastan(' [ERROR] Found 2 errors'), 'Larastan 2');
});

test('parseGitleaks: clean → 0 leaks', () => {
  const out = 'INF 442 commits scanned.\nINF no leaks found';
  assert.equal(parseGitleaks(out, 0), 'gitleaks 0/442');
});
test('parseGitleaks: leaks found (non-zero exit)', () => {
  const out = 'INF 442 commits scanned.\nWRN 3 leaks found';
  assert.equal(parseGitleaks(out, 1), 'gitleaks 3/442');
});

test('parseLychee: OK + errors', () => {
  const out = '🔍 325 Total (in 9s)\n✅ 325 OK\n🚫 0 Errors';
  assert.equal(parseLychee(out), 'lychee 325/0');
});
test('parseLychee: with broken links', () => {
  const out = '🔍 327 Total\n✅ 325 OK\n🚫 2 Errors';
  assert.equal(parseLychee(out), 'lychee 325/2');
});

test('computeVerdict: all exit 0 → GREEN, exit code 0', () => {
  const v = computeVerdict([
    { label: 'Pint', code: 0, skipped: false },
    { label: 'ESLint', code: 0, skipped: false },
  ]);
  assert.equal(v.verdict, 'GREEN');
  assert.equal(v.exitCode, 0);
  assert.deepEqual(v.failed, []);
});
test('computeVerdict: one non-zero exit → RED, exit code 1', () => {
  const v = computeVerdict([
    { label: 'Pint', code: 0, skipped: false },
    { label: 'Larastan', code: 1, skipped: false },
  ]);
  assert.equal(v.verdict, 'RED');
  assert.equal(v.exitCode, 1);
  assert.deepEqual(v.failed, ['Larastan']);
});
test('computeVerdict: a skipped check → RED-INCOMPLETE', () => {
  const v = computeVerdict([
    { label: 'Pint', code: 0, skipped: false },
    { label: 'gitleaks', code: null, skipped: true },
  ]);
  assert.equal(v.verdict, 'RED-INCOMPLETE');
  assert.equal(v.exitCode, 1);
  assert.deepEqual(v.skipped, ['gitleaks']);
});
test('computeVerdict: skipped takes precedence over a failure', () => {
  const v = computeVerdict([
    { label: 'Larastan', code: 1, skipped: false },
    { label: 'lychee', code: null, skipped: true },
  ]);
  assert.equal(v.verdict, 'RED-INCOMPLETE');
  assert.deepEqual(v.failed, ['Larastan']);
  assert.deepEqual(v.skipped, ['lychee']);
});

test('buildCanonicalLine: joins tokens in result order with " / "', () => {
  const results = [
    { token: 'Pint 0' }, { token: 'ESLint 0' }, { token: 'Pest 742/739/3sk/0' },
  ];
  assert.equal(buildCanonicalLine(results), 'Pint 0 / ESLint 0 / Pest 742/739/3sk/0');
});

test('formatRow: passed check → ✅ mark, label, code, time', () => {
  const row = formatRow({ label: 'Pint', code: 0, ms: 1800, skipped: false });
  assert.ok(row.startsWith('[✅] Pint'));
  assert.ok(row.includes('1.8s'));
});
test('formatRow: failed check → ❌ mark', () => {
  assert.ok(formatRow({ label: 'Larastan', code: 1, ms: 8400, skipped: false }).startsWith('[❌] Larastan'));
});
test('formatRow: skipped check → ⚠ mark + SKIPPED', () => {
  const row = formatRow({ label: 'gitleaks', code: null, ms: 0, skipped: true });
  assert.ok(row.startsWith('[⚠] gitleaks'));
  assert.ok(row.includes('SKIPPED'));
});

test('verdictLine: GREEN', () => {
  const line = verdictLine({ verdict: 'GREEN', failed: [], skipped: [] }, 12);
  assert.ok(line.includes('🟢 GREEN'));
  assert.ok(line.includes('12'));
});
test('verdictLine: RED lists failed checks', () => {
  const line = verdictLine({ verdict: 'RED', failed: ['Larastan'], skipped: [] }, 12);
  assert.ok(line.includes('🔴 RED'));
  assert.ok(line.includes('Larastan'));
});
test('verdictLine: RED-INCOMPLETE lists skipped checks', () => {
  const line = verdictLine({ verdict: 'RED-INCOMPLETE', failed: [], skipped: ['gitleaks'] }, 12);
  assert.ok(line.includes('🟠 RED-INCOMPLETE'));
  assert.ok(line.includes('gitleaks'));
});

test('CHECKS: quick tier has exactly 6 checks', () => {
  assert.equal(CHECKS.filter((c) => c.tiers.includes('quick')).length, 6);
});
test('CHECKS: full tier has exactly 12 checks', () => {
  assert.equal(CHECKS.filter((c) => c.tiers.includes('full')).length, 12);
});
test('CHECKS: quick is a strict subset of full', () => {
  const full = new Set(CHECKS.filter((c) => c.tiers.includes('full')).map((c) => c.id));
  for (const c of CHECKS.filter((c) => c.tiers.includes('quick'))) {
    assert.ok(full.has(c.id), `${c.id} in quick must also be in full`);
  }
});
test('CHECKS: every check has id, label, cwd, parse, and a command source', () => {
  for (const c of CHECKS) {
    assert.ok(c.id && c.label && c.cwd, `${c.id}: id/label/cwd`);
    assert.equal(typeof c.parse, 'function', `${c.id}: parse is a function`);
    assert.ok(c.cmd || (c.bin && Array.isArray(c.argv)), `${c.id}: has cmd or bin+argv`);
  }
});
test('CHECKS: ids are unique', () => {
  assert.equal(new Set(CHECKS.map((c) => c.id)).size, CHECKS.length);
});

const RUN = fileURLToPath(new URL('./run.mjs', import.meta.url));

test('main: unknown argument → exit code 2 + error on stderr', () => {
  try {
    execFileSync(process.execPath, [RUN, 'bogus'], { encoding: 'utf8', stdio: 'pipe' });
    assert.fail('expected non-zero exit');
  } catch (err) {
    assert.equal(err.status, 2);
    assert.match(String(err.stderr), /unknown argument/i);
  }
});
test('main: importing run.mjs does not auto-run the sweep', () => {
  // If the import.meta guard were broken, importing run.mjs at the top of this
  // file would have spawned a full sweep. Reaching this assertion proves it did not.
  assert.ok(true);
});
