/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { strict as assert } from "assert";
import { Multiplicity, genericFieldKind, FieldKindWithEditor, chunkTree, defaultChunkPolicy, cursorForJsonableTreeField, chunkFieldSingle, makeFieldBatchCodec, } from "../../../feature-libraries/index.js";
import { makeAnonChange, makeDetachedNodeId, tagChange, revisionMetadataSourceFromInfo, } from "../../../core/index.js";
import { brand, nestedMapFromFlatList, tryGetFromNestedMap } from "../../../util/index.js";
import { makeCodecFamily } from "../../../codec/index.js";
import { assertDeltaEqual, deepFreeze, makeEncodingTestSuite, mintRevisionTag, testChangeReceiver, testRevisionTagCodec, } from "../../utils.js";
import { ModularChangeFamily, relevantRemovedRoots as relevantDetachedTreesImplementation, intoDelta, updateRefreshers,
// eslint-disable-next-line import/no-internal-modules
 } from "../../../feature-libraries/modular-schema/modularChangeFamily.js";
import { jsonObject, singleJsonCursor } from "../../../domains/index.js";
import { ajvValidator } from "../../codec/index.js";
import { valueField } from "./basicRebasers.js";
const singleNodeRebaser = {
    compose: (change1, change2, composeChild) => composeChild(change1.change, change2.change),
    invert: (change, invertChild) => invertChild(change.change),
    rebase: (change, base, rebaseChild) => rebaseChild(change, base.change) ?? {},
    prune: (change) => change,
};
const singleNodeEditor = {
    buildChildChange: (index, change) => {
        assert(index === 0, "This field kind only supports one node in its field");
        return change;
    },
};
const singleNodeHandler = {
    rebaser: singleNodeRebaser,
    codecsFactory: (childCodec) => makeCodecFamily([[0, childCodec]]),
    editor: singleNodeEditor,
    intoDelta: ({ change }, deltaFromChild) => ({
        local: [{ count: 1, fields: deltaFromChild(change) }],
    }),
    relevantRemovedRoots: (change, relevantRemovedRootsFromChild) => relevantRemovedRootsFromChild(change.change),
    isEmpty: (change) => change.fieldChanges === undefined,
    createEmpty: () => ({}),
};
const singleNodeField = new FieldKindWithEditor("SingleNode", Multiplicity.Single, singleNodeHandler, (a, b) => false, new Set());
const fieldKinds = new Map([singleNodeField, valueField].map((field) => [field.identifier, field]));
const codecOptions = {
    jsonValidator: ajvValidator,
};
const family = new ModularChangeFamily(fieldKinds, testRevisionTagCodec, makeFieldBatchCodec(codecOptions), codecOptions);
const tag1 = mintRevisionTag();
const tag2 = mintRevisionTag();
const tag3 = mintRevisionTag();
const fieldA = brand("a");
const fieldB = brand("b");
const valueChange1a = { old: 0, new: 1 };
const valueChange1b = { old: 0, new: 2 };
const valueChange2 = { old: 1, new: 2 };
const nodeChange1a = {
    fieldChanges: new Map([
        [fieldA, { fieldKind: valueField.identifier, change: brand(valueChange1a) }],
    ]),
};
const nodeChanges1b = {
    fieldChanges: new Map([
        [
            fieldA,
            {
                fieldKind: valueField.identifier,
                change: brand(valueChange1b),
            },
        ],
        [
            fieldB,
            {
                fieldKind: valueField.identifier,
                change: brand(valueChange1a),
            },
        ],
    ]),
};
const nodeChanges2 = {
    fieldChanges: new Map([
        [
            fieldA,
            {
                fieldKind: valueField.identifier,
                change: brand(valueChange2),
            },
        ],
        [
            fieldB,
            {
                fieldKind: valueField.identifier,
                change: brand(valueChange1a),
            },
        ],
    ]),
};
const nodeChange3 = {
    fieldChanges: new Map([
        [fieldA, { fieldKind: valueField.identifier, change: brand(valueChange1a) }],
    ]),
};
const nodeChange4 = {
    fieldChanges: new Map([
        [fieldA, { fieldKind: valueField.identifier, change: brand(valueChange1a) }],
    ]),
    nodeExistsConstraint: {
        violated: false,
    },
};
const nodeChangeWithoutFieldChanges = {
    nodeExistsConstraint: {
        violated: false,
    },
};
const rootChange1a = {
    fieldChanges: new Map([
        [
            fieldA,
            {
                fieldKind: singleNodeField.identifier,
                change: brand(nodeChange1a),
            },
        ],
        [
            fieldB,
            {
                fieldKind: valueField.identifier,
                change: brand(valueChange2),
            },
        ],
    ]),
};
const rootChange1aGeneric = {
    fieldChanges: new Map([
        [
            fieldA,
            {
                fieldKind: genericFieldKind.identifier,
                change: brand(genericFieldKind.changeHandler.editor.buildChildChange(0, nodeChange1a)),
            },
        ],
        [
            fieldB,
            {
                fieldKind: valueField.identifier,
                change: brand(valueChange2),
            },
        ],
    ]),
};
const rootChange1b = {
    fieldChanges: new Map([
        [
            fieldA,
            {
                fieldKind: singleNodeField.identifier,
                change: brand(nodeChanges1b),
            },
        ],
    ]),
};
const rootChange1bGeneric = {
    fieldChanges: new Map([
        [
            fieldA,
            {
                fieldKind: genericFieldKind.identifier,
                change: brand(genericFieldKind.changeHandler.editor.buildChildChange(0, nodeChanges1b)),
            },
        ],
    ]),
};
const rootChange2 = {
    fieldChanges: new Map([
        [
            fieldA,
            {
                fieldKind: singleNodeField.identifier,
                change: brand(nodeChanges2),
            },
        ],
    ]),
};
const rootChange2Generic = {
    fieldChanges: new Map([
        [
            fieldA,
            {
                fieldKind: genericFieldKind.identifier,
                change: brand(genericFieldKind.changeHandler.editor.buildChildChange(0, nodeChanges2)),
            },
        ],
    ]),
};
const rootChange3 = {
    fieldChanges: new Map([
        [
            fieldA,
            {
                fieldKind: singleNodeField.identifier,
                change: brand(nodeChange3),
            },
        ],
    ]),
};
const dummyMaxId = 10;
const dummyRevisionTag = mintRevisionTag();
const rootChange4 = {
    maxId: brand(dummyMaxId),
    revisions: [{ revision: dummyRevisionTag }],
    fieldChanges: new Map([
        [
            fieldA,
            {
                fieldKind: singleNodeField.identifier,
                change: brand(nodeChange4),
            },
        ],
    ]),
};
const rootChangeWithoutNodeFieldChanges = {
    maxId: brand(dummyMaxId),
    revisions: [{ revision: dummyRevisionTag }],
    fieldChanges: new Map([
        [
            fieldA,
            {
                fieldKind: singleNodeField.identifier,
                change: brand(nodeChangeWithoutFieldChanges),
            },
        ],
    ]),
};
const node1 = singleJsonCursor(1);
const objectNode = singleJsonCursor({});
const node1Chunk = treeChunkFromCursor(node1);
const nodesChunk = chunkFieldSingle(cursorForJsonableTreeField([{ type: jsonObject.name }, { type: jsonObject.name }]), defaultChunkPolicy);
describe("ModularChangeFamily", () => {
    describe("compose", () => {
        const composedValues = { old: 0, new: 2 };
        const composedNodeChange = {
            fieldChanges: new Map([
                [
                    fieldA,
                    {
                        fieldKind: valueField.identifier,
                        change: brand(composedValues),
                    },
                ],
                [
                    fieldB,
                    {
                        fieldKind: valueField.identifier,
                        change: brand(valueChange1a),
                    },
                ],
            ]),
        };
        it("prioritizes earlier build entries when faced with duplicates", () => {
            const change1 = {
                fieldChanges: new Map(),
                builds: new Map([[undefined, new Map([[brand(0), node1Chunk]])]]),
            };
            const change2 = {
                fieldChanges: new Map(),
                builds: new Map([
                    [undefined, new Map([[brand(0), treeChunkFromCursor(singleJsonCursor(2))]])],
                ]),
            };
            assert.deepEqual(family.compose([makeAnonChange(change1), makeAnonChange(change2)]), change1);
        });
        it("compose specific ○ specific", () => {
            const expectedCompose = {
                fieldChanges: new Map([
                    [
                        fieldA,
                        {
                            fieldKind: singleNodeField.identifier,
                            change: brand(composedNodeChange),
                        },
                    ],
                    [
                        fieldB,
                        {
                            fieldKind: valueField.identifier,
                            change: brand(valueChange2),
                        },
                    ],
                ]),
            };
            assert.deepEqual(family.compose([makeAnonChange(rootChange1a), makeAnonChange(rootChange2)]), expectedCompose);
        });
        it("compose specific ○ generic", () => {
            const expectedCompose = {
                fieldChanges: new Map([
                    [
                        fieldA,
                        {
                            fieldKind: singleNodeField.identifier,
                            change: brand(composedNodeChange),
                        },
                    ],
                    [
                        fieldB,
                        {
                            fieldKind: valueField.identifier,
                            change: brand(valueChange2),
                        },
                    ],
                ]),
            };
            assert.deepEqual(family.compose([makeAnonChange(rootChange1a), makeAnonChange(rootChange2Generic)]), expectedCompose);
        });
        it("compose generic ○ specific", () => {
            const expectedCompose = {
                fieldChanges: new Map([
                    [
                        fieldA,
                        {
                            fieldKind: singleNodeField.identifier,
                            change: brand(composedNodeChange),
                        },
                    ],
                    [
                        fieldB,
                        {
                            fieldKind: valueField.identifier,
                            change: brand(valueChange2),
                        },
                    ],
                ]),
            };
            assert.deepEqual(family.compose([makeAnonChange(rootChange1aGeneric), makeAnonChange(rootChange2)]), expectedCompose);
        });
        it("compose generic ○ generic", () => {
            const expectedCompose = {
                fieldChanges: new Map([
                    [
                        fieldA,
                        {
                            fieldKind: genericFieldKind.identifier,
                            change: brand(genericFieldKind.changeHandler.editor.buildChildChange(0, composedNodeChange)),
                        },
                    ],
                    [
                        fieldB,
                        {
                            fieldKind: valueField.identifier,
                            change: brand(valueChange2),
                        },
                    ],
                ]),
            };
            assert.deepEqual(family.compose([
                makeAnonChange(rootChange1aGeneric),
                makeAnonChange(rootChange2Generic),
            ]), expectedCompose);
        });
        it("compose tagged changes", () => {
            const change1A = {
                fieldKind: valueField.identifier,
                change: brand(valueChange1a),
            };
            const change1 = tagChange({
                fieldChanges: new Map([[fieldA, change1A]]),
            }, tag1);
            const nodeChange2 = {
                fieldChanges: new Map([
                    [
                        fieldA,
                        {
                            fieldKind: valueField.identifier,
                            change: brand(valueChange2),
                        },
                    ],
                ]),
            };
            const change2B = {
                fieldKind: singleNodeField.identifier,
                change: brand(nodeChange2),
            };
            deepFreeze(change2B);
            const change2 = tagChange({
                fieldChanges: new Map([[fieldB, change2B]]),
            }, tag2);
            deepFreeze(change1);
            deepFreeze(change2);
            const composed = family.compose([change1, change2]);
            const expectedNodeChange = {
                fieldChanges: new Map([
                    [
                        fieldA,
                        {
                            fieldKind: valueField.identifier,
                            change: brand(valueChange2),
                        },
                    ],
                ]),
            };
            const expected = {
                fieldChanges: new Map([
                    [
                        fieldA,
                        {
                            fieldKind: valueField.identifier,
                            change: brand(valueChange1a),
                        },
                    ],
                    [
                        fieldB,
                        {
                            fieldKind: singleNodeField.identifier,
                            change: brand(expectedNodeChange),
                        },
                    ],
                ]),
                revisions: [{ revision: tag1 }, { revision: tag2 }],
            };
            assert.deepEqual(composed, expected);
        });
        it("build ○ matching destroy = ε", () => {
            const change1 = tagChange({
                fieldChanges: new Map([]),
                builds: new Map([
                    [undefined, new Map([[brand(0), node1Chunk]])],
                    [tag2, new Map([[brand(0), node1Chunk]])],
                ]),
            }, tag1);
            const change2 = tagChange({
                fieldChanges: new Map([]),
                destroys: new Map([
                    [tag1, new Map([[brand(0), 1]])],
                    [undefined, new Map([[brand(0), 1]])],
                ]),
            }, tag2);
            deepFreeze(change1);
            deepFreeze(change2);
            const composed = family.compose([change1, change2]);
            const expected = {
                fieldChanges: new Map(),
                revisions: [{ revision: tag1 }, { revision: tag2 }],
            };
            assert.deepEqual(composed, expected);
        });
        it("destroy ○ matching build = ε", () => {
            const change1 = tagChange({
                fieldChanges: new Map([]),
                destroys: new Map([
                    [tag1, new Map([[brand(0), 1]])],
                    [undefined, new Map([[brand(0), 1]])],
                ]),
            }, tag2);
            const change2 = tagChange({
                fieldChanges: new Map([]),
                builds: new Map([
                    [undefined, new Map([[brand(0), node1Chunk]])],
                    [tag2, new Map([[brand(0), node1Chunk]])],
                ]),
            }, tag1);
            deepFreeze(change1);
            deepFreeze(change2);
            const composed = family.compose([change1, change2]);
            const expected = {
                fieldChanges: new Map(),
                revisions: [{ revision: tag2 }, { revision: tag1 }],
            };
            assert.deepEqual(composed, expected);
        });
        it("non-matching builds and destroys", () => {
            const change1 = tagChange({
                fieldChanges: new Map([]),
                builds: new Map([
                    [undefined, new Map([[brand(0), treeChunkFromCursor(node1)]])],
                    [tag3, new Map([[brand(0), treeChunkFromCursor(node1)]])],
                ]),
                destroys: new Map([
                    [undefined, new Map([[brand(1), 1]])],
                    [tag3, new Map([[brand(1), 1]])],
                ]),
            }, tag1);
            const change2 = tagChange({
                fieldChanges: new Map([]),
                builds: new Map([
                    [undefined, new Map([[brand(2), treeChunkFromCursor(node1)]])],
                    [tag3, new Map([[brand(2), treeChunkFromCursor(node1)]])],
                ]),
                destroys: new Map([
                    [undefined, new Map([[brand(3), 1]])],
                    [tag3, new Map([[brand(3), 1]])],
                ]),
                revisions: [{ revision: tag2 }],
            }, undefined);
            deepFreeze(change1);
            deepFreeze(change2);
            const composed = family.compose([change1, change2]);
            const expected = {
                fieldChanges: new Map(),
                builds: new Map([
                    [tag1, new Map([[brand(0), treeChunkFromCursor(node1)]])],
                    [tag2, new Map([[brand(2), treeChunkFromCursor(node1)]])],
                    [
                        tag3,
                        new Map([
                            [brand(0), treeChunkFromCursor(node1)],
                            [brand(2), treeChunkFromCursor(node1)],
                        ]),
                    ],
                ]),
                destroys: new Map([
                    [tag1, new Map([[brand(1), 1]])],
                    [tag2, new Map([[brand(3), 1]])],
                    [
                        tag3,
                        new Map([
                            [brand(1), 1],
                            [brand(3), 1],
                        ]),
                    ],
                ]),
                revisions: [{ revision: tag1 }, { revision: tag2 }],
            };
            assert.deepEqual(composed, expected);
        });
        it("refreshers", () => {
            const change1 = tagChange({
                fieldChanges: new Map([]),
                refreshers: new Map([
                    [tag3, new Map([[brand(0), treeChunkFromCursor(node1)]])],
                ]),
            }, tag1);
            const change2 = tagChange({
                fieldChanges: new Map([]),
                refreshers: new Map([
                    [undefined, new Map([[brand(2), treeChunkFromCursor(node1)]])],
                    [tag3, new Map([[brand(2), treeChunkFromCursor(node1)]])],
                ]),
                revisions: [{ revision: tag2 }],
            }, undefined);
            deepFreeze(change1);
            deepFreeze(change2);
            const composed = family.compose([change1, change2]);
            const expected = {
                fieldChanges: new Map(),
                refreshers: new Map([
                    [undefined, new Map([[brand(2), treeChunkFromCursor(node1)]])],
                    [
                        tag3,
                        new Map([
                            [brand(0), treeChunkFromCursor(node1)],
                            [brand(2), treeChunkFromCursor(node1)],
                        ]),
                    ],
                ]),
                revisions: [{ revision: tag1 }, { revision: tag2 }],
            };
            assert.deepEqual(composed, expected);
        });
        it("refreshers with the same detached node id", () => {
            const change1 = tagChange({
                fieldChanges: new Map([]),
                refreshers: new Map([
                    [tag3, new Map([[brand(0), treeChunkFromCursor(node1)]])],
                ]),
            }, tag1);
            const change2 = tagChange({
                fieldChanges: new Map([]),
                refreshers: new Map([
                    [tag3, new Map([[brand(0), treeChunkFromCursor(objectNode)]])],
                ]),
                revisions: [{ revision: tag2 }],
            }, undefined);
            deepFreeze(change1);
            deepFreeze(change2);
            const composed = family.compose([change1, change2]);
            const expected = {
                fieldChanges: new Map(),
                refreshers: new Map([[tag3, new Map([[brand(0), treeChunkFromCursor(node1)]])]]),
                revisions: [{ revision: tag1 }, { revision: tag2 }],
            };
            assert.deepEqual(composed, expected);
        });
    });
    describe("invert", () => {
        const valueInverse1 = { old: 1, new: 0 };
        const valueInverse2 = { old: 2, new: 1 };
        const nodeInverse = {
            fieldChanges: new Map([
                [
                    fieldA,
                    {
                        fieldKind: valueField.identifier,
                        change: brand(valueInverse1),
                    },
                ],
            ]),
        };
        it("specific", () => {
            const expectedInverse = {
                fieldChanges: new Map([
                    [fieldA, { fieldKind: singleNodeField.identifier, change: brand(nodeInverse) }],
                    [fieldB, { fieldKind: valueField.identifier, change: brand(valueInverse2) }],
                ]),
            };
            assert.deepEqual(family.invert(makeAnonChange(rootChange1a), false), expectedInverse);
        });
        it("generic", () => {
            const fieldChange = genericFieldKind.changeHandler.editor.buildChildChange(0, nodeInverse);
            const expectedInverse = {
                fieldChanges: new Map([
                    [
                        fieldA,
                        { fieldKind: genericFieldKind.identifier, change: brand(fieldChange) },
                    ],
                    [fieldB, { fieldKind: valueField.identifier, change: brand(valueInverse2) }],
                ]),
            };
            assert.deepEqual(family.invert(makeAnonChange(rootChange1aGeneric), false), expectedInverse);
        });
        it("build => destroy but only for rollback", () => {
            const change1 = tagChange({
                fieldChanges: new Map([]),
                builds: new Map([
                    [undefined, new Map([[brand(0), node1Chunk]])],
                    [tag2, new Map([[brand(1), node1Chunk]])],
                ]),
            }, tag1);
            const expectedRollback = {
                fieldChanges: new Map([]),
                destroys: new Map([
                    [tag1, new Map([[brand(0), 1]])],
                    [tag2, new Map([[brand(1), 1]])],
                ]),
            };
            const expectedUndo = {
                fieldChanges: new Map([]),
            };
            deepFreeze(change1);
            const actualRollback = family.invert(change1, true);
            const actualUndo = family.invert(change1, false);
            assert.deepEqual(actualRollback, expectedRollback);
            assert.deepEqual(actualUndo, expectedUndo);
        });
    });
    describe("rebase", () => {
        it("rebase specific ↷ specific", () => {
            const rebased = family.rebase(rootChange1b, makeAnonChange(rootChange1a), revisionMetadataSourceFromInfo([]));
            assert.deepEqual(rebased, rootChange2);
        });
        it("rebase specific ↷ generic", () => {
            const rebased = family.rebase(rootChange1b, makeAnonChange(rootChange1aGeneric), revisionMetadataSourceFromInfo([]));
            assert.deepEqual(rebased, rootChange2);
        });
        it("rebase generic ↷ specific", () => {
            const rebased = family.rebase(rootChange1bGeneric, makeAnonChange(rootChange1a), revisionMetadataSourceFromInfo([]));
            assert.deepEqual(rebased, rootChange2);
        });
        it("rebase generic ↷ generic", () => {
            const rebased = family.rebase(rootChange1bGeneric, makeAnonChange(rootChange1aGeneric), revisionMetadataSourceFromInfo([]));
            assert.deepEqual(rebased, rootChange2Generic);
        });
    });
    describe("intoDelta", () => {
        it("fieldChanges", () => {
            const nodeDelta = {
                local: [
                    {
                        count: 1,
                        fields: new Map([
                            [
                                fieldA,
                                {
                                    local: [
                                        { count: 1, detach: { minor: 0 }, attach: { minor: 1 } },
                                    ],
                                },
                            ],
                        ]),
                    },
                ],
            };
            const expectedDelta = {
                fields: new Map([
                    [fieldA, nodeDelta],
                    [fieldB, { local: [{ count: 1, detach: { minor: 1 }, attach: { minor: 2 } }] }],
                ]),
            };
            const actual = intoDelta(makeAnonChange(rootChange1a), family.fieldKinds);
            assertDeltaEqual(actual, expectedDelta);
        });
        it("builds", () => {
            const change1 = tagChange({
                fieldChanges: new Map([]),
                builds: new Map([
                    [undefined, new Map([[brand(1), node1Chunk]])],
                    [
                        tag2,
                        new Map([
                            [brand(2), node1Chunk],
                            [brand(3), nodesChunk],
                        ]),
                    ],
                ]),
            }, tag1);
            const expectedDelta = {
                build: [
                    { id: { major: tag1, minor: 1 }, trees: [node1] },
                    { id: { major: tag2, minor: 2 }, trees: [node1] },
                    { id: { major: tag2, minor: 3 }, trees: [objectNode, objectNode] },
                ],
            };
            const actual = intoDelta(change1, family.fieldKinds);
            assertDeltaEqual(actual, expectedDelta);
        });
        it("destroys", () => {
            const change1 = tagChange({
                fieldChanges: new Map([]),
                destroys: new Map([
                    [undefined, new Map([[brand(1), 1]])],
                    [tag2, new Map([[brand(2), 1]])],
                    [tag2, new Map([[brand(3), 10]])],
                ]),
            }, tag1);
            const expectedDelta = {
                destroy: [
                    { id: { major: tag1, minor: 1 }, count: 1 },
                    { id: { major: tag2, minor: 2 }, count: 1 },
                    { id: { major: tag2, minor: 3 }, count: 10 },
                ],
            };
            const actual = intoDelta(change1, family.fieldKinds);
            assertDeltaEqual(actual, expectedDelta);
        });
        it("refreshers", () => {
            const change1 = tagChange({
                fieldChanges: new Map([]),
                refreshers: new Map([
                    [undefined, new Map([[brand(1), node1Chunk]])],
                    [
                        tag2,
                        new Map([
                            [brand(2), node1Chunk],
                            [brand(3), nodesChunk],
                        ]),
                    ],
                ]),
            }, tag1);
            const expectedDelta = {
                refreshers: [
                    { id: { major: tag1, minor: 1 }, trees: [node1] },
                    { id: { major: tag2, minor: 2 }, trees: [node1] },
                    { id: { major: tag2, minor: 3 }, trees: [objectNode, objectNode] },
                ],
            };
            const actual = intoDelta(change1, family.fieldKinds);
            assertDeltaEqual(actual, expectedDelta);
        });
    });
    describe("relevantRemovedRoots", () => {
        const fieldKind = brand("HasRemovedRootsRefs");
        const handler = {
            relevantRemovedRoots: ({ change, revision }, relevantRemovedRootsFromChild) => {
                return [
                    ...change.shallow.map((id) => makeDetachedNodeId(id.major ?? revision, id.minor)),
                    ...change.nested.flatMap((c) => Array.from(relevantRemovedRootsFromChild({
                        fieldChanges: new Map([
                            [brand("nested"), { fieldKind, change: brand(c) }],
                        ]),
                    }))),
                ];
            },
        };
        const hasRemovedRootsRefsField = new FieldKindWithEditor(fieldKind, Multiplicity.Single, handler, () => false, new Set());
        const mockFieldKinds = new Map([[fieldKind, hasRemovedRootsRefsField]]);
        function relevantRemovedRoots(input) {
            deepFreeze(input);
            return Array.from(relevantDetachedTreesImplementation(input, mockFieldKinds));
        }
        it("sibling fields", () => {
            const aMajor = mintRevisionTag();
            const a1 = { major: aMajor, minor: 1 };
            const a2 = { major: aMajor, minor: 2 };
            const bMajor = mintRevisionTag();
            const b1 = { major: bMajor, minor: 1 };
            const changeA = {
                shallow: [a1, a2],
                nested: [],
            };
            const changeB = {
                shallow: [b1],
                nested: [],
            };
            const input = {
                fieldChanges: new Map([
                    [brand("fA"), { fieldKind, change: brand(changeA) }],
                    [brand("fB"), { fieldKind, change: brand(changeB) }],
                ]),
            };
            const actual = relevantRemovedRoots(makeAnonChange(input));
            assert.deepEqual(actual, [a1, a2, b1]);
        });
        it("nested fields", () => {
            const aMajor = mintRevisionTag();
            const cMajor = mintRevisionTag();
            const a1 = { major: aMajor, minor: 1 };
            const c1 = { major: cMajor, minor: 1 };
            const changeC = {
                shallow: [c1],
                nested: [],
            };
            const changeB = {
                shallow: [],
                nested: [changeC],
            };
            const changeA = {
                shallow: [a1],
                nested: [changeB],
            };
            const input = {
                fieldChanges: new Map([[brand("fA"), { fieldKind, change: brand(changeA) }]]),
            };
            const actual = relevantRemovedRoots(makeAnonChange(input));
            assert.deepEqual(actual, [a1, c1]);
        });
        it("default revision from tag", () => {
            const major = mintRevisionTag();
            const changeB = {
                shallow: [{ minor: 2 }],
                nested: [],
            };
            const changeA = {
                shallow: [{ minor: 1 }],
                nested: [changeB],
            };
            const input = {
                fieldChanges: new Map([[brand("fA"), { fieldKind, change: brand(changeA) }]]),
            };
            const actual = relevantRemovedRoots(tagChange(input, major));
            assert.deepEqual(actual, [
                { major, minor: 1 },
                { major, minor: 2 },
            ]);
        });
        it("default revision from field", () => {
            const majorAB = mintRevisionTag();
            const majorC = mintRevisionTag();
            const changeB = {
                shallow: [{ minor: 2 }],
                nested: [],
            };
            const changeA = {
                shallow: [{ minor: 1 }],
                nested: [changeB],
            };
            const changeC = {
                shallow: [{ minor: 1 }],
                nested: [],
            };
            const input = {
                fieldChanges: new Map([
                    [
                        brand("fA"),
                        {
                            fieldKind,
                            change: brand(changeA),
                            revision: majorAB,
                        },
                    ],
                    [
                        brand("fC"),
                        {
                            fieldKind,
                            change: brand(changeC),
                            revision: majorC,
                        },
                    ],
                ]),
            };
            const actual = relevantRemovedRoots(makeAnonChange(input));
            assert.deepEqual(actual, [
                { major: majorAB, minor: 1 },
                { major: majorAB, minor: 2 },
                { major: majorC, minor: 1 },
            ]);
        });
    });
    describe("update refreshers", () => {
        const aMajor = mintRevisionTag();
        const a1 = { major: aMajor, minor: 1 };
        const a2 = { major: aMajor, minor: 2 };
        const bMajor = mintRevisionTag();
        const b1 = { major: bMajor, minor: 1 };
        const node2 = singleJsonCursor(2);
        const node2Chunk = treeChunkFromCursor(node2);
        const node3 = singleJsonCursor(3);
        const node3Chunk = treeChunkFromCursor(node3);
        const nodesArray = [
            [a1, node1Chunk],
            [a2, node2Chunk],
            [b1, node3Chunk],
        ];
        const nodeMap = nestedMapFromFlatList(nodesArray.map(([{ major, minor }, chunk]) => [major, minor, chunk]));
        const getDetachedNode = ({ major, minor }) => {
            return tryGetFromNestedMap(nodeMap, major, minor);
        };
        it("preserves relevant refreshers that are present in the input", () => {
            const input = {
                fieldChanges: new Map([]),
                refreshers: new Map([[aMajor, new Map([[brand(2), node2Chunk]])]]),
            };
            const expected = {
                fieldChanges: new Map([]),
                refreshers: new Map([[aMajor, new Map([[brand(2), node2Chunk]])]]),
            };
            const withBuilds = updateRefreshers(makeAnonChange(input), getDetachedNode, [a2]);
            assert.deepEqual(withBuilds, expected);
        });
        it("removes irrelevant refreshers that are present in the input", () => {
            const input = {
                fieldChanges: new Map([]),
                refreshers: new Map([
                    [
                        aMajor,
                        new Map([
                            [brand(1), node1Chunk],
                            [brand(2), node2Chunk],
                        ]),
                    ],
                    [bMajor, new Map([[brand(1), node3Chunk]])],
                ]),
            };
            const expected = {
                fieldChanges: new Map([]),
            };
            const filtered = updateRefreshers(makeAnonChange(input), getDetachedNode, []);
            assert.deepEqual(filtered, expected);
        });
        describe("attempts to add relevant refreshers that are missing from the input", () => {
            it("adds the missing refresher if the detached node is available", () => {
                const input = {
                    fieldChanges: new Map([]),
                };
                const expected = {
                    fieldChanges: new Map([]),
                    refreshers: new Map([
                        [
                            aMajor,
                            new Map([
                                [brand(1), node1Chunk],
                                [brand(2), node2Chunk],
                            ]),
                        ],
                        [bMajor, new Map([[brand(1), node3Chunk]])],
                    ]),
                };
                const withBuilds = updateRefreshers(makeAnonChange(input), getDetachedNode, [
                    a1,
                    a2,
                    b1,
                ]);
                assert.deepEqual(withBuilds, expected);
            });
            it("replaces outdated refreshers", () => {
                const input = {
                    fieldChanges: new Map([]),
                    refreshers: new Map([
                        [
                            aMajor,
                            new Map([
                                [brand(1), node2Chunk],
                                [brand(2), node1Chunk],
                            ]),
                        ],
                    ]),
                };
                const expected = {
                    fieldChanges: new Map([]),
                    refreshers: new Map([
                        [
                            aMajor,
                            new Map([
                                [brand(1), node1Chunk],
                                [brand(2), node2Chunk],
                            ]),
                        ],
                    ]),
                };
                const filtered = updateRefreshers(makeAnonChange(input), getDetachedNode, [a1, a2]);
                assert.deepEqual(filtered, expected);
            });
            it("does not add a refresher that is present in the builds", () => {
                const input = {
                    fieldChanges: new Map([]),
                    builds: new Map([
                        [
                            aMajor,
                            new Map([
                                [brand(1), node1Chunk],
                                [brand(2), node2Chunk],
                            ]),
                        ],
                        [bMajor, new Map([[brand(1), node3Chunk]])],
                    ]),
                };
                const expected = {
                    fieldChanges: new Map([]),
                    builds: new Map([
                        [
                            aMajor,
                            new Map([
                                [brand(1), node1Chunk],
                                [brand(2), node2Chunk],
                            ]),
                        ],
                        [bMajor, new Map([[brand(1), node3Chunk]])],
                    ]),
                };
                const withBuilds = updateRefreshers(makeAnonChange(input), getDetachedNode, [
                    a1,
                    a2,
                    b1,
                ]);
                assert.deepEqual(withBuilds, expected);
            });
            it("throws if the detached node is not available", () => {
                const input = {
                    fieldChanges: new Map([]),
                };
                assert.throws(() => updateRefreshers(makeAnonChange(input), getDetachedNode, [{ minor: 2 }]));
            });
        });
    });
    describe("Encoding", () => {
        const sessionId = "session1";
        const context = { originatorId: sessionId };
        const encodingTestData = {
            successes: [
                ["without constraint", rootChange1a, context],
                ["with constraint", rootChange3, context],
                ["with node existence constraint", rootChange4, context],
                ["without node field changes", rootChangeWithoutNodeFieldChanges, context],
            ],
        };
        makeEncodingTestSuite(family.codecs, encodingTestData);
    });
    it("build child change", () => {
        const [changeReceiver, getChanges] = testChangeReceiver(family);
        const editor = family.buildEditor(changeReceiver);
        const path = {
            parent: undefined,
            parentField: fieldA,
            parentIndex: 0,
        };
        editor.submitChange({ parent: path, field: fieldB }, valueField.identifier, brand(valueChange1a));
        const changes = getChanges();
        const nodeChange = {
            fieldChanges: new Map([
                [fieldB, { fieldKind: valueField.identifier, change: brand(valueChange1a) }],
            ]),
        };
        const fieldChange = genericFieldKind.changeHandler.editor.buildChildChange(0, nodeChange);
        const expectedChange = {
            fieldChanges: new Map([
                [fieldA, { fieldKind: genericFieldKind.identifier, change: brand(fieldChange) }],
            ]),
        };
        assert.deepEqual(changes, [expectedChange]);
    });
});
function treeChunkFromCursor(cursor) {
    return chunkTree(cursor, defaultChunkPolicy);
}
//# sourceMappingURL=modularChangeFamily.spec.js.map