/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import { assert, expect } from "chai";
import * as path from "path";
import { Guid } from "@itwin/core-bentley";
import { Code, IModel } from "@itwin/core-common";
import { ChannelControl, DataTransformationStrategy, IModelJsFs, StandaloneDb } from "../../core-backend";
import { HubWrappers, IModelTestUtils } from "../IModelTestUtils";
import { KnownTestLocations } from "../KnownTestLocations";
import { HubMock } from "../../internal/HubMock";
describe("Schema Import Callbacks", () => {
    let imodel;
    // Test schema with version changes
    const testSchemaV100 = () => `<?xml version="1.0" encoding="UTF-8"?>
    <ECSchema schemaName="TestSchema" alias="ts" version="1.0.0" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
      <ECSchemaReference name="BisCore" version="1.0.0" alias="bis"/>
      <ECEntityClass typeName="TestElement">
        <BaseClass>bis:DefinitionElement</BaseClass>
        <ECProperty propertyName="StringProp" typeName="string" />
        <ECProperty propertyName="IntProp" typeName="int" />
        <ECProperty propertyName="ModelName" typeName="string" />
      </ECEntityClass>
    </ECSchema>`;
    const testSchemaV101 = () => `<?xml version="1.0" encoding="UTF-8"?>
    <ECSchema schemaName="TestSchema" alias="ts" version="1.0.1" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
      <ECSchemaReference name="BisCore" version="1.0.0" alias="bis"/>
      <ECEntityClass typeName="TestElement">
        <BaseClass>bis:DefinitionElement</BaseClass>
        <ECProperty propertyName="StringProp" typeName="string" />
        <ECProperty propertyName="IntProp" typeName="int" />
        <ECProperty propertyName="NewProp" typeName="string" />
        <ECProperty propertyName="ModelName" typeName="string" />
      </ECEntityClass>
    </ECSchema>`;
    beforeEach(() => {
        const testFileName = IModelTestUtils.prepareOutputFile("SchemaImportCallbacks", `SchemaCallbackTest_${Guid.createValue()}.bim`);
        imodel = StandaloneDb.createEmpty(testFileName, { rootSubject: { name: "TestSubject" }, allowEdit: JSON.stringify({ txns: true }) });
        assert.exists(imodel);
    });
    afterEach(() => {
        if (imodel?.isOpen)
            imodel.close();
    });
    describe("Basic Callback Execution", () => {
        it("should work without callbacks", async () => {
            // This should not throw and should work as before
            await imodel.importSchemaStrings([testSchemaV100()]);
            assert.isTrue(imodel.containsClass(`TestSchema:TestElement`));
        });
        it("should call both callbacks in correct order", async () => {
            const callOrder = [];
            await imodel.importSchemaStrings([testSchemaV100()], {
                schemaImportCallbacks: {
                    preSchemaImportCallback: async () => {
                        callOrder.push("before");
                        return { transformStrategy: DataTransformationStrategy.None };
                    },
                    postSchemaImportCallback: async () => {
                        callOrder.push("after");
                    },
                },
            });
            assert.deepEqual(callOrder, ["before", "after"]);
        });
    });
    describe("DataTransformationStrategy.None", () => {
        it("should not create snapshot or cache data with None strategy", async () => {
            await imodel.importSchemaStrings([testSchemaV100()], {
                schemaImportCallbacks: {
                    postSchemaImportCallback: async (context) => {
                        assert.isUndefined(context.resources.snapshot);
                        assert.isUndefined(context.resources.cachedData);
                    },
                },
            });
        });
    });
    describe("DataTransformationStrategy.InMemory", () => {
        it("should pass cached data from preImport to postImport", async () => {
            let receivedCachedData;
            const expectedCachedData = {
                elements: [
                    { id: "0x1", classFullName: "BisCore:DefinitionElement" },
                    { id: "0x2", classFullName: "BisCore:DefinitionElement" },
                ],
                cachedNumber: 1234,
            };
            await imodel.importSchemaStrings([testSchemaV100()], {
                schemaImportCallbacks: {
                    preSchemaImportCallback: async () => ({
                        transformStrategy: DataTransformationStrategy.InMemory,
                        cachedData: {
                            elements: [
                                { id: "0x1", classFullName: "BisCore:DefinitionElement" },
                                { id: "0x2", classFullName: "BisCore:DefinitionElement" },
                            ],
                            cachedNumber: 1234,
                        },
                    }),
                    postSchemaImportCallback: async (context) => {
                        receivedCachedData = context.resources.cachedData;
                        assert.isDefined(context.resources.cachedData);
                        assert.equal(context.resources.cachedData.elements.length, 2);
                        assert.deepEqual(receivedCachedData.elements, expectedCachedData.elements);
                        assert.equal(receivedCachedData.cachedNumber, expectedCachedData.cachedNumber);
                        assert.isUndefined(context.resources.snapshot);
                    },
                },
            });
        });
        it("should cache element properties and use them after import", async () => {
            // First import the schema
            await imodel.importSchemaStrings([testSchemaV100()]);
            // Create a test element
            const model = imodel.models.getModel(IModel.dictionaryId);
            const elementProps = {
                classFullName: `TestSchema:TestElement`,
                model: model.id,
                code: Code.createEmpty(),
                stringProp: "original value of first element",
                intProp: 42,
            };
            const elementId1 = imodel.elements.insertElement(elementProps);
            const elementIds = [elementId1];
            await imodel.importSchemaStrings([testSchemaV101()], {
                schemaImportCallbacks: {
                    preSchemaImportCallback: async (context) => {
                        // Create another element before the schema import
                        assert.equal(elementIds.length, 1);
                        const element = context.iModel.elements.getElementProps(elementIds[0]);
                        assert.isDefined(element.stringProp);
                        assert.isDefined(element.intProp);
                        elementProps.stringProp = "original value of second element";
                        elementProps.intProp = 84;
                        elementIds.push(imodel.elements.insertElement(elementProps));
                        const cached = { ids: elementIds };
                        return {
                            transformStrategy: DataTransformationStrategy.InMemory,
                            cachedData: cached,
                        };
                    },
                    postSchemaImportCallback: async (context) => {
                        // Use cached data to update element with new property
                        assert.isDefined(context.resources.cachedData?.ids);
                        const updatedElementProps = context.iModel.elements.getElementProps(context.resources.cachedData.ids[1]);
                        assert.isDefined(updatedElementProps.stringProp);
                        assert.isDefined(updatedElementProps.intProp);
                        assert.isUndefined(updatedElementProps.newProp);
                        updatedElementProps.stringProp = "modified in postImport";
                        updatedElementProps.newProp = `New Prop Added`;
                        context.iModel.elements.updateElement(updatedElementProps);
                    },
                },
            });
            // Verify the transformation
            let finalElementProps = imodel.elements.getElementProps(elementIds[0]);
            assert.equal(finalElementProps.stringProp, "original value of first element");
            assert.isUndefined(finalElementProps.newProp);
            finalElementProps = imodel.elements.getElementProps(elementIds[1]);
            assert.equal(finalElementProps.stringProp, "modified in postImport");
            assert.isDefined(finalElementProps.newProp);
            assert.equal(finalElementProps.newProp, "New Prop Added");
        });
    });
    describe("DataTransformationStrategy.Snapshot", () => {
        it("should provide snapshot in postImport callback", async () => {
            let snapshotProvided = false;
            await imodel.importSchemaStrings([testSchemaV100()], {
                schemaImportCallbacks: {
                    preSchemaImportCallback: async () => ({ transformStrategy: DataTransformationStrategy.Snapshot }),
                    postSchemaImportCallback: async (context) => {
                        assert.isDefined(context.resources.snapshot);
                        assert.notEqual(context.resources.snapshot, context.iModel);
                        assert.isTrue(context.resources.snapshot.isSnapshot);
                        snapshotProvided = true;
                    },
                },
            });
            assert.isTrue(snapshotProvided);
        });
        it("should allow reading pre-import state from snapshot", async () => {
            // First import initial schema
            await imodel.importSchemaStrings([testSchemaV100()]);
            // Create element with original schema
            const model = imodel.models.getModel(IModel.dictionaryId);
            const elementProps = {
                classFullName: `TestSchema:TestElement`,
                model: model.id,
                code: Code.createEmpty(),
                stringProp: "snapshot test",
                intProp: 123,
            };
            const elementId = imodel.elements.insertElement(elementProps);
            imodel.saveChanges("Insert element before schema upgrade");
            // Import updated schema with snapshot strategy
            let originalStringValue;
            await imodel.importSchemaStrings([testSchemaV101()], {
                schemaImportCallbacks: {
                    preSchemaImportCallback: async () => ({ transformStrategy: DataTransformationStrategy.Snapshot }),
                    postSchemaImportCallback: async (context) => {
                        assert.isDefined(context.resources.snapshot);
                        assert.equal(context.resources.snapshot?.getSchemaProps("TestSchema").version, "01.00.00");
                        assert.equal(imodel.getSchemaProps("TestSchema").version, "01.00.01");
                        // Read original value from snapshot
                        const snapshotElementProps = context.resources.snapshot.elements.getElementProps(elementId);
                        originalStringValue = snapshotElementProps.stringProp;
                        // Update element in main iModel with new property based on snapshot data
                        const updatedElementProps = context.iModel.elements.getElementProps(elementId);
                        updatedElementProps.newProp = `Original was: ${originalStringValue}`;
                        context.iModel.elements.updateElement(updatedElementProps);
                    },
                },
            });
            assert.equal(originalStringValue, "snapshot test");
            const finalElement = imodel.elements.getElement(elementId);
            assert.equal(finalElement.newProp, "Original was: snapshot test");
        });
        it("should clean up snapshot after successful import", async () => {
            let snapshotPath;
            await imodel.importSchemaStrings([testSchemaV100()], {
                schemaImportCallbacks: {
                    preSchemaImportCallback: async () => ({ transformStrategy: DataTransformationStrategy.Snapshot }),
                    postSchemaImportCallback: async (context) => {
                        assert.isDefined(context.resources.snapshot);
                        snapshotPath = context.resources.snapshot.pathName;
                        assert.isTrue(IModelJsFs.existsSync(snapshotPath));
                    },
                },
            });
            if (snapshotPath) {
                assert.isFalse(IModelJsFs.existsSync(snapshotPath), "Snapshot file should be cleaned up");
            }
        });
    });
    describe("Error Handling", () => {
        it("In memory strategy selected without caching any data pre import", async () => {
            try {
                await imodel.importSchemaStrings([testSchemaV100()], {
                    schemaImportCallbacks: {
                        preSchemaImportCallback: async () => ({ transformStrategy: DataTransformationStrategy.InMemory }),
                        postSchemaImportCallback: async (context) => {
                            assert.isUndefined(context.resources.snapshot);
                        },
                    },
                });
                assert.fail("Should have thrown error");
            }
            catch (err) {
                expect(err.message).to.equal("Failed to execute preSchemaImportCallback: InMemory transform strategy requires cachedData to be provided.");
            }
        });
        it("should abandon changes if postImport callback throws", async () => {
            // First import initial schema
            await imodel.importSchemaStrings([testSchemaV100()]);
            // Create element
            const model = imodel.models.getModel(IModel.dictionaryId);
            const elementProps = {
                classFullName: `TestSchema:TestElement`,
                model: model.id,
                code: Code.createEmpty(),
                stringProp: "test",
                intProp: 1,
            };
            assert.equal(imodel.getSchemaProps("TestSchema").version, "01.00.00");
            const elementId = imodel.elements.insertElement(elementProps);
            imodel.saveChanges("Insert test element");
            // Try to import with failing callback
            try {
                await imodel.importSchemaStrings([testSchemaV101()], {
                    schemaImportCallbacks: {
                        postSchemaImportCallback: async (context) => {
                            // Make a change
                            const updatedElementProps = context.iModel.elements.getElementProps(elementId);
                            updatedElementProps.intProp += 1;
                            updatedElementProps.stringProp = "should be reverted";
                            updatedElementProps.newProp = "should be reverted";
                            context.iModel.elements.updateElement(updatedElementProps);
                            // Then throw error
                            throw new Error("Intentional callback failure");
                        },
                    },
                });
                assert.fail("Should have thrown error");
            }
            catch (err) {
                assert.equal(err.message, "Failed to execute postSchemaImportCallback: Intentional callback failure");
            }
            // Schema import should have been successful
            assert.equal(imodel.getSchemaProps("TestSchema").version, "01.00.01");
            // Changes should be abandoned - element should not have newProp
            const finalElementProps = imodel.elements.getElementProps(elementId);
            assert.equal(finalElementProps.intProp, 1);
            assert.equal(finalElementProps.stringProp, "test");
            assert.isUndefined(finalElementProps.newProp);
        });
        it("should clean up snapshot when postImport throws error", async () => {
            let snapshotPath;
            try {
                await imodel.importSchemaStrings([testSchemaV100()], {
                    schemaImportCallbacks: {
                        preSchemaImportCallback: async () => ({ transformStrategy: DataTransformationStrategy.Snapshot }),
                        postSchemaImportCallback: async (context) => {
                            snapshotPath = context.resources.snapshot.pathName;
                            throw new Error("Error after snapshot created");
                        },
                    },
                });
                assert.fail("Should have thrown error");
            }
            catch (err) {
                assert.equal(err.message, "Failed to execute postSchemaImportCallback: Error after snapshot created");
            }
            if (snapshotPath) {
                assert.isFalse(IModelJsFs.existsSync(snapshotPath), "Snapshot should be cleaned up even after error");
            }
        });
        it("should handle error in preImport callback", async () => {
            await imodel.importSchemaStrings([testSchemaV100()]);
            assert.equal(imodel.getSchemaProps("TestSchema").version, "01.00.00");
            try {
                await imodel.importSchemaStrings([testSchemaV101()], {
                    schemaImportCallbacks: {
                        preSchemaImportCallback: async () => {
                            throw new Error("Error in preImport");
                        },
                    },
                });
                assert.fail("Should have thrown error");
            }
            catch (err) {
                assert.equal(err.message, "Failed to execute preSchemaImportCallback: Error in preImport");
            }
            // Schema should not have been imported
            assert.equal(imodel.getSchemaProps("TestSchema").version, "01.00.00");
        });
    });
    describe("File-based Schema Import", () => {
        it("should work with importSchemas() using file paths", async () => {
            const schemaPath = path.join(KnownTestLocations.outputDir, `TestSchema.01.00.00.ecschema.xml`);
            IModelJsFs.writeFileSync(schemaPath, testSchemaV100());
            let callbackExecuted = false;
            try {
                await imodel.importSchemas([schemaPath], {
                    schemaImportCallbacks: {
                        preSchemaImportCallback: async (context) => {
                            assert.isDefined(context.schemaData);
                            assert.equal(context.schemaData.length, 1);
                            assert.equal(context.schemaData[0], schemaPath);
                            callbackExecuted = true;
                            return { transformStrategy: DataTransformationStrategy.None };
                        },
                    },
                });
                assert.isTrue(callbackExecuted);
                assert.isTrue(imodel.containsClass(`TestSchema:TestElement`));
            }
            finally {
                if (IModelJsFs.existsSync(schemaPath)) {
                    IModelJsFs.removeSync(schemaPath);
                }
            }
        });
        it("should have consistent behavior between importSchemas and importSchemaStrings", async () => {
            const schemaPath = path.join(KnownTestLocations.outputDir, `TestSchema.01.00.00.ecschema.xml`);
            IModelJsFs.writeFileSync(schemaPath, testSchemaV100());
            const executionLogFile = [];
            const executionLogString = [];
            try {
                // Test with file-based import
                await imodel.importSchemas([schemaPath], {
                    data: { testId: "file-based" },
                    channelUpgrade: {
                        channelKey: "shared",
                        fromVersion: "1.0.0",
                        toVersion: "2.0.0",
                        callback: async (context) => {
                            executionLogFile.push(`channel: ${context.data.testId}`);
                        },
                    },
                    schemaImportCallbacks: {
                        preSchemaImportCallback: async (context) => {
                            executionLogFile.push(`pre: ${context.data.testId}`);
                            return { transformStrategy: DataTransformationStrategy.None };
                        },
                        postSchemaImportCallback: async (context) => {
                            executionLogFile.push(`post: ${context.data.testId}`);
                        },
                    },
                });
                // Clean iModel for next test
                imodel.close();
                const testFileName2 = IModelTestUtils.prepareOutputFile("SchemaImportCallbacks", `SchemaCallbackTest_${Guid.createValue()}.bim`);
                imodel = StandaloneDb.createEmpty(testFileName2, { rootSubject: { name: "TestSubject" }, allowEdit: JSON.stringify({ txns: true }) });
                // Test with string-based import
                await imodel.importSchemaStrings([testSchemaV100()], {
                    data: { testId: "string-based" },
                    channelUpgrade: {
                        channelKey: "shared",
                        fromVersion: "1.0.0",
                        toVersion: "2.0.0",
                        callback: async (context) => {
                            executionLogString.push(`channel: ${context.data.testId}`);
                        },
                    },
                    schemaImportCallbacks: {
                        preSchemaImportCallback: async (context) => {
                            executionLogString.push(`pre: ${context.data.testId}`);
                            return { transformStrategy: DataTransformationStrategy.None };
                        },
                        postSchemaImportCallback: async (context) => {
                            executionLogString.push(`post: ${context.data.testId}`);
                        },
                    },
                });
                // Both should have identical execution patterns
                assert.equal(executionLogFile.length, executionLogString.length);
                assert.deepEqual(executionLogFile.map(s => s.split(":")[0]), executionLogString.map(s => s.split(":")[0]));
            }
            finally {
                if (IModelJsFs.existsSync(schemaPath))
                    IModelJsFs.removeSync(schemaPath);
            }
        });
    });
    describe("Channel Access Validation", () => {
        beforeEach(async () => {
            // Import schema first
            await imodel.importSchemaStrings([testSchemaV100()]);
            assert.equal(imodel.getSchemaProps("TestSchema").version, "01.00.00");
            imodel.channels.addAllowedChannel("shared");
        });
        it("should throw ChannelConstraintViolation when modifying element from disabled channel pre import", async () => {
            const model = imodel.models.getModel(IModel.dictionaryId);
            const elementProps = {
                classFullName: `TestSchema:TestElement`,
                model: model.id,
                code: Code.createEmpty(),
                stringProp: "test element",
                intProp: 100,
            };
            const elementId = imodel.elements.insertElement(elementProps);
            imodel.saveChanges("Create test element");
            // Now REMOVE the shared channel permission
            imodel.channels.removeAllowedChannel("shared");
            // Try to import schema and modify element - should fail
            try {
                await imodel.importSchemaStrings([testSchemaV101()], {
                    schemaImportCallbacks: {
                        preSchemaImportCallback: async (context) => {
                            // This should throw because shared channel is not allowed
                            const updatedProps = context.iModel.elements.getElementProps(elementId);
                            updatedProps.newProp = "This should fail";
                            context.iModel.elements.updateElement(updatedProps); // Should throw here
                            return { transformStrategy: DataTransformationStrategy.None };
                        }
                    },
                });
                assert.fail("Should have thrown ChannelConstraintViolation");
            }
            catch (err) {
                // Verify it's a channel constraint error
                assert.include(err.message.toLowerCase(), "error updating element [channel shared is not allowed]", "Error should mention channel constraint");
            }
            assert.equal(imodel.getSchemaProps("TestSchema").version, "01.00.00");
        });
        it("should throw ChannelConstraintViolation when modifying element from disabled channel post import", async () => {
            const model = imodel.models.getModel(IModel.dictionaryId);
            const elementProps = {
                classFullName: `TestSchema:TestElement`,
                model: model.id,
                code: Code.createEmpty(),
                stringProp: "test element",
                intProp: 100,
            };
            const elementId = imodel.elements.insertElement(elementProps);
            imodel.saveChanges("Create test element");
            // Now REMOVE the shared channel permission
            imodel.channels.removeAllowedChannel("shared");
            // Try to import schema and modify element - should fail
            try {
                await imodel.importSchemaStrings([testSchemaV101()], {
                    schemaImportCallbacks: {
                        preSchemaImportCallback: async () => ({ transformStrategy: DataTransformationStrategy.None }),
                        postSchemaImportCallback: async (context) => {
                            // This should throw because shared channel is not allowed
                            const updatedProps = context.iModel.elements.getElementProps(elementId);
                            updatedProps.newProp = "This should fail";
                            context.iModel.elements.updateElement(updatedProps); // Should throw here
                        },
                    },
                });
                assert.fail("Should have thrown ChannelConstraintViolation");
            }
            catch (err) {
                // Verify it's a channel constraint error
                assert.include(err.message.toLowerCase(), "error updating element [channel shared is not allowed]", "Error should mention channel constraint");
            }
            // Verify element was NOT modified (changes were abandoned)
            imodel.channels.addAllowedChannel("shared"); // Re-enable to read
            const finalProps = imodel.elements.getElementProps(elementId);
            assert.isUndefined(finalProps.newProp, "Element should not have been modified");
        });
        it("should handle channel permission check in snapshot strategy", async () => {
            // Enable channel and create element
            imodel.channels.addAllowedChannel("shared");
            await imodel.importSchemaStrings([testSchemaV100()]);
            const model = imodel.models.getModel(IModel.dictionaryId);
            const elementProps = {
                classFullName: `TestSchema:TestElement`,
                model: model.id,
                code: Code.createEmpty(),
                stringProp: "snapshot test",
                intProp: 123,
            };
            const elementId = imodel.elements.insertElement(elementProps);
            imodel.saveChanges("Create test element");
            // Disable channel before schema import with snapshot
            imodel.channels.removeAllowedChannel("shared");
            try {
                await imodel.importSchemaStrings([testSchemaV101()], {
                    schemaImportCallbacks: {
                        preSchemaImportCallback: async () => ({ transformStrategy: DataTransformationStrategy.Snapshot }),
                        postSchemaImportCallback: async (context) => {
                            // Can read from snapshot (it's read-only, no channel check)
                            const snapshotProps = context.resources.snapshot.elements.getElementProps(elementId);
                            assert.equal(snapshotProps.stringProp, "snapshot test");
                            // But can't modify in main iModel without channel permission
                            const updatedProps = context.iModel.elements.getElementProps(elementId);
                            updatedProps.newProp = "This should fail";
                            context.iModel.elements.updateElement(updatedProps); // Should throw
                        },
                    },
                });
                assert.fail("Should have thrown ChannelConstraintViolation");
            }
            catch (err) {
                assert.include(err.message.toLowerCase(), "channel", "Error should mention channel constraint");
            }
        });
    });
    describe("User Data Propagation", () => {
        it("should pass user data to all callbacks", async () => {
            const userData = {
                processId: "test-process-123",
                cachedNumber: 1234,
                metadata: { version: "1.0.0" },
            };
            const receivedData = {};
            await imodel.importSchemaStrings([testSchemaV100()], {
                data: userData,
                channelUpgrade: {
                    channelKey: "shared",
                    fromVersion: "1.0.0",
                    toVersion: "1.1.0",
                    callback: async (context) => {
                        receivedData.channel = context.data;
                    },
                },
                schemaImportCallbacks: {
                    preSchemaImportCallback: async (context) => {
                        receivedData.pre = context.data;
                        return { transformStrategy: DataTransformationStrategy.None };
                    },
                    postSchemaImportCallback: async (context) => {
                        receivedData.post = context.data;
                    },
                },
            });
            // Verify all callbacks received the user data passed
            assert.deepEqual(receivedData.channel, userData);
            assert.deepEqual(receivedData.pre, userData);
            assert.deepEqual(receivedData.post, userData);
        });
        it("should handle more complicated user data structures", async () => {
            const userData = {
                elementMap: new Map([["0x1", "Element1"], ["0x2", "Element2"]]),
                counters: { inserted: 0, updated: 0 },
                handlers: [
                    { name: "Handler1", enabled: true },
                    { name: "Handler2", enabled: false },
                ],
            };
            let receivedData;
            await imodel.importSchemaStrings([testSchemaV100()], {
                schemaImportCallbacks: {
                    preSchemaImportCallback: async () => ({
                        transformStrategy: DataTransformationStrategy.None,
                    }),
                    postSchemaImportCallback: async (context) => {
                        receivedData = context.data;
                    },
                },
                data: userData,
            });
            assert.isDefined(receivedData);
            assert.equal(receivedData.elementMap.size, 2);
            assert.equal(receivedData.elementMap.get("0x1"), "Element1");
            assert.deepEqual(receivedData.counters, { inserted: 0, updated: 0 });
            assert.equal(receivedData.handlers.length, 2);
        });
        it("should work with undefined user data", async () => {
            await imodel.importSchemaStrings([testSchemaV100()], {
                schemaImportCallbacks: {
                    preSchemaImportCallback: async (context) => {
                        assert.isUndefined(context.data);
                        return { transformStrategy: DataTransformationStrategy.None };
                    },
                    postSchemaImportCallback: async (context) => {
                        assert.isUndefined(context.data);
                    },
                },
            });
        });
        it("should allow user data mutation across callbacks", async () => {
            let userData = {
                step: 0,
                log: [],
            };
            // All 3 callbacks called
            await imodel.importSchemaStrings([testSchemaV100()], {
                data: userData,
                channelUpgrade: {
                    channelKey: "shared",
                    fromVersion: "1.0.0",
                    toVersion: "1.1.0",
                    callback: async (context) => {
                        context.data.step += 1;
                        context.data.log.push("channel-upgrade");
                    },
                },
                schemaImportCallbacks: {
                    preSchemaImportCallback: async (context) => {
                        assert.equal(context.data.step, 1);
                        assert.deepEqual(context.data.log, ["channel-upgrade"]);
                        context.data.step += 1;
                        context.data.log.push("pre-import");
                        return { transformStrategy: DataTransformationStrategy.None };
                    },
                    postSchemaImportCallback: async (context) => {
                        assert.equal(context.data.step, 2);
                        assert.deepEqual(context.data.log, ["channel-upgrade", "pre-import"]);
                        context.data.step += 1;
                        context.data.log.push("post-import");
                    },
                },
            });
            // Verify final state
            assert.equal(userData.step, 3); // All three callbacks executed
            assert.deepEqual(userData.log, ["channel-upgrade", "pre-import", "post-import"]);
            userData = {
                step: 0,
                log: [],
            };
            await imodel.importSchemaStrings([testSchemaV100()], {
                data: userData,
                schemaImportCallbacks: {
                    postSchemaImportCallback: async (context) => {
                        context.data.step += 1;
                        context.data.log.push("post-import");
                    },
                },
            });
            // Verify final state
            assert.equal(userData.step, 1); // Only post-import was called
            assert.deepEqual(userData.log, ["post-import"]);
        });
    });
    describe("Channel Reorganization Before Schema Import", () => {
        let channelRootId;
        /**
         * Helper functions to get and set the version from a ChannelRootAspect
         */
        function getChannelVersion(iModel) {
            const aspects = iModel.elements.getAspects(channelRootId, "BisCore:ChannelRootAspect");
            if (aspects.length === 0)
                return undefined;
            return aspects[0].asAny.version;
        }
        function setChannelVersion(iModel, version) {
            const aspects = iModel.elements.getAspects(channelRootId, "BisCore:ChannelRootAspect");
            assert.equal(aspects.length, 1, "Should have exactly one ChannelRootAspect");
            const aspect = aspects[0];
            aspect.asAny.version = version;
            iModel.elements.updateAspect(aspect.toJSON());
        }
        // Code that simulates a channel upgrade.
        // This simulates elements being moved to different models as per the new channel organization.
        const channelUpgradeCallback = async (context) => {
            context.data.elementIds.forEach((id) => {
                const elementProps = context.iModel.elements.getElementProps(id);
                if (elementProps.stringProp === "Material1") {
                    elementProps.modelName = "MaterialModel";
                }
                else if (elementProps.stringProp === "LineStyle1") {
                    elementProps.modelName = "StyleModel";
                }
                else if (elementProps.stringProp === "Category1") {
                    elementProps.modelName = "StyleModel";
                }
                context.iModel.elements.updateElement(elementProps);
            });
            setChannelVersion(context.iModel, "1.0.1");
            assert.equal(getChannelVersion(context.iModel), "1.0.1");
            context.iModel.saveChanges();
        };
        it("need for channel reorganization before schema import", async () => {
            // Initial setup: Import v1.0.0 schema
            await imodel.importSchemaStrings([testSchemaV100()]);
            assert.equal(imodel.getSchemaProps("TestSchema").version, "01.00.00");
            const channelKey = "TestChannel";
            imodel.channels.addAllowedChannel(channelKey);
            // Create a channel
            channelRootId = imodel.channels.insertChannelSubject({
                subjectName: "Test Channel",
                channelKey,
            });
            assert.isUndefined(getChannelVersion(imodel), "Version should be undefined initially");
            // Set version to 1.0.0
            setChannelVersion(imodel, "1.0.0");
            // Enable shared channel and create elements
            imodel.channels.addAllowedChannel("shared");
            const rootModel = imodel.models.getModel(IModel.dictionaryId);
            assert.equal(getChannelVersion(imodel), "1.0.0");
            // Create test elements in the "old" channel structure (all in root)
            const elementIds = [];
            for (const [index, name] of ["Material1", "LineStyle1", "Category1"].entries()) {
                const elementProps = {
                    classFullName: `TestSchema:TestElement`,
                    model: rootModel.id,
                    code: Code.createEmpty(),
                    stringProp: name,
                    intProp: 100 + index * 100,
                    modelName: "DefinitionModel"
                };
                elementIds.push(imodel.elements.insertElement(elementProps));
            }
            imodel.saveChanges("Create elements in a single model under the root channel structure");
            // Scenario: Let's assume schema has evolved and v1.0.1 expects elements to be in their dedicated models (i.e. it relies on channel v1.0.1)
            // The post schema upgrade code will look for elements in their own models.
            // If the channel is still in version 1.0.0, the callback will obviously fail.
            await imodel.importSchemaStrings([testSchemaV101()], {
                data: { elementIds },
                channelUpgrade: {
                    channelKey,
                    fromVersion: "1.0.0",
                    toVersion: "1.0.1",
                    callback: channelUpgradeCallback,
                },
                schemaImportCallbacks: {
                    preSchemaImportCallback: async () => {
                        return {
                            transformStrategy: DataTransformationStrategy.InMemory,
                            cachedData: {
                                elementIds,
                            },
                        };
                    },
                    postSchemaImportCallback: async (context) => {
                        // Now schema v1.0.1 expects elements to be in their dedicated models.
                        const elementIdList = context.resources.cachedData.elementIds;
                        assert.equal(imodel.getSchemaProps("TestSchema").version, "01.00.01");
                        assert.equal(getChannelVersion(context.iModel), "1.0.1");
                        elementIdList.forEach((elementId) => {
                            const elementProps = context.iModel.elements.getElementProps(elementId);
                            assert.notEqual(elementProps.modelName, "DefinitionModel", "Element should have been moved to new model as part of the channel upgrade");
                        });
                    },
                },
            });
        });
    });
    describe("Locking behavior", () => {
        let iModelId;
        const channelKey = "TestChannel";
        before(async () => {
            HubMock.startup("SchemaImportCallbackLockingTest", KnownTestLocations.outputDir);
            iModelId = await HubMock.createNewIModel({ accessToken: "User1", iTwinId: HubMock.iTwinId, iModelName: "Test", description: "TestSubject" });
        });
        after(async () => {
            HubMock.shutdown();
        });
        async function setupBriefcase() {
            const briefcaseDb = await HubWrappers.downloadAndOpenBriefcase({ accessToken: "User1", iTwinId: HubMock.iTwinId, iModelId });
            briefcaseDb.channels.addAllowedChannel(ChannelControl.sharedChannelName);
            await briefcaseDb.importSchemaStrings([testSchemaV100()]);
            briefcaseDb.channels.addAllowedChannel(channelKey);
            if (briefcaseDb.channels.queryChannelRoot(channelKey) === undefined) {
                briefcaseDb.channels.insertChannelSubject({
                    subjectName: "Test Channel",
                    channelKey,
                });
            }
            const model = briefcaseDb.models.getModel(IModel.dictionaryId);
            const elementProps = {
                classFullName: `TestSchema:TestElement`,
                model: model.id,
                code: Code.createEmpty(),
                stringProp: "test",
                intProp: 100,
            };
            const elementId = briefcaseDb.elements.insertElement(elementProps);
            briefcaseDb.saveChanges("Create test element");
            await briefcaseDb.pushChanges({ description: "Create test element" });
            return [briefcaseDb, elementId];
        }
        it("channel upgrade callback fails without locks", async () => {
            const [briefcaseDb, elementId] = await setupBriefcase();
            // Try to modify without acquiring locks in the callback
            try {
                await briefcaseDb.importSchemaStrings([testSchemaV101()], {
                    channelUpgrade: {
                        channelKey,
                        fromVersion: "1.0.0",
                        toVersion: "1.0.1",
                        callback: async (context) => {
                            // Intentionally NOT acquiring locks
                            const props = context.iModel.elements.getElementProps(elementId);
                            props.stringProp = "should fail";
                            context.iModel.elements.updateElement(props);
                            context.iModel.saveChanges("Should fail");
                        },
                    },
                });
                assert.fail("Should have thrown error about missing locks");
            }
            catch (err) {
                assert.equal(err.iTwinErrorId.key.name, "Lock Not Held");
                briefcaseDb.abandonChanges();
            }
            // Try again with locks
            try {
                await briefcaseDb.importSchemaStrings([testSchemaV101()], {
                    channelUpgrade: {
                        channelKey,
                        fromVersion: "1.0.0",
                        toVersion: "1.0.1",
                        callback: async (context) => {
                            await context.iModel.locks.acquireLocks({ exclusive: elementId });
                            const props = context.iModel.elements.getElementProps(elementId);
                            props.stringProp = "updated with locks";
                            context.iModel.elements.updateElement(props);
                            context.iModel.saveChanges("Updated with locks");
                        },
                    },
                });
            }
            catch (err) {
                assert.fail(`Should not have thrown error when locks are acquired: ${err.message}`);
            }
            await HubWrappers.closeAndDeleteBriefcaseDb("User1", briefcaseDb);
        });
        it("preSchemaImportCallback fails without locks", async () => {
            const [briefcaseDb, elementId] = await setupBriefcase();
            // Try to modify without acquiring locks in the callback
            try {
                await briefcaseDb.importSchemaStrings([testSchemaV101()], {
                    schemaImportCallbacks: {
                        preSchemaImportCallback: async (context) => {
                            // Intentionally NOT acquiring locks
                            const props = context.iModel.elements.getElementProps(elementId);
                            props.stringProp = "should fail";
                            context.iModel.elements.updateElement(props);
                            return { transformStrategy: DataTransformationStrategy.None };
                        },
                    },
                });
                assert.fail("Should have thrown error about missing locks");
            }
            catch (err) {
                assert.equal(err.name, "Lock Not Held");
                briefcaseDb.abandonChanges();
            }
            // Try again with locks
            try {
                await briefcaseDb.importSchemaStrings([testSchemaV101()], {
                    schemaImportCallbacks: {
                        preSchemaImportCallback: async (context) => {
                            await context.iModel.locks.acquireLocks({ exclusive: elementId });
                            const props = context.iModel.elements.getElementProps(elementId);
                            props.stringProp = "should fail";
                            context.iModel.elements.updateElement(props);
                            context.iModel.saveChanges("Should fail");
                            return { transformStrategy: DataTransformationStrategy.None };
                        },
                    },
                });
            }
            catch (err) {
                assert.fail(`Should not have thrown error when locks are acquired: ${err.message}`);
            }
            await HubWrappers.closeAndDeleteBriefcaseDb("User1", briefcaseDb);
        });
        it("lock acquisition is not required during postSchemaImportCallback", async () => {
            const [briefcaseDb, elementId] = await setupBriefcase();
            let postImportCalled = false;
            await briefcaseDb.importSchemaStrings([testSchemaV101()], {
                schemaImportCallbacks: {
                    postSchemaImportCallback: async (context) => {
                        postImportCalled = true;
                        // Schema lock is already held at this point
                        const props = context.iModel.elements.getElementProps(elementId);
                        props.newProp = "added in postImport with schema lock held";
                        context.iModel.elements.updateElement(props);
                    },
                },
            });
            assert.isTrue(postImportCalled, "Post-import callback should have been called");
            const finalProps = briefcaseDb.elements.getElementProps(elementId);
            assert.equal(finalProps.newProp, "added in postImport with schema lock held");
            await HubWrappers.closeAndDeleteBriefcaseDb("User1", briefcaseDb);
        });
    });
});
//# sourceMappingURL=SchemaImportCallbacks.test.js.map