/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/* eslint-disable @typescript-eslint/naming-convention */
import { assert } from "chai";
import * as fs from "fs";
import * as path from "path";
import { Guid, Logger, OpenMode } from "@itwin/core-bentley";
import { EditTxn, withEditTxn } from "../../EditTxn";
import { CodeScopeSpec, CodeSpec, ColorByName, DomainOptions, GeometryStreamBuilder, IModel, SubCategoryAppearance, } from "@itwin/core-common";
import { LineSegment3d, Point3d, YawPitchRollAngles } from "@itwin/core-geometry";
import { _nativeDb, ChannelControl, IModelJsFs, PhysicalModel, SpatialCategory, StandaloneDb } from "../../core-backend";
import { IModelTestUtils, TestElementDrivesElement, TestPhysicalObject } from "../IModelTestUtils";
import { IModelNative } from "../../internal/NativePlatform";
function saveTestChanges(txn, args) {
    txn.saveChanges(args);
}
export function copyFile(newName, pathToCopy) {
    const newPath = path.join(path.dirname(pathToCopy), newName);
    try {
        fs.unlinkSync(newPath);
    }
    catch {
    }
    fs.copyFileSync(pathToCopy, newPath);
    return newPath;
}
function assertRels(list, rels) {
    assert.equal(list.length, rels.length);
    for (let i = 0; i < rels.length; ++i) {
        assert.equal(list[i].id, rels[i].id);
    }
}
class DependencyCallbackResults {
    beforeOutputs = [];
    allInputsHandled = [];
    rootChanged = [];
    deletedDependency = [];
}
class TestHelper {
    db;
    txn;
    dres = new DependencyCallbackResults();
    removals = [];
    codeSpecId = "";
    physicalModelId = "";
    spatialCategoryid = "";
    constructor(testName, dbInfo) {
        this.codeSpecId = dbInfo.codeSpecId;
        this.physicalModelId = dbInfo.physicalModelId;
        this.spatialCategoryid = dbInfo.spatialCategoryId;
        const writeDbFileName = copyFile(`${testName}.bim`, dbInfo.seedFileName);
        this.db = StandaloneDb.openFile(writeDbFileName, OpenMode.ReadWrite);
        assert.isTrue(this.db !== undefined);
        this.db.channels.addAllowedChannel(ChannelControl.sharedChannelName);
        this.txn = new EditTxn(this.db, `${testName} element dependency graph`);
        this.txn.start();
        this.db[_nativeDb].enableTxnTesting();
        assert.equal(this.db[_nativeDb].addChildPropagatesChangesToParentRelationship("TestBim", "ChildPropagatesChangesToParent"), 0);
        this.setElementDependencyGraphCallbacks();
    }
    terminate() {
        if (this.txn.isActive)
            this.txn.end("abandon");
        this.db.close();
        assert.isFalse(this.db.isOpen);
        this.removeElementDependencyGraphCallbacks();
    }
    makeElement(codeValue, parent) {
        const builder = new GeometryStreamBuilder();
        builder.appendGeometry(LineSegment3d.create(Point3d.createZero(), Point3d.create(5, 0, 0)));
        return {
            classFullName: "TestBim:TestPhysicalObject",
            model: this.physicalModelId,
            category: this.spatialCategoryid,
            code: { spec: this.codeSpecId, scope: this.physicalModelId, value: codeValue },
            intProperty: 100,
            placement: {
                origin: new Point3d(0, 0, 0),
                angles: new YawPitchRollAngles(),
            },
            geom: builder.geometryStream,
            parent,
        };
    }
    insertElement(codeValue, parent) {
        return this.txn.insertElement(this.makeElement(codeValue, parent));
    }
    updateElement(elid, newLabel) {
        const ed2 = this.db.elements.getElement({ id: elid });
        ed2.userLabel = newLabel;
        this.txn.updateElement(ed2.toJSON());
    }
    fmtElem(elId) { return this.db.elements.getElement(elId).code.value; }
    fmtRel(props) { return `${props.classFullName} ${this.fmtElem(props.sourceId)}  -->  ${this.fmtElem(props.targetId)}`; }
    resetDependencyResults() {
        this.dres = new DependencyCallbackResults();
    }
    setElementDependencyGraphCallbacks() {
        this.removals.push(TestElementDrivesElement.deletedDependency.addListener((arg) => {
            Logger.logTrace("EDGTest", `_onDeletedDependency ${this.fmtRel(arg.props)}`);
            this.dres.deletedDependency.push(arg.props);
        }));
        this.removals.push(TestElementDrivesElement.rootChanged.addListener((arg) => {
            Logger.logTrace("EDGTest", `_onRootChanged ${this.fmtRel(arg.props)}`);
            this.dres.rootChanged.push(arg.props);
        }));
        this.removals.push(TestPhysicalObject.beforeOutputsHandled.addListener((arg) => {
            Logger.logTrace("EDGTest", `_onBeforeOutputsHandled ${this.fmtElem(arg.elId)}`);
            this.dres.beforeOutputs.push(arg.elId);
        }));
        this.removals.push(TestPhysicalObject.allInputsHandled.addListener((arg) => {
            Logger.logTrace("EDGTest", `_onAllInputsHandled ${this.fmtElem(arg.elId)}`);
            this.dres.allInputsHandled.push(arg.elId);
        }));
    }
    removeElementDependencyGraphCallbacks() {
        this.removals.forEach((drop) => drop());
    }
}
describe("ElementDependencyGraph", () => {
    let testFileName;
    let dbInfo;
    const performUpgrade = (pathname) => {
        const nativeDb = new IModelNative.platform.DgnDb();
        const upgradeOptions = {
            domain: DomainOptions.Upgrade,
            schemaLockHeld: true,
        };
        nativeDb.openIModel(pathname, OpenMode.ReadWrite, upgradeOptions);
        nativeDb.deleteAllTxns();
        nativeDb.closeFile();
    };
    before(async () => {
        IModelTestUtils.registerTestBimSchema();
        // make a unique name for the output file so this test can be run in parallel
        testFileName = IModelTestUtils.prepareOutputFile("ElementDependencyGraph", `${Guid.createValue()}.bim`);
        const seedFileName = IModelTestUtils.resolveAssetFile("test.bim");
        const schemaFileName = IModelTestUtils.resolveAssetFile("TestBim.ecschema.xml");
        IModelJsFs.copySync(seedFileName, testFileName);
        performUpgrade(testFileName);
        const imodel = StandaloneDb.openFile(testFileName, OpenMode.ReadWrite);
        await imodel.importSchemas([schemaFileName]); // will throw an exception if import fails
        imodel.channels.addAllowedChannel(ChannelControl.sharedChannelName);
        const physicalModelId = withEditTxn(imodel, (txn) => PhysicalModel.insert(txn, IModel.rootSubjectId, "EDGTestModel"));
        const codeSpecId = withEditTxn(imodel, (txn) => imodel.codeSpecs.insert(txn, CodeSpec.create(imodel, "EDGTestCodeSpec", CodeScopeSpec.Type.Model)));
        const spatialCategoryId = withEditTxn(imodel, (txn) => SpatialCategory.insert(txn, IModel.dictionaryId, "EDGTestSpatialCategory", new SubCategoryAppearance({ color: ColorByName.darkRed })));
        dbInfo = { physicalModelId, codeSpecId, spatialCategoryId, seedFileName: testFileName };
        imodel[_nativeDb].deleteAllTxns();
        imodel.close();
    });
    after(() => {
        IModelJsFs.removeSync(testFileName);
    });
    it("should invokeCallbacks EDE only", () => {
        const helper = new TestHelper("EDE", dbInfo);
        const e1id = helper.insertElement("e1");
        const e2id = helper.insertElement("e2");
        const e3id = helper.insertElement("e3");
        saveTestChanges(helper.txn); // get the elements into the iModel
        const ede_1_2 = TestElementDrivesElement.create(helper.db, e1id, e2id);
        const ede_2_3 = TestElementDrivesElement.create(helper.db, e2id, e3id);
        for (const ede of [ede_1_2, ede_2_3]) {
            ede.id = helper.txn.insertRelationship(ede.toJSON());
        }
        // The full graph:
        //  e1 --> e2 --> e3
        helper.resetDependencyResults();
        saveTestChanges(helper.txn); // this will react to EDE inserts only.
        assert.deepEqual(helper.dres.beforeOutputs, []); // only roots get this callback, and only if they have been directly changed.
        assert.deepEqual(helper.dres.allInputsHandled, []); // No input elements have changed
        assertRels(helper.dres.rootChanged, [ede_1_2.toJSON(), ede_2_3.toJSON()]); // we send out this callback even if only the relationship itself is new or changed.
        helper.updateElement(e1id, "change e1");
        helper.resetDependencyResults();
        saveTestChanges(helper.txn);
        assert.deepEqual(helper.dres.beforeOutputs, [e1id]); // only called on directly changed root elements.
        assert.deepEqual(helper.dres.allInputsHandled, [e2id, e3id]);
        assertRels(helper.dres.rootChanged, [ede_1_2.toJSON(), ede_2_3.toJSON()]);
        helper.terminate();
    });
    it("should invokeCallbacks through parents only", () => {
        const helper = new TestHelper("Parents", dbInfo);
        const p2id = helper.insertElement("p2");
        const e1id = helper.insertElement("e1", { id: p2id, relClassName: "TestBim.ChildPropagatesChangesToParent" });
        saveTestChanges(helper.txn); // get the elements into the iModel
        // The full graph:
        //     .-parent-> p2
        //    /
        //  e1
        //
        helper.resetDependencyResults();
        saveTestChanges(helper.txn);
        assert.deepEqual(helper.dres.beforeOutputs, []); // only roots get this callback, and only if they have been directly changed.
        assert.deepEqual(helper.dres.allInputsHandled, []); // No input elements have changed
        assertRels(helper.dres.rootChanged, []); // we send out this callback even if only the relationship itself is new or changed.
        helper.updateElement(e1id, "change e1");
        helper.resetDependencyResults();
        saveTestChanges(helper.txn);
        assert.deepEqual(helper.dres.beforeOutputs, [e1id]); // only called on directly changed root elements.
        assert.deepEqual(helper.dres.allInputsHandled, [p2id]);
        assertRels(helper.dres.rootChanged, []);
        helper.updateElement(p2id, "change p2");
        helper.resetDependencyResults();
        saveTestChanges(helper.txn);
        assert.deepEqual(helper.dres.beforeOutputs, []); // only called on directly changed root elements
        assert.deepEqual(helper.dres.allInputsHandled, []);
        assertRels(helper.dres.rootChanged, []);
        helper.terminate();
    });
    it("should invokeCallbacks through EDEs and parents", () => {
        const helper = new TestHelper("EDEsAndParents", dbInfo);
        const p2id = helper.insertElement("p2");
        const p3id = helper.insertElement("p3");
        const e1id = helper.insertElement("e1", { id: p2id, relClassName: "TestBim.ChildPropagatesChangesToParent" });
        const e2id = helper.insertElement("e2");
        const e3id = helper.insertElement("e3");
        saveTestChanges(helper.txn); // get the elements into the iModel
        const ede_1_2 = TestElementDrivesElement.create(helper.db, e1id, e2id);
        const ede_2_3 = TestElementDrivesElement.create(helper.db, e2id, e3id);
        const ede_p2_p3 = TestElementDrivesElement.create(helper.db, p2id, p3id);
        for (const ede of [ede_1_2, ede_2_3, ede_p2_p3]) {
            ede.id = helper.txn.insertRelationship(ede.toJSON());
        }
        // db[_nativeDb].writeFullElementDependencyGraphToFile(`${writeDbFileName}.dot`);
        // The full graph:
        //     .-parent-> p2 -EDE-> p3
        //    /
        //  e1 -EDE-> e2 -EDE-> e3
        //
        helper.resetDependencyResults();
        saveTestChanges(helper.txn); // this will react to EDE inserts only.
        assert.deepEqual(helper.dres.beforeOutputs, []); // only roots get this callback, and only if they have been directly changed.
        assert.deepEqual(helper.dres.allInputsHandled, []); // No input elements have changed
        assertRels(helper.dres.rootChanged, [ede_1_2.toJSON(), ede_2_3.toJSON(), ede_p2_p3.toJSON()]); // we send out this callback even if only the relationship itself is new or changed.
        helper.updateElement(e1id, "change e1");
        helper.resetDependencyResults();
        saveTestChanges(helper.txn);
        assert.deepEqual(helper.dres.beforeOutputs, [e1id]); // only called on directly changed root elements.
        assert.deepEqual(helper.dres.allInputsHandled, [e2id, p2id, e3id, p3id]);
        assertRels(helper.dres.rootChanged, [ede_1_2.toJSON(), ede_2_3.toJSON(), ede_p2_p3.toJSON()]);
        helper.updateElement(p2id, "change p2");
        helper.resetDependencyResults();
        saveTestChanges(helper.txn);
        assert.deepEqual(helper.dres.beforeOutputs, [p2id]); // only called on directly changed root elements.
        assert.deepEqual(helper.dres.allInputsHandled, [p3id]);
        assertRels(helper.dres.rootChanged, [ede_p2_p3.toJSON()]);
        helper.updateElement(e2id, "change e2");
        helper.resetDependencyResults();
        saveTestChanges(helper.txn);
        assert.deepEqual(helper.dres.beforeOutputs, [e2id]); // only called on directly changed root elements.
        assert.deepEqual(helper.dres.allInputsHandled, [e3id]);
        assertRels(helper.dres.rootChanged, [ede_2_3.toJSON()]);
        helper.terminate();
    });
    it("should invokeCallbacks through parents - geomodeler schema", () => {
        const helper = new TestHelper("GeoModeler", dbInfo);
        // The full graph:
        //                                        BoreholeSource -EDE-> GroundGeneration
        //                                        / parent
        //                                  Borehole
        //                                  / parent
        // Material -EDE-> MaterialDepthRange
        const boreholeSource = helper.insertElement("BoreholeSource");
        const borehole = helper.insertElement("Borehole", { id: boreholeSource, relClassName: "TestBim.ChildPropagatesChangesToParent" });
        const materialDepthRange = helper.insertElement("MaterialDepthRange", { id: borehole, relClassName: "TestBim.ChildPropagatesChangesToParent" });
        const material = helper.insertElement("Material");
        const groundGeneration = helper.insertElement("GroundGeneration");
        saveTestChanges(helper.txn); // get the elements into the iModel
        const ede_material_materialDepthRange = TestElementDrivesElement.create(helper.db, material, materialDepthRange);
        const ede_boreholeSource_groundGeneration = TestElementDrivesElement.create(helper.db, boreholeSource, groundGeneration);
        for (const ede of [ede_material_materialDepthRange, ede_boreholeSource_groundGeneration]) {
            ede.id = helper.txn.insertRelationship(ede.toJSON());
        }
        helper.resetDependencyResults();
        saveTestChanges(helper.txn);
        helper.updateElement(material, "change material");
        helper.resetDependencyResults();
        saveTestChanges(helper.txn);
        assert.deepEqual(helper.dres.beforeOutputs, [material]); // only called on directly changed root elements.
        assert.deepEqual(helper.dres.allInputsHandled, [materialDepthRange, borehole, boreholeSource, groundGeneration]);
        assertRels(helper.dres.rootChanged, [ede_material_materialDepthRange.toJSON(), ede_boreholeSource_groundGeneration.toJSON()]);
        helper.terminate();
    });
    it("should invokeCallbacks many:1", () => {
        const helper = new TestHelper("ManyToOne", dbInfo);
        const e1id = helper.insertElement("e1");
        const e11id = helper.insertElement("e11");
        const e2id = helper.insertElement("e2");
        const e21id = helper.insertElement("e21");
        const e3id = helper.insertElement("e3");
        const e4id = helper.insertElement("e4");
        const ede_1_2 = TestElementDrivesElement.create(helper.db, e1id, e2id);
        const ede_11_2 = TestElementDrivesElement.create(helper.db, e11id, e2id);
        const ede_2_3 = TestElementDrivesElement.create(helper.db, e2id, e3id);
        const ede_21_3 = TestElementDrivesElement.create(helper.db, e21id, e3id);
        const ede_3_4 = TestElementDrivesElement.create(helper.db, e3id, e4id);
        for (const ede of [ede_1_2, ede_11_2, ede_2_3, ede_21_3, ede_3_4]) {
            ede.id = helper.txn.insertRelationship(ede.toJSON());
        }
        // The full graph:
        //        e21
        //            \
        //  e1 --> e2 --> e3 --> e4
        //      /
        //  e11
        // On the very first validation, everything is new and is considered directly changed
        // resulting graph:
        //        e21
        //            \
        //  e1 --> e2 --> e3 --> e4
        //      /
        //  e11
        helper.resetDependencyResults();
        saveTestChanges(helper.txn);
        assert.deepEqual(helper.dres.beforeOutputs, [e1id, e11id, e21id]); // only called on directly changed root elements.
        assert.deepEqual(helper.dres.allInputsHandled, [e2id, e3id, e4id]);
        assertRels(helper.dres.rootChanged, [ede_1_2.toJSON(), ede_11_2.toJSON(), ede_2_3.toJSON(), ede_21_3.toJSON(), ede_3_4.toJSON()]);
        // modify e4 directly. That is a leaf. None of its inputs are changed.
        // resulting subgraph:
        //                      *
        //                      e4
        //
        //
        helper.updateElement(e4id, "change e4");
        helper.resetDependencyResults();
        saveTestChanges(helper.txn);
        assert.deepEqual(helper.dres.beforeOutputs, []); // only called on directly changed root elements.
        assert.deepEqual(helper.dres.allInputsHandled, []);
        assertRels(helper.dres.rootChanged, []);
        // modify e3 directly.
        // resulting subgraph:
        //
        //
        //               *
        //               e3 --> e4
        //
        //
        helper.updateElement(e3id, "change e3");
        helper.resetDependencyResults();
        saveTestChanges(helper.txn);
        assert.deepEqual(helper.dres.beforeOutputs, [e3id]); // only called on directly changed root elements.
        assert.deepEqual(helper.dres.allInputsHandled, [e4id]);
        assertRels(helper.dres.rootChanged, [ede_3_4.toJSON()]);
        // modify e2 directly. That is a node in middle of the graph. None of its inputs is modified.
        // resulting subgraph:
        //
        //         *
        //         e2 --> e3 --> e4
        //
        //
        //
        helper.updateElement(e2id, "change e2");
        helper.resetDependencyResults();
        saveTestChanges(helper.txn);
        assert.deepEqual(helper.dres.beforeOutputs, [e2id]); // only called on directly changed root elements
        assert.deepEqual(helper.dres.allInputsHandled, [e3id, e4id]);
        assertRels(helper.dres.rootChanged, [ede_2_3.toJSON(), ede_3_4.toJSON()]);
        // Modify e1 directly. That should propagate to the rest of the nodes. Each should get an _onAllInputsHandled callback
        // resulting graph:
        //
        //    *
        //    e1 --> e2 --> e3 --> e4
        //
        helper.updateElement(e1id, "change e1");
        helper.resetDependencyResults();
        saveTestChanges(helper.txn);
        assert.deepEqual(helper.dres.beforeOutputs, [e1id]); // only called on directly changed root elements
        assert.deepEqual(helper.dres.allInputsHandled, [e2id, e3id, e4id]);
        assertRels(helper.dres.rootChanged, [ede_1_2.toJSON(), ede_2_3.toJSON(), ede_3_4.toJSON()]);
        // Modify e11 directly. That should propagate to the rest of the nodes. Each should get an _onAllInputsHandled callback
        // resulting graph:
        //
        //       > e2 --> e3 --> e4
        //      /
        //  e11
        //  *
        //
        // Note that the e1 -> e2 and e21 -> e3 edges are NOT in the sub-graph. These edges should be validated, nevertheless -- TBD
        helper.updateElement(e11id, "change e11");
        helper.resetDependencyResults();
        saveTestChanges(helper.txn);
        // assert.deepEqual(helper.dres.directChange, []); // only called on directly changed non-root elements that have no directly changed inputs
        assert.deepEqual(helper.dres.beforeOutputs, [e11id]); // only called on directly changed root elements
        assert.deepEqual(helper.dres.allInputsHandled, [e2id, e3id, e4id]);
        assertRels(helper.dres.rootChanged, [ede_11_2.toJSON(), ede_2_3.toJSON(), ede_3_4.toJSON()]);
        // assertRels(helper.dres.validateOutput, [ede_1_2, ede_21_3]); // this callback is made only on rels that not in the graph but share an output with another rel or have an output that was directly changed
        helper.terminate();
    });
});
//# sourceMappingURL=ElementDependencyGraph.test.js.map