/**
 * @specverse/engines/inference/ui-contracts
 *
 * Spec-inferred UI contract test generation. The realize pipeline
 * calls `generateContractTests(spec)` which runs every registered
 * rule over the normalized spec and returns a map of filename →
 * spec file content. The caller writes those to disk.
 *
 * Design: see docs/proposals/UI-CONTRACT-INFERENCE.md.
 */
import { renderSpecFile } from './translator.js';
import { navHasModelEntries, RULE_ID as NAV_HAS_MODEL_ENTRIES_RULE_ID } from './rules/nav-has-model-entries.js';
import { listViewRenders, RULE_ID as LIST_VIEW_RENDERS_RULE_ID } from './rules/list-view-renders.js';
import { detailViewRenders, RULE_ID as DETAIL_VIEW_RENDERS_RULE_ID } from './rules/detail-view-renders.js';
import { listShowsBusinessColumns, RULE_ID as LIST_SHOWS_BUSINESS_COLUMNS_RULE_ID } from './rules/list-shows-business-columns.js';
import { formShowsRequiredIndicators, RULE_ID as FORM_SHOWS_REQUIRED_INDICATORS_RULE_ID } from './rules/form-shows-required-indicators.js';
import { lifecycleStateVisibleInDetail, RULE_ID as LIFECYCLE_STATE_VISIBLE_RULE_ID } from './rules/lifecycle-state-visible-in-detail.js';
import { hasManyShowsChildrenInDetail, RULE_ID as HASMANY_SHOWS_CHILDREN_RULE_ID } from './rules/hasmany-shows-children-in-detail.js';
import { actionButtonsPresent, RULE_ID as ACTION_BUTTONS_PRESENT_RULE_ID } from './rules/action-buttons-present.js';
import { createReflectsInList, RULE_ID as CREATE_REFLECTS_RULE_ID } from './rules/create-reflects-in-list.js';
import { deleteReflectsInList, RULE_ID as DELETE_REFLECTS_RULE_ID } from './rules/delete-reflects-in-list.js';
import { evolveReflectsInList, RULE_ID as EVOLVE_REFLECTS_RULE_ID } from './rules/evolve-reflects-in-list.js';
export * from './test-case-types.js';
export { renderSpecFile } from './translator.js';
/**
 * Registered rules. Order doesn't matter for output — each rule
 * produces its own spec file. Adding a rule = pushing a new entry
 * here + creating a file under rules/.
 */
const RULES = [
    // Rendering
    { id: NAV_HAS_MODEL_ENTRIES_RULE_ID, rule: navHasModelEntries },
    { id: LIST_VIEW_RENDERS_RULE_ID, rule: listViewRenders },
    { id: DETAIL_VIEW_RENDERS_RULE_ID, rule: detailViewRenders },
    // Shape
    { id: LIST_SHOWS_BUSINESS_COLUMNS_RULE_ID, rule: listShowsBusinessColumns },
    { id: FORM_SHOWS_REQUIRED_INDICATORS_RULE_ID, rule: formShowsRequiredIndicators },
    { id: LIFECYCLE_STATE_VISIBLE_RULE_ID, rule: lifecycleStateVisibleInDetail },
    { id: HASMANY_SHOWS_CHILDREN_RULE_ID, rule: hasManyShowsChildrenInDetail },
    { id: ACTION_BUTTONS_PRESENT_RULE_ID, rule: actionButtonsPresent },
    // State-sync
    { id: CREATE_REFLECTS_RULE_ID, rule: createReflectsInList },
    { id: DELETE_REFLECTS_RULE_ID, rule: deleteReflectsInList },
    { id: EVOLVE_REFLECTS_RULE_ID, rule: evolveReflectsInList },
];
/**
 * Run every rule over the spec and render each rule's output as a
 * single .spec.ts file. Rules that produce zero test cases (e.g.
 * because no spec elements matched) are skipped.
 */
export function generateContractTests(spec) {
    const files = [];
    for (const { id, rule } of RULES) {
        const cases = rule(spec);
        if (cases.length === 0)
            continue;
        files.push({
            filename: `${id}.spec.ts`,
            contents: renderSpecFile(id, cases),
            testCount: cases.length,
        });
    }
    return files;
}
/**
 * Convert a parsed/inferred spec (as produced by the parser or the
 * inference engine) into the NormalizedSpec shape that rules consume.
 *
 * The parser returns components as an array with each holding
 * `models`, `controllers`, etc. This function flattens to a single
 * top-level view. It handles both object-keyed and array-shaped
 * collections.
 */
export function normalizeSpec(raw) {
    const components = raw?.components || {};
    const componentList = Array.isArray(components)
        ? components
        : Object.values(components);
    const models = [];
    const controllers = [];
    const services = [];
    const views = [];
    // Root-level collections (used by inferred specs that have already
    // been flattened by realize)
    const collect = (collection, into) => {
        if (!collection)
            return;
        if (Array.isArray(collection)) {
            into.push(...collection);
        }
        else {
            for (const [name, value] of Object.entries(collection)) {
                into.push({ name, ...value });
            }
        }
    };
    collect(raw?.models, models);
    collect(raw?.controllers, controllers);
    collect(raw?.services, services);
    collect(raw?.views, views);
    for (const comp of componentList) {
        collect(comp?.models, models);
        collect(comp?.controllers, controllers);
        collect(comp?.services, services);
        collect(comp?.views, views);
    }
    // Dedupe by name — the realize pipeline often passes both a
    // root-level flattened collection AND the component-level source,
    // so we'd otherwise double-count every model.
    const dedupeByName = (items) => {
        const seen = new Set();
        const out = [];
        for (const item of items) {
            const name = item?.name;
            if (!name || seen.has(name))
                continue;
            seen.add(name);
            out.push(item);
        }
        return out;
    };
    const uniqModels = dedupeByName(models);
    const uniqControllers = dedupeByName(controllers);
    const uniqServices = dedupeByName(services);
    const uniqViews = dedupeByName(views);
    /**
     * Normalize an attributes collection from either array or object
     * form, and each entry from either shorthand string
     * ("String required") or object form.
     */
    const normalizeAttributes = (raw) => {
        if (!raw)
            return undefined;
        const entries = Array.isArray(raw)
            ? raw.map((a) => [a?.name || '', a])
            : Object.entries(raw);
        return entries
            .filter(([name]) => !!name)
            .map(([name, def]) => {
            if (typeof def === 'string') {
                const tokens = def.split(/\s+/);
                const type = tokens[0] || 'String';
                const required = /\brequired\b/.test(def);
                const category = /\bcategory=metadata\b/.test(def)
                    ? 'metadata'
                    : /\bcategory=relationship\b/.test(def)
                        ? 'relationship'
                        : /\bauto=now\b/.test(def) || name === 'id'
                            ? 'metadata'
                            : 'business';
                return { name, type, required, category };
            }
            return {
                name,
                type: def?.type || 'String',
                required: !!def?.required,
                category: def?.category || (name === 'id' ? 'metadata' : 'business'),
            };
        });
    };
    /**
     * Normalize a relationships collection. Supports:
     *   - Array of {name, type, target}
     *   - Object keyed by name, values as {type, target} or shorthand "hasMany Post"
     */
    const normalizeRelationships = (raw) => {
        if (!raw)
            return undefined;
        const entries = Array.isArray(raw)
            ? raw.map((r) => [r?.name || '', r])
            : Object.entries(raw);
        return entries
            .filter(([name]) => !!name)
            .map(([name, def]) => {
            if (typeof def === 'string') {
                // "hasMany Post cascade" → type=hasMany, target=Post
                const tokens = def.split(/\s+/);
                const type = tokens[0] || '';
                const target = tokens[1];
                return { name, type, target };
            }
            return {
                name,
                type: def?.type || '',
                target: def?.target || def?.targetModel,
            };
        });
    };
    return {
        models: uniqModels.map(m => ({
            name: m.name,
            attributes: normalizeAttributes(m.attributes),
            lifecycles: m.lifecycles,
            relationships: normalizeRelationships(m.relationships),
        })),
        controllers: uniqControllers.map(c => ({
            name: c.name,
            model: c.model || c.modelReference,
            cured: c.cured,
        })),
        services: uniqServices.map(s => ({
            name: s.name,
            operations: s.operations,
        })),
        views: uniqViews.map(v => ({
            name: v.name,
            type: v.type,
            model: v.primaryModel || v.model,
        })),
    };
}
//# sourceMappingURL=index.js.map