import test from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { loadSchema, splitPipeRow } from "../schema-loader.js";
function writeBrandJson(content) {
    const dir = mkdtempSync(join(tmpdir(), "schema-loader-brand-"));
    const path = join(dir, "brand.json");
    writeFileSync(path, JSON.stringify(content), "utf-8");
    return path;
}
// schema-loader reads from platform/plugins/memory/references/schema-*.md
// at runtime — these tests assert the contract surface (the renamed field
// `markdownLabels` plus the loader's invariants) against the actual schema
// fixtures shipped with the plugin. The fixtures are stable enough to make
// this test robust without a separate fixture set.
test("loadSchema: returns markdownLabels (renamed from labels)", () => {
    const schema = loadSchema();
    // The new field name. A regression from the rename would surface here.
    assert.ok(schema.markdownLabels instanceof Map, "markdownLabels must be a Map");
    assert.equal("labels" in schema, false, "old `labels` field must be removed — callers must use markdownLabels");
});
test("loadSchema: documents Person with no identifying-field requirement", () => {
    const schema = loadSchema();
    const person = schema.markdownLabels.get("Person");
    assert.ok(person, "Person must be documented");
    assert.equal(person.label, "Person");
    //, Person is open: any single identifying property suffices,
    // and the agent + operator manage dedup/conflicts downstream. The
    // markdown row therefore declares no required identifying-field group —
    // accountId is enforced separately by the validator's accountId check.
    const hasIdentifyingRequirement = person.required.some((g) => {
        if (g.kind === "simple")
            return g.name !== "accountId";
        return g.names.some((n) => n !== "accountId");
    });
    assert.equal(hasIdentifyingRequirement, false, "Person must not require any identifying field (givenName, email, telephone, ...)");
});
test("loadSchema: does NOT document platform-native LocalBusiness", () => {
    // The regression case from the incident. LocalBusiness lives in
    // schema.cypher + prose only; no `| Entity | Neo4j Label | ... |` row in
    // schema-base.md. This is the gap the task closes — a write to
    // LocalBusiness must succeed via the live source, not via markdown.
    const schema = loadSchema();
    assert.equal(schema.markdownLabels.has("LocalBusiness"), false, "LocalBusiness is platform-native, not markdown-documented (regression case)");
});
test("loadSchema: wrongToCorrect synonym map is non-empty", () => {
    const schema = loadSchema();
    // schema-base.md ships Schema.org naming-rule rows; the synonym map
    // should be populated. firstName→givenName is the canonical example.
    assert.ok(schema.wrongToCorrect.size > 0);
    const hit = schema.wrongToCorrect.get("firstName");
    assert.ok(hit, "firstName must map to a correct name");
    assert.equal(hit.correct, "givenName");
});
test("loadSchema: sourceFiles are absolute paths to schema-*.md files", () => {
    const schema = loadSchema();
    assert.ok(schema.sourceFiles.length > 0);
    for (const f of schema.sourceFiles) {
        assert.ok(f.startsWith("/"), `expected absolute path, got ${f}`);
        assert.match(f, /schema-.*\.md$/);
    }
});
// splitPipeRow must skip `|` inside backtick code-spans, otherwise
// a row like `Person|Organization` shreds into a 4-cell row and findTables
// throws at module load before any stderr can flush. The original-content
// case is asserted directly so the parser fix remains load-bearing even if
// the schema-base.md content fix is reverted.
test("splitPipeRow: pipe inside backtick code-span is literal", () => {
    const row = "| `PARTY` | `KnowledgeDocument` → `Person|Organization` | Contract Parties |";
    const cells = splitPipeRow(row);
    assert.equal(cells.length, 3, `expected 3 cells (regression), got ${cells.length}: ${JSON.stringify(cells)}`);
    assert.equal(cells[0], "`PARTY`");
    assert.equal(cells[1], "`KnowledgeDocument` → `Person|Organization`");
    assert.equal(cells[2], "Contract Parties");
});
test("splitPipeRow: multiple pipes inside multiple backtick spans are all literal", () => {
    const row = "| `a|b` | `c|d|e` | f |";
    const cells = splitPipeRow(row);
    assert.deepEqual(cells, ["`a|b`", "`c|d|e`", "f"]);
});
// ---------------------------------------------------------------------------
// brand.json#vertical resolution (Task 193)
// ---------------------------------------------------------------------------
test("loadSchema: no brand.json discovery → brandVertical=null (dev default)", () => {
    // No explicit path, no env var: matches paths.ts dev-default mode.
    const prev = process.env.MAXY_PLATFORM_ROOT;
    delete process.env.MAXY_PLATFORM_ROOT;
    try {
        const schema = loadSchema();
        assert.equal(schema.brandVertical, null);
    }
    finally {
        if (prev !== undefined)
            process.env.MAXY_PLATFORM_ROOT = prev;
    }
});
test("loadSchema: brand.json with vertical=null → brandVertical=null", () => {
    const path = writeBrandJson({ vertical: null });
    try {
        const schema = loadSchema({ brandJsonPath: path });
        assert.equal(schema.brandVertical, null);
    }
    finally {
        rmSync(path, { force: true });
    }
});
test("loadSchema: brand.json absent vertical field → brandVertical=null", () => {
    // Backwards compatibility: a brand.json that pre-dates Task 193 has no
    // `vertical` key. The loader must treat that as "no vertical declared",
    // not as an error.
    const path = writeBrandJson({ productName: "Test", configDir: ".test" });
    try {
        const schema = loadSchema({ brandJsonPath: path });
        assert.equal(schema.brandVertical, null);
    }
    finally {
        rmSync(path, { force: true });
    }
});
test("loadSchema: brand.json vertical naming existing file → brandVertical set", () => {
    // schema-estate-agent.md is shipped with the plugin; the loader should
    // accept it without throwing and surface the name.
    const path = writeBrandJson({ vertical: "schema-estate-agent" });
    try {
        const schema = loadSchema({ brandJsonPath: path });
        assert.equal(schema.brandVertical, "schema-estate-agent");
    }
    finally {
        rmSync(path, { force: true });
    }
});
test("loadSchema: brand.json vertical naming a missing file → loud-fail at boot", () => {
    // Task 193 verification: misconfigured vertical must block boot, with the
    // missing file named in the error.
    const path = writeBrandJson({ vertical: "schema-fake" });
    try {
        assert.throws(() => loadSchema({ brandJsonPath: path }), (err) => {
            assert.match(err.message, /schema-fake\.md/);
            assert.match(err.message, /not present/);
            return true;
        });
    }
    finally {
        rmSync(path, { force: true });
    }
});
test("loadSchema: brand.json vertical of wrong type → loud-fail at boot", () => {
    const path = writeBrandJson({ vertical: 42 });
    try {
        assert.throws(() => loadSchema({ brandJsonPath: path }), /brand\.json#vertical.*must be a string or null/);
    }
    finally {
        rmSync(path, { force: true });
    }
});
test("loadSchema: brand.json vertical empty string → loud-fail with explicit message", () => {
    // Operator typo guard: an empty string is misconfiguration, not "no
    // vertical". The error must name the field and suggest the two valid
    // shapes (null or a reference-file name).
    const path = writeBrandJson({ vertical: "" });
    try {
        assert.throws(() => loadSchema({ brandJsonPath: path }), (err) => {
            assert.match(err.message, /empty string/);
            assert.match(err.message, /Use null/);
            return true;
        });
    }
    finally {
        rmSync(path, { force: true });
    }
});
test("splitPipeRow: ordinary row with no backticks splits as before", () => {
    const cells = splitPipeRow("| one | two | three |");
    assert.deepEqual(cells, ["one", "two", "three"]);
});
// Forbidden Properties table parses into forbiddenByLabel
// (the live schema-base.md ships a Person+name row).
test("loadSchema: Forbidden Properties table is parsed into forbiddenByLabel", () => {
    const schema = loadSchema();
    assert.ok(schema.forbiddenByLabel instanceof Map, "forbiddenByLabel must be a Map");
    const personForbidden = schema.forbiddenByLabel.get("Person");
    assert.ok(personForbidden, "Person must carry forbidden-property entries (schema-base.md ships a row)");
    const nameRule = personForbidden.get("name");
    assert.ok(nameRule, "Person.name must be in the forbidden-property map");
    assert.match(nameRule.rationale, /givenName/, "rationale must reference the canonical pair so operators can grep the rule");
});
// success criterion #1: live schema-base.md must parse cleanly
// after the fix (loadSchema would throw on the line-168 row otherwise).
// This is the integration counterpart to the unit cases above — a green
// loadSchema() call is the closest local proxy for the boot-smoke gate.
test("loadSchema: schema-base.md line-168 PARTY row parses", () => {
    const schema = loadSchema();
    // PARTY isn't a label — it's an edge type — so it shouldn't appear in
    // markdownLabels. The assertion is that loadSchema returns at all without
    // throwing; reaching this point is the success signal.
    assert.ok(schema.markdownLabels.size > 0);
});
// Task 370 — schema-loader strips trailing footnote markers from
// required-property tokens. schema-estate-agent.md declares
// `` `heroImageUrl`*, `pageUrl`* `` where the trailing `*` points at a
// conditional-requirement footnote. The parser must recover the canonical
// property names; otherwise the validator stores `heroImageUrl*` and every
// Listing write fails with `missing required property 'heroImageUrl*'`.
test("loadSchema: Listing required-props strip footnote asterisks", () => {
    const brandPath = writeBrandJson({ vertical: "schema-estate-agent" });
    const schema = loadSchema({ brandJsonPath: brandPath });
    const listing = schema.markdownLabels.get("Listing");
    assert.ok(listing, "Listing must be documented in schema-estate-agent.md");
    const names = listing.required.flatMap((g) => g.kind === "simple" ? [g.name] : g.names);
    const withMarker = names.filter((n) => /[*†‡]/.test(n));
    assert.deepEqual(withMarker, [], `no required-property name may carry a trailing footnote marker; got ${JSON.stringify(withMarker)}`);
    assert.ok(names.includes("heroImageUrl"), "heroImageUrl must appear with footnote marker stripped");
    assert.ok(names.includes("pageUrl"), "pageUrl must appear with footnote marker stripped");
});
//# sourceMappingURL=schema-loader.test.js.map