/**
 * No user-controlled execution path receives platform secrets.
 *
 * Two real breaches of this rule, both found by auditing rather than by any
 * test:
 *
 *  - src/lib/preview-builder.ts ran `sh -c <repo.previewBuildCommand>` — a
 *    string any repo owner sets from the repo settings form — with the FULL
 *    process.env, and captured stdout/stderr into `buildLog`, which is
 *    rendered back to that same user. `previewBuildCommand = "env"` therefore
 *    printed DATABASE_URL (production Neon credentials), ANTHROPIC_API_KEY,
 *    GLUECRON_PAT, WORKFLOW_SECRETS_KEY, SERVER_TARGETS_KEY and the OAuth
 *    secrets into a page the attacker controls. Complete platform compromise
 *    from a settings field.
 *
 *  - src/lib/hosted-claude-loop.ts passed ANTHROPIC_API_KEY and GLUECRON_PAT
 *    into the env of arbitrary user-supplied JavaScript whose stdout is
 *    returned to the caller.
 *
 * src/lib/workflow-runner.ts already had the correct pattern — a curated
 * allowlist plus a credential-shaped denylist — for user `run:` steps. The
 * rule these tests enforce is that EVERY user-code path uses it, because the
 * threat model is identical and the decision should not be made twice.
 */

import { describe, expect, it } from "bun:test";
import { readFileSync } from "fs";

const SECRET_NAMES = [
  "ANTHROPIC_API_KEY",
  "GLUECRON_PAT",
  "DATABASE_URL",
  "WORKFLOW_SECRETS_KEY",
  "SERVER_TARGETS_KEY",
  "GOOGLE_OAUTH_CLIENT_SECRET",
  "STRIPE_SECRET_KEY",
  "EMERGENCY_PAT_SECRET",
];

/**
 * Every site that executes a user-supplied command or script. Adding a new one
 * without adding it here is what this suite is designed to catch — see the
 * "no unlisted shell-exec sites" test below.
 */
const USER_CODE_PATHS = [
  "src/lib/workflow-runner.ts", // user `run:` steps from workflow YAML
  "src/lib/preview-builder.ts", // repo.previewBuildCommand
  "src/lib/hosted-claude-loop.ts", // user-authored loop source
];

describe("user-code paths do not inherit the process environment", () => {
  for (const path of USER_CODE_PATHS) {
    it(`${path} never spreads process.env into a spawn`, () => {
      const src = readFileSync(path, "utf8")
        .replace(/\/\*[\s\S]*?\*\//g, "")
        .replace(/^\s*\/\/.*$/gm, "");
      // `env: { ...process.env }` is the exact shape that leaked everything.
      expect(src).not.toMatch(/env:\s*\{\s*\.\.\.process\.env/);
    });
  }
});

describe("no secret is named in a user-code path's env construction", () => {
  for (const path of USER_CODE_PATHS) {
    const src = readFileSync(path, "utf8")
      .replace(/\/\*[\s\S]*?\*\//g, "")
      .replace(/^\s*\/\/.*$/gm, "");
    for (const secret of SECRET_NAMES) {
      it(`${path} does not read process.env.${secret}`, () => {
        // Assert on the READ, not the mention. workflow-runner names these
        // secrets in its RUNNER_ENV_DENYLIST, which is the correct thing to
        // do — a substring check would flag the very defence being asserted.
        //
        // hosted-claude-loop still reads ANTHROPIC_API_KEY, but only behind an
        // explicit operator opt-in, asserted separately below.
        if (path.includes("hosted-claude-loop") && secret === "ANTHROPIC_API_KEY") return;
        expect(src).not.toMatch(new RegExp(`process\\.env\\.${secret}\\b`));
        expect(src).not.toMatch(new RegExp(`process\\.env\\[["'\`]${secret}["'\`]\\]`));
      });
    }
  }
});

describe("the shared allowlist is the only way user code gets an env", () => {
  it("preview-builder uses buildRunnerEnv rather than its own env object", () => {
    const src = readFileSync("src/lib/preview-builder.ts", "utf8");
    expect(src).toContain("buildRunnerEnv");
  });

  it("workflow-runner still filters through allowlist AND denylist", () => {
    const src = readFileSync("src/lib/workflow-runner.ts", "utf8");
    expect(src).toContain("RUNNER_ENV_ALLOWLIST");
    expect(src).toContain("RUNNER_ENV_DENYLIST");
    // The denylist is the net: even an allowlisted key that looks like a
    // credential is dropped.
    expect(src).toMatch(/RUNNER_ENV_DENYLIST\.test\(key\)/);
  });

  it("the allowlist contains no secret-shaped names", () => {
    const src = readFileSync("src/lib/workflow-runner.ts", "utf8");
    const list = src.slice(
      src.indexOf("const RUNNER_ENV_ALLOWLIST"),
      src.indexOf("];", src.indexOf("const RUNNER_ENV_ALLOWLIST"))
    );
    for (const secret of SECRET_NAMES) expect(list).not.toContain(secret);
    expect(list).not.toMatch(/KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL/);
  });
});

describe("hosted loops keep the platform key opt-in", () => {
  // Comments stripped: this file documents the original bug, and that prose
  // necessarily contains the very strings being asserted against.
  const src = readFileSync("src/lib/hosted-claude-loop.ts", "utf8")
    .replace(/\/\*[\s\S]*?\*\//g, "")
    .replace(/^\s*\/\/.*$/gm, "");

  it("the operator PAT is never injected", () => {
    expect(src).not.toMatch(/env\.GLUECRON_PAT\s*=/);
    expect(src).not.toMatch(/GLUECRON_PAT:/);
  });

  it("the Anthropic key requires an explicit opt-in flag", () => {
    expect(src).toContain('process.env.HOSTED_LOOPS_PLATFORM_KEY === "1"');
    // Never as a bare property on the env literal — that was the bug.
    expect(src).not.toMatch(/^\s*ANTHROPIC_API_KEY:/m);
  });
});

describe("no unlisted shell-exec site", () => {
  it("every `sh -c` / `bash -c` spawn lives in a declared user-code path", () => {
    const { readdirSync, statSync } = require("fs") as typeof import("fs");
    const { join } = require("path") as typeof import("path");
    const walk = (d: string): string[] =>
      readdirSync(d).flatMap((e) => {
        const p = join(d, e);
        return statSync(p).isDirectory() ? walk(p) : [p];
      });

    const found: string[] = [];
    for (const dir of ["src/lib", "src/routes", "src/hooks"]) {
      for (const f of walk(dir)) {
        if (!/\.tsx?$/.test(f)) continue;
        const src = readFileSync(f, "utf8").replace(/^\s*\/\/.*$/gm, "");
        if (/\[\s*["'`](?:ba)?sh["'`]\s*,\s*["'`]-c["'`]/.test(src)) {
          found.push(f.replace(/\\/g, "/"));
        }
      }
    }
    // A new shell-exec site must be reviewed and added to USER_CODE_PATHS,
    // which subjects it to every assertion above.
    expect(found.sort()).toEqual(
      USER_CODE_PATHS.filter((p) => found.includes(p)).sort()
    );
  });
});
