import { mkdir, mkdtemp, realpath, rm, symlink } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { createAgentSecurityPolicy } from "../../src/agent/security.js";

describe("agent subprocess security policy", () => {
  const roots: string[] = [];

  afterEach(async () => {
    await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
  });

  it("passes only the run proxy capability and safe runtime variables and protects the capability from tools", async () => {
    const { workspaceRoot, workspace } = await makeWorkspace();
    const policy = await createAgentSecurityPolicy({
      runId: "run-test",
      workspaceRoot,
      workspaceDir: workspace,
      providerProxyEnv: {
        ANTHROPIC_BASE_URL: "http://127.0.0.1:43123",
        ANTHROPIC_AUTH_TOKEN: "run-proxy-capability",
        ANTHROPIC_API_KEY: "",
      },
      hostEnv: {
        PATH: "/usr/local/bin:/usr/bin:/bin",
        LANG: "C.UTF-8",
        CPH_BIN: "/usr/local/bin/cph",
        DATABASE_URL: "postgresql://platform-secret",
        FEISHU_APP_SECRET: "feishu-secret",
        HUB_SESSION_SECRET: "session-secret",
      },
    });
    const canonicalWorkspace = await realpath(workspace);

    expect(policy.cwd).toBe(canonicalWorkspace);
    expect(policy.env).toMatchObject({
      PATH: "/usr/local/bin:/usr/bin:/bin",
      LANG: "C.UTF-8",
      CPH_BIN: "/usr/local/bin/cph",
      ANTHROPIC_BASE_URL: "http://127.0.0.1:43123",
      ANTHROPIC_AUTH_TOKEN: "run-proxy-capability",
      ANTHROPIC_API_KEY: "",
    });
    expect(policy.env).not.toHaveProperty("DATABASE_URL");
    expect(policy.env).not.toHaveProperty("FEISHU_APP_SECRET");
    expect(policy.env).not.toHaveProperty("HUB_SESSION_SECRET");
    expect(policy.env.HOME).toMatch(new RegExp(`^${escapeRegExp(canonicalWorkspace)}/`));
    expect(policy.env.CLAUDE_CONFIG_DIR).toMatch(new RegExp(`^${escapeRegExp(canonicalWorkspace)}/`));
    expect(policy.env.CLAUDE_CODE_TMPDIR).toBe(policy.env.TMPDIR);
    expect(policy.env.TMP).toBe(policy.env.TMPDIR);
    expect(policy.env.TEMP).toBe(policy.env.TMPDIR);
    expect(policy.env.TMPDIR).toBe(join(canonicalWorkspace, ".cph", "t"));
    expect(Buffer.byteLength(policy.env.TMPDIR!)).toBeLessThanOrEqual(56);
    expect(policy.skillIds).toEqual([]);
    expect(policy.skillPluginRoot).toBeUndefined();

    expect(policy.sandbox).toMatchObject({
      enabled: true,
      failIfUnavailable: true,
      autoAllowBashIfSandboxed: true,
      allowUnsandboxedCommands: false,
      filesystem: {
        allowWrite: [canonicalWorkspace],
        denyRead: ["/"],
        allowRead: expect.arrayContaining([canonicalWorkspace, "/usr/bin"]),
      },
      credentials: {
        envVars: expect.arrayContaining([
          { name: "ANTHROPIC_AUTH_TOKEN", mode: "deny" },
          { name: "ANTHROPIC_API_KEY", mode: "deny" },
        ]),
      },
    });
  });

  it("rejects provider environment keys outside the explicit protocol", async () => {
    const { workspaceRoot, workspace } = await makeWorkspace();

    await expect(createAgentSecurityPolicy({
      runId: "run-test",
      workspaceRoot,
      workspaceDir: workspace,
      providerProxyEnv: {
        ANTHROPIC_AUTH_TOKEN: "run-proxy-capability",
        DATABASE_URL: "must-not-cross-boundary",
      },
      hostEnv: { PATH: "/usr/bin:/bin" },
    })).rejects.toThrow("unsupported provider environment variable: DATABASE_URL");
  });

  it("keeps every SDK temp variable on a short path inside the project workspace", async () => {
    const { workspaceRoot, workspace } = await makeWorkspace();
    const policy = await createAgentSecurityPolicy({
      runId: "run-test",
      workspaceRoot,
      workspaceDir: workspace,
      hostEnv: { PATH: "/usr/bin:/bin" },
    });

    const canonicalWorkspace = await realpath(workspace);
    const temp = policy.env.TMPDIR!;
    expect(policy.env.CLAUDE_CODE_TMPDIR).toBe(temp);
    expect(policy.env.TMP).toBe(temp);
    expect(policy.env.TEMP).toBe(temp);
    expect(temp).toBe(join(canonicalWorkspace, ".cph", "t"));
    expect(Buffer.byteLength(temp)).toBeLessThanOrEqual(56);
    expect(policy.sandbox.filesystem.allowWrite).toEqual([canonicalWorkspace]);
    expect(policy.sandbox.filesystem.denyWrite).toEqual(["/"]);
    expect(policy.sandbox.filesystem.allowRead).toContain(canonicalWorkspace);
  });

  it("fails before spawning Claude when the workspace makes bridge socket paths unsafe", async () => {
    const root = await mkdtemp(join(process.platform === "win32" ? tmpdir() : "/tmp", "hub-agent-long-"));
    roots.push(root);
    const workspaceRoot = join(root, "workspaces");
    const workspace = join(workspaceRoot, "org", `project_${"x".repeat(80)}`);
    await mkdir(workspace, { recursive: true });

    await expect(createAgentSecurityPolicy({
      runId: "run-test",
      workspaceRoot,
      workspaceDir: workspace,
      hostEnv: { PATH: "/usr/bin:/bin" },
    })).rejects.toThrow("Agent temp path is too long for sandbox bridge sockets");
  });

  it("rejects a project workspace whose real path escapes the configured workspace root", async () => {
    const { root, workspaceRoot } = await makeWorkspace();
    const outside = join(root, "outside");
    const linked = join(workspaceRoot, "o", "linked-project");
    await mkdir(outside);
    await symlink(outside, linked);

    await expect(createAgentSecurityPolicy({
      runId: "run-test",
      workspaceRoot,
      workspaceDir: linked,
      providerProxyEnv: { ANTHROPIC_AUTH_TOKEN: "run-proxy-capability" },
      hostEnv: { PATH: "/usr/bin:/bin" },
    })).rejects.toThrow("project workspace contains a symlink");
  });

  it("rejects a project workspace symlink whose target is a sibling under the same root", async () => {
    const { workspaceRoot } = await makeWorkspace();
    const sibling = join(workspaceRoot, "o", "sibling-project");
    const linked = join(workspaceRoot, "o", "linked-project");
    await mkdir(sibling);
    await symlink(sibling, linked);

    await expect(createAgentSecurityPolicy({
      runId: "run-test",
      workspaceRoot,
      workspaceDir: linked,
      providerProxyEnv: { ANTHROPIC_AUTH_TOKEN: "run-proxy-capability" },
      hostEnv: { PATH: "/usr/bin:/bin" },
    })).rejects.toThrow("symlink");
  });

  async function makeWorkspace(): Promise<{ root: string; workspaceRoot: string; workspace: string }> {
    const root = await mkdtemp(join(process.platform === "win32" ? tmpdir() : "/tmp", "h-"));
    roots.push(root);
    const workspaceRoot = join(root, "w");
    const workspace = join(workspaceRoot, "o", "p");
    await mkdir(workspace, { recursive: true });
    return { root, workspaceRoot, workspace };
  }
});

function escapeRegExp(value: string): string {
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
