/**
 * Workspace sandbox: scaffolds the generated artifact's project and runs the
 * deterministic external checkers (vitest, tsc). The checkers — not the model —
 * are the arbiters of pass/fail. Generated artifacts are dependency-free
 * TypeScript (Node stdlib only) in V1; vitest/typescript resolve from this
 * repo's node_modules via normal upward resolution.
 */
import { execFileSync, spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import type { FeatureTestResult, Finding } from './types';

const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');

export function scaffoldWorkspace(dir: string, title: string): void {
  fs.mkdirSync(path.join(dir, 'src'), { recursive: true });
  fs.mkdirSync(path.join(dir, 'tests'), { recursive: true });
  fs.writeFileSync(
    path.join(dir, 'package.json'),
    JSON.stringify({ name: slug(title), version: '0.1.0', private: true, type: 'module' }, null, 2) + '\n',
  );
  fs.writeFileSync(
    path.join(dir, 'tsconfig.json'),
    JSON.stringify(
      {
        compilerOptions: {
          target: 'ES2022',
          module: 'ESNext',
          moduleResolution: 'Bundler',
          strict: true,
          skipLibCheck: true,
          noEmit: true,
          types: ['node'],
        },
        include: ['src', 'tests'],
      },
      null,
      2,
    ) + '\n',
  );
  // Collect EVERY suite under tests/ regardless of how the model named the
  // file. tests/ contains only frozen suites (one per feature, no helpers), so
  // `*.ts` is exact — and it does not depend on the model obeying the
  // `<id>-<slug>.test.ts` naming instruction. A narrower glob (e.g.
  // `*.test.ts`) silently collects ZERO files when a weak model writes
  // `foo-test.ts`, and the run then "passes" with 0 tests executed.
  fs.writeFileSync(
    path.join(dir, 'vitest.config.ts'),
    `import { defineConfig } from 'vitest/config';\nexport default defineConfig({ test: { include: ['tests/**/*.ts'], watch: false } });\n`,
  );
  fs.writeFileSync(path.join(dir, '.env.example'), '');
  fs.writeFileSync(path.join(dir, '.gitignore'), 'node_modules/\n');
}

function slug(s: string): string {
  return s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 40) || 'artifact';
}

// ---------------------------------------------------------------------------
// vitest
// ---------------------------------------------------------------------------

export interface VitestRun {
  ok: boolean;
  passed: number;
  failed: number;
  /** Number of test files vitest collected (json.testResults.length). */
  collected: number;
  durationMs: number;
  failures: { file: string; name: string; message: string }[];
  raw: string;
}

const VITEST_BIN = path.join(ROOT, 'node_modules', 'vitest', 'vitest.mjs');

/** Run vitest in the workspace; testFiles narrows to specific frozen suites. */
export function runVitest(dir: string, testFiles?: string[]): VitestRun {
  const outFile = path.join(os.tmpdir(), `rha-vitest-${Date.now()}-${Math.random().toString(36).slice(2)}.json`);
  // NB: `--silent` must be the =true form — bare `--silent` swallows the next
  // positional argument (the test-file filter).
  const args = [VITEST_BIN, 'run', '--reporter=json', `--outputFile=${outFile}`, '--silent=true'];
  if (testFiles && testFiles.length) args.push(...testFiles);
  const started = Date.now();
  const res = spawnSync(process.execPath, args, {
    cwd: dir,
    encoding: 'utf8',
    env: { ...process.env, CI: 'true', NO_COLOR: '1', VITEST: undefined },
    timeout: 120_000,
  });
  const durationMs = Date.now() - started;
  let json: any = null;
  try {
    json = JSON.parse(fs.readFileSync(outFile, 'utf8'));
  } catch {
    // vitest crashed before writing output (e.g. syntax error in a test file)
  } finally {
    try { fs.unlinkSync(outFile); } catch { /* ignore */ }
  }
  if (!json) {
    return {
      ok: false, passed: 0, failed: 1, collected: 0, durationMs,
      failures: [{ file: '(runner)', name: 'vitest', message: (res.stderr || res.stdout || 'vitest failed to produce output').slice(0, 4000) }],
      raw: (res.stderr || '') + (res.stdout || ''),
    };
  }
  const failures: VitestRun['failures'] = [];
  let passed = 0;
  let failed = 0;
  const collected = Array.isArray(json.testResults) ? json.testResults.length : 0;
  for (const tr of json.testResults ?? []) {
    for (const a of tr.assertionResults ?? []) {
      if (a.status === 'passed') passed++;
      else if (a.status === 'failed') {
        failed++;
        failures.push({
          file: path.relative(dir, tr.name ?? ''),
          name: a.fullName ?? a.title ?? 'unknown test',
          message: (a.failureMessages ?? []).join('\n').slice(0, 2000),
        });
      }
    }
    // whole-file failure (import crash) with no assertions
    if ((tr.assertionResults ?? []).length === 0 && tr.status === 'failed') {
      failed++;
      failures.push({
        file: path.relative(dir, tr.name ?? ''),
        name: '(module load)',
        message: (tr.message ?? 'test file failed to load').slice(0, 2000),
      });
    }
  }
  return { ok: failed === 0 && passed > 0, passed, failed, collected, durationMs, failures, raw: '' };
}

export function toFeatureResult(featureId: string, run: VitestRun): FeatureTestResult {
  return {
    featureId,
    passed: run.passed,
    failed: run.failed,
    failures: run.failures.map(f => ({ name: f.name, message: f.message })),
    durationMs: run.durationMs,
  };
}

// ---------------------------------------------------------------------------
// tsc
// ---------------------------------------------------------------------------

export interface TscDiagnostic {
  file: string;
  line: number;
  code: number;
  message: string;
}

const TSC_BIN = path.join(ROOT, 'node_modules', 'typescript', 'bin', 'tsc');

export function runTsc(dir: string): TscDiagnostic[] {
  const res = spawnSync(process.execPath, [TSC_BIN, '--noEmit', '--pretty', 'false', '-p', dir], {
    cwd: dir,
    encoding: 'utf8',
    timeout: 120_000,
  });
  const out = (res.stdout || '') + (res.stderr || '');
  const diags: TscDiagnostic[] = [];
  // format: path/to/file.ts(12,5): error TS2307: Cannot find module './x'.
  const re = /^(.+?)\((\d+),\d+\): error TS(\d+): (.+)$/gm;
  let m: RegExpExecArray | null;
  while ((m = re.exec(out)) !== null) {
    diags.push({ file: m[1].replace(/\\/g, '/'), line: Number(m[2]), code: Number(m[3]), message: m[4] });
  }
  return diags;
}

export function classifyTsc(d: TscDiagnostic): Finding['bugClass'] {
  if (d.code >= 1000 && d.code < 2000) return 'syntax';
  if (d.code === 2307 || d.code === 2792) return 'unresolved-import';
  if (d.code === 2304 || d.code === 2552 || d.code === 2305 || d.code === 2724) return 'undefined-symbol';
  return 'type-mismatch';
}

// ---------------------------------------------------------------------------
// misc
// ---------------------------------------------------------------------------

export function sha256(content: string): string {
  return createHash('sha256').update(content).digest('hex');
}

export function gitAvailable(): boolean {
  try {
    execFileSync('git', ['--version'], { encoding: 'utf8' });
    return true;
  } catch {
    return false;
  }
}
