import assert from "node:assert/strict";
import { mkdtemp, mkdir, readFile, rm, stat, utimes, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import test from "node:test";

import { getWorkspaceArtifact, projectWorkspaceArtifacts } from "./workspace-artifacts.js";

test("projects inline HTML, local links, code locations, and images to opaque session URLs", async () => {
  const root = await mkdtemp(join(tmpdir(), "code-remote-manager-artifacts-"));
  const workspacePath = join(root, "workspace");
  const dataDir = join(root, "data");
  await mkdir(join(workspacePath, "src"), { recursive: true });
  await writeFile(join(workspacePath, "preview.html"), "<h1>Preview</h1>");
  await writeFile(join(workspacePath, "README.md"), "# Read me");
  await writeFile(join(workspacePath, "src", "app.js"), "export const app = true;\n");
  await writeFile(join(workspacePath, "preview.png"), Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
  const session = { id: "session-1", attachments: [] };

  try {
    const projected = projectWorkspaceArtifacts({
      text: [
        "::codex-inline-vis{file=\"preview.html\"}",
        "[Read me](README.md)",
        "[App](src/app.js:1)",
        "![Preview](preview.png)"
      ].join("\n"),
      session,
      workspacePath,
      dataDir
    });

    assert.match(projected, /::codex-inline-artifact\{id="artifact-[a-f0-9]{24}" name="preview\.html" url="\/api\/sessions\/session-1\/artifacts\/artifact-[a-f0-9]{24}\/preview\.html"\}/);
    assert.match(projected, /\[Read me\]\(\/api\/sessions\/session-1\/artifacts\/artifact-[a-f0-9]{24}\?name=README\.md&mime=text%2Fmarkdown&size=9\)/);
    assert.match(projected, /\[App\]\(\/api\/sessions\/session-1\/artifacts\/artifact-[a-f0-9]{24}\?name=app\.js&mime=text%2Fjavascript&size=25&line=1\)/);
    assert.match(projected, /!\[Preview\]\(\/api\/sessions\/session-1\/artifacts\/artifact-[a-f0-9]{24}\?name=preview\.png&mime=image%2Fpng&size=8\)/);
    assert.equal(projected.includes(workspacePath), false);
    assert.equal(session.attachments.every((item) => item.kind === "artifact"), true);
    assert.equal(session.attachments.length, 4);
  } finally {
    await rm(root, { recursive: true, force: true });
  }
});

test("projects JSX code locations as browser-readable plain text", async () => {
  const root = await mkdtemp(join(tmpdir(), "code-remote-manager-artifact-jsx-"));
  const workspacePath = join(root, "workspace");
  const dataDir = join(root, "data");
  await mkdir(join(workspacePath, "src"), { recursive: true });
  await writeFile(join(workspacePath, "src", "App.jsx"), "export function App() {}\n");
  const session = { id: "session-jsx", attachments: [] };

  try {
    const projected = projectWorkspaceArtifacts({ text: "[App](src/App.jsx:10)", session, workspacePath, dataDir });
    assert.match(projected, /mime=text%2Fplain/);
    assert.equal(session.attachments[0].mimeType, "text/plain");
  } finally {
    await rm(root, { recursive: true, force: true });
  }
});

test("serves registered artifact bytes after private source paths are absent", async () => {
  const root = await mkdtemp(join(tmpdir(), "code-remote-manager-artifact-read-"));
  const workspacePath = join(root, "workspace");
  const dataDir = join(root, "data");
  await mkdir(workspacePath, { recursive: true });
  await writeFile(join(workspacePath, "report.pdf"), "%PDF-safe");
  const session = { id: "session-read", attachments: [] };

  try {
    projectWorkspaceArtifacts({ text: "[Report](report.pdf)", session, workspacePath, dataDir });
    const persistedSession = {
      id: session.id,
      attachments: session.attachments.map(({ path: _path, ...item }) => item)
    };
    const artifact = await getWorkspaceArtifact({ session: persistedSession, artifactId: session.attachments[0].id, dataDir });

    assert.equal(artifact.data.toString(), "%PDF-safe");
    assert.equal(artifact.mimeType, "application/pdf");
    assert.equal(artifact.name, "report.pdf");
  } finally {
    await rm(root, { recursive: true, force: true });
  }
});

test("snapshots only local resources referenced by an HTML artifact", async () => {
  const root = await mkdtemp(join(tmpdir(), "code-remote-manager-html-bundle-"));
  const workspacePath = join(root, "workspace");
  const dataDir = join(root, "data");
  await mkdir(join(workspacePath, "styles"), { recursive: true });
  await mkdir(join(workspacePath, "scripts"), { recursive: true });
  await mkdir(join(workspacePath, "images"), { recursive: true });
  await writeFile(join(workspacePath, "preview.html"), '<link rel="stylesheet" href="styles/site.css"><script src="scripts/app.js"></script><img src="images/hero.png">');
  await writeFile(join(workspacePath, "styles", "site.css"), "body{color:#123}");
  await writeFile(join(workspacePath, "scripts", "app.js"), "document.body.dataset.ready='true'");
  await writeFile(join(workspacePath, "images", "hero.png"), Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
  await writeFile(join(workspacePath, "secret.txt"), "not referenced");
  const session = { id: "session-html", attachments: [] };

  try {
    const projected = projectWorkspaceArtifacts({ text: '::codex-inline-vis{file="preview.html"}', session, workspacePath, dataDir });
    const id = projected.match(/id="(artifact-[a-f0-9]{24})"/)?.[1];
    assert.match(projected, new RegExp(`url="/api/sessions/session-html/artifacts/${id}/preview\\.html"`));
    assert.equal((await getWorkspaceArtifact({ session, artifactId: id, resourcePath: "styles/site.css", dataDir })).data.toString(), "body{color:#123}");
    assert.equal((await getWorkspaceArtifact({ session, artifactId: id, resourcePath: "scripts/app.js", dataDir })).data.toString(), "document.body.dataset.ready='true'");
    assert.deepEqual((await getWorkspaceArtifact({ session, artifactId: id, resourcePath: "images/hero.png", dataDir })).data, Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
    await assert.rejects(getWorkspaceArtifact({ session, artifactId: id, resourcePath: "secret.txt", dataDir }), /artifact_not_found/);
  } finally {
    await rm(root, { recursive: true, force: true });
  }
});

test("preserves workspace-relative HTML layout for parent resources and module imports", async () => {
  const root = await mkdtemp(join(tmpdir(), "code-remote-manager-html-parent-"));
  const workspacePath = join(root, "workspace");
  const dataDir = join(root, "data");
  await mkdir(join(workspacePath, "pages"), { recursive: true });
  await mkdir(join(workspacePath, "assets"), { recursive: true });
  await writeFile(join(workspacePath, "pages", "preview.html"), '<meta http-equiv="refresh" content="0;url=https://attacker.invalid"><a/href=https://attacker.invalid>leave</a><link rel="prefetch" href="https://attacker.invalid"><link rel="stylesheet" href="../assets/site.css"><script type="module" src="../assets/app.js"></script>');
  await writeFile(join(workspacePath, "assets", "site.css"), "body{color:#123}");
  await writeFile(join(workspacePath, "assets", "app.js"), 'import "./setup.js";');
  await writeFile(join(workspacePath, "assets", "setup.js"), "export const ready = true;");
  const session = { id: "session-parent", attachments: [] };
  try {
    const projected = projectWorkspaceArtifacts({ text: '::codex-inline-vis{file="pages/preview.html"}', session, workspacePath, dataDir });
    const id = projected.match(/id="(artifact-[a-f0-9]{24})"/)?.[1];
    assert.match(projected, new RegExp(`/artifacts/${id}/pages/preview\\.html`));
    const html = (await getWorkspaceArtifact({ session, artifactId: id, dataDir })).data.toString();
    assert.equal(html.includes("http-equiv"), false);
    assert.equal(html.includes("attacker.invalid"), false);
    assert.equal(html.includes("<script"), false);
    assert.equal((await getWorkspaceArtifact({ session, artifactId: id, resourcePath: "assets/site.css", dataDir })).data.toString(), "body{color:#123}");
    assert.equal((await getWorkspaceArtifact({ session, artifactId: id, resourcePath: "assets/setup.js", dataDir })).data.toString(), "export const ready = true;");
  } finally {
    await rm(root, { recursive: true, force: true });
  }
});

test("artifact identity includes relative path and referenced dependency contents", async () => {
  const root = await mkdtemp(join(tmpdir(), "code-remote-manager-artifact-bundle-id-"));
  const workspacePath = join(root, "workspace");
  const dataDir = join(root, "data");
  await mkdir(join(workspacePath, "one"), { recursive: true });
  await mkdir(join(workspacePath, "two"), { recursive: true });
  const html = '<link rel="stylesheet" href="site.css"><h1>Same</h1>';
  await writeFile(join(workspacePath, "one", "preview.html"), html);
  await writeFile(join(workspacePath, "two", "preview.html"), html);
  await writeFile(join(workspacePath, "one", "site.css"), "body{color:red}");
  await writeFile(join(workspacePath, "two", "site.css"), "body{color:red}");
  const session = { id: "session-bundle-id", attachments: [] };
  try {
    const first = projectWorkspaceArtifacts({ text: '::codex-inline-vis{file="one/preview.html"}', session, workspacePath, dataDir });
    const second = projectWorkspaceArtifacts({ text: '::codex-inline-vis{file="two/preview.html"}', session, workspacePath, dataDir });
    await writeFile(join(workspacePath, "one", "site.css"), "body{color:blue}");
    const changedDependency = projectWorkspaceArtifacts({ text: '::codex-inline-vis{file="one/preview.html"}', session, workspacePath, dataDir });
    assert.notEqual(first.match(/id="([^"]+)/)?.[1], second.match(/id="([^"]+)/)?.[1]);
    assert.notEqual(first.match(/id="([^"]+)/)?.[1], changedDependency.match(/id="([^"]+)/)?.[1]);
  } finally {
    await rm(root, { recursive: true, force: true });
  }
});

test("does not project missing, remote, traversal, or outside-workspace targets", async () => {
  const root = await mkdtemp(join(tmpdir(), "code-remote-manager-artifact-boundary-"));
  const workspacePath = join(root, "workspace");
  const dataDir = join(root, "data");
  const outsidePath = join(root, "secret.txt");
  await mkdir(workspacePath, { recursive: true });
  await writeFile(join(workspacePath, "inside.txt"), "inside");
  await writeFile(outsidePath, "secret");
  const session = { id: "session-boundary", attachments: [] };
  const text = [
    "[Missing](missing.txt)",
    "[Remote](https://example.com/file.pdf)",
    "[Traversal](../secret.txt)",
    "[Encoded traversal](..%2Fsecret.txt)",
    "[File URL](file:///C:/private/secret.txt)",
    `[Absolute outside](${outsidePath.replaceAll("\\", "/")})`,
    `[Absolute inside](${join(workspacePath, "inside.txt").replaceAll("\\", "/")})`,
    "::codex-inline-vis{file=\"../secret.txt\"}"
  ].join("\n");

  try {
    assert.equal(projectWorkspaceArtifacts({ text, session, workspacePath, dataDir }), text);
    assert.deepEqual(session.attachments, []);
  } finally {
    await rm(root, { recursive: true, force: true });
  }
});

test("projects links outside inline code on the same line", async () => {
  const root = await mkdtemp(join(tmpdir(), "code-remote-manager-artifact-inline-code-"));
  const workspacePath = join(root, "workspace");
  await mkdir(workspacePath, { recursive: true });
  await writeFile(join(workspacePath, "README.md"), "# Read me");
  const session = { id: "session-inline-code", attachments: [] };
  try {
    const projected = projectWorkspaceArtifacts({ text: "[Read me](README.md) (see `config`)", session, workspacePath, dataDir: join(root, "data") });
    assert.match(projected, /^\[Read me\]\(\/api\/sessions\/session-inline-code\/artifacts\/artifact-/);
    assert.match(projected, /\(see `config`\)$/);
  } finally {
    await rm(root, { recursive: true, force: true });
  }
});

test("does not trust an image extension when file contents are not an image", async () => {
  const root = await mkdtemp(join(tmpdir(), "code-remote-manager-artifact-mime-"));
  const workspacePath = join(root, "workspace");
  await mkdir(workspacePath, { recursive: true });
  await writeFile(join(workspacePath, "fake.png"), "not an image");
  const session = { id: "session-mime", attachments: [] };
  try {
    const text = "![Fake](fake.png)";
    assert.equal(projectWorkspaceArtifacts({ text, session, workspacePath, dataDir: join(root, "data") }), text);
    assert.deepEqual(session.attachments, []);
  } finally {
    await rm(root, { recursive: true, force: true });
  }
});

test("does not activate artifact syntax shown inside Markdown code", async () => {
  const root = await mkdtemp(join(tmpdir(), "code-remote-manager-artifact-code-"));
  const workspacePath = join(root, "workspace");
  await mkdir(workspacePath, { recursive: true });
  await writeFile(join(workspacePath, "preview.html"), "<h1>Preview</h1>");
  const session = { id: "session-code", attachments: [] };
  const text = [
    "```markdown",
    '::codex-inline-vis{file="preview.html"}',
    "[Preview](preview.html)",
    "```",
    '`::codex-inline-vis{file="preview.html"}`'
  ].join("\n");
  try {
    assert.equal(projectWorkspaceArtifacts({ text, session, workspacePath, dataDir: join(root, "data") }), text);
    assert.deepEqual(session.attachments, []);
  } finally {
    await rm(root, { recursive: true, force: true });
  }
});

test("artifact identity follows file contents even when size and timestamp match", async () => {
  const root = await mkdtemp(join(tmpdir(), "code-remote-manager-artifact-content-"));
  const workspacePath = join(root, "workspace");
  const dataDir = join(root, "data");
  const sourcePath = join(workspacePath, "preview.html");
  await mkdir(workspacePath, { recursive: true });
  await writeFile(sourcePath, "<h1>One</h1>");
  const fixedTime = new Date("2026-01-01T00:00:00.000Z");
  await utimes(sourcePath, fixedTime, fixedTime);
  const original = await stat(sourcePath);
  const session = { id: "session-content", attachments: [] };
  try {
    const first = projectWorkspaceArtifacts({ text: '::codex-inline-vis{file="preview.html"}', session, workspacePath, dataDir });
    await writeFile(sourcePath, "<h1>Two</h1>");
    await utimes(sourcePath, original.atime, original.mtime);
    const second = projectWorkspaceArtifacts({ text: '::codex-inline-vis{file="preview.html"}', session, workspacePath, dataDir });
    const secondId = second.match(/id="(artifact-[a-f0-9]{24})"/)?.[1];

    assert.notEqual(first, second);
    assert.match((await getWorkspaceArtifact({ session, artifactId: secondId, dataDir })).data.toString(), /<h1>Two<\/h1>/);
  } finally {
    await rm(root, { recursive: true, force: true });
  }
});

test("reuses a turn-scoped registration without rereading the source file", async () => {
  const root = await mkdtemp(join(tmpdir(), "code-remote-manager-artifact-cache-"));
  const workspacePath = join(root, "workspace");
  const dataDir = join(root, "data");
  const sourcePath = join(workspacePath, "report.pdf");
  await mkdir(workspacePath, { recursive: true });
  await writeFile(sourcePath, "%PDF-safe");
  const session = { id: "session-cache", attachments: [] };
  const registrationCache = new Map();
  try {
    const first = projectWorkspaceArtifacts({ text: "[Report](report.pdf)", session, workspacePath, dataDir, registrationCache });
    await rm(sourcePath);
    const second = projectWorkspaceArtifacts({ text: "[Report](report.pdf) continues", session, workspacePath, dataDir, registrationCache });
    assert.equal(second, `${first} continues`);
  } finally {
    await rm(root, { recursive: true, force: true });
  }
});
