import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import {
  mkdirSync,
  mkdtempSync,
  readFileSync,
  rmSync,
  writeFileSync,
} from "node:fs";
import { createServer } from "node:http";
import type { Server } from "node:http";
import { tmpdir } from "node:os";
import { join } from "node:path";

import { resolveCodemapConfig } from "../config";
import { closeDb, createTables, openDb, upsertQueryBaseline } from "../db";
import { initCodemap } from "../runtime";
import { installCodemapTestTeardown } from "../test-helpers/runtime-reset";
import { handleRequest } from "./http-server";
import { MCP_TOOL_NAMES } from "./mcp-tool-allowlist";
import { MCP_TOOL_ANNOTATIONS } from "./mcp-tool-annotations";
import { createManagedWatchSession } from "./session-lifecycle";
import { _resetWatchStateForTests } from "./watcher";

installCodemapTestTeardown();

let benchDir: string;
let serverHandle: { close: () => Promise<void>; port: number } | undefined;

beforeEach(() => {
  benchDir = mkdtempSync(join(tmpdir(), "http-server-"));
  mkdirSync(join(benchDir, "src"), { recursive: true });
  writeFileSync(join(benchDir, "src", "a.ts"), "export const A = 1;\n");
  initCodemap(resolveCodemapConfig(benchDir, undefined));
  const db = openDb();
  try {
    createTables(db);
    db.run(
      "INSERT INTO files (path, content_hash, size, line_count, language, last_modified, indexed_at) VALUES ('src/a.ts', 'h1', 10, 1, 'typescript', 1, 1), ('src/b.ts', 'h2', 10, 1, 'typescript', 1, 1)",
    );
    db.run(
      "INSERT INTO symbols (file_path, name, kind, line_start, line_end, signature, doc_comment) VALUES ('src/a.ts', 'foo', 'function', 1, 5, 'function foo()', NULL), ('src/a.ts', 'bar', 'const', 7, 7, 'const bar = 1', '/** @deprecated */')",
    );
  } finally {
    closeDb(db);
  }
});

afterEach(async () => {
  if (serverHandle !== undefined) {
    await serverHandle.close();
    serverHandle = undefined;
  }
  _resetWatchStateForTests();
  rmSync(benchDir, { recursive: true, force: true });
});

/**
 * Boot a test HTTP server. Calls the exported `handleRequest` directly
 * (skipping `runHttpServer`'s SIGINT-awaiting outer loop, which the test
 * runner can't drive). Port 0 → OS picks a free one — avoids collisions
 * across parallel test files.
 */
async function startServer(
  opts: {
    token?: string | undefined;
  } = {},
): Promise<{ port: number; close: () => Promise<void> }> {
  const serverRef: Server = createServer((req, res) => {
    void handleRequest(req, res, {
      version: "0.0.0-test",
      root: benchDir,
      host: "127.0.0.1",
      port: 0,
      token: opts.token,
    }).catch((err: unknown) => {
      const msg = err instanceof Error ? err.message : String(err);
      res.statusCode = 500;
      res.end(JSON.stringify({ error: msg }));
    });
  });
  await new Promise<void>((resolve, reject) => {
    serverRef.once("error", reject);
    serverRef.listen(0, "127.0.0.1", () => resolve());
  });
  const addr = serverRef.address();
  if (typeof addr !== "object" || addr === null) {
    throw new Error("expected AddressInfo");
  }
  return {
    port: addr.port,
    close: () =>
      new Promise<void>((resolve) => {
        serverRef.close(() => resolve());
      }),
  };
}

async function postTool(
  port: number,
  name: string,
  body: unknown,
  opts: { token?: string } = {},
): Promise<{ status: number; json: any; headers: Headers }> {
  const headers: Record<string, string> = {
    "Content-Type": "application/json",
  };
  if (opts.token !== undefined)
    headers["Authorization"] = `Bearer ${opts.token}`;
  const r = await fetch(`http://127.0.0.1:${port}/tool/${name}`, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
  });
  return {
    status: r.status,
    headers: r.headers,
    json: r.headers.get("content-type")?.includes("json")
      ? await r.json()
      : await r.text(),
  };
}

describe("http-server — health + tools catalog", () => {
  it("GET /health returns ok + version", async () => {
    serverHandle = await startServer();
    const r = await fetch(`http://127.0.0.1:${serverHandle.port}/health`);
    expect(r.status).toBe(200);
    expect(r.headers.get("X-Codemap-Version")).toBe("0.0.0-test");
    const body = (await r.json()) as { ok: boolean; version: string };
    expect(body.ok).toBe(true);
    expect(body.version).toBe("0.0.0-test");
  });

  it("GET /tools returns the catalog with annotation hints", async () => {
    serverHandle = await startServer();
    const r = await fetch(`http://127.0.0.1:${serverHandle.port}/tools`);
    const body = (await r.json()) as {
      tools: {
        name: string;
        readOnlyHint: boolean;
        destructiveHint: boolean;
        idempotentHint: boolean;
      }[];
    };
    expect(body.tools.map((t) => t.name)).toContain("query");
    expect(body.tools.map((t) => t.name)).toContain("audit");
    expect(body.tools.map((t) => t.name)).toContain("affected");
    expect(body.tools.map((t) => t.name)).toContain("trace");
    const query = body.tools.find((t) => t.name === "query");
    const apply = body.tools.find((t) => t.name === "apply");
    expect(query).toMatchObject({
      readOnlyHint: true,
      destructiveHint: false,
      idempotentHint: true,
    });
    expect(apply).toMatchObject({
      readOnlyHint: false,
      destructiveHint: true,
      idempotentHint: false,
    });
    expect(body.tools).toHaveLength(MCP_TOOL_NAMES.length);
    for (const name of MCP_TOOL_NAMES) {
      const entry = body.tools.find((t) => t.name === name);
      expect(entry).toMatchObject(MCP_TOOL_ANNOTATIONS[name]);
    }
  });

  it("404 for unknown route", async () => {
    serverHandle = await startServer();
    const r = await fetch(`http://127.0.0.1:${serverHandle.port}/nope`);
    expect(r.status).toBe(404);
  });
});

describe("http-server — managed watch session", () => {
  it("acquires a watch client for tool routes but not /health", async () => {
    let backendStarted = false;
    const session = createManagedWatchSession({
      root: benchDir,
      excludeDirNames: new Set(["node_modules", ".git", "dist"]),
      recipesWatchPrefix: ".codemap/recipes/",
      debounceMs: 0,
      onChange: () => {},
      releaseGraceMs: 25,
      backend: {
        start() {
          backendStarted = true;
        },
        async stop() {},
      },
    });

    const serverRef: Server = createServer((req, res) => {
      void handleRequest(req, res, {
        version: "0.0.0-test",
        root: benchDir,
        host: "127.0.0.1",
        port: 0,
        token: undefined,
        managedWatchSession: session,
      }).catch((err: unknown) => {
        const msg = err instanceof Error ? err.message : String(err);
        res.statusCode = 500;
        res.end(JSON.stringify({ error: msg }));
      });
    });
    await new Promise<void>((resolve, reject) => {
      serverRef.once("error", reject);
      serverRef.listen(0, "127.0.0.1", () => resolve());
    });
    const addr = serverRef.address();
    if (typeof addr !== "object" || addr === null) {
      throw new Error("expected AddressInfo");
    }

    try {
      expect(session.isWatching()).toBe(false);
      const health = await fetch(`http://127.0.0.1:${addr.port}/health`);
      expect(health.status).toBe(200);
      expect(session.clientCount()).toBe(0);
      expect(session.isWatching()).toBe(false);
      expect(backendStarted).toBe(false);

      const tool = await postTool(addr.port, "query", {
        sql: "SELECT COUNT(*) AS count FROM symbols",
      });
      expect(tool.status).toBe(200);
      expect(session.clientCount()).toBe(0);
      expect(backendStarted).toBe(true);
      expect(session.isWatching()).toBe(true);

      await Bun.sleep(50);
      await session.forceStop();
    } finally {
      await new Promise<void>((resolve) => {
        serverRef.close(() => resolve());
      });
    }
  });
});

describe("http-server — POST /tool/query", () => {
  it("returns row array for ad-hoc SQL", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "query", {
      sql: "SELECT name, kind FROM symbols ORDER BY name",
    });
    expect(r.status).toBe(200);
    expect(r.headers.get("content-type")).toContain("application/json");
    expect(r.headers.get("X-Codemap-Pending-Sync")).toBe("false");
    expect(r.headers.get("X-Codemap-Commit-Drift")).toBe("false");
    expect(r.json).toEqual([
      { name: "bar", kind: "const" },
      { name: "foo", kind: "function" },
    ]);
  });

  it("returns 400 + {error} for bad SQL", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "query", {
      sql: "SELECT * FROM nonexistent",
    });
    expect(r.status).toBe(400);
    expect(r.json.error).toContain("nonexistent");
  });

  it("returns 400 for invalid JSON body", async () => {
    serverHandle = await startServer();
    const r = await fetch(`http://127.0.0.1:${serverHandle.port}/tool/query`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: "not valid json",
    });
    expect(r.status).toBe(400);
    const body = (await r.json()) as { error: string };
    expect(body.error).toContain("invalid JSON body");
  });

  it("format=sarif returns application/sarif+json", async () => {
    serverHandle = await startServer();
    const r = await fetch(`http://127.0.0.1:${serverHandle.port}/tool/query`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        sql: "SELECT name, file_path, line_start FROM symbols WHERE name = 'bar'",
        format: "sarif",
      }),
    });
    expect(r.status).toBe(200);
    expect(r.headers.get("content-type")).toContain("application/sarif+json");
    const doc = (await r.json()) as { version: string };
    expect(doc.version).toBe("2.1.0");
  });

  it("format=codeclimate returns application/json", async () => {
    serverHandle = await startServer();
    const r = await fetch(`http://127.0.0.1:${serverHandle.port}/tool/query`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        sql: "SELECT name, file_path, line_start FROM symbols WHERE name = 'bar'",
        format: "codeclimate",
      }),
    });
    expect(r.status).toBe(200);
    expect(r.headers.get("content-type")).toContain("application/json");
    const issues = (await r.json()) as unknown[];
    expect(Array.isArray(issues)).toBe(true);
    expect(issues.length).toBeGreaterThan(0);
  });

  it("format=badge markdown returns text/plain", async () => {
    serverHandle = await startServer();
    const r = await fetch(`http://127.0.0.1:${serverHandle.port}/tool/query`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        sql: "SELECT name, file_path, line_start FROM symbols WHERE name = 'bar'",
        format: "badge",
      }),
    });
    expect(r.status).toBe(200);
    expect(r.headers.get("content-type")).toContain("text/plain");
    expect(await r.text()).toBe("codemap: 1 issue");
  });

  it("format=badge badge_style=json returns application/json", async () => {
    serverHandle = await startServer();
    const r = await fetch(`http://127.0.0.1:${serverHandle.port}/tool/query`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        sql: "SELECT name, file_path, line_start FROM symbols WHERE name = 'bar'",
        format: "badge",
        badge_style: "json",
      }),
    });
    expect(r.status).toBe(200);
    expect(r.headers.get("content-type")).toContain("application/json");
    const doc = (await r.json()) as { schema: string; count: number };
    expect(doc.schema).toBe("codemap-badge/v1");
    expect(doc.count).toBe(1);
  });

  it("format=annotations returns text/plain", async () => {
    serverHandle = await startServer();
    const r = await fetch(`http://127.0.0.1:${serverHandle.port}/tool/query`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        sql: "SELECT name, file_path, line_start FROM symbols WHERE name = 'bar'",
        format: "annotations",
      }),
    });
    expect(r.status).toBe(200);
    expect(r.headers.get("content-type")).toContain("text/plain");
    const text = await r.text();
    expect(text).toMatch(/^::notice file=src\/a\.ts,line=7::bar/);
  });
});

describe("http-server — POST /tool/{other tools}", () => {
  it("query_batch returns N envelopes", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "query_batch", {
      statements: [{ sql: "SELECT 1 AS n" }, { sql: "SELECT 2 AS n" }],
    });
    expect(r.status).toBe(200);
    expect(r.json).toEqual([[{ n: 1 }], [{ n: 2 }]]);
  });

  it("query_recipe routes to the recipe SQL", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "query_recipe", {
      recipe: "deprecated-symbols",
    });
    expect(r.status).toBe(200);
    expect(Array.isArray(r.json)).toBe(true);
    expect(r.json[0]).toMatchObject({ name: "bar" });
  });

  it("query_recipe returns 404 for unknown recipe (not 400 — semantics matter)", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "query_recipe", {
      recipe: "does-not-exist",
    });
    expect(r.status).toBe(404);
    expect(r.json.error).toContain("does-not-exist");
  });

  it("context returns the bootstrap envelope", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "context", { compact: true });
    expect(r.status).toBe(200);
    expect(r.json.codemap.schema_version).toBeGreaterThan(0);
  });

  it("context returns start_here and include_snippets on full envelope", async () => {
    writeFileSync(
      join(benchDir, "src", "snippet.ts"),
      "export const SNIP = 1;\n",
    );
    const db = openDb();
    try {
      db.run(
        "INSERT INTO files (path, content_hash, size, line_count, language, last_modified, indexed_at) VALUES ('src/snippet.ts', 'hs', 10, 1, 'typescript', 1, 1)",
      );
      db.run(
        "INSERT INTO symbols (file_path, name, kind, line_start, line_end, signature, is_exported, is_default_export, members, doc_comment, value, parent_name, visibility, complexity, name_column_start, name_column_end, scope_local_id, body_line_count, param_count, nesting_depth, return_type, is_async, is_generator) VALUES ('src/snippet.ts', 'SNIP', 'const', 1, 1, 'export const SNIP = 1', 1, 0, NULL, NULL, NULL, NULL, NULL, NULL, 0, 0, 0, NULL, NULL, NULL, NULL, 0, 0)",
      );
      db.run(
        "INSERT INTO dependencies (from_path, to_path) VALUES ('src/b.ts', 'src/snippet.ts')",
      );
    } finally {
      closeDb(db);
    }

    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "context", {
      include_snippets: true,
    });
    expect(r.status).toBe(200);
    expect(r.json.start_here?.index_summary).toBeDefined();
    const leader = r.json.start_here?.hub_leaders?.find(
      (h: { file_path: string }) => h.file_path === "src/snippet.ts",
    );
    expect(leader?.signatures?.[0]?.snippet).toContain("export const SNIP");
  });

  it("context includes map_id and codebase_map by default", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "context", {});
    expect(r.status).toBe(200);
    expect(r.json.map_id).toMatch(/^[0-9a-f]{16}$/);
    expect(r.json.codebase_map?.cli_entry_hints?.length).toBe(12);
  });

  it("context omits map fields when include_codebase_map is false", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "context", {
      include_codebase_map: false,
    });
    expect(r.status).toBe(200);
    expect(r.json.start_here).toBeDefined();
    expect(r.json.map_id).toBeUndefined();
    expect(r.json.codebase_map).toBeUndefined();
  });

  it("validate returns staleness rows", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "validate", {});
    expect(r.status).toBe(200);
    expect(Array.isArray(r.json)).toBe(true);
  });

  it("show returns matches envelope", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "show", { name: "foo" });
    expect(r.status).toBe(200);
    expect(r.json.matches).toHaveLength(1);
    expect(r.json.matches[0].name).toBe("foo");
  });

  it("snippet returns matches with source/stale/missing fields", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "snippet", { name: "foo" });
    expect(r.status).toBe(200);
    expect(r.json.matches[0]).toHaveProperty("stale");
    expect(r.json.matches[0]).toHaveProperty("missing");
  });

  it("show with query fast tier returns exact name matches", async () => {
    const db = openDb();
    try {
      db.run(
        `INSERT INTO symbols (file_path, name, kind, line_start, line_end, signature, doc_comment)
         VALUES ('src/a.ts', 'AuthService', 'class', 1, 1, 'class AuthService', NULL)`,
      );
    } finally {
      closeDb(db);
    }
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "show", {
      query: "name:AuthService",
    });
    expect(r.status).toBe(200);
    expect(r.json.matches).toHaveLength(1);
    expect(r.json.matches[0].name).toBe("AuthService");
  });

  it("show with query returns field-qualified substring matches", async () => {
    const db = openDb();
    try {
      db.run(
        `INSERT INTO symbols (file_path, name, kind, line_start, line_end, signature, doc_comment)
         VALUES ('src/a.ts', 'AuthService', 'class', 1, 1, 'class AuthService', NULL)`,
      );
    } finally {
      closeDb(db);
    }
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "show", {
      query: "kind:class name:Auth",
    });
    expect(r.status).toBe(200);
    expect(r.json.matches).toHaveLength(1);
    expect(r.json.matches[0].name).toBe("AuthService");
  });

  it("show with query returns empty matches when nothing matches", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "show", {
      query: "name:DefinitelyNotIndexed",
    });
    expect(r.status).toBe(200);
    expect(r.json.matches).toEqual([]);
  });

  it("show with with_fts and empty source_fts returns warning", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "show", {
      query: "secretToken",
      with_fts: true,
    });
    expect(r.status).toBe(200);
    expect(r.json.matches).toEqual([]);
    expect(r.json.warning).toContain("source_fts is empty");
  });

  it("snippet with query returns source for matches", async () => {
    const db = openDb();
    try {
      db.run(
        `INSERT INTO symbols (file_path, name, kind, line_start, line_end, signature, doc_comment)
         VALUES ('src/a.ts', 'AuthService', 'class', 1, 1, 'class AuthService', NULL)`,
      );
    } finally {
      closeDb(db);
    }
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "snippet", {
      query: "kind:class name:Auth",
    });
    expect(r.status).toBe(200);
    expect(r.json.matches).toHaveLength(1);
    expect(r.json.matches[0].name).toBe("AuthService");
    expect(r.json.matches[0].source).toBeDefined();
  });

  it("impact returns the {target, matches, summary} envelope", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "impact", {
      target: "foo",
      direction: "down",
    });
    expect(r.status).toBe(200);
    expect(r.json.target).toMatchObject({ kind: "symbol", name: "foo" });
    expect(r.json.summary).toBeDefined();
    expect(Array.isArray(r.json.matches)).toBe(true);
  });

  it("affected returns transitive test paths for explicit paths", async () => {
    const db = openDb();
    try {
      db.run(
        `INSERT INTO files (path, content_hash, size, line_count, language, last_modified, indexed_at)
         VALUES ('src/lib/util.ts', 'h3', 10, 1, 'typescript', 1, 1),
                ('src/__tests__/util.test.ts', 'h4', 10, 1, 'typescript', 1, 1)`,
      );
      db.run(
        `INSERT INTO dependencies (from_path, to_path)
         VALUES ('src/__tests__/util.test.ts', 'src/lib/util.ts')`,
      );
      db.run(
        `INSERT INTO test_suites (file_path, name, kind, line_start, line_end, framework)
         VALUES ('src/__tests__/util.test.ts', 'util', 'describe', 1, 10, 'bun-test')`,
      );
    } finally {
      closeDb(db);
    }
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "affected", {
      paths: ["src/lib/util.ts"],
    });
    expect(r.status).toBe(200);
    expect(r.json).toEqual([
      {
        test_path: "src/__tests__/util.test.ts",
        impact_depth: 1,
        actions: [
          {
            type: "run-affected-tests",
            description:
              "Test file paths only — CI composes the exit policy and runner command.",
          },
        ],
      },
    ]);
  });

  it("affected with non-integer max_depth → 400 (Zod rejects)", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "affected", {
      paths: ["src/a.ts"],
      max_depth: 1.5,
    });
    expect(r.status).toBe(400);
    expect(r.json.error).toContain('"affected"');
  });

  it("affected with paths: [] returns empty array", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "affected", { paths: [] });
    expect(r.status).toBe(200);
    expect(r.json).toEqual([]);
  });

  it("does not record recency when paths: []", async () => {
    serverHandle = await startServer();
    const dbBefore = openDb();
    let before = 0;
    try {
      before =
        dbBefore
          .query<{ run_count: number }>(
            "SELECT run_count FROM recipe_recency WHERE recipe_id = 'affected-tests'",
          )
          .get()?.run_count ?? 0;
    } finally {
      closeDb(dbBefore);
    }
    const r = await postTool(serverHandle.port, "affected", { paths: [] });
    expect(r.status).toBe(200);
    const dbAfter = openDb();
    try {
      const after =
        dbAfter
          .query<{ run_count: number }>(
            "SELECT run_count FROM recipe_recency WHERE recipe_id = 'affected-tests'",
          )
          .get()?.run_count ?? 0;
      expect(after).toBe(before);
    } finally {
      closeDb(dbAfter);
    }
  });

  it("records recipe recency after a successful affected run", async () => {
    const db = openDb();
    try {
      db.run(
        `INSERT INTO files (path, content_hash, size, line_count, language, last_modified, indexed_at)
         VALUES ('src/lib/util.ts', 'h3', 10, 1, 'typescript', 1, 1),
                ('src/__tests__/util.test.ts', 'h4', 10, 1, 'typescript', 1, 1)`,
      );
      db.run(
        `INSERT INTO dependencies (from_path, to_path)
         VALUES ('src/__tests__/util.test.ts', 'src/lib/util.ts')`,
      );
      db.run(
        `INSERT INTO test_suites (file_path, name, kind, line_start, line_end, framework)
         VALUES ('src/__tests__/util.test.ts', 'util', 'describe', 1, 10, 'bun-test')`,
      );
    } finally {
      closeDb(db);
    }
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "affected", {
      paths: ["src/lib/util.ts"],
    });
    expect(r.status).toBe(200);
    const dbAfter = openDb();
    try {
      const row = dbAfter
        .query<{ run_count: number }>(
          "SELECT run_count FROM recipe_recency WHERE recipe_id = 'affected-tests'",
        )
        .get();
      expect(row?.run_count).toBeGreaterThanOrEqual(1);
    } finally {
      closeDb(dbAfter);
    }
  });

  it("affected git error uses changed_since label", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "affected", {
      changed_since: "not-a-real-ref-xyz",
    });
    expect(r.status).toBe(400);
    expect(r.json.error).toContain("changed_since");
    expect(r.json.error).not.toContain("--changed-since");
  });

  function seedTraceGraph() {
    writeFileSync(
      join(benchDir, "src", "trace.ts"),
      "export function alpha() {\n  return beta();\n}\nexport function beta() {\n  return 1;\n}\n",
    );
    const db = openDb();
    try {
      db.run(
        `INSERT INTO files (path, content_hash, size, line_count, language, last_modified, indexed_at)
         VALUES ('src/trace.ts', 'ht', 100, 6, 'typescript', 1, 1)`,
      );
      db.run(
        `INSERT INTO symbols (name, kind, file_path, line_start, line_end, signature, is_exported, parent_name, visibility)
         VALUES ('alpha', 'function', 'src/trace.ts', 1, 3, 'alpha()', 1, NULL, 'export'),
                ('beta', 'function', 'src/trace.ts', 4, 6, 'beta()', 1, NULL, 'export')`,
      );
      db.run(
        `INSERT INTO calls (file_path, caller_name, caller_scope, callee_name, line_start, column_start, column_end)
         VALUES ('src/trace.ts', 'alpha', 'alpha', 'beta', 2, 0, 0)`,
      );
    } finally {
      closeDb(db);
    }
  }

  it("trace returns path and snippets", async () => {
    seedTraceGraph();
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "trace", {
      from: "alpha",
      to: "beta",
    });
    expect(r.status).toBe(200);
    expect(r.json.path).toHaveLength(1);
    expect(r.json.snippets.length).toBeGreaterThan(0);
    expect(r.json.truncated).toBe(false);
  });

  it("explore merges neighborhoods", async () => {
    seedTraceGraph();
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "explore", {
      names: ["alpha", "beta"],
    });
    expect(r.status).toBe(200);
    expect(r.json.names).toEqual(["alpha", "beta"]);
    expect(r.json.rows.length).toBeGreaterThan(0);
  });

  it("node returns center + neighborhood", async () => {
    seedTraceGraph();
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "node", {
      name: "alpha",
      include_snippets: true,
    });
    expect(r.status).toBe(200);
    expect(r.json.center.matches[0]?.name).toBe("alpha");
    expect(
      r.json.neighborhood.some((row: { name: string }) => row.name === "beta"),
    ).toBe(true);
  });

  it("trace with non-integer max_depth → 400 (Zod rejects)", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "trace", {
      from: "a",
      to: "b",
      max_depth: 1.5,
    });
    expect(r.status).toBe(400);
    expect(r.json.error).toContain('"trace"');
  });

  it("explore with empty names → 400 (Zod rejects)", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "explore", { names: [] });
    expect(r.status).toBe(400);
    expect(r.json.error).toContain('"explore"');
  });

  it("trace sets truncated when budget_chars is tiny", async () => {
    seedTraceGraph();
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "trace", {
      from: "alpha",
      to: "beta",
      budget_chars: 1,
    });
    expect(r.status).toBe(200);
    expect(r.json.truncated).toBe(true);
    expect(r.json.truncation?.snippets).toBe(true);
  });

  it("records recipe recency after trace", async () => {
    seedTraceGraph();
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "trace", {
      from: "alpha",
      to: "beta",
    });
    expect(r.status).toBe(200);
    const db = openDb();
    try {
      const row = db
        .query<{ run_count: number }>(
          "SELECT run_count FROM recipe_recency WHERE recipe_id = 'call-path'",
        )
        .get();
      expect(row?.run_count).toBeGreaterThanOrEqual(1);
    } finally {
      closeDb(db);
    }
  });

  it("records recipe recency after explore", async () => {
    seedTraceGraph();
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "explore", {
      names: ["alpha"],
    });
    expect(r.status).toBe(200);
    const db = openDb();
    try {
      const row = db
        .query<{ run_count: number }>(
          "SELECT run_count FROM recipe_recency WHERE recipe_id = 'symbol-neighborhood'",
        )
        .get();
      expect(row?.run_count).toBeGreaterThanOrEqual(1);
    } finally {
      closeDb(db);
    }
  });

  it("list_baselines returns array (empty when none saved)", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "list_baselines", {});
    expect(r.status).toBe(200);
    expect(Array.isArray(r.json)).toBe(true);
  });

  it("unknown tool returns 404 (not 501)", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "bogus", {});
    expect(r.status).toBe(404);
    expect(r.json.error).toContain("unknown tool");
  });

  it("drop_baseline returns 404 for unknown name (not 400)", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "drop_baseline", {
      name: "does-not-exist",
    });
    expect(r.status).toBe(404);
    expect(r.json.error).toContain("does-not-exist");
  });

  it("ingest_coverage returns 400 when path is missing", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "ingest_coverage", {
      path: "no-such/coverage-final.json",
    });
    expect(r.status).toBe(400);
    expect(r.json.error).toContain("path not found");
  });

  it("query with missing baseline returns 404", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "query", {
      sql: "SELECT 1",
      baseline: "does-not-exist",
    });
    expect(r.status).toBe(404);
    expect(r.json.error).toContain("does-not-exist");
  });

  it("query with baseline diff returns 200 envelope", async () => {
    const db = openDb();
    try {
      upsertQueryBaseline(db, {
        name: "snap-files",
        recipe_id: null,
        sql: "SELECT path FROM files ORDER BY path",
        rows_json: JSON.stringify([{ path: "src/a.ts" }]),
        row_count: 1,
        git_ref: null,
        created_at: 1,
      });
    } finally {
      closeDb(db);
    }
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "query", {
      sql: "SELECT path FROM files ORDER BY path",
      baseline: "snap-files",
    });
    expect(r.status).toBe(200);
    expect(r.json).toMatchObject({
      added: [{ path: "src/b.ts" }],
      removed: [],
    });
  });

  it("query rejects baseline + group_by with 400", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "query", {
      sql: "SELECT path FROM files",
      baseline: "snap",
      group_by: "directory",
    });
    expect(r.status).toBe(400);
    expect(r.json.error).toContain("group_by");
  });

  it("query with corrupt baseline rows_json returns 400", async () => {
    const db = openDb();
    try {
      upsertQueryBaseline(db, {
        name: "bad",
        recipe_id: null,
        sql: "SELECT 1",
        rows_json: "not-json",
        row_count: 0,
        git_ref: null,
        created_at: 1,
      });
    } finally {
      closeDb(db);
    }
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "query", {
      sql: "SELECT 1",
      baseline: "bad",
    });
    expect(r.status).toBe(400);
    expect(r.json.error).toContain("corrupt rows_json");
  });

  it("ingest_churn returns 400 when path is missing", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "ingest_churn", {
      path: "no-such/churn.json",
    });
    expect(r.status).toBe(400);
    expect(r.json.error).toContain("churn file not found");
  });

  it("ingest_churn ingests churn JSON successfully", async () => {
    const churnDir = join(benchDir, "fixtures");
    mkdirSync(churnDir);
    writeFileSync(
      join(churnDir, "churn.json"),
      JSON.stringify([
        {
          file_path: "src/a.ts",
          commit_count: 5,
          weighted_commits: 4,
          lines_added: 10,
          lines_removed: 2,
          last_commit_at: "2026-06-01T00:00:00Z",
          churn_trend: "stable",
          computed_at: "2026-06-10T00:00:00Z",
        },
      ]),
    );
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "ingest_churn", {
      path: "fixtures/churn.json",
    });
    expect(r.status).toBe(200);
    expect(r.json).toMatchObject({
      ingested: 1,
      skipped_unindexed: 0,
    });
  });

  it("ingest_coverage ingests istanbul artifact successfully", async () => {
    const db = openDb();
    try {
      db.run(
        `INSERT INTO symbols (file_path, name, kind, line_start, line_end, signature, is_exported, is_default_export)
         VALUES ('src/a.ts', 'A', 'const', 1, 1, 'const A', 1, 0)`,
      );
    } finally {
      closeDb(db);
    }
    const coverageDir = join(benchDir, "coverage");
    mkdirSync(coverageDir);
    writeFileSync(
      join(coverageDir, "coverage-final.json"),
      JSON.stringify({
        [`${benchDir}/src/a.ts`]: {
          path: `${benchDir}/src/a.ts`,
          statementMap: {
            "0": { start: { line: 1, column: 0 }, end: { line: 1, column: 1 } },
          },
          s: { "0": 1 },
        },
      }),
    );
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "ingest_coverage", {
      path: "coverage/coverage-final.json",
    });
    expect(r.status).toBe(200);
    expect(r.json).toMatchObject({
      format: "istanbul",
      ingested: { symbols: 1 },
    });
  });

  it("query_recipe with missing baseline returns 404", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "query_recipe", {
      recipe: "deprecated-symbols",
      baseline: "does-not-exist",
    });
    expect(r.status).toBe(404);
    expect(r.json.error).toContain("does-not-exist");
  });

  it("save_baseline returns 404 for unknown recipe", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "save_baseline", {
      name: "x",
      recipe: "does-not-exist",
    });
    expect(r.status).toBe(404);
  });
});

describe("http-server — IPv6 host bracketing", () => {
  // Regression test for CodeRabbit finding: opts.host like '::1' was
  // interpolated raw into `http://${host}:${port}`, throwing in
  // new URL(). The fix wraps IPv6 literals in brackets per RFC 3986.
  it("does not throw for IPv6 host in opts (the URL parse path)", async () => {
    // We don't actually bind to ::1 in test (CI loopback config varies);
    // this just exercises the URL-construction path with an IPv6-looking
    // opts.host value to confirm new URL() doesn't throw.
    // The real bind-and-serve smoke would need a CI matrix with IPv6.
    const { createServer } = await import("node:http");
    const { handleRequest } = await import("./http-server");
    const server = createServer((req, res) => {
      void handleRequest(req, res, {
        version: "0.0.0-test",
        root: benchDir,
        host: "::1", // would have thrown before the fix
        port: 7879,
        token: undefined,
      }).catch((err: unknown) => {
        res.statusCode = 500;
        res.end(String(err));
      });
    });
    await new Promise<void>((resolve, reject) => {
      server.once("error", reject);
      server.listen(0, "127.0.0.1", () => resolve());
    });
    const addr = server.address();
    if (typeof addr !== "object" || addr === null) {
      throw new Error("expected AddressInfo");
    }
    const r = await fetch(`http://127.0.0.1:${addr.port}/health`);
    expect(r.status).toBe(200);
    await new Promise<void>((resolve) => server.close(() => resolve()));
  });
});

describe("http-server — Zod input validation at HTTP boundary", () => {
  // CodeRabbit caught: HTTP path was casting to `any` and forwarding
  // unvalidated bodies to handlers. These tests lock the per-tool
  // safeParse step that now runs before each dispatch.

  it("query without sql → 400 with structured error", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "query", {});
    expect(r.status).toBe(400);
    expect(r.json.error).toContain("sql");
    expect(r.json.error).toContain('"query"');
  });

  it("query with sql=number → 400 with type-mismatch error", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "query", { sql: 42 });
    expect(r.status).toBe(400);
    expect(r.json.error).toContain("sql");
  });

  it("show with name=number → 400 (not deep handler crash)", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "show", { name: 1 });
    expect(r.status).toBe(400);
    expect(r.json.error).toContain("name");
  });

  it("query_recipe without recipe → 400", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "query_recipe", {});
    expect(r.status).toBe(400);
    expect(r.json.error).toContain("recipe");
  });

  it("save_baseline without name → 400", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "save_baseline", {
      sql: "SELECT 1",
    });
    expect(r.status).toBe(400);
    expect(r.json.error).toContain("name");
  });

  it("query_batch with empty statements → 400 (.min(1) fires)", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "query_batch", {
      statements: [],
    });
    expect(r.status).toBe(400);
    expect(r.json.error).toContain("statements");
  });

  it("validation error message names the offending tool + path", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "snippet", { name: 1 });
    expect(r.status).toBe(400);
    expect(r.json.error).toContain('"snippet"');
    expect(r.json.error).toContain("name");
  });

  it("audit rejects base + baseline_prefix combo (mutually exclusive)", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "audit", {
      base: "origin/main",
      baseline_prefix: "v1",
      no_index: true,
    });
    // ToolResult error → 400 by default; mutual-exclusion error has no
    // dedicated status, so it falls through to 400.
    expect(r.status).toBe(400);
    expect(r.json.error).toContain("mutually exclusive");
  });

  it("audit --base in non-git project errors cleanly", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "audit", {
      base: "HEAD~1",
      no_index: true,
    });
    expect(r.status).toBe(400);
    expect(r.json.error).toContain("requires a git repository");
  });

  it("impact without target → 400 with structured error", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "impact", {});
    expect(r.status).toBe(400);
    expect(r.json.error).toContain('"impact"');
  });

  it("impact with non-integer depth → 400 (Zod rejects)", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "impact", {
      target: "foo",
      depth: 1.5,
    });
    expect(r.status).toBe(400);
  });

  it("impact with unknown direction → 400 (Zod enum)", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "impact", {
      target: "foo",
      direction: "sideways",
    });
    expect(r.status).toBe(400);
  });

  it("trace without from → 400 with structured error", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "trace", { to: "bar" });
    expect(r.status).toBe(400);
    expect(r.json.error).toContain('"trace"');
    expect(r.json.error).toContain("from");
  });

  it("trace without to → 400 with structured error", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "trace", { from: "foo" });
    expect(r.status).toBe(400);
    expect(r.json.error).toContain('"trace"');
    expect(r.json.error).toContain("to");
  });

  it("node with name=number → 400 (not deep handler crash)", async () => {
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "node", { name: 1 });
    expect(r.status).toBe(400);
    expect(r.json.error).toContain("name");
  });
});

describe("http-server — GET /resources", () => {
  it("GET /resources returns the catalog", async () => {
    serverHandle = await startServer();
    const r = await fetch(`http://127.0.0.1:${serverHandle.port}/resources`);
    expect(r.status).toBe(200);
    const body = (await r.json()) as { resources: { uri: string }[] };
    const uris = body.resources.map((x) => x.uri);
    expect(uris).toContain("codemap://recipes");
    expect(uris).toContain("codemap://schema");
    expect(uris).toContain("codemap://skill");
    expect(uris).toContain("codemap://rule");
  });

  it("GET /resources/{encoded uri} returns the recipes catalog", async () => {
    serverHandle = await startServer();
    const r = await fetch(
      `http://127.0.0.1:${serverHandle.port}/resources/${encodeURIComponent("codemap://recipes")}`,
    );
    expect(r.status).toBe(200);
    expect(r.headers.get("content-type")).toContain("application/json");
    const body = (await r.json()) as { id: string }[];
    expect(Array.isArray(body)).toBe(true);
  });

  it("GET /resources/{encoded uri} returns one recipe by id", async () => {
    serverHandle = await startServer();
    const r = await fetch(
      `http://127.0.0.1:${serverHandle.port}/resources/${encodeURIComponent("codemap://recipes/fan-out")}`,
    );
    expect(r.status).toBe(200);
    const body = (await r.json()) as { id: string };
    expect(body.id).toBe("fan-out");
  });

  it("GET /resources/{encoded uri} returns the schema DDL", async () => {
    serverHandle = await startServer();
    const r = await fetch(
      `http://127.0.0.1:${serverHandle.port}/resources/${encodeURIComponent("codemap://schema")}`,
    );
    expect(r.status).toBe(200);
    const body = (await r.json()) as { name: string; ddl: string }[];
    expect(body.find((t) => t.name === "files")).toBeDefined();
  });

  it("GET /resources/{encoded uri} 404s on unknown URI", async () => {
    serverHandle = await startServer();
    const r = await fetch(
      `http://127.0.0.1:${serverHandle.port}/resources/${encodeURIComponent("codemap://nope")}`,
    );
    expect(r.status).toBe(404);
  });

  it("GET /resources/{encoded uri} 404s on unknown recipe id", async () => {
    serverHandle = await startServer();
    const r = await fetch(
      `http://127.0.0.1:${serverHandle.port}/resources/${encodeURIComponent("codemap://recipes/does-not-exist")}`,
    );
    expect(r.status).toBe(404);
  });
});

describe("http-server — CSRF + DNS-rebinding guard", () => {
  // Threat model documented on csrfCheck() in http-server.ts. The test
  // matrix covers every headers-set combination a malicious local webpage
  // could produce vs every legitimate non-browser client (curl, fetch
  // from Node, MCP hosts, CI scripts).

  it("rejects POST with Sec-Fetch-Site: cross-site (modern-browser CSRF)", async () => {
    serverHandle = await startServer();
    const r = await fetch(`http://127.0.0.1:${serverHandle.port}/tool/query`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Sec-Fetch-Site": "cross-site",
      },
      body: JSON.stringify({ sql: "SELECT 1" }),
    });
    expect(r.status).toBe(403);
    const body = (await r.json()) as { error: string };
    expect(body.error).toContain("Sec-Fetch-Site");
  });

  it("rejects POST with Sec-Fetch-Site: same-site (subdomain CSRF)", async () => {
    serverHandle = await startServer();
    const r = await fetch(
      `http://127.0.0.1:${serverHandle.port}/tool/save_baseline`,
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "Sec-Fetch-Site": "same-site",
        },
        body: JSON.stringify({ name: "x", sql: "SELECT 1" }),
      },
    );
    expect(r.status).toBe(403);
  });

  it("allows Sec-Fetch-Site: none (direct user navigation)", async () => {
    serverHandle = await startServer();
    const r = await fetch(`http://127.0.0.1:${serverHandle.port}/health`, {
      headers: { "Sec-Fetch-Site": "none" },
    });
    expect(r.status).toBe(200);
  });

  it("allows Sec-Fetch-Site: same-origin (self-served page on same port)", async () => {
    serverHandle = await startServer();
    const r = await fetch(`http://127.0.0.1:${serverHandle.port}/health`, {
      headers: { "Sec-Fetch-Site": "same-origin" },
    });
    expect(r.status).toBe(200);
  });

  it("rejects Origin header (browser cross-origin POST)", async () => {
    serverHandle = await startServer();
    const r = await fetch(`http://127.0.0.1:${serverHandle.port}/tool/query`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Origin: "http://evil.com",
      },
      body: JSON.stringify({ sql: "SELECT 1" }),
    });
    expect(r.status).toBe(403);
    const body = (await r.json()) as { error: string };
    expect(body.error).toContain("Origin");
  });

  it("rejects Origin: null (opaque browser contexts)", async () => {
    serverHandle = await startServer();
    const r = await fetch(
      `http://127.0.0.1:${serverHandle.port}/tool/save_baseline`,
      {
        method: "POST",
        headers: { "Content-Type": "application/json", Origin: "null" },
        body: JSON.stringify({ name: "csrf-test", sql: "SELECT 1" }),
      },
    );
    expect(r.status).toBe(403);
    const body = (await r.json()) as { error: string };
    expect(body.error).toContain("Origin: null");
  });

  it("rejects POST with mismatched Host header (DNS rebinding)", async () => {
    serverHandle = await startServer();
    const r = await fetch(`http://127.0.0.1:${serverHandle.port}/tool/query`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Host: "evil.com:9999",
      },
      body: JSON.stringify({ sql: "SELECT 1" }),
    });
    expect(r.status).toBe(403);
    const body = (await r.json()) as { error: string };
    expect(body.error).toContain("DNS rebinding");
  });

  it("CSRF guard runs before /health (auth-exempt liveness still gated)", async () => {
    serverHandle = await startServer();
    const r = await fetch(`http://127.0.0.1:${serverHandle.port}/health`, {
      headers: { "Sec-Fetch-Site": "cross-site" },
    });
    expect(r.status).toBe(403);
  });

  it("legitimate curl-style POST (no Origin / no Sec-Fetch-Site) passes", async () => {
    serverHandle = await startServer();
    const r = await fetch(`http://127.0.0.1:${serverHandle.port}/tool/query`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ sql: "SELECT 1 AS n" }),
    });
    expect(r.status).toBe(200);
    expect(await r.json()).toEqual([{ n: 1 }]);
  });

  it("allows localhost Host header on loopback bind", async () => {
    serverHandle = await startServer();
    // Host is set automatically by fetch when targeting 127.0.0.1; this test
    // confirms that 127.0.0.1:<port> in the Host header passes (the default
    // for any client that targets the loopback IP literally).
    const r = await fetch(`http://127.0.0.1:${serverHandle.port}/tool/query`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ sql: "SELECT 1" }),
    });
    expect(r.status).toBe(200);
  });
});

describe("http-server — --token auth", () => {
  it("rejects POST without Authorization when token set (401)", async () => {
    serverHandle = await startServer({ token: "secret" });
    const r = await postTool(serverHandle.port, "query", {
      sql: "SELECT 1",
    });
    expect(r.status).toBe(401);
    expect(r.json.error).toContain("Bearer");
  });

  it("rejects POST with wrong Authorization (401)", async () => {
    serverHandle = await startServer({ token: "secret" });
    const r = await postTool(
      serverHandle.port,
      "query",
      { sql: "SELECT 1" },
      { token: "wrong" },
    );
    expect(r.status).toBe(401);
  });

  it("accepts POST with correct Bearer token", async () => {
    serverHandle = await startServer({ token: "secret" });
    const r = await postTool(
      serverHandle.port,
      "query",
      { sql: "SELECT 1 AS n" },
      { token: "secret" },
    );
    expect(r.status).toBe(200);
    expect(r.json).toEqual([{ n: 1 }]);
  });

  it("GET /health is auth-exempt even when token is set", async () => {
    serverHandle = await startServer({ token: "secret" });
    const r = await fetch(`http://127.0.0.1:${serverHandle.port}/health`);
    expect(r.status).toBe(200);
  });

  it("GET /tools requires the token (catalog leak protection)", async () => {
    serverHandle = await startServer({ token: "secret" });
    const r = await fetch(`http://127.0.0.1:${serverHandle.port}/tools`);
    expect(r.status).toBe(401);
  });
});

describe("http-server — apply tools", () => {
  function seedRenameApplyTarget(): void {
    writeFileSync(
      join(benchDir, "src", "rename-target.ts"),
      "export function runQuery() {}\n",
      "utf8",
    );
    const db = openDb();
    try {
      db.run(
        `INSERT INTO files (path, content_hash, size, line_count, language, last_modified, indexed_at)
         VALUES ('src/rename-target.ts', 'hrt', 30, 1, 'typescript', 1, 1)`,
      );
      db.run(
        `INSERT INTO symbols (file_path, name, kind, line_start, line_end, signature, is_exported, is_default_export)
         VALUES ('src/rename-target.ts', 'runQuery', 'function', 1, 1, 'runQuery()', 1, 0)`,
      );
    } finally {
      closeDb(db);
    }
  }

  it("POST /tool/apply dry_run returns native JSON envelope", async () => {
    seedRenameApplyTarget();
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "apply", {
      recipe: "rename-preview",
      params: { old: "runQuery", new: "runQry", kind: "function" },
      dry_run: true,
    });
    expect(r.status).toBe(200);
    expect(r.json.mode).toBe("dry-run");
    expect(r.json.applied).toBe(false);
  });

  it("POST /tool/apply_rows dry_run returns native JSON envelope", async () => {
    seedRenameApplyTarget();
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "apply_rows", {
      rows: [
        {
          file_path: "src/rename-target.ts",
          line_start: 1,
          before_pattern: "runQuery",
          after_pattern: "runQry",
        },
      ],
      dry_run: true,
    });
    expect(r.status).toBe(200);
    expect(r.json.mode).toBe("dry-run");
  });

  it("POST /tool/apply_diff_input dry_run parses diff text", async () => {
    seedRenameApplyTarget();
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "apply_diff_input", {
      diff_text: `--- a/src/rename-target.ts
+++ b/src/rename-target.ts
@@ -1,1 +1,1 @@
-runQuery
+runQry
`,
      dry_run: true,
    });
    expect(r.status).toBe(200);
    expect(r.json.summary.rows).toBeGreaterThanOrEqual(1);
  });

  it("POST /tool/apply without yes returns 400", async () => {
    seedRenameApplyTarget();
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "apply", {
      recipe: "rename-preview",
      params: { old: "runQuery", new: "runQry" },
    });
    expect(r.status).toBe(400);
    expect(r.json.error).toContain("yes: true");
  });

  it("POST /tool/apply yes:true writes disk", async () => {
    seedRenameApplyTarget();
    serverHandle = await startServer();
    const r = await postTool(serverHandle.port, "apply", {
      recipe: "rename-preview",
      params: { old: "runQuery", new: "runQry", kind: "function" },
      yes: true,
    });
    expect(r.status).toBe(200);
    expect(r.json.applied).toBe(true);
    expect(
      readFileSync(join(benchDir, "src", "rename-target.ts"), "utf8"),
    ).toBe("export function runQry() {}\n");
  });
});
