import assert from "node:assert/strict";
import { mkdtemp, mkdir, readFile, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, it } from "node:test";
import {
  buildGeminiWorkspaceToolDeclarations,
  invokeGeminiWorkspaceTool,
  isFcopEnvelopeRelPath,
  isGeminiWorkspaceTool,
  isWriteAllowedRelPath,
  resolveWorkspacePath,
} from "../geminiWorkspaceTools.ts";

describe("geminiWorkspaceTools", () => {
  it("isGeminiWorkspaceTool recognizes native tools", () => {
    assert.equal(isGeminiWorkspaceTool("read_file"), true);
    assert.equal(isGeminiWorkspaceTool("grep_files"), true);
    assert.equal(isGeminiWorkspaceTool("list_dir"), true);
    assert.equal(isGeminiWorkspaceTool("write_file"), true);
    assert.equal(isGeminiWorkspaceTool("web_search"), true);
    assert.equal(isGeminiWorkspaceTool("web_extract"), true);
    assert.equal(isGeminiWorkspaceTool("web_research"), true);
    assert.equal(isGeminiWorkspaceTool("skill_search"), true);
    assert.equal(isGeminiWorkspaceTool("skill_learn"), true);
    assert.equal(isGeminiWorkspaceTool("skill_publish"), true);
    assert.equal(isGeminiWorkspaceTool("fcop_report"), false);
  });

  it("buildGeminiWorkspaceToolDeclarations returns workspace and web research tools by default", () => {
    const decls = buildGeminiWorkspaceToolDeclarations();
    assert.equal(decls.length, 9);
    assert.deepEqual(
      decls.map((d) => d.name),
      ["read_file", "grep_files", "list_dir", "web_search", "web_extract", "web_research", "skill_search", "skill_learn", "skill_publish"],
    );
  });

  it("buildGeminiWorkspaceToolDeclarations adds write_file when requested", () => {
    const decls = buildGeminiWorkspaceToolDeclarations({ includeWriteFile: true });
    assert.equal(decls.length, 10);
    assert.deepEqual(
      decls.map((d) => d.name),
      ["read_file", "grep_files", "list_dir", "web_search", "web_extract", "web_research", "skill_search", "skill_learn", "skill_publish", "write_file"],
    );
  });

  it("buildGeminiWorkspaceToolDeclarations can omit list_dir", () => {
    const decls = buildGeminiWorkspaceToolDeclarations({ includeListDir: false });
    assert.deepEqual(
      decls.map((d) => d.name),
      ["read_file", "grep_files", "web_search", "web_extract", "web_research", "skill_search", "skill_learn", "skill_publish"],
    );
  });

  it("grep_files finds regex matches with line numbers", async () => {
    const tmp = await mkdtemp(path.join(os.tmpdir(), "cfmu-ws-"));
    await mkdir(path.join(tmp, "src"), { recursive: true });
    await writeFile(
      path.join(tmp, "src", "foo.ts"),
      "export const TASK_ID = 'TASK-20260606-001';\n// noop\n",
      "utf8",
    );
    await writeFile(path.join(tmp, "readme.md"), "no match here\n", "utf8");
    const out = await invokeGeminiWorkspaceTool(tmp, "grep_files", {
      pattern: "TASK-20260606",
      path: "src",
    });
    assert.match(out, /src\/foo\.ts:1:/);
    assert.match(out, /TASK-20260606-001/);
  });

  it("grep_files skips node_modules", async () => {
    const tmp = await mkdtemp(path.join(os.tmpdir(), "cfmu-ws-"));
    await mkdir(path.join(tmp, "node_modules", "pkg"), { recursive: true });
    await writeFile(
      path.join(tmp, "node_modules", "pkg", "index.js"),
      "SECRET_TASK_MARKER\n",
      "utf8",
    );
    const out = await invokeGeminiWorkspaceTool(tmp, "grep_files", {
      pattern: "SECRET_TASK_MARKER",
    });
    assert.match(out, /no matches/);
  });

  it("resolveWorkspacePath blocks escape via ..", async () => {
    const tmp = await mkdtemp(path.join(os.tmpdir(), "cfmu-ws-"));
    assert.throws(
      () => resolveWorkspacePath(tmp, "../../../etc/passwd"),
      /escapes project root/,
    );
  });

  it("read_file returns line-numbered content", async () => {
    const tmp = await mkdtemp(path.join(os.tmpdir(), "cfmu-ws-"));
    const file = path.join(tmp, "hello.txt");
    await writeFile(file, "line1\nline2\nline3\n", "utf8");
    const out = await invokeGeminiWorkspaceTool(tmp, "read_file", {
      path: "hello.txt",
      offset: 2,
      limit: 1,
    });
    assert.match(out, /2\|line2/);
  });

  it("list_dir marks directories with trailing slash", async () => {
    const tmp = await mkdtemp(path.join(os.tmpdir(), "cfmu-ws-"));
    await mkdir(path.join(tmp, "skills"));
    await writeFile(path.join(tmp, "readme.md"), "x", "utf8");
    const out = await invokeGeminiWorkspaceTool(tmp, "list_dir", { path: "." });
    assert.match(out, /readme\.md/);
    assert.match(out, /skills\//);
  });

  it("isWriteAllowedRelPath allows governance prefixes only", () => {
    assert.equal(isWriteAllowedRelPath(".codeflowmu/panel-ui-lang.json"), true);
    assert.equal(isWriteAllowedRelPath("docs/skills/foo.json"), true);
    assert.equal(isWriteAllowedRelPath("src/index.ts"), false);
  });

  it("isFcopEnvelopeRelPath blocks lifecycle and legacy fcop paths", () => {
    assert.equal(isFcopEnvelopeRelPath("fcop/_lifecycle/inbox/TASK-x.md"), true);
    assert.equal(isFcopEnvelopeRelPath("fcop/reports/REPORT-x.md"), true);
    assert.equal(isFcopEnvelopeRelPath("fcop/tasks/TASK-x.md"), true);
    assert.equal(isFcopEnvelopeRelPath(".codeflowmu/foo.json"), false);
  });

  it("write_file rejects fcop envelope paths", async () => {
    const tmp = await mkdtemp(path.join(os.tmpdir(), "cfmu-ws-"));
    await mkdir(path.join(tmp, "fcop", "_lifecycle", "inbox"), {
      recursive: true,
    });
    await assert.rejects(
      () =>
        invokeGeminiWorkspaceTool(tmp, "write_file", {
          path: "fcop/_lifecycle/inbox/TASK-20260607-001-ADMIN-to-DEV.md",
          content: "evil",
        }),
      /not allowed/,
    );
  });

  it("write_file rejects paths outside allowlist", async () => {
    const tmp = await mkdtemp(path.join(os.tmpdir(), "cfmu-ws-"));
    await assert.rejects(
      () =>
        invokeGeminiWorkspaceTool(tmp, "write_file", {
          path: "src/evil.ts",
          content: "x",
        }),
      /not allowed/,
    );
  });

  it("read_file resolves TASK-* basename via fcop/_lifecycle", async () => {
    const tmp = await mkdtemp(path.join(os.tmpdir(), "cfmu-ws-"));
    const reviewDir = path.join(tmp, "fcop", "_lifecycle", "review");
    await mkdir(reviewDir, { recursive: true });
    const name = "TASK-20260606-007-ADMIN-to-PM.md";
    await writeFile(
      path.join(reviewDir, name),
      "---\nprotocol: fcop\n---\n# Task body\n",
      "utf8",
    );
    const out = await invokeGeminiWorkspaceTool(tmp, "read_file", {
      path: name,
    });
    assert.match(out, /fcop\/_lifecycle\/review\/TASK-20260606-007-ADMIN-to-PM\.md/);
    assert.match(out, /Task body/);
  });

  it("write_file writes allowlisted governance file", async () => {
    const tmp = await mkdtemp(path.join(os.tmpdir(), "cfmu-ws-"));
    await mkdir(path.join(tmp, ".codeflowmu"), { recursive: true });
    const out = await invokeGeminiWorkspaceTool(tmp, "write_file", {
      path: ".codeflowmu/panel-ui-lang.json",
      content: '{"ok":true}\n',
    });
    assert.match(out, /Wrote .* bytes/);
    const written = await readFile(
      path.join(tmp, ".codeflowmu", "panel-ui-lang.json"),
      "utf8",
    );
    assert.equal(written, '{"ok":true}\n');
  });
});
