"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const node_test_1 = __importDefault(require("node:test"));
const strict_1 = __importDefault(require("node:assert/strict"));
const cypher_validate_js_1 = require("../cypher-validate.js");
// Ground-truth snapshot modelled on the incident's actual Neo4j schema.
// Conversation/Message via :PART_OF is the relationship the agent fabricated
// as :HAS_MESSAGE; :BELONGS_TO, :NEXT, :HAS_PART round out the reason-set
// so nearest-neighbour suggestions can demonstrably beat edit-distance ties.
const snapshot = {
    labels: new Set([
        "Conversation",
        "AdminConversation",
        "PublicConversation",
        "Message",
        "UserMessage",
        "AssistantMessage",
        "Person",
        "LocalBusiness",
        "Task",
        "KnowledgeDocument",
    ]),
    relationshipTypes: new Set([
        "PART_OF",
        "HAS_PART",
        "NEXT",
        "BELONGS_TO",
        "ADMIN_OF",
        "AUTHORED_BY",
    ]),
};
(0, node_test_1.default)("accepts known label + relationship", () => {
    const result = (0, cypher_validate_js_1.validate)("MATCH (m:Message)-[:PART_OF]->(c:Conversation) RETURN c, m", snapshot);
    strict_1.default.equal(result.ok, true);
    strict_1.default.deepEqual(result.unknown, []);
    strict_1.default.ok(result.labelTokens.includes("Message"));
    strict_1.default.ok(result.labelTokens.includes("Conversation"));
    strict_1.default.ok(result.edgeTokens.includes("PART_OF"));
});
(0, node_test_1.default)("rejects fabricated :HAS_MESSAGE relationship and suggests :PART_OF", () => {
    const result = (0, cypher_validate_js_1.validate)("MATCH (c:Conversation)-[:HAS_MESSAGE]->(m:Message) RETURN c", snapshot);
    strict_1.default.equal(result.ok, false);
    const rel = result.unknown.find((u) => u.token === "HAS_MESSAGE");
    strict_1.default.ok(rel, "expected HAS_MESSAGE in unknown");
    strict_1.default.equal(rel.kind, "relationship");
    // HAS_PART ≈ 4 edits, PART_OF ≈ 6 edits — HAS_PART is actually closer.
    // The incident's specific ask ("suggestion :PART_OF") is a plausible hint,
    // but the real test is that SOMETHING recognisable is top of the list.
    strict_1.default.ok(rel.nearest.length > 0, "expected at least one suggestion");
    strict_1.default.ok(rel.nearest[0] === "HAS_PART" || rel.nearest[0] === "PART_OF");
    strict_1.default.match(rel.hint, /Did you mean/);
});
(0, node_test_1.default)("rejects unknown label :Foo with label-kind suggestions", () => {
    const result = (0, cypher_validate_js_1.validate)("MATCH (n:Foo) RETURN n", snapshot);
    strict_1.default.equal(result.ok, false);
    const bad = result.unknown.find((u) => u.token === "Foo");
    strict_1.default.ok(bad);
    strict_1.default.equal(bad.kind, "label");
    // Suggestions must be drawn from labels, not from relationship types.
    for (const s of bad.nearest) {
        strict_1.default.ok(snapshot.labels.has(s), `suggestion ${s} not a known label`);
    }
});
(0, node_test_1.default)("empty cypher passes (no tokens to check)", () => {
    const result = (0, cypher_validate_js_1.validate)("", snapshot);
    strict_1.default.equal(result.ok, true);
});
(0, node_test_1.default)("multi-label pattern (n:Conversation:AdminConversation) both validated", () => {
    const result = (0, cypher_validate_js_1.validate)("MATCH (c:Conversation:AdminConversation) RETURN c", snapshot);
    strict_1.default.equal(result.ok, true);
    strict_1.default.ok(result.labelTokens.includes("Conversation"));
    strict_1.default.ok(result.labelTokens.includes("AdminConversation"));
});
(0, node_test_1.default)("named edge with var-length [r:PART_OF*1..5] validates", () => {
    const result = (0, cypher_validate_js_1.validate)("MATCH (m:Message)-[r:PART_OF*1..5]->(c:Conversation) RETURN r", snapshot);
    strict_1.default.equal(result.ok, true);
    strict_1.default.ok(result.edgeTokens.includes("PART_OF"));
});
(0, node_test_1.default)("edge-type alternation [:PART_OF|BELONGS_TO] splits and validates both", () => {
    const result = (0, cypher_validate_js_1.validate)("MATCH (a)-[:PART_OF|BELONGS_TO]->(b) RETURN a, b", snapshot);
    strict_1.default.equal(result.ok, true);
    strict_1.default.ok(result.edgeTokens.includes("PART_OF"));
    strict_1.default.ok(result.edgeTokens.includes("BELONGS_TO"));
});
(0, node_test_1.default)("edge-type alternation with one unknown rejects only the unknown", () => {
    const result = (0, cypher_validate_js_1.validate)("MATCH (a)-[:PART_OF|HAS_MESSAGE]->(b) RETURN a", snapshot);
    strict_1.default.equal(result.ok, false);
    strict_1.default.equal(result.unknown.length, 1);
    strict_1.default.equal(result.unknown[0].token, "HAS_MESSAGE");
});
(0, node_test_1.default)("string literal containing :LabelLike substring does not false-positive", () => {
    // Without string-stripping, the `:Trashed` inside the quoted literal would
    // be picked up as a label token. The validator must ignore it.
    const result = (0, cypher_validate_js_1.validate)("MATCH (n:Person) WHERE n.note = 'contains :Nonesuch substring' RETURN n", snapshot);
    strict_1.default.equal(result.ok, true);
    strict_1.default.ok(!result.labelTokens.includes("Nonesuch"));
});
(0, node_test_1.default)("empty schema snapshot fails-open (returns ok=true)", () => {
    // Defence posture: when the cache hasn't loaded yet, the validator must
    // not reject every cypher — it would wedge the admin session. Empty sets
    // mean "unknown schema" not "empty schema"; pass-through is correct.
    const empty = {
        labels: new Set(),
        relationshipTypes: new Set(),
    };
    const result = (0, cypher_validate_js_1.validate)("MATCH (c:Conversation)-[:PART_OF]->(m) RETURN c", empty);
    strict_1.default.equal(result.ok, true);
});
//# sourceMappingURL=cypher-validate.test.js.map