// Neo4j envelope-warning passthrough.
//
// Pure-function tests for the helpers in cypher-shim-read.ts:
//   - filterEnvelopeNotifications: keeps only ^0[12]N5\d$ GQL status codes
//   - stitchWarningsIntoResponse:  prepends a warnings content block
//   - runReadProbe:                runs cypher through a shim-side session
//                                  and returns notifications from the summary
//
// Failure mode this closes: the agent ran `RETURN h.hostname` against a node
// whose property is `hostnameValue`, Neo4j emitted gql_status=01N52
// "property does not exist", but the warning was dropped by the upstream
// envelope and the agent saw `[]`. Without these warnings reaching the tool
// result, the same recurrence will hit other property names too.
import test from "node:test";
import assert from "node:assert/strict";
import {
  filterEnvelopeNotifications,
  stitchWarningsIntoResponse,
  runReadProbe,
  type DriverNotification,
} from "../cypher-shim-read.js";

test("filterEnvelopeNotifications keeps 01N5x and 02N5x, drops everything else", () => {
  const input: DriverNotification[] = [
    { gqlStatus: "01N52", title: "...", description: "property does not exist (foo)", position: { offset: 0, line: 1, column: 1 } },
    { gqlStatus: "02N50", title: "...", description: "label does not exist (Foo)", position: null },
    { gqlStatus: "01N40", title: "...", description: "wrong family", position: null },
    { gqlStatus: "03N42", title: "...", description: "unrelated", position: null },
    { gqlStatus: "00N00", title: "...", description: "neutral status", position: null },
  ];
  const filtered = filterEnvelopeNotifications(input);
  assert.equal(filtered.length, 2);
  assert.equal(filtered[0].gql_status, "01N52");
  assert.equal(filtered[0].description, "property does not exist (foo)");
  assert.deepEqual(filtered[0].position, { offset: 0, line: 1, column: 1 });
  assert.equal(filtered[1].gql_status, "02N50");
  assert.equal(filtered[1].position, null);
});

test("filterEnvelopeNotifications on an empty or all-irrelevant list returns []", () => {
  assert.deepEqual(filterEnvelopeNotifications([]), []);
  assert.deepEqual(
    filterEnvelopeNotifications([
      { gqlStatus: "03N42", title: "", description: "", position: null },
      { gqlStatus: "01N40", title: "", description: "", position: null },
    ]),
    [],
  );
});

test("stitchWarningsIntoResponse prepends a warnings content block when warnings exist", () => {
  const original = {
    jsonrpc: "2.0",
    id: 42,
    result: { content: [{ type: "text", text: "[]" }] },
  };
  const warnings = [
    { gql_status: "01N52", description: "property hostname does not exist", position: null },
  ];
  const out = stitchWarningsIntoResponse(original, warnings);
  assert.notEqual(out, null);
  const parsed = JSON.parse(out!);
  assert.equal(parsed.id, 42);
  assert.equal(parsed.result.content.length, 2);
  assert.equal(parsed.result.content[0].type, "text");
  assert.match(parsed.result.content[0].text, /Neo4j envelope warnings/);
  assert.match(parsed.result.content[0].text, /01N52/);
  assert.match(parsed.result.content[0].text, /property hostname does not exist/);
  // Original row block preserved verbatim, after the warnings prefix.
  assert.equal(parsed.result.content[1].text, "[]");
});

test("stitchWarningsIntoResponse returns null when no warnings (no envelope rewrite)", () => {
  const original = { jsonrpc: "2.0", id: 1, result: { content: [{ type: "text", text: "[]" }] } };
  assert.equal(stitchWarningsIntoResponse(original, []), null);
});

test("stitchWarningsIntoResponse handles missing result.content defensively", () => {
  const original = { jsonrpc: "2.0", id: 1, result: {} };
  const warnings = [{ gql_status: "01N52", description: "x", position: null }];
  const out = stitchWarningsIntoResponse(original as never, warnings);
  assert.notEqual(out, null);
  const parsed = JSON.parse(out!);
  assert.equal(parsed.result.content.length, 1);
  assert.match(parsed.result.content[0].text, /01N52/);
});

test("runReadProbe returns notifications captured from the driver summary", async () => {
  const session = {
    run: async () => ({
      summary: {
        notifications: [
          { gqlStatus: "01N52", title: "", description: "missing prop", position: null },
        ] as DriverNotification[],
      },
    }),
    close: async () => {},
  };
  const driver = { session: () => session };
  const notifs = await runReadProbe(driver, "MATCH (n) RETURN n.foo", {});
  assert.equal(notifs.length, 1);
  assert.equal(notifs[0].gqlStatus, "01N52");
});

test("runReadProbe returns [] when driver summary has no notifications", async () => {
  const session = {
    run: async () => ({ summary: {} }),
    close: async () => {},
  };
  const driver = { session: () => session };
  assert.deepEqual(await runReadProbe(driver, "RETURN 1", {}), []);
});

test("runReadProbe closes the session even when run() throws", async () => {
  let closed = false;
  const session = {
    run: async () => {
      throw new Error("boom");
    },
    close: async () => {
      closed = true;
    },
  };
  const driver = { session: () => session };
  await assert.rejects(() => runReadProbe(driver, "RETURN 1", {}));
  assert.equal(closed, true, "session must be closed on error");
});

test("end-to-end: filter + stitch on a real-shape upstream response surfaces 01N52", () => {
  // Models the failure mode in the original log: upstream returned an empty
  // row list; Neo4j attached an 01N52 notification that the upstream Python
  // server dropped.
  const upstreamResponse = {
    jsonrpc: "2.0",
    id: 99,
    result: { content: [{ type: "text", text: "[]" }] },
  };
  const driverNotifications: DriverNotification[] = [
    {
      gqlStatus: "01N52",
      title: "Property does not exist",
      description: "The property 'hostname' does not exist on the node",
      position: { offset: 38, line: 1, column: 39 },
    },
  ];
  const warnings = filterEnvelopeNotifications(driverNotifications);
  const stitched = stitchWarningsIntoResponse(upstreamResponse, warnings);
  const parsed = JSON.parse(stitched!);
  assert.match(parsed.result.content[0].text, /01N52/);
  assert.match(parsed.result.content[0].text, /hostname/);
});
