/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { strict as assert } from "assert";
import { done, createWeightedGenerator, } from "@fluid-private/stochastic-test-utils";
import { brand, fail, getOrCreate } from "../../../util/index.js";
import { AllowedUpdateType, } from "../../../core/index.js";
import { toDownPath } from "../../../feature-libraries/index.js";
import { schematizeFlexTree } from "../../utils.js";
import { fuzzNode, fuzzSchema } from "./fuzzUtils.js";
export function viewFromState(state, client = state.client, initialTree = undefined) {
    state.view ?? (state.view = new Map());
    return (state.transactionViews?.get(client.channel) ??
        getOrCreate(state.view, client.channel, (tree) => {
            const flexView = schematizeFlexTree(tree, {
                initialTree,
                schema: fuzzSchema,
                allowedSchemaModifications: AllowedUpdateType.Initialize,
            });
            const fuzzView = flexView;
            assert.equal(fuzzView.currentSchema, undefined);
            fuzzView.currentSchema = fuzzNode;
            return fuzzView;
        }));
}
const defaultFieldSelectionWeights = {
    optional: 1,
    required: 1,
    sequence: 1,
    recurse: 4,
    filter: () => true,
};
const defaultEditGeneratorOpWeights = {
    insert: 0,
    remove: 0,
    start: 0,
    commit: 0,
    abort: 0,
    undo: 0,
    redo: 0,
    move: 0,
    fieldSelection: defaultFieldSelectionWeights,
    synchronizeTrees: 0,
};
export const makeEditGenerator = (opWeightsArg) => {
    const weights = {
        ...defaultEditGeneratorOpWeights,
        ...opWeightsArg,
    };
    const jsonableTree = (state) => {
        // Heuristics around what type of tree we insert could be made customizable to tend toward trees of certain characteristics.
        return state.random.bool(0.3)
            ? {
                type: brand("com.fluidframework.leaf.number"),
                value: state.random.integer(Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER),
            }
            : {
                type: brand("tree2fuzz.node"),
                fields: {
                    requiredChild: [
                        {
                            type: brand("com.fluidframework.leaf.number"),
                            value: state.random.integer(Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER),
                        },
                    ],
                },
            };
    };
    const insert = (state) => {
        const fieldInfo = selectTreeField(viewFromState(state), state.random, weights.fieldSelection, weights.fieldSelection.filter);
        switch (fieldInfo.type) {
            case "optional":
            case "required": {
                const { type: fieldType, content: field } = fieldInfo;
                const contents = {
                    type: "set",
                    parent: maybeDownPathFromNode(field.parent),
                    key: field.key,
                    value: jsonableTree(state),
                };
                return {
                    type: fieldType,
                    edit: contents,
                };
            }
            case "sequence": {
                const { content: field } = fieldInfo;
                const contents = {
                    type: "insert",
                    parent: maybeDownPathFromNode(field.parent),
                    key: field.key,
                    index: state.random.integer(0, field.length),
                    value: jsonableTree(state),
                };
                return {
                    type: "sequence",
                    edit: contents,
                };
            }
            default:
                fail(`Invalid field type: ${fieldInfo.type}`);
        }
    };
    const deletableFieldFilter = (fieldInfo) => isNonEmptyField(fieldInfo) &&
        fieldInfo.type !== "required" &&
        (weights.fieldSelection.filter?.(fieldInfo) ?? true);
    const removeContent = (state) => {
        const fieldInfo = selectTreeField(viewFromState(state), state.random, weights.fieldSelection, deletableFieldFilter);
        switch (fieldInfo.type) {
            case "optional": {
                const { content: field } = fieldInfo;
                const { content } = field;
                // Note: if we ever decide to generate removals for currently empty optional fields, the logic
                // in the reducer needs to be adjusted (it hard-codes `wasEmpty` to `false`).
                assert(content !== undefined, "Optional field should have content for it to be selected for deletion");
                return {
                    type: "optional",
                    edit: {
                        type: "remove",
                        firstNode: downPathFromNode(content),
                        count: 1,
                    },
                };
            }
            case "sequence": {
                const { content: field } = fieldInfo;
                assert(field.length > 0, "Sequence field should have content for it to be selected for deletion");
                const start = state.random.integer(0, field.length - 1);
                // It'd be reasonable to move this to config. The idea is that by avoiding large deletions,
                // we're more likely to generate more interesting outcomes.
                const count = state.random.integer(1, Math.min(3, field.length - start));
                const node = field.at(start);
                // We computed 'start' in a way that guarantees it's in-bounds, so at() shouldn't have returned undefined.
                assert(node !== undefined, "Tried to access a node that doesn't exist");
                return {
                    type: "sequence",
                    edit: {
                        type: "remove",
                        firstNode: downPathFromNode(node),
                        count,
                    },
                };
            }
            default:
                fail(`Invalid field type for deletion of content: ${fieldInfo.type}`);
        }
    };
    const move = (state) => {
        const tree = state.client.channel;
        const fieldInfo = selectTreeField(viewFromState(state), state.random, weights.fieldSelection, (f) => f.type === "sequence" && f.content.length > 0);
        assert(fieldInfo.type === "sequence", "Move should only be performed on sequence fields");
        const { content: field } = fieldInfo;
        assert(field.length > 0, "Sequence must have at least one element to perform a move");
        const start = state.random.integer(0, field.length - 1);
        const count = state.random.integer(1, field.length - start);
        const dstIndex = state.random.integer(0, field.length);
        const node = field.at(start);
        assert(node !== undefined, "Node should be defined at chosen index");
        return {
            type: "sequence",
            edit: {
                type: "move",
                dstIndex,
                count,
                firstNode: downPathFromNode(node),
            },
        };
    };
    const fieldEdit = createWeightedGeneratorWithBailout([
        [
            insert,
            weights.insert,
            (state) => trySelectTreeField(viewFromState(state), state.random, weights.fieldSelection, weights.fieldSelection.filter) !== "no-valid-fields",
        ],
        [
            removeContent,
            weights.remove,
            (state) => trySelectTreeField(viewFromState(state), state.random, weights.fieldSelection, deletableFieldFilter) !== "no-valid-fields",
        ],
        [
            move,
            weights.move,
            (state) => trySelectTreeField(viewFromState(state), state.random, weights.fieldSelection, (f) => f.type === "sequence" && f.content.length > 0) !== "no-valid-fields",
        ],
    ]);
    return (state) => {
        const change = fieldEdit(state);
        // This assert is typically hit when restricting the features a fuzz test executes such that it can reach a state
        // where no edit is valid to generate. E.g. a fuzz test which can only create edits from within transactions but
        // can never start a transaction, or a fuzz test which can only edit sequence fields but the tree is empty (and
        // the root schema is an optional field).
        assert(change !== "no-valid-selections", "Unable to generate a valid field edit. This typically indicates a problematic fuzz test generator setup.");
        return change === done
            ? done
            : {
                type: "edit",
                contents: {
                    type: "fieldEdit",
                    change,
                },
            };
    };
};
export const makeTransactionEditGenerator = (opWeights) => {
    const passedOpWeights = {
        ...defaultEditGeneratorOpWeights,
        ...opWeights,
    };
    const start = { fuzzType: "transactionStart" };
    const commit = { fuzzType: "transactionCommit" };
    const abort = { fuzzType: "transactionAbort" };
    const transactionBoundaryType = createWeightedGenerator([
        [start, passedOpWeights.start],
        [
            commit,
            passedOpWeights.commit,
            (state) => viewFromState(state).checkout.transaction.inProgress(),
        ],
        [
            abort,
            passedOpWeights.abort,
            (state) => viewFromState(state).checkout.transaction.inProgress(),
        ],
    ]);
    return (state) => {
        const contents = transactionBoundaryType(state);
        return contents === done
            ? done
            : {
                type: "transaction",
                contents,
            };
    };
};
export const makeUndoRedoEditGenerator = (opWeights) => {
    const passedOpWeights = {
        ...defaultEditGeneratorOpWeights,
        ...opWeights,
    };
    const undo = { type: "undo" };
    const redo = { type: "redo" };
    const undoRedoType = createWeightedGenerator([
        [undo, passedOpWeights.undo],
        [redo, passedOpWeights.redo],
    ]);
    return (state) => {
        const contents = undoRedoType(state);
        return contents === done
            ? done
            : {
                type: "undoRedo",
                contents,
            };
    };
};
export function makeOpGenerator(weightsArg = defaultEditGeneratorOpWeights) {
    const weights = {
        ...defaultEditGeneratorOpWeights,
        ...weightsArg,
    };
    const { insert, remove, abort, commit, start, undo, redo } = weights;
    const editWeight = sumWeights([remove, insert]);
    const transactionWeight = sumWeights([abort, commit, start]);
    const undoRedoWeight = sumWeights([undo, redo]);
    const syncGenerator = createWeightedGenerator([
        [() => makeEditGenerator(weights), editWeight],
        [() => makeTransactionEditGenerator(weights), transactionWeight],
        [() => makeUndoRedoEditGenerator(weights), undoRedoWeight],
        [
            () => ({
                type: "synchronizeTrees",
            }),
            weights.synchronizeTrees,
        ],
    ]
        .filter(([, weight]) => weight > 0)
        .map(([f, weight]) => [f(), weight]));
    return async (state) => {
        return syncGenerator(state);
    };
}
function sumWeights(values) {
    let sum = 0;
    for (const value of values) {
        if (value !== undefined) {
            sum += value;
        }
    }
    return sum;
}
function upPathFromNode(node) {
    const parentField = node.parentField.parent;
    return {
        parent: parentField.parent ? upPathFromNode(parentField.parent) : undefined,
        parentField: parentField.key,
        parentIndex: node.parentField.index,
    };
}
function downPathFromNode(node) {
    return toDownPath(upPathFromNode(node));
}
function maybeDownPathFromNode(node) {
    return node === undefined ? undefined : downPathFromNode(node);
}
const isNonEmptyField = (field) => field.content !== undefined &&
    ((field.type === "sequence" && field.content.length > 0) ||
        (field.type !== "sequence" && field.content.content !== undefined));
function selectField(node, random, weights, filter = () => true, nodeSchema) {
    const optional = { type: "optional", content: node.boxedOptionalChild };
    const value = { type: "required", content: node.boxedRequiredChild };
    const sequence = { type: "sequence", content: node.boxedSequenceChildren };
    const recurse = (state) => {
        const childNodes = [];
        // Checking "=== true" causes tsc to fail to typecheck, as it is no longer able to narrow according
        // to the .is typeguard.
        // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
        if (node.optionalChild?.is(nodeSchema)) {
            childNodes.push(node.optionalChild);
        }
        if (node.requiredChild?.is(nodeSchema)) {
            childNodes.push(node.requiredChild);
        }
        node.sequenceChildren.map((child) => {
            if (child.is(nodeSchema)) {
                childNodes.push(child);
            }
        });
        state.random.shuffle(childNodes);
        for (const child of childNodes) {
            const childResult = selectField(child, random, weights, filter, nodeSchema);
            if (childResult !== "no-valid-selections") {
                return childResult;
            }
        }
        return "no-valid-selections";
    };
    const generator = createWeightedGeneratorWithBailout([
        [optional, weights.optional, () => filter(optional)],
        [value, weights.required, () => filter(value)],
        [sequence, weights.sequence, () => filter(sequence)],
        [recurse, weights.recurse],
    ]);
    const result = generator({ random });
    assert(result !== done, "createWeightedGenerators should never return done");
    return result;
}
function trySelectTreeField(tree, random, weights, filter = () => true) {
    const editable = tree.flexTree;
    const options = weights.optional === 0
        ? ["recurse"]
        : weights.recurse === 0
            ? ["optional"]
            : random.bool(weights.optional / (weights.optional + weights.recurse))
                ? ["optional", "recurse"]
                : ["recurse", "optional"];
    const nodeSchema = tree.currentSchema;
    for (const option of options) {
        switch (option) {
            case "optional": {
                const field = { type: "optional", content: editable };
                if (filter(field)) {
                    return field;
                }
                break;
            }
            case "recurse": {
                // Checking "=== true" causes tsc to fail to typecheck, as it is no longer able to narrow according
                // to the .is typeguard.
                // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
                if (editable.content?.is(nodeSchema)) {
                    const result = selectField(editable.content, random, weights, filter, nodeSchema);
                    if (result !== "no-valid-selections") {
                        return result;
                    }
                }
                break;
            }
            default:
                fail(`Invalid option: ${option}`);
        }
    }
    return "no-valid-fields";
}
function selectTreeField(tree, random, weights, filter = () => true) {
    const result = trySelectTreeField(tree, random, weights, filter);
    assert(result !== "no-valid-fields", "No valid fields found");
    return result;
}
/**
 * Like `createWeightedGenerator`, except it will only attempt to select each option once.
 * If all options have been exhausted and no value other than 'no-valid-selections' is generated,
 * it will return 'no-valid-selections'.
 *
 * This helps prevent infinite loops for bad fuzz config.
 * Note: `T` cannot extend function, as otherwise `T | Generator<T>` cannot be distinguished.
 */
function createWeightedGeneratorWithBailout(weights) {
    const nonzeroWeights = weights.filter(([, weight]) => weight > 0);
    const selectedIndices = new Set();
    const newWeights = nonzeroWeights.map(([f, weight, acceptanceCondition], index) => [
        (state) => {
            selectedIndices.add(index);
            if (typeof f === "function") {
                const result = f(state);
                assert(typeof result !== "function", "Generator should not return a function: this prevents correct type deduction.");
                return result;
            }
            return f;
        },
        weight,
        (state) => {
            if (selectedIndices.has(index)) {
                return false;
            }
            selectedIndices.add(index);
            return acceptanceCondition?.(state) !== false;
        },
    ]);
    const generator = createWeightedGenerator([
        ...newWeights,
        [
            "no-valid-selections",
            // The weight here is arbitrary: we select one that will be selected a reasonable portion of the time.
            Math.max(1, sumWeights(nonzeroWeights.map(([, weight]) => weight)) / nonzeroWeights.length),
            () => selectedIndices.size === nonzeroWeights.length,
        ],
    ]);
    return (state) => {
        let result = "no-valid-selections";
        do {
            result = generator(state);
            assert(result !== done, "createWeightedGenerators should never return done");
        } while (result === "no-valid-selections" && selectedIndices.size < nonzeroWeights.length);
        selectedIndices.clear();
        return result;
    };
}
//# sourceMappingURL=fuzzEditGenerators.js.map