// Deterministic structural test baseline from a Screen-IR (Epic #750, Issue #754).
//
// Pure domain: a Screen-IR (#752) is reduced by structural rules to a reproducible, citation-ready
// list of test items — every input field → presence + validation tests; every control (button/link)
// → action + expected-result tests; every screen → a render/navigation test; every state/variant
// → a state test. No IO, no model, no network: the same IR yields a byte-identical baseline so the
// QI source ships a usable baseline with NO model available.
//
// Generic by construction: the rules read only the IR's structural shape (interactionHint, node
// type, text, and a generic `state`/`variant` naming convention shared across design tools). No
// rule, threshold, name, or template is tuned to a specific board.
//
// Additive seam: `deriveScreenTestBaseline` accepts optional `extraItems`, so sibling derivations
// (navigation/flow #811, a11y #812) can contribute extra per-screen test items WITHOUT changing the
// baseline shape. Vision-derived semantics are layered separately (see visionAugmentation.ts) and
// never replace these structural items.
const INTERACTION_HINTS = new Set([
    "button",
    "input",
    "link",
    "text",
    "image",
    "container",
]);
function isObject(value) {
    return typeof value === "object" && value !== null && !Array.isArray(value);
}
function isString(value) {
    return typeof value === "string";
}
function isFiniteNumber(value) {
    return typeof value === "number" && Number.isFinite(value);
}
// Parse the optional bounding box from the serialised `irJson` (used by the a11y focus-order and
// target-size derivation, #812). A malformed or partial box is dropped rather than crashing.
function parseBoundingBox(value) {
    if (!isObject(value))
        return undefined;
    const { x, y, width, height } = value;
    if (!isFiniteNumber(x) ||
        !isFiniteNumber(y) ||
        !isFiniteNumber(width) ||
        !isFiniteNumber(height)) {
        return undefined;
    }
    return { x, y, width, height };
}
function parseImageFills(value) {
    if (!Array.isArray(value))
        return [];
    const refs = [];
    for (const entry of value) {
        if (isObject(entry) && isString(entry.imageRef))
            refs.push({ imageRef: entry.imageRef });
    }
    return refs;
}
// Parse a node's children list, dropping any malformed child rather than failing the whole node.
function parseIrChildren(value) {
    if (!Array.isArray(value))
        return [];
    const children = [];
    for (const child of value) {
        const parsed = parseIrNode(child);
        if (parsed !== undefined)
            children.push(parsed);
    }
    return children;
}
// The optional, additive node fields (text, bounding box, a11y colours #812). Each is present only
// when well-typed; a malformed value is dropped so a corrupt node degrades rather than crashing.
function parseOptionalNodeFields(value) {
    const boundingBox = parseBoundingBox(value.boundingBox);
    return {
        ...(isString(value.text) ? { text: value.text } : {}),
        ...(boundingBox !== undefined ? { boundingBox } : {}),
        ...(isString(value.textColor) ? { textColor: value.textColor } : {}),
        ...(isString(value.backgroundColor) ? { backgroundColor: value.backgroundColor } : {}),
    };
}
// Total, defensive IR-node parser: an opaque serialised node (from the snapshot's `irJson`) is
// accepted only when its required structural fields are present and well-typed; anything malformed
// yields `undefined` so a corrupt screen degrades to "no items" rather than crashing the run.
function parseIrNode(value) {
    if (!isObject(value))
        return undefined;
    const { id, name, type, interactionHint } = value;
    if (!isString(id) || !isString(name) || !isString(type))
        return undefined;
    if (!isString(interactionHint) || !INTERACTION_HINTS.has(interactionHint))
        return undefined;
    return {
        id,
        name,
        type,
        interactionHint: interactionHint,
        ...parseOptionalNodeFields(value),
        imageFills: parseImageFills(value.imageFills),
        children: parseIrChildren(value.children),
    };
}
/**
 * Total, defensive Screen-IR parser for the snapshot's opaque `irJson`. Returns `undefined` for a
 * missing or malformed value (no `root`, malformed node tree) so the caller can skip an unparseable
 * screen without crashing. A valid IR is returned verbatim through the typed shape.
 */
export function parseScreenIr(value) {
    if (!isObject(value) || !isString(value.id) || !isString(value.name))
        return undefined;
    const root = parseIrNode(value.root);
    if (root === undefined)
        return undefined;
    return { id: value.id, name: value.name, root };
}
// A generic state/variant naming convention shared across design tools: a node whose name carries an
// explicit `state=` / `variant=` property segment, or a slash-delimited property. NOT tuned to any
// board's vocabulary — it matches the structural property syntax, never specific state names.
const STATE_PROPERTY = /(?:^|[,\s])(?:state|variant)\s*=\s*([^,]+)/iu;
function stateLabel(name) {
    const match = STATE_PROPERTY.exec(name);
    const captured = match?.[1]?.trim();
    return captured !== undefined && captured.length > 0 ? captured : undefined;
}
function shortHash(input) {
    // Deterministic non-cryptographic id suffix (FNV-1a) — stable across runs, no IO, no import.
    let hash = 0x811c9dc5;
    for (let i = 0; i < input.length; i += 1) {
        hash ^= input.charCodeAt(i);
        hash = Math.imul(hash, 0x01000193);
    }
    return (hash >>> 0).toString(16).padStart(8, "0");
}
function itemId(screenId, category, nodeId, ordinal) {
    return `fst-${shortHash(`${screenId}|${category}|${nodeId}|${String(ordinal)}`)}`;
}
function fieldItems(node, ctx) {
    const label = node.text !== undefined && node.text.length > 0 ? node.text : node.name;
    const base = { screenId: ctx.screenId, screenName: ctx.screenName, sourceNodeId: node.id };
    return [
        {
            ...base,
            id: itemId(ctx.screenId, "field-presence", node.id, 0),
            category: "field-presence",
            title: `Field "${label}" is present and editable on screen "${ctx.screenName}"`,
        },
        {
            ...base,
            id: itemId(ctx.screenId, "field-validation", node.id, 0),
            category: "field-validation",
            title: `Field "${label}" rejects empty / malformed input on screen "${ctx.screenName}"`,
        },
    ];
}
function controlItems(node, ctx) {
    const label = node.text !== undefined && node.text.length > 0 ? node.text : node.name;
    const verb = node.interactionHint === "link" ? "Following" : "Activating";
    return [
        {
            screenId: ctx.screenId,
            screenName: ctx.screenName,
            sourceNodeId: node.id,
            id: itemId(ctx.screenId, "control-action", node.id, 0),
            category: "control-action",
            title: `${verb} "${label}" produces its expected result on screen "${ctx.screenName}"`,
        },
    ];
}
function stateItem(node, ctx, label) {
    return {
        screenId: ctx.screenId,
        screenName: ctx.screenName,
        sourceNodeId: node.id,
        id: itemId(ctx.screenId, "state", node.id, 0),
        category: "state",
        title: `Element "${node.name}" renders correctly in state "${label}" on screen "${ctx.screenName}"`,
    };
}
function collectNodeItems(node, ctx, out) {
    if (node.interactionHint === "input")
        out.push(...fieldItems(node, ctx));
    if (node.interactionHint === "button" || node.interactionHint === "link") {
        out.push(...controlItems(node, ctx));
    }
    const state = stateLabel(node.name);
    if (state !== undefined)
        out.push(stateItem(node, ctx, state));
    for (const child of node.children)
        collectNodeItems(child, ctx, out);
}
function screenRenderItem(ctx) {
    return {
        screenId: ctx.screenId,
        screenName: ctx.screenName,
        id: itemId(ctx.screenId, "screen-render", ctx.screenId, 0),
        category: "screen-render",
        title: `Screen "${ctx.screenName}" renders and is reachable`,
    };
}
/**
 * Derive the deterministic structural baseline for one screen. The screen-render item comes first,
 * then field / control / state items in stable depth-first node order. `extraItems` is the additive
 * seam (#811 navigation, #812 a11y): a sibling derivation may contribute already-built test items
 * for this screen without changing the baseline shape. The result is reproducible for a given IR.
 */
export function deriveScreenTestBaseline(screen, extraItems = []) {
    const ctx = { screenId: screen.id, screenName: screen.name };
    const items = [screenRenderItem(ctx)];
    collectNodeItems(screen.root, ctx, items);
    items.push(...extraItems);
    return { screenId: screen.id, screenName: screen.name, items };
}
/**
 * Render the structural baseline as citation-ready canonical text carrying per-screen provenance.
 * The text is deterministic (no timestamps, stable order) and is what the QI ingestion turns into a
 * content-bearing atom. Vision-derived hints, when present, are appended SEPARATELY by the caller
 * (visionAugmentation.ts) and never replace these lines.
 */
export function renderBaselineText(baseline) {
    const header = `Screen: ${baseline.screenName} [${baseline.screenId}]`;
    const lines = baseline.items.map((item) => `- (${item.category}) ${item.title}`);
    return [
        header,
        "Structural test baseline (deterministic, derived from Screen-IR):",
        ...lines,
    ].join("\n");
}
