"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getJsonStepId = getJsonStepId;
exports.getJsonStepGroupChildren = getJsonStepGroupChildren;
exports.applyEdits = applyEdits;
exports.enforceFindEditPolicy = enforceFindEditPolicy;
function stepTypeKey(step) {
    if (!step || typeof step !== 'object') {
        return undefined;
    }
    const keys = Object.keys(step);
    return keys.length >= 1 ? keys[0] : undefined;
}
function stepBody(step) {
    const key = stepTypeKey(step);
    if (key === undefined) {
        return undefined;
    }
    const body = step[key];
    return body && typeof body === 'object'
        ? body
        : undefined;
}
function getJsonStepId(step) {
    const id = stepBody(step)?.id;
    return typeof id === 'string' ? id : undefined;
}
function getJsonStepGroupChildren(step) {
    if (stepTypeKey(step) !== 'StepGroup') {
        return undefined;
    }
    const steps = stepBody(step)?.steps;
    return Array.isArray(steps) ? steps : undefined;
}
function withNestedSteps(group, steps) {
    const key = stepTypeKey(group);
    const body = stepBody(group) ?? {};
    return { [key]: { ...body, steps } };
}
function withId(step, id) {
    const key = stepTypeKey(step);
    if (key === undefined) {
        return step;
    }
    const body = stepBody(step) ?? {};
    return { [key]: { ...body, id } };
}
function idExists(steps, id) {
    for (const step of steps) {
        if (getJsonStepId(step) === id) {
            return true;
        }
        const nested = getJsonStepGroupChildren(step);
        if (nested && idExists(nested, id)) {
            return true;
        }
    }
    return false;
}
function findStepById(steps, id) {
    for (const step of steps) {
        if (getJsonStepId(step) === id) {
            return step;
        }
        const nested = getJsonStepGroupChildren(step);
        if (nested) {
            const found = findStepById(nested, id);
            if (found !== undefined) {
                return found;
            }
        }
    }
    return undefined;
}
function resolveAnchor(stepId, index, rootLength, allowPrepend, steps) {
    if (stepId !== undefined && index !== undefined) {
        return {
            reason: 'ambiguous_anchor',
            detail: 'specify either step_id or index, not both',
        };
    }
    if (stepId !== undefined) {
        if (!idExists(steps, stepId)) {
            return {
                reason: 'step_id_not_found',
                detail: `no step with id "${stepId}" exists (steps inside referenced flows are not editable)`,
            };
        }
        return { kind: 'id', id: stepId };
    }
    if (index !== undefined) {
        const minIndex = allowPrepend ? -1 : 0;
        if (!Number.isInteger(index) || index < minIndex || index >= rootLength) {
            return {
                reason: 'index_out_of_range',
                detail: `index ${index} is outside [${minIndex}, ${rootLength - 1}]${allowPrepend ? ' (use -1 to prepend)' : ''}`,
            };
        }
        if (allowPrepend && index === -1) {
            return { kind: 'prepend' };
        }
        return { kind: 'rootIndex', index };
    }
    if (allowPrepend) {
        return { kind: 'prepend' };
    }
    return { reason: 'missing_anchor', detail: 'step_id or index is required' };
}
function anchorKey(anchor) {
    switch (anchor.kind) {
        case 'id':
            return `id:${anchor.id}`;
        case 'rootIndex':
            return `idx:${anchor.index}`;
        case 'prepend':
            return 'prepend';
    }
}
const KNOWN_OPS = [
    'replace',
    'insert_after',
    'insert_before',
    'delete',
    'move',
];
function isKnownOp(op) {
    return (typeof op === 'string' && KNOWN_OPS.includes(op));
}
function hasStepTypeKey(step) {
    return (!!step &&
        typeof step === 'object' &&
        stepTypeKey(step) !== undefined);
}
function requireStepAtAnchor(anchor, steps) {
    if (anchor.kind === 'id') {
        const step = findStepById(steps, anchor.id);
        if (step === undefined) {
            throw new Error(`unreachable: resolveAnchor verified step id "${anchor.id}" exists`);
        }
        return step;
    }
    if (anchor.kind === 'rootIndex') {
        return steps[anchor.index];
    }
    throw new Error('unreachable: requireStepAtAnchor called with a prepend anchor');
}
function describeAnchor(anchor) {
    switch (anchor.kind) {
        case 'id':
            return `step id "${anchor.id}"`;
        case 'rootIndex':
            return `root index ${anchor.index}`;
        case 'prepend':
            return 'root prepend';
    }
}
function validateAndCanonicalize(ops, steps) {
    const errors = [];
    const canonical = [];
    const exclusiveTargets = new Map();
    const pushError = (opIndex, op, err) => {
        errors.push({
            op_index: opIndex,
            op,
            reason: err.reason,
            detail: err.detail,
        });
    };
    ops.forEach((op, opIndex) => {
        if (!isKnownOp(op.op)) {
            pushError(opIndex, op.op ?? '<missing>', {
                reason: 'unknown_op',
                detail: `expected one of: ${KNOWN_OPS.join(', ')}`,
            });
            return;
        }
        if (op.op === 'replace' ||
            op.op === 'insert_after' ||
            op.op === 'insert_before') {
            if (!hasStepTypeKey(op.step)) {
                pushError(opIndex, op.op, {
                    reason: 'invalid_step',
                    detail: '`step` must be a json_steps singleton-map object keyed by step type',
                });
                return;
            }
        }
        const noteExclusive = (anchor) => {
            if (anchor.kind === 'prepend') {
                return;
            }
            const key = anchorKey(anchor);
            const existing = exclusiveTargets.get(key);
            if (existing) {
                existing.opIndices.push(opIndex);
            }
            else {
                exclusiveTargets.set(key, { anchor, opIndices: [opIndex] });
            }
        };
        if (op.op === 'replace') {
            const anchor = resolveAnchor(op.step_id, op.index, steps.length, false, steps);
            if ('reason' in anchor) {
                pushError(opIndex, op.op, anchor);
                return;
            }
            noteExclusive(anchor);
            canonical.push({ kind: 'replace', anchor, step: op.step });
            return;
        }
        if (op.op === 'insert_after') {
            const anchor = resolveAnchor(op.step_id, op.index, steps.length, true, steps);
            if ('reason' in anchor) {
                pushError(opIndex, op.op, anchor);
                return;
            }
            canonical.push({ kind: 'insert_after', anchor, step: op.step });
            return;
        }
        if (op.op === 'insert_before') {
            const anchor = resolveAnchor(op.step_id, op.index, steps.length, false, steps);
            if ('reason' in anchor) {
                pushError(opIndex, op.op, anchor);
                return;
            }
            canonical.push({ kind: 'insert_before', anchor, step: op.step });
            return;
        }
        if (op.op === 'delete') {
            const anchor = resolveAnchor(op.step_id, op.index, steps.length, false, steps);
            if ('reason' in anchor) {
                pushError(opIndex, op.op, anchor);
                return;
            }
            noteExclusive(anchor);
            canonical.push({ kind: 'delete', anchor });
            return;
        }
        const from = resolveAnchor(op.from_step_id, op.from, steps.length, false, steps);
        if ('reason' in from) {
            pushError(opIndex, op.op, from);
            return;
        }
        const to = resolveAnchor(op.to_step_id, op.to, steps.length, false, steps);
        if ('reason' in to) {
            pushError(opIndex, op.op, to);
            return;
        }
        noteExclusive(from);
        canonical.push({
            kind: 'move',
            from,
            to,
            lifted: requireStepAtAnchor(from, steps),
        });
    });
    for (const { anchor, opIndices } of exclusiveTargets.values()) {
        if (opIndices.length > 1) {
            for (const opIndex of opIndices) {
                errors.push({
                    op_index: opIndex,
                    op: ops[opIndex].op,
                    reason: 'conflicting_target',
                    detail: `${describeAnchor(anchor)} is targeted by multiple mutating ops (${opIndices.join(', ')})`,
                });
            }
        }
    }
    return { canonical, errors };
}
function rewrite(steps, canonical) {
    const replaceById = new Map();
    const replaceByRootIndex = new Map();
    const insertAfterById = new Map();
    const insertAfterByRootIndex = new Map();
    const insertBeforeById = new Map();
    const insertBeforeByRootIndex = new Map();
    const deleteIds = new Set();
    const deleteRootIndices = new Set();
    const prepends = [];
    const pushAt = (map, key, step) => {
        const existing = map.get(key);
        if (existing) {
            existing.push(step);
        }
        else {
            map.set(key, [step]);
        }
    };
    const registerInsertAfter = (anchor, step) => {
        switch (anchor.kind) {
            case 'id':
                pushAt(insertAfterById, anchor.id, step);
                return;
            case 'rootIndex':
                pushAt(insertAfterByRootIndex, anchor.index, step);
                return;
            case 'prepend':
                prepends.push(step);
                return;
        }
    };
    const registerInsertBefore = (anchor, step) => {
        if (anchor.kind === 'id') {
            pushAt(insertBeforeById, anchor.id, step);
        }
        else if (anchor.kind === 'rootIndex') {
            pushAt(insertBeforeByRootIndex, anchor.index, step);
        }
    };
    const registerDelete = (anchor) => {
        if (anchor.kind === 'id') {
            deleteIds.add(anchor.id);
        }
        else if (anchor.kind === 'rootIndex') {
            deleteRootIndices.add(anchor.index);
        }
    };
    for (const op of canonical) {
        switch (op.kind) {
            case 'replace':
                if (op.anchor.kind === 'id') {
                    replaceById.set(op.anchor.id, op.step);
                }
                else if (op.anchor.kind === 'rootIndex') {
                    replaceByRootIndex.set(op.anchor.index, op.step);
                }
                break;
            case 'insert_after':
                registerInsertAfter(op.anchor, op.step);
                break;
            case 'insert_before':
                registerInsertBefore(op.anchor, op.step);
                break;
            case 'delete':
                registerDelete(op.anchor);
                break;
            case 'move':
                registerDelete(op.from);
                registerInsertAfter(op.to, op.lifted);
                break;
        }
    }
    const transform = (nodes, isRoot) => {
        const out = [];
        nodes.forEach((step, i) => {
            const id = getJsonStepId(step);
            const wholeNodeReplaced = (id !== undefined && replaceById.has(id)) ||
                (isRoot && replaceByRootIndex.has(i));
            let node = step;
            const nested = getJsonStepGroupChildren(node);
            if (nested && !wholeNodeReplaced) {
                node = withNestedSteps(node, transform(nested, false));
            }
            if (id !== undefined && replaceById.has(id)) {
                node = withId(replaceById.get(id), id);
            }
            else if (isRoot && replaceByRootIndex.has(i)) {
                node = replaceByRootIndex.get(i);
            }
            const insertsBeforeId = id !== undefined ? insertBeforeById.get(id) : undefined;
            if (insertsBeforeId) {
                out.push(...insertsBeforeId);
            }
            if (isRoot) {
                const insertsBeforeIndex = insertBeforeByRootIndex.get(i);
                if (insertsBeforeIndex) {
                    out.push(...insertsBeforeIndex);
                }
            }
            const deletedHere = (id !== undefined && deleteIds.has(id)) ||
                (isRoot && deleteRootIndices.has(i));
            if (!deletedHere) {
                out.push(node);
            }
            const insertsAfterId = id !== undefined ? insertAfterById.get(id) : undefined;
            if (insertsAfterId) {
                out.push(...insertsAfterId);
            }
            if (isRoot) {
                const insertsAfterIndex = insertAfterByRootIndex.get(i);
                if (insertsAfterIndex) {
                    out.push(...insertsAfterIndex);
                }
            }
        });
        return out;
    };
    return [...prepends, ...transform(steps, true)];
}
function applyEdits(currentSteps, ops) {
    const { canonical, errors } = validateAndCanonicalize(ops, currentSteps);
    if (errors.length > 0) {
        return { steps: [], errors };
    }
    return { steps: rewrite(currentSteps, canonical), errors: [] };
}
function hasFind(step) {
    const body = stepBody(step);
    return body !== undefined && 'find' in body && Boolean(body.find);
}
function findSelectorUuids(find) {
    const out = [];
    const walk = (value) => {
        if (Array.isArray(value)) {
            value.forEach(walk);
            return;
        }
        if (value && typeof value === 'object') {
            const obj = value;
            if (typeof obj.uuid === 'string') {
                out.push(obj.uuid);
            }
            for (const key of Object.keys(obj)) {
                walk(obj[key]);
            }
        }
    };
    walk(find);
    return out;
}
function rehydrateFind(find, resolveSelector) {
    if (Array.isArray(find)) {
        return find.map((entry) => rehydrateFind(entry, resolveSelector));
    }
    if (find && typeof find === 'object') {
        const obj = find;
        if (typeof obj.uuid === 'string') {
            const resolved = resolveSelector(obj.uuid);
            if (resolved) {
                return { ...resolved, uuid: obj.uuid };
            }
        }
        const next = {};
        for (const key of Object.keys(obj)) {
            next[key] = rehydrateFind(obj[key], resolveSelector);
        }
        return next;
    }
    return find;
}
function enforceFindEditPolicy(currentSteps, ops, options = {}) {
    const findById = new Map();
    const hasFindById = new Set();
    const findByRootIndex = new Map();
    const hasFindByRootIndex = new Set();
    const knownUuids = new Set(options.knownFindUuids ?? []);
    const walk = (steps) => {
        for (const step of steps) {
            const id = getJsonStepId(step);
            const body = stepBody(step);
            if (body !== undefined && 'find' in body && Boolean(body.find)) {
                if (id !== undefined) {
                    findById.set(id, body.find);
                    hasFindById.add(id);
                }
                for (const uuid of findSelectorUuids(body.find)) {
                    knownUuids.add(uuid);
                }
            }
            const kids = getJsonStepGroupChildren(step);
            if (kids) {
                walk(kids);
            }
        }
    };
    walk(currentSteps);
    currentSteps.forEach((step, i) => {
        const body = stepBody(step);
        if (body !== undefined && 'find' in body) {
            findByRootIndex.set(i, body.find);
            hasFindByRootIndex.add(i);
        }
    });
    const errors = [];
    const outOps = [];
    const UNKNOWN_CAPTURE_DETAIL = 'edit_steps can only carry a find the session already captured. ' +
        'To re-use an element already in the test, copy its find with get_step_json so its selector uuid matches a capture; ' +
        'to target a new element, record the interaction (execute_click, execute_enter_text, …). ' +
        "On replace, omit the find entirely to keep the existing step's recorded find untouched.";
    ops.forEach((op, opIndex) => {
        if (op.op !== 'replace' &&
            op.op !== 'insert_after' &&
            op.op !== 'insert_before') {
            outOps.push(op);
            return;
        }
        const key = stepTypeKey(op.step);
        if (key === undefined) {
            outOps.push(op);
            return;
        }
        const body = { ...(stepBody(op.step) ?? {}) };
        if (hasFind(op.step)) {
            const uuids = findSelectorUuids(body.find);
            if (uuids.length === 0 || uuids.some((uuid) => !knownUuids.has(uuid))) {
                errors.push({
                    op_index: opIndex,
                    op: op.op,
                    reason: 'find_unknown_capture',
                    detail: UNKNOWN_CAPTURE_DETAIL,
                });
                return;
            }
            if (options.resolveSelector) {
                body.find = rehydrateFind(body.find, options.resolveSelector);
            }
        }
        else if (op.op === 'replace') {
            const recordedFind = op.step_id !== undefined && hasFindById.has(op.step_id)
                ? findById.get(op.step_id)
                : op.index !== undefined && hasFindByRootIndex.has(op.index)
                    ? findByRootIndex.get(op.index)
                    : undefined;
            if (recordedFind !== undefined) {
                body.find = recordedFind;
            }
        }
        outOps.push({ ...op, step: { [key]: body } });
    });
    return { ops: outOps, errors };
}
