/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
/* eslint-disable import/no-internal-modules */
import { strict as assert, fail } from "assert";
import { noopValidator } from "../../../codec/index.js";
import { LazyFieldNode, LazyLeaf, LazyMap, LazyTreeNode, buildLazyObjectNode, propertyNameFromFieldKey, reservedObjectNodeFieldPropertyNameSet, } from "../../../feature-libraries/flex-tree/lazyNode.js";
import { Any, typeNameSymbol, createMockNodeKeyManager, nodeKeyFieldKey, DefaultEditBuilder, DefaultChangeFamily, cursorForJsonableTreeNode, } from "../../../feature-libraries/index.js";
import { EmptyKey, rootFieldKey, } from "../../../core/index.js";
import { brand, capitalize } from "../../../util/index.js";
import { getTreeContext } from "../../../feature-libraries/flex-tree/context.js";
import { leaf as leafDomain, SchemaBuilder } from "../../../domains/index.js";
import { forestWithContent, flexTreeViewWithContent, failCodec, testRevisionTagCodec, } from "../../utils.js";
import { reservedObjectNodeFieldPropertyNamePrefixes, } from "../../../feature-libraries/flex-tree/flexTreeTypes.js";
import { contextWithContentReadonly } from "./utils.js";
function collectPropertyNames(obj) {
    if (obj == null) {
        return new Set();
    }
    return new Set([
        ...Object.getOwnPropertyNames(obj),
        ...collectPropertyNames(Object.getPrototypeOf(obj)),
    ]);
}
const rootFieldAnchor = { parent: undefined, fieldKey: rootFieldKey };
/**
 * Creates a cursor from the provided `context` and moves it to the provided `anchor`.
 */
function initializeCursor(context, anchor) {
    const cursor = context.forest.allocateCursor();
    assert.equal(context.forest.tryMoveCursorToField(anchor, cursor), 1 /* TreeNavigationResult.Ok */);
    return cursor;
}
/**
 * Initializes a test tree, context, and cursor, and moves the cursor to the tree's root.
 *
 * @returns The initialized context and cursor.
 */
function initializeTreeWithContent(treeContent) {
    const context = contextWithContentReadonly(treeContent);
    const cursor = initializeCursor(context, rootFieldAnchor);
    return {
        context,
        cursor,
    };
}
/**
 * Test {@link LazyTreeNode} implementation.
 */
class TestLazyTree extends LazyTreeNode {
}
/**
 * Creates an {@link Anchor} and an {@link AnchorNode} for the provided cursor's location.
 */
function createAnchors(context, cursor) {
    const anchor = context.forest.anchors.track(cursor.getPath() ?? fail());
    const anchorNode = context.forest.anchors.locate(anchor) ?? fail();
    return { anchor, anchorNode };
}
describe("LazyNode", () => {
    describe("LazyNode", () => {
        it("property names", () => {
            const builder = new SchemaBuilder({ scope: "LazyNode" });
            const emptyStruct = builder.object("empty", {});
            const testSchema = builder.intoSchema(SchemaBuilder.optional(emptyStruct));
            const { cursor, context } = initializeTreeWithContent({
                schema: testSchema,
                initialTree: {},
            });
            cursor.enterNode(0);
            const { anchor, anchorNode } = createAnchors(context, cursor);
            const struct = buildLazyObjectNode(context, emptyStruct, cursor, anchorNode, anchor);
            const existingProperties = collectPropertyNames(struct);
            const existingPropertiesExtended = new Set(existingProperties);
            for (const name of existingProperties) {
                for (const prefix of reservedObjectNodeFieldPropertyNamePrefixes) {
                    // Ensure properties won't collide with prefixed field name based properties.
                    if (name.startsWith(prefix)) {
                        // If the property does have a reserved prefix, that's okay as long as the rest of name after the prefix is also banned.
                        const bannedName = name.substring(prefix.length);
                        const lowercaseBannedName = `${bannedName[0].toLowerCase()}${bannedName.substring(1)}`;
                        assert(reservedObjectNodeFieldPropertyNameSet.has(lowercaseBannedName), lowercaseBannedName);
                        existingPropertiesExtended.add(lowercaseBannedName);
                    }
                }
                // Confirm escapeFieldKey escapes all used names:
                assert.equal(propertyNameFromFieldKey(name), `field${capitalize(name)}`);
            }
            // Ensure all existing properties are banned as field names:
            // Note that this currently also ensure that there are no names that are unnecessary banned:
            // this restriction may need to be relaxed in the future to reserve names so they can be used in the API later as a non breaking change.
            assert.deepEqual(reservedObjectNodeFieldPropertyNameSet, new Set(existingPropertiesExtended));
        });
        it("is", () => {
            // #region Create schemas
            const schemaBuilder = new SchemaBuilder({
                scope: "testShared",
            });
            const fieldNodeOptionalAnySchema = schemaBuilder.fieldNode("optionalAny", SchemaBuilder.optional(Any));
            const fieldNodeOptionalStringSchema = schemaBuilder.fieldNode("optionalString", SchemaBuilder.optional(leafDomain.string));
            const fieldNodeRequiredAnySchema = schemaBuilder.fieldNode("requiredAny", Any);
            const fieldNodeRequiredStringSchema = schemaBuilder.fieldNode("valueString", leafDomain.string);
            const structNodeSchema = schemaBuilder.object("object", {});
            const mapNodeAnySchema = schemaBuilder.map("mapAny", SchemaBuilder.optional(Any));
            const mapNodeStringSchema = schemaBuilder.map("mapString", SchemaBuilder.optional(leafDomain.string));
            const schema = schemaBuilder.intoSchema(fieldNodeOptionalAnySchema);
            // #endregion
            const { context, cursor } = initializeTreeWithContent({ schema, initialTree: {} });
            cursor.enterNode(0);
            const { anchor, anchorNode } = createAnchors(context, cursor);
            const node = new TestLazyTree(context, fieldNodeOptionalAnySchema, cursor, anchorNode, anchor);
            assert(node.is(fieldNodeOptionalAnySchema));
            assert(!node.is(fieldNodeOptionalStringSchema));
            assert(!node.is(fieldNodeRequiredAnySchema));
            assert(!node.is(fieldNodeRequiredStringSchema));
            assert(!node.is(mapNodeAnySchema));
            assert(!node.is(mapNodeStringSchema));
            assert(!node.is(leafDomain.string));
            assert(!node.is(structNodeSchema));
        });
        it("parent", () => {
            const schemaBuilder = new SchemaBuilder({
                scope: "test",
                libraries: [leafDomain.library],
            });
            const fieldNodeSchema = schemaBuilder.fieldNode("field", SchemaBuilder.optional(leafDomain.string));
            const schema = schemaBuilder.intoSchema(fieldNodeSchema);
            const { context, cursor } = initializeTreeWithContent({
                schema,
                initialTree: {
                    [EmptyKey]: "Hello world",
                },
            });
            cursor.enterNode(0);
            const { anchor, anchorNode } = createAnchors(context, cursor);
            const node = new TestLazyTree(context, fieldNodeSchema, cursor, anchorNode, anchor);
            const { index, parent } = node.parentField;
            assert.equal(index, 0);
            assert.equal(parent.key, rootFieldKey);
        });
    });
    describe("LazyFieldNode", () => {
        const schemaBuilder = new SchemaBuilder({
            scope: "test",
            libraries: [leafDomain.library],
        });
        const fieldNodeSchema = schemaBuilder.fieldNode("field", SchemaBuilder.optional(leafDomain.string));
        const schema = schemaBuilder.intoSchema(fieldNodeSchema);
        const { context, cursor } = initializeTreeWithContent({
            schema,
            initialTree: {
                [EmptyKey]: "Hello world",
            },
        });
        cursor.enterNode(0);
        const { anchor, anchorNode } = createAnchors(context, cursor);
        const node = new LazyFieldNode(context, fieldNodeSchema, cursor, anchorNode, anchor);
        it("value", () => {
            assert.equal(node.value, undefined); // FieldNode_s do not have a value
        });
        it("tryGetField", () => {
            const field = node.tryGetField(EmptyKey);
            assert(field !== undefined);
            assert(field.is(SchemaBuilder.optional(leafDomain.string)));
        });
    });
    describe("LazyLeaf", () => {
        const schemaBuilder = new SchemaBuilder({
            scope: "test",
            libraries: [leafDomain.library],
        });
        const schema = schemaBuilder.intoSchema(leafDomain.string);
        const { context, cursor } = initializeTreeWithContent({
            schema,
            initialTree: "Hello world",
        });
        cursor.enterNode(0);
        const { anchor, anchorNode } = createAnchors(context, cursor);
        const node = new LazyLeaf(context, leafDomain.string, cursor, anchorNode, anchor);
        it("value", () => {
            assert.equal(node.value, "Hello world");
        });
    });
    describe("LazyMap", () => {
        const schemaBuilder = new SchemaBuilder({
            scope: "test",
            libraries: [leafDomain.library],
        });
        const mapNodeSchema = schemaBuilder.map("mapString", SchemaBuilder.optional(leafDomain.string));
        const schema = schemaBuilder.intoSchema(mapNodeSchema);
        // Count the number of times edits have been generated.
        let editCallCount = 0;
        beforeEach(() => {
            editCallCount = 0;
        });
        const editBuilder = new DefaultEditBuilder(new DefaultChangeFamily(testRevisionTagCodec, failCodec, {
            jsonValidator: noopValidator,
        }), (change) => {
            editCallCount++;
        });
        const forest = forestWithContent({
            schema,
            initialTree: {
                foo: "Hello",
                bar: "world",
            },
        });
        const context = getTreeContext(schema, forest, editBuilder, createMockNodeKeyManager(), brand(nodeKeyFieldKey));
        const cursor = initializeCursor(context, rootFieldAnchor);
        cursor.enterNode(0);
        const { anchor, anchorNode } = createAnchors(context, cursor);
        const node = new LazyMap(context, mapNodeSchema, cursor, anchorNode, anchor);
        it("value", () => {
            assert.equal(node.value, undefined); // Map nodes do not have a value
        });
        it("tryGetField", () => {
            assert.notEqual(node.tryGetField(brand("foo")), undefined);
            assert.notEqual(node.tryGetField(brand("bar")), undefined);
            assert.equal(node.tryGetField(brand("baz")), undefined);
        });
        it("set", () => {
            const view = flexTreeViewWithContent({ schema, initialTree: {} });
            const mapNode = view.flexTree.content;
            assert(mapNode.is(mapNodeSchema));
            mapNode.set("baz", "First edit");
            mapNode.set("foo", "Second edit");
            assert.equal(mapNode.get("baz"), "First edit");
            assert.equal(mapNode.get("foo"), "Second edit");
            mapNode.set("foo", cursorForJsonableTreeNode({ type: leafDomain.string.name, value: "X" }));
            assert.equal(mapNode.get("foo"), "X");
            mapNode.set("foo", undefined);
            assert.equal(mapNode.get("foo"), undefined);
            assert.equal(mapNode.has("foo"), false);
        });
        it("getBoxed empty", () => {
            const view = flexTreeViewWithContent({ schema, initialTree: {} });
            const mapNode = view.flexTree.content;
            assert(mapNode.is(mapNodeSchema));
            const empty = mapNode.getBoxed("foo");
            assert.equal(empty.parent, mapNode);
            assert.equal(empty.key, "foo");
        });
        it("delete", () => {
            assert.equal(editCallCount, 0);
            // Even though there is no value currently associated with "baz", we still need to
            // emit a delete op, so this should generate an edit.
            node.delete(brand("baz"));
            assert.equal(editCallCount, 1);
            node.delete(brand("foo"));
            assert.equal(editCallCount, 2);
        });
    });
    describe("LazyObjectNode", () => {
        const schemaBuilder = new SchemaBuilder({
            scope: "test",
            libraries: [leafDomain.library],
        });
        const structNodeSchema = schemaBuilder.object("object", {
            foo: SchemaBuilder.optional(leafDomain.string),
            bar: SchemaBuilder.sequence(leafDomain.number),
        });
        const schema = schemaBuilder.intoSchema(SchemaBuilder.optional(Any));
        // Count the number of times edits have been generated.
        let editCallCount = 0;
        beforeEach(() => {
            editCallCount = 0;
        });
        const editBuilder = new DefaultEditBuilder(new DefaultChangeFamily(testRevisionTagCodec, failCodec, {
            jsonValidator: noopValidator,
        }), (change) => {
            editCallCount++;
        });
        const initialTree = {
            [typeNameSymbol]: structNodeSchema.name,
            foo: "Hello world",
            bar: [], // Won't unbox
        };
        const forest = forestWithContent({ schema, initialTree });
        const context = getTreeContext(schema, forest, editBuilder, createMockNodeKeyManager(), brand(nodeKeyFieldKey));
        const cursor = initializeCursor(context, rootFieldAnchor);
        cursor.enterNode(0);
        const { anchor, anchorNode } = createAnchors(context, cursor);
        const node = buildLazyObjectNode(context, structNodeSchema, cursor, anchorNode, anchor);
        it("boxing", () => {
            assert.equal(node.foo, node.boxedFoo.content);
            assert(node.bar.isSameAs(node.boxedBar));
        });
        it("value", () => {
            assert.equal(node.value, undefined); // object nodes do not have a value
        });
        it("tryGetField", () => {
            assert.notEqual(node.tryGetField(brand("foo")), undefined);
            assert.equal(node.tryGetField(brand("bar")), undefined); // TODO: this is presumably wrong - empty array shouldn't yield undefined
            assert.equal(node.tryGetField(brand("baz")), undefined);
        });
        it("Value assignment generates edits", () => {
            assert.equal(editCallCount, 0);
            node.foo = "First edit";
            assert.equal(editCallCount, 1);
            node.setFoo("Second edit");
            assert.equal(editCallCount, 2);
        });
    });
    describe("buildLazyObjectNode", () => {
        const schemaBuilder = new SchemaBuilder({ scope: "test" });
        const objectNodeSchema = schemaBuilder.object("object", {
            optional: SchemaBuilder.optional(leafDomain.string),
            required: SchemaBuilder.required(leafDomain.boolean),
            sequence: SchemaBuilder.sequence(leafDomain.number),
            // Gets escaped
            value: SchemaBuilder.optional(leafDomain.string),
        });
        const schema = schemaBuilder.intoSchema(SchemaBuilder.optional(Any));
        const context = contextWithContentReadonly({
            schema,
            initialTree: {
                optional: "Hello",
                required: true,
                sequence: [1, 2, 3],
                value: "x",
            },
        });
        const cursor = initializeCursor(context, rootFieldAnchor);
        cursor.enterNode(0);
        const { anchor, anchorNode } = createAnchors(context, cursor);
        const node = buildLazyObjectNode(context, objectNodeSchema, cursor, anchorNode, anchor);
        it("Binds setter properties for values, but not other field kinds", () => {
            assert(Object.getOwnPropertyDescriptor(Reflect.getPrototypeOf(node), "optional")?.set !==
                undefined);
            assert(Object.getOwnPropertyDescriptor(Reflect.getPrototypeOf(node), "required")?.set !==
                undefined);
            assert(Object.getOwnPropertyDescriptor(Reflect.getPrototypeOf(node), "sequence")?.set ===
                undefined);
        });
        it('Binds "set" methods for values, but not other field kinds', () => {
            const record = node;
            assert(record.setOptional !== undefined);
            assert(record.setRequired !== undefined);
            assert(record.setSequence === undefined);
        });
        it("escaped fields handled correctly", () => {
            assert(Object.getOwnPropertyDescriptor(Reflect.getPrototypeOf(node), "fieldValue")?.set !==
                undefined);
            const s = node.fieldValue;
            assert.equal(s, "x");
            assert.equal(node.value, undefined); // Not the field, but the node's value.
            assert.equal(node.boxedFieldValue?.content, "x");
        });
    });
    it("PropertyNameFromFieldKey", () => {
        // Strict typing on this allow testing of compile time PropertyNameFromFieldKey and runtime both by calling this function
        function expect(input, out) {
            assert.equal(propertyNameFromFieldKey(input), out);
        }
        // Unescaped
        expect("", "");
        expect("simpleCase", "simpleCase");
        expect("setting", "setting"); // has "set" prefix, but lower case afterwards
        // Escaped
        expect("constructor", "fieldConstructor");
        expect("field", "fieldField"); // Name collides with prefix.
        expect("setExample", "fieldSetExample"); // Prefix, then capitalized
        expect("set-", "fieldSet-"); // Prefix, then non-lowercase
    });
});
function fieldToMapTree(field) {
    const results = [];
    for (const child of field.boxedIterator()) {
        results.push(nodeToMapTree(child));
    }
    return results;
}
function nodeToMapTree(node) {
    const fields = new Map();
    for (const field of node.boxedIterator()) {
        fields.set(field.key, fieldToMapTree(field));
    }
    return { fields, type: node.schema.name, value: node.value };
}
//# sourceMappingURL=lazyNode.spec.js.map