import test from "node:test";
import assert from "node:assert/strict";
import { validate, type SchemaSnapshot } from "../cypher-validate.js";

const snapshot: SchemaSnapshot = {
  labels: new Set(["Person", "Organization", "Task", "KnowledgeDocument"]),
  relationshipTypes: new Set(["KNOWS", "PARTICIPANT", "REFERENCES", "MENTIONS", "PART_OF"]),
};

test("write mode allows CREATE node + relationship statement", () => {
  const result = validate(
    "MATCH (a:Person), (b:Organization) WHERE a.name = $a AND b.name = $b CREATE (a)-[:KNOWS]->(b)",
    snapshot,
    { mode: "write" },
  );
  assert.equal(result.ok, true, "expected valid write");
  assert.deepEqual(result.unknown, []);
  assert.deepEqual(result.forbidden, []);
});

test("write mode allows MERGE with SET clause", () => {
  const result = validate(
    "MATCH (a:Person {name: $name}) MERGE (a)-[r:KNOWS]->(b:Person {name: $other}) SET r.createdAt = datetime()",
    snapshot,
    { mode: "write" },
  );
  assert.equal(result.ok, true);
});

test("write mode allows DETACH DELETE", () => {
  const result = validate(
    "MATCH (n:Person) WHERE n.archived = true DETACH DELETE n",
    snapshot,
    { mode: "write" },
  );
  assert.equal(result.ok, true);
});

test("write mode allows REMOVE label + SET label (re-label)", () => {
  // Allow renaming labels — the new label is unknown to the schema until the
  // write commits, but the validator must not block legitimate normalisation.
  const result = validate(
    "MATCH (n:Person) WHERE n.legacy = true REMOVE n:Person SET n:LegacyPerson",
    snapshot,
    { mode: "write" },
  );
  // LegacyPerson is unknown — fail-soft for write mode (audit emits warning, not reject).
  // The token check is shared with read-mode; the write mode adds DDL/admin reject layer.
  // For this test we accept the write-mode-specific outcome: no forbidden patterns.
  assert.deepEqual(result.forbidden, []);
});

test("write mode allows CALL apoc.refactor.mergeNodes", () => {
  const result = validate(
    "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",
    snapshot,
    { mode: "write" },
  );
  assert.deepEqual(result.forbidden, []);
});

test("write mode REJECTS DROP DATABASE", () => {
  const result = validate("DROP DATABASE neo4j", snapshot, { mode: "write" });
  assert.equal(result.ok, false);
  assert.equal(result.forbidden.length, 1);
  assert.equal(result.forbidden[0].kind, "drop-database");
});

test("write mode REJECTS CREATE INDEX", () => {
  const result = validate(
    "CREATE INDEX person_name_idx FOR (n:Person) ON (n.name)",
    snapshot,
    { mode: "write" },
  );
  assert.equal(result.ok, false);
  const f = result.forbidden.find((x) => x.kind === "create-index");
  assert.ok(f, "expected create-index forbidden");
});

test("write mode REJECTS CREATE CONSTRAINT", () => {
  const result = validate(
    "CREATE CONSTRAINT person_email_unique FOR (n:Person) REQUIRE n.email IS UNIQUE",
    snapshot,
    { mode: "write" },
  );
  assert.equal(result.ok, false);
  const f = result.forbidden.find((x) => x.kind === "create-constraint");
  assert.ok(f);
});

test("write mode REJECTS CALL dbms.security.*", () => {
  const result = validate(
    "CALL dbms.security.createUser('attacker', 'pwd', false)",
    snapshot,
    { mode: "write" },
  );
  assert.equal(result.ok, false);
  const f = result.forbidden.find((x) => x.kind === "call-dbms");
  assert.ok(f);
});

test("write mode REJECTS CALL db.create*", () => {
  const result = validate(
    "CALL db.createLabel('Foo')",
    snapshot,
    { mode: "write" },
  );
  assert.equal(result.ok, false);
  const f = result.forbidden.find((x) => x.kind === "call-db-create");
  assert.ok(f);
});

test("write mode allows CALL db.labels (read-only db.* call)", () => {
  // db.labels is a read primitive — not a create/drop. Validator only
  // forbids db.create* family; CALL db.labels remains allowed.
  const result = validate("CALL db.labels()", snapshot, { mode: "write" });
  assert.deepEqual(result.forbidden, []);
});

test("read mode does NOT apply forbidden checks (DDL pattern in MATCH still allowed)", () => {
  // String literals containing forbidden tokens never trip — DDL forbidden check
  // is mode-gated. Read-mode WHERE clauses comparing on a string with the
  // word DROP must not reject.
  const result = validate(
    "MATCH (n:Person) WHERE n.bio CONTAINS 'DROP DATABASE' RETURN n",
    snapshot,
    { mode: "read" },
  );
  assert.deepEqual(result.forbidden, []);
  assert.equal(result.ok, true);
});

test("write mode strips string literals before forbidden check", () => {
  // The forbidden check must use stripStringLiterals so a string property
  // value like 'DROP DATABASE archive' does not trip the rejection.
  const result = validate(
    "MATCH (n:Person) WHERE n.bio CONTAINS 'DROP DATABASE' SET n.flagged = true",
    snapshot,
    { mode: "write" },
  );
  assert.deepEqual(result.forbidden, [], "string-literal content must not trip DDL check");
});

test("default mode is read (backward compatibility)", () => {
  // Existing call sites pass no mode option — must remain read-mode behavior.
  const result = validate("MATCH (n:Person) RETURN n", snapshot);
  assert.equal(result.ok, true);
  assert.equal(Array.isArray(result.forbidden), true);
  assert.equal(result.forbidden.length, 0);
});
