/**
 * L3 Verification Runner
 *
 * Runs the Quint-transpiled guards against a spec snapshot and reports
 * pass / fail / skipped. A guard is skipped when any of its state-var
 * dependencies is missing from the snapshot — a raw (pre-inference)
 * spec, for example, won't have `inferredControllers`, so guards that
 * need it are skipped rather than failed.
 *
 * Usage:
 *   const result = await verifySpec(parsedSpec, { entitiesDir });
 *   if (result.failed.length > 0) process.exit(1);
 *
 * The caller is responsible for deciding what "the spec" is — raw,
 * inferred, or something else. `verifySpec` runs whichever guards are
 * applicable.
 */
import { createRequire } from 'module';
import { dirname, join } from 'path';
import { transpileEntityGuards, extractStateVariables, generateGuardsModule } from './quint-transpiler.js';
/**
 * Build a Quint-shaped snapshot from a SpecVerse AST.
 *
 * The AST nests collections under `components` and uses shorthand
 * strings for relationships and convention-format attributes. The
 * guards were written against a flatter, more structured shape — each
 * top-level state var (`models`, `controllers`, etc.) is an array of
 * entities, and each entity's nested collections (`attributes`,
 * `relationships`, …) are keyed maps.
 *
 * This function performs the normalization. The caller passes any spec
 * (raw or inferred); only keys actually present in the spec are
 * populated in the returned snapshot.
 */
export function buildSnapshot(spec, stateVars) {
    const toArray = (v) => {
        if (!v)
            return [];
        if (Array.isArray(v))
            return v;
        return Object.entries(v).map(([name, data]) => {
            if (data && typeof data === 'object')
                return { name, ...data };
            return { name, value: data };
        });
    };
    const toKeyedMap = (v) => {
        if (!v)
            return {};
        if (Array.isArray(v)) {
            const out = {};
            for (const item of v) {
                const key = item?.name || item?.id;
                if (key)
                    out[key] = item;
            }
            return out;
        }
        return v;
    };
    // Parse shorthand relationship strings into structured objects so
    // Quint guards that read `.relType` and `.targetModel` work on raw
    // specs too. Examples:
    //   "hasMany Component"               → {relType, targetModel}
    //   "hasMany Post cascade"            → + {cascade: true}
    //   "manyToMany Tag through=TagLink"  → + {through: "TagLink"}
    //   "belongsTo User"                  → {relType, targetModel}
    const parseRelationshipShorthand = (s, name) => {
        const parts = s.trim().split(/\s+/);
        const relType = parts[0];
        const targetModel = parts[1];
        const rest = parts.slice(2);
        const obj = { name, relType, targetModel };
        for (const token of rest) {
            if (token.includes('=')) {
                const [k, v] = token.split('=');
                obj[k] = v;
            }
            else {
                obj[token] = true;
            }
        }
        return obj;
    };
    const normalizeRelationships = (rels) => {
        if (!rels)
            return {};
        const result = {};
        const entries = Array.isArray(rels)
            ? rels.map((r) => [r.name, r])
            : Object.entries(rels);
        for (const [name, value] of entries) {
            if (typeof value === 'string') {
                result[name] = parseRelationshipShorthand(value, name);
            }
            else if (value && typeof value === 'object') {
                // The parser stores structured relationships as { type, target }
                // (e.g. { name: 'components', type: 'hasMany', target: 'Component' }).
                // Quint invariants read them as { relType, targetModel }. Map
                // between the two so guards can run against the parsed shape.
                const relType = value.relType ?? value.type;
                const targetModel = value.targetModel ?? value.target;
                result[name] = { ...value, name, relType, targetModel };
            }
        }
        return result;
    };
    const MODEL_NESTED = ['attributes', 'lifecycles', 'behaviors', 'profiles'];
    // relationships handled specially so shorthand strings get parsed
    const CONTROLLER_NESTED = ['endpoints', 'actions', 'operations'];
    const SERVICE_NESTED = ['operations'];
    const EVENT_NESTED = ['attributes'];
    const VIEW_NESTED = ['layouts', 'uiComponents'];
    const DEPLOYMENT_NESTED = ['instances'];
    const normalizeEntity = (entity, keys) => {
        if (!entity || typeof entity !== 'object')
            return entity;
        const out = { ...entity };
        for (const k of keys)
            if (k in out)
                out[k] = toKeyedMap(out[k]);
        // Relationships need their own normalization to parse shorthand strings.
        if ('relationships' in out)
            out.relationships = normalizeRelationships(out.relationships);
        return out;
    };
    const normList = (arr, keys) => arr.map(e => normalizeEntity(e, keys));
    // Field aliases and derivations: convenience flags the Quint specs
    // use that are simple views of data already in the spec. These are
    // NOT synthesizing new information — they expose existing data
    // under Quint's field naming.
    /**
     * Parser stores lifecycle states as a flat array of strings for the
     * shorthand form (`flow: "draft -> open -> closed"` → `states: ['draft',
     * 'open', 'closed']`). Quint invariants expect structured
     * LifecycleState records with `isInitial` / `isTerminal` flags. The
     * shorthand form always makes the first state initial and the last
     * terminal by convention, so we synthesize the structure here.
     */
    const structureLifecycle = (lc) => {
        if (!lc || typeof lc !== 'object')
            return { name: '', states: [], transitions: [] };
        const rawStates = Array.isArray(lc.states) ? lc.states : [];
        const structuredStates = rawStates.map((s, i) => {
            if (typeof s === 'string') {
                return { name: s, isInitial: i === 0, isTerminal: i === rawStates.length - 1 };
            }
            // Already structured — pass through, filling in flags if missing
            return {
                isInitial: false,
                isTerminal: false,
                ...s,
            };
        });
        // Transitions: if the lifecycle has a `flow` shorthand, derive
        // `from`/`to` pairs for adjacent states.
        let structuredTransitions = [];
        if (Array.isArray(lc.transitions) && lc.transitions.length > 0 && typeof lc.transitions[0] === 'object') {
            structuredTransitions = lc.transitions;
        }
        else if (rawStates.length > 1) {
            structuredTransitions = [];
            for (let i = 0; i < rawStates.length - 1; i++) {
                const from = typeof rawStates[i] === 'string' ? rawStates[i] : rawStates[i].name;
                const to = typeof rawStates[i + 1] === 'string' ? rawStates[i + 1] : rawStates[i + 1].name;
                structuredTransitions.push({ name: `${from}_to_${to}`, from, to });
            }
        }
        return {
            ...lc,
            states: structuredStates,
            transitions: structuredTransitions,
        };
    };
    const aliasModelFields = (m) => {
        const lifecycleEntries = m.lifecycles && typeof m.lifecycles === 'object'
            ? Object.values(m.lifecycles)
            : [];
        const firstLifecycle = lifecycleEntries[0];
        return {
            ...m,
            hasLifecycle: lifecycleEntries.length > 0,
            lifecycle: firstLifecycle
                ? structureLifecycle(firstLifecycle)
                : { name: '', states: [], transitions: [] },
        };
    };
    const aliasViewFields = (v) => ({
        ...v,
        // Parser stores `type` (matches YAML `type: list`), Quint uses
        // `viewType` to avoid keyword collision. Keep both.
        viewType: v.viewType ?? v.type,
    });
    // Collect top-level + component-nested entities for each state var.
    // Only populate keys where the spec actually has data; leave others
    // unset so the verification runner can skip dependent guards.
    //
    // When flattening, tag each entity with `.component` (the parent
    // component name) and `.system` (true for entities under
    // build-system / AI / tooling components — these are meta-models
    // describing the language itself and don't need runtime controllers).
    const SYSTEM_COMPONENT_PATTERN = /^(BuildSystem|ToolsSupport|AISupport|Tooling|MetaModel)/i;
    const snapshot = {};
    const componentList = spec?.components
        ? (Array.isArray(spec.components) ? spec.components : Object.entries(spec.components).map(([name, data]) => ({ name, ...(data || {}) })))
        : [];
    const tagWithComponent = (item, componentName) => {
        if (!item || typeof item !== 'object')
            return item;
        const isSystem = SYSTEM_COMPONENT_PATTERN.test(componentName);
        return { component: componentName, system: isSystem, ...item };
    };
    for (const v of stateVars) {
        const topLevel = toArray(spec?.[v]);
        const nested = [];
        for (const comp of componentList) {
            const val = comp?.[v];
            if (val) {
                const compName = comp?.name || '';
                for (const item of toArray(val)) {
                    nested.push(tagWithComponent(item, compName));
                }
            }
        }
        if (topLevel.length > 0 || nested.length > 0) {
            snapshot[v] = [...topLevel, ...nested];
        }
    }
    // `components` is its own state var, populated from the list
    if (stateVars.includes('components') && componentList.length > 0) {
        snapshot.components = componentList;
    }
    // Normalize per-entity nested collections
    if (snapshot.models)
        snapshot.models = normList(snapshot.models, MODEL_NESTED).map(aliasModelFields);
    if (snapshot.controllers)
        snapshot.controllers = normList(snapshot.controllers, CONTROLLER_NESTED);
    if (snapshot.services)
        snapshot.services = normList(snapshot.services, SERVICE_NESTED);
    if (snapshot.events)
        snapshot.events = normList(snapshot.events, EVENT_NESTED);
    if (snapshot.views)
        snapshot.views = normList(snapshot.views, VIEW_NESTED).map(aliasViewFields);
    if (snapshot.deployments)
        snapshot.deployments = normList(snapshot.deployments, DEPLOYMENT_NESTED);
    // `inferredControllers` / `inferredServices` are Quint internal aliases
    // for inference output. Populate them only when the respective
    // non-inferred collection is present AND non-empty — this is the
    // signal that inference (or a user writing the expanded form) has run.
    if (stateVars.includes('inferredControllers') && snapshot.controllers?.length) {
        snapshot.inferredControllers = snapshot.controllers;
    }
    if (stateVars.includes('inferredServices') && snapshot.services?.length) {
        snapshot.inferredServices = snapshot.services;
    }
    return snapshot;
}
/**
 * Scan a guard body for `<loopVar>.<field>` accesses that are bound to
 * a state-var collection (e.g. `c.depth` inside `commands.forall(c => ...)`).
 * For each such field, check whether it's populated on AT LEAST ONE
 * entity in the relevant snapshot collection. Return the fields that
 * aren't populated anywhere — those are the reason to skip the guard.
 *
 * This is a best-effort heuristic; it only catches the common case
 * where a Quint invariant references a field the runtime data doesn't
 * populate (typically fields added by inference that a raw spec lacks).
 */
function findUnusableFields(guard, snapshot) {
    const quint = guard.quint || '';
    // Find forall / exists / filter / map bindings: e.g. `commands.forall(c => ...)`
    // produces the binding `c -> commands`. We scan for `<stateVar>.<op>(<var> =>`
    // and then look for `<var>.<field>` accesses elsewhere in the body.
    const bindingRe = /(\w+)\.(?:forall|exists|filter|map|flatMap)\(\s*(\w+)\s*=>/g;
    const bindings = new Map(); // var → state var name
    let m;
    while ((m = bindingRe.exec(quint)) !== null) {
        const [, stateVar, varName] = m;
        if (guard.dependencies.includes(stateVar) && !bindings.has(varName)) {
            bindings.set(varName, stateVar);
        }
    }
    if (bindings.size === 0)
        return [];
    const missing = new Set();
    for (const [varName, stateVar] of bindings) {
        const fieldRe = new RegExp(`\\b${varName}\\.(\\w+)`, 'g');
        const fields = new Set();
        let fm;
        while ((fm = fieldRe.exec(quint)) !== null) {
            // Skip methods (followed by `(`)
            const after = quint[fm.index + fm[0].length];
            if (after === '(')
                continue;
            fields.add(fm[1]);
        }
        const collection = snapshot[stateVar] || [];
        for (const field of fields) {
            const anyHas = collection.some(item => item && typeof item === 'object' && field in item);
            if (!anyHas)
                missing.add(field);
        }
    }
    return [...missing].sort();
}
/**
 * Resolve the entities source directory when not explicitly provided.
 */
function resolveEntitiesDir() {
    const req = createRequire(import.meta.url);
    const pkgPath = req.resolve('@specverse/entities/package.json');
    return join(dirname(pkgPath), 'src');
}
/**
 * Run the L3 verification gate against the provided spec.
 *
 * `spec` can be any SpecVerse AST — raw, inferred, or anywhere in
 * between. Only guards whose dependencies are present in the
 * resulting snapshot run; the rest are reported as skipped.
 */
export async function verifySpec(spec, opts = {}) {
    const entitiesDir = opts.entitiesDir || resolveEntitiesDir();
    const guards = transpileEntityGuards(entitiesDir);
    const stateVars = extractStateVariables(entitiesDir);
    // Deduplicate guards by (kind, name) to match generateGuardsModule
    const seen = new Set();
    const uniqueGuards = guards.filter(g => {
        const key = `${g.kind}:${g.name}`;
        if (seen.has(key))
            return false;
        seen.add(key);
        return true;
    });
    // Build the snapshot — caller can override which vars are "available"
    // by passing an explicit availableVars list, but by default we derive
    // it from what buildSnapshot populates.
    const snapshot = buildSnapshot(spec, stateVars);
    const availableVars = new Set(opts.availableVars || Object.keys(snapshot));
    const invariants = uniqueGuards.filter(g => g.kind === 'invariant');
    const failed = [];
    const skipped = [];
    const errors = [];
    let passed = 0;
    // Partition: which guards can run against this snapshot?
    // A guard is runnable when (a) all its state-var dependencies are in
    // the snapshot AND (b) for every `<loopVar>.<field>` access in its
    // body, that field exists on at least one entity of the relevant
    // state var. This second check catches cases like `c.depth <= 3`
    // where the field is populated only by inference — on a raw spec,
    // the guard would silently return false from `undefined <= 3`, which
    // is a data-completeness issue, not an invariant violation.
    const runnable = [];
    for (const g of invariants) {
        const missingVars = g.dependencies.filter(d => !availableVars.has(d));
        if (missingVars.length > 0) {
            skipped.push({ name: g.name, entity: g.entity, missingDeps: missingVars });
            continue;
        }
        const missingFields = findUnusableFields(g, snapshot);
        if (missingFields.length > 0) {
            skipped.push({ name: g.name, entity: g.entity, missingDeps: missingFields.map(f => `field ${f}`) });
            continue;
        }
        runnable.push(g);
    }
    if (runnable.length === 0) {
        return { total: invariants.length, passed: 0, failed, skipped, errors };
    }
    // Transpile guards to a runnable module — only the ones we need, so
    // state vars filter down naturally.
    const guardsCode = generateGuardsModule(runnable, stateVars);
    const esbuild = await import('esbuild');
    const compiled = esbuild.transformSync(guardsCode, {
        loader: 'ts',
        format: 'esm',
        target: 'es2022',
    }).code;
    const dataUrl = 'data:text/javascript;base64,' + Buffer.from(compiled).toString('base64');
    const mod = await import(dataUrl);
    if (typeof mod.setSpecSnapshot === 'function') {
        mod.setSpecSnapshot(snapshot);
    }
    // Classify errors: a thrown exception from undefined field access
    // (e.g. `.every` on undefined, `.X` on undefined, `X is not a function`
    // on a missing array method) means the guard expected a field the
    // spec doesn't have. Report it as "skipped — data-completeness",
    // not as a transpiler error.
    const classifyError = (msg) => {
        if (/Cannot read properties of (undefined|null)/.test(msg) ||
            /\bof undefined\b/.test(msg) ||
            /Cannot convert undefined or null to object/.test(msg) ||
            / is not a function\b/.test(msg)) {
            const field = msg.match(/reading '([^']+)'/)?.[1]
                || msg.match(/\b(\w+) is not a function/)?.[1];
            return { isShape: true, field };
        }
        return { isShape: false };
    };
    for (const g of runnable) {
        const fn = mod[`check_${g.name}`];
        if (typeof fn !== 'function')
            continue;
        try {
            const ok = fn();
            if (ok === false) {
                failed.push({
                    name: g.name,
                    entity: g.entity,
                    module: g.module,
                    quint: g.quint,
                });
            }
            else {
                passed++;
            }
        }
        catch (e) {
            const msg = e.message || String(e);
            const cls = classifyError(msg);
            if (cls.isShape) {
                skipped.push({
                    name: g.name,
                    entity: g.entity,
                    missingDeps: cls.field ? [`field ${cls.field}`] : ['undefined field'],
                });
            }
            else {
                errors.push({ name: g.name, entity: g.entity, message: msg });
            }
        }
    }
    return {
        total: invariants.length,
        passed,
        failed,
        skipped,
        errors,
    };
}
/**
 * Format a verification result for CLI output.
 */
export function formatVerificationResult(result, verbose = false) {
    const lines = [];
    const applicable = result.passed + result.failed.length + result.errors.length;
    lines.push(`L3 verification: ${result.passed}/${applicable} invariants hold`);
    if (result.skipped.length > 0) {
        lines.push(`  ${result.skipped.length} skipped (not applicable to this spec)`);
    }
    if (result.failed.length > 0) {
        lines.push(`  ${result.failed.length} failed:`);
        const toShow = verbose ? result.failed : result.failed.slice(0, 10);
        for (const f of toShow) {
            lines.push(`    ✗ ${f.entity}/${f.name}`);
            if (verbose && f.quint) {
                const first = f.quint.split('\n')[0].slice(0, 120);
                lines.push(`        ${first}`);
            }
        }
        if (!verbose && result.failed.length > 10) {
            lines.push(`    ...and ${result.failed.length - 10} more`);
        }
    }
    if (result.errors.length > 0) {
        lines.push(`  ${result.errors.length} errored (shape mismatch):`);
        const toShow = verbose ? result.errors : result.errors.slice(0, 5);
        for (const e of toShow) {
            lines.push(`    ⚠ ${e.entity}/${e.name}: ${e.message}`);
        }
        if (!verbose && result.errors.length > 5) {
            lines.push(`    ...and ${result.errors.length - 5} more`);
        }
    }
    if (verbose && result.skipped.length > 0) {
        lines.push('  skipped guards:');
        for (const s of result.skipped) {
            lines.push(`    - ${s.entity}/${s.name} (needs: ${s.missingDeps.join(', ')})`);
        }
    }
    return lines.join('\n');
}
//# sourceMappingURL=verification.js.map