/**
 * Sandbox configuration and lifecycle extensions for agent harness tasks.
 *
 * Builds Promptfoo beforeEach/afterEach hooks for provisioning and
 * tearing down sandbox working directories.
 */
import { existsSync } from "fs";
import { resolve } from "path";
// ---------------------------------------------------------------------------
// Sandbox configuration
// ---------------------------------------------------------------------------
/**
 * Resolve fixture specs to absolute `{src, dest}` pairs against `cwd`.
 * Throws (fail fast) if any source path does not exist, so a broken task
 * aborts config generation instead of launching an agent into an empty sandbox.
 */
export function normalizeFixtures(specs, cwd) {
    return specs.map((spec) => {
        const rawFrom = typeof spec === "string" ? spec : spec.from;
        const from = rawFrom.startsWith("file://") ? rawFrom.slice(7) : rawFrom;
        const dest = typeof spec === "string" ? null : (spec.to ?? null);
        const src = resolve(cwd, from);
        if (!existsSync(src)) {
            throw new Error(`Agent-harness fixture not found: "${rawFrom}" resolved to ${src} (relative to ${cwd})`);
        }
        return { src, dest };
    });
}
/**
 * Build sandbox configuration from a task definition.
 *
 * Fixture paths are resolved to absolute at compile time using callerCwd
 * (the directory the pipeline was invoked from), because promptfoo runs
 * with cwd set to packages/eval/ — not the monorepo root where apps/ lives.
 */
export function buildSandboxConfig(task, callerCwd) {
    const cwd = callerCwd ?? process.cwd();
    const specs = task.context?.fixtures ?? task.fixtures ?? [];
    const fixtures = normalizeFixtures(specs, cwd);
    const canonical = resolve(cwd, `contexts/canonical/${task.id}.md`);
    if ((task.context?.docs?.length ?? 0) > 0 && existsSync(canonical)) {
        fixtures.push({ src: canonical, dest: `docs/${task.id}.md` });
    }
    return {
        type: task.sandbox?.type ?? "tempdir",
        image: task.sandbox?.image,
        fixtures,
        limits: task.sandbox?.limits
            ? {
                cpus: task.sandbox.limits.cpus,
                memoryBytes: task.sandbox.limits.memoryBytes,
                networkAccess: task.sandbox.limits.networkAccess,
            }
            : undefined,
    };
}
// ---------------------------------------------------------------------------
// Lifecycle extensions
// ---------------------------------------------------------------------------
export function buildLifecycleExtensions(task) {
    const extensions = [];
    // beforeEach: provision sandbox + inject fixtures
    extensions.push({
        type: "beforeEach",
        code: buildBeforeEachHook(task.id),
    });
    // afterEach: collect artifacts + teardown
    extensions.push({
        type: "afterEach",
        code: buildAfterEachHook(task.id),
    });
    return extensions;
}
export function buildBeforeEachHook(taskId) {
    // Promptfoo extension hooks receive (hookName, context).
    // beforeEach context is { test } — vars live at context.test.vars.
    // Must return context for mutations to persist.
    //
    // The sandbox directory (an os.tmpdir() path shared across the mode's tasks)
    // is created by the YAML writer at config-gen time so it exists before the
    // provider is initialized. This hook wipes it, then copies THIS task's
    // fixtures into it — so each test row starts from a clean sandbox holding
    // only its own fixtures (per-task isolation on a shared working dir).
    //
    // The provisioning logic is inlined rather than `require`d from a sibling
    // module. Promptfoo runs the hook via plain `require` in whatever process
    // executes the eval; when the CLI runs from TypeScript source (tsx,
    // --conditions=ailf-source) there is no compiled `.js` sibling on disk to
    // resolve, so a `require('./provision-fixtures.js')` fails. Inlining keeps
    // the hook self-contained — it resolves only Node builtins, exactly like
    // buildAfterEachHook. Keep the body in sync with the fs semantics the
    // agent-harness provision-hook test exercises.
    //
    // @see https://www.promptfoo.dev/docs/configuration/reference/ — extensions
    return `// beforeEach: provision fixtures into sandbox for ${taskId}
async function(context) {
  const { cpSync, existsSync, mkdirSync, readdirSync, rmSync, statSync } = require('fs');
  const { basename, dirname, resolve, sep } = require('path');
  const vars = (context.test && context.test.vars) || {};
  const workDir = vars.__workingDir;
  const fixtures = vars.__fixtures || [];
  if (!workDir) return context;
  mkdirSync(workDir, { recursive: true });
  for (const entry of readdirSync(workDir)) {
    rmSync(resolve(workDir, entry), { recursive: true, force: true });
  }
  for (const { src, dest } of fixtures) {
    if (!existsSync(src)) {
      throw new Error('Agent-harness fixture source vanished: ' + src);
    }
    const isDir = statSync(src).isDirectory();
    const target = dest != null
      ? resolve(workDir, dest)
      : isDir ? workDir : resolve(workDir, basename(src));
    const base = resolve(workDir);
    if (target !== base && !target.startsWith(base + sep)) {
      throw new Error('Agent-harness fixture dest escapes the sandbox: ' + dest);
    }
    if (isDir) {
      mkdirSync(target, { recursive: true });
      cpSync(src, target, { recursive: true });
    } else {
      mkdirSync(dirname(target), { recursive: true });
      cpSync(src, target);
    }
  }
  return context;
}`;
}
export function buildAfterEachHook(taskId) {
    // Promptfoo extension hooks receive (hookName, context).
    // afterEach context is { test, result } — vars live at context.test.vars.
    // @see https://www.promptfoo.dev/docs/configuration/reference/ — extensions
    return (`// afterEach: collect artifacts + teardown for ${taskId}\n` +
        `async function(context) {\n` +
        `  const { rmSync, readdirSync, existsSync } = require('fs');\n` +
        `  const vars = context.test.vars || {};\n` +
        `  const workDir = vars.__workingDir;\n` +
        `  if (workDir && existsSync(workDir)) {\n` +
        `    try {\n` +
        `      // Collect modified files list\n` +
        `      const files = [];\n` +
        `      function collect(dir, prefix) {\n` +
        `        for (const e of readdirSync(dir, { withFileTypes: true })) {\n` +
        `          const rel = prefix ? prefix + '/' + e.name : e.name;\n` +
        `          if (e.isDirectory()) collect(require('path').resolve(dir, e.name), rel);\n` +
        `          else files.push(rel);\n` +
        `        }\n` +
        `      }\n` +
        `      collect(workDir, '');\n` +
        `      vars.__artifacts = { modifiedFiles: files };\n` +
        `    } finally {\n` +
        `      // Guard: only delete directories under os.tmpdir()\n` +
        `      const tmp = require('os').tmpdir();\n` +
        `      if (require('path').resolve(workDir).startsWith(require('path').resolve(tmp))) {\n` +
        `        rmSync(workDir, { recursive: true, force: true });\n` +
        `      }\n` +
        `    }\n` +
        `  }\n` +
        `}`);
}
