import test from "node:test";
import assert from "node:assert/strict";
import {
  auditCypherWrite,
  formatAuditLine,
  type CypherWriteAuditInput,
} from "../audit.js";

const baseInput: Omit<CypherWriteAuditInput, "cypher"> = {
  schema: {
    labels: new Set(["Person", "Organization", "Task", "KnowledgeDocument"]),
    relationshipTypes: new Set(["KNOWS", "PARTICIPANT", "REFERENCES", "MENTIONS", "PART_OF"]),
  },
  agentName: "database-operator",
  sessionId: "session-abc",
  nodesCreated: 0,
  relsCreated: 0,
  orphanIds: [],
};

test("audit: clean write with provenance + known types yields zero warnings", () => {
  const cypher = `
    MATCH (p:Person {name: $name}), (o:Organization {name: $org})
    MERGE (p)-[r:KNOWS]->(o)
    SET r.createdAt = datetime(),
        r.createdByAgent = $agent,
        r.createdByTool = 'graph-cypher-write',
        r.createdBySession = $sessionId
  `;
  const warnings = auditCypherWrite({ ...baseInput, cypher, relsCreated: 1 });
  assert.deepEqual(warnings, []);
});

test("audit: unknown edge type yields unknown-type-warning", () => {
  const cypher = "MATCH (a:Person), (b:Person) CREATE (a)-[:HAS_KNOWN]->(b)";
  const warnings = auditCypherWrite({ ...baseInput, cypher, relsCreated: 1 });
  const w = warnings.find((x) => x.kind === "unknown-type-warning");
  assert.ok(w, "expected unknown-type-warning");
  assert.equal(w.kind, "unknown-type-warning");
  if (w.kind === "unknown-type-warning") {
    assert.equal(w.type, "HAS_KNOWN");
  }
});

test("audit: known APOC edge in YIELD does not trip unknown-type", () => {
  const cypher =
    "MATCH (a:Person), (b:Person) WHERE a.email = b.email AND id(a) < id(b) WITH a, b CALL apoc.refactor.mergeNodes([a, b]) YIELD node RETURN node";
  const warnings = auditCypherWrite({ ...baseInput, cypher });
  const unknownTypeWarn = warnings.find((x) => x.kind === "unknown-type-warning");
  assert.equal(unknownTypeWarn, undefined);
});

test("audit: CREATE with no provenance stamps yields missing-provenance-warning", () => {
  const cypher = "CREATE (n:Person {name: 'Foo'})";
  const warnings = auditCypherWrite({ ...baseInput, cypher, nodesCreated: 1 });
  const w = warnings.find((x) => x.kind === "missing-provenance-warning");
  assert.ok(w, "expected missing-provenance-warning");
  if (w.kind === "missing-provenance-warning") {
    assert.equal(w.created, 1);
    assert.equal(w.stamped, 0);
  }
});

test("audit: MERGE with createdBy* stamps does not trip missing-provenance", () => {
  const cypher =
    "MERGE (n:Person {email: $email}) ON CREATE SET n.createdByAgent = $agent, n.createdBySession = $sid";
  const warnings = auditCypherWrite({ ...baseInput, cypher, nodesCreated: 1 });
  const w = warnings.find((x) => x.kind === "missing-provenance-warning");
  assert.equal(w, undefined);
});

test("audit: provenance check ignores stamps inside string literals", () => {
  const cypher =
    "CREATE (n:Person {bio: 'I have createdByAgent in my bio'})";
  const warnings = auditCypherWrite({ ...baseInput, cypher, nodesCreated: 1 });
  const w = warnings.find((x) => x.kind === "missing-provenance-warning");
  assert.ok(w, "expected missing-provenance-warning despite stamp-like substring in literal");
});

test("audit: idempotent MERGE that matched existing node (nodesCreated=0) emits no missing-provenance-warning", () => {
  // Reviewer-flagged false-positive case. MERGE (n:Person {email}) without
  // ON CREATE SET stamps — static count = 1, stamps = 0. If the node
  // already existed, upstream reports nodesCreated=0 and no provenance was
  // needed; the audit must not warn.
  const cypher = "MERGE (n:Person {email: $email})";
  const warnings = auditCypherWrite({ ...baseInput, cypher, nodesCreated: 0 });
  const w = warnings.find((x) => x.kind === "missing-provenance-warning");
  assert.equal(w, undefined, "must not warn when nodesCreated=0");
});

test("audit: orphanIds populated yields orphan-warning", () => {
  const warnings = auditCypherWrite({
    ...baseInput,
    cypher: "CREATE (n:Person)",
    nodesCreated: 1,
    orphanIds: ["4:abc:1", "4:abc:2"],
  });
  const w = warnings.find((x) => x.kind === "orphan-warning");
  assert.ok(w, "expected orphan-warning");
  if (w.kind === "orphan-warning") {
    assert.deepEqual(w.orphanIds, ["4:abc:1", "4:abc:2"]);
  }
});

test("audit: empty orphanIds emits no orphan-warning", () => {
  const warnings = auditCypherWrite({
    ...baseInput,
    cypher: "MATCH (a:Person), (b:Person) CREATE (a)-[:KNOWS]->(b)",
    relsCreated: 1,
    orphanIds: [],
  });
  const w = warnings.find((x) => x.kind === "orphan-warning");
  assert.equal(w, undefined);
});

test("formatAuditLine: accepted line includes counters + provenance", () => {
  const line = formatAuditLine({
    kind: "accepted",
    cypherPrefix: "MATCH (a:Person) CREATE",
    nodesCreated: 1,
    relsCreated: 4,
    agentName: "database-operator",
    sessionId: "abc-123",
  });
  assert.match(line, /^\[graph-cypher-write\] accepted /);
  assert.match(line, /nodesCreated=1/);
  assert.match(line, /relsCreated=4/);
  assert.match(line, /agentName=database-operator/);
  assert.match(line, /sessionId=abc-123/);
});

test("formatAuditLine: orphan-warning line lists orphanIds", () => {
  const line = formatAuditLine({
    kind: "orphan-warning",
    cypherPrefix: "CREATE (n:Person)",
    orphanIds: ["4:abc:1", "4:abc:2"],
  });
  assert.match(line, /^\[graph-cypher-write\] orphan-warning /);
  assert.match(line, /orphanIds=4:abc:1,4:abc:2/);
});

test("formatAuditLine: unknown-type-warning names the type", () => {
  const line = formatAuditLine({
    kind: "unknown-type-warning",
    cypherPrefix: "CREATE (a)-[:HAS_KNOWN]->(b)",
    type: "HAS_KNOWN",
  });
  assert.match(line, /^\[graph-cypher-write\] unknown-type-warning /);
  assert.match(line, /type=HAS_KNOWN/);
});

test("formatAuditLine: missing-provenance-warning shows created vs stamped counts", () => {
  const line = formatAuditLine({
    kind: "missing-provenance-warning",
    cypherPrefix: "CREATE (n:Person)",
    created: 3,
    stamped: 1,
  });
  assert.match(line, /^\[graph-cypher-write\] missing-provenance-warning /);
  assert.match(line, /created=3/);
  assert.match(line, /stamped=1/);
});
