import test from "node:test";
import assert from "node:assert/strict";
import {
  parseLabelsFromSchemaCypher,
  levenshtein,
  nearestLabel,
} from "../schema-cypher-parser.js";

test("parseLabelsFromSchemaCypher: extracts labels from constraint and index forms", () => {
  const text = `
CREATE CONSTRAINT person_email IF NOT EXISTS
FOR (p:Person) REQUIRE p.email IS UNIQUE;

CREATE CONSTRAINT business_account IF NOT EXISTS
FOR (b:LocalBusiness) REQUIRE b.accountId IS UNIQUE;

CREATE INDEX admin_user_account IF NOT EXISTS
FOR (au:AdminUser) ON (au.accountId);

CREATE INDEX knowledge_doc IF NOT EXISTS
FOR (k:KnowledgeDocument) ON (k.embedding) OPTIONS { ... };
`;
  const labels = parseLabelsFromSchemaCypher(text);
  assert.deepEqual(labels, [
    "AdminUser",
    "KnowledgeDocument",
    "LocalBusiness",
    "Person",
  ]);
});

test("parseLabelsFromSchemaCypher: deduplicates labels declared multiple times", () => {
  const text = `
FOR (p:Person) REQUIRE p.email IS UNIQUE;
FOR (p:Person) REQUIRE p.telephone IS UNIQUE;
FOR (p:Person) ON (p.status);
`;
  assert.deepEqual(parseLabelsFromSchemaCypher(text), ["Person"]);
});

test("parseLabelsFromSchemaCypher: returns sorted output", () => {
  const text = `FOR (z:Zebra) ON (z.x); FOR (a:Aardvark) ON (a.x); FOR (m:Mongoose) ON (m.x);`;
  assert.deepEqual(parseLabelsFromSchemaCypher(text), [
    "Aardvark",
    "Mongoose",
    "Zebra",
  ]);
});

test("parseLabelsFromSchemaCypher: ignores non-constraint cypher", () => {
  const text = `
MATCH (n:Person) RETURN n;
CREATE (b:Business { name: 'X' });
FOR (p:Person) REQUIRE p.email IS UNIQUE;
`;
  // MATCH/CREATE patterns don't include FOR, so they're ignored.
  assert.deepEqual(parseLabelsFromSchemaCypher(text), ["Person"]);
});

test("parseLabelsFromSchemaCypher: empty input returns empty array", () => {
  assert.deepEqual(parseLabelsFromSchemaCypher(""), []);
});

test("levenshtein: identical strings", () => {
  assert.equal(levenshtein("Person", "Person"), 0);
});

test("levenshtein: substitution distance", () => {
  // LocaIBusiness vs LocalBusiness — capital-I-as-l typo from the incident.
  assert.equal(levenshtein("LocaIBusiness", "LocalBusiness"), 1);
});

test("levenshtein: insertion + deletion", () => {
  assert.equal(levenshtein("Person", "Persons"), 1);
  assert.equal(levenshtein("Persons", "Person"), 1);
});

test("levenshtein: handles empty strings", () => {
  assert.equal(levenshtein("", "abc"), 3);
  assert.equal(levenshtein("abc", ""), 3);
  assert.equal(levenshtein("", ""), 0);
});

test("nearestLabel: finds the closest match", () => {
  const candidates = ["Person", "LocalBusiness", "AdminUser"];
  assert.equal(nearestLabel("LocaIBusiness", candidates), "LocalBusiness");
  assert.equal(nearestLabel("AdminUsr", candidates), "AdminUser");
});

test("nearestLabel: returns null when no match within maxDistance", () => {
  const candidates = ["Person", "LocalBusiness"];
  assert.equal(nearestLabel("Quokka", candidates), null);
  // Default maxDistance is 3; "abcde" is too far from any candidate.
  assert.equal(nearestLabel("abcde", candidates, 1), null);
});

test("nearestLabel: empty candidates returns null", () => {
  assert.equal(nearestLabel("Person", []), null);
});
