/*---------------------------------------------------------------------------------------------
 * Copyright (c) Bentley Systems, Incorporated. All rights reserved.
 * See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import { Logger, OpenMode } from "@itwin/core-bentley";
import { expect } from "chai";
import * as sinon from "sinon";
import { EditTxnError } from "@itwin/core-common";
import { EditTxn, IModelJsFs, StandaloneDb } from "../../core-backend";
import { IModelTestUtils } from "../IModelTestUtils";
class TestEditTxn extends EditTxn {
    constructor(iModel) {
        super(iModel, "test");
    }
    writeFileProperty(name, value) {
        super.saveFileProperty({ name, namespace: "EditTxnTest" }, value);
    }
}
class AbandonOnCloseEditTxn extends TestEditTxn {
    onClose() {
        if (this.isActive)
            this.abandonChanges();
    }
}
function expectEditTxnError(fn, key) {
    expect(fn).to.throw().that.satisfies((error) => EditTxnError.isError(error, key));
}
function legacyWriteFileProperty(iModel, name, value) {
    // eslint-disable-next-line @typescript-eslint/no-deprecated
    iModel.saveFileProperty({ name, namespace: "EditTxnTest" }, value);
}
describe("EditTxn", () => {
    let iModel;
    let fileName;
    beforeEach(() => {
        EditTxn.implicitWriteEnforcement = "allow";
        fileName = IModelTestUtils.prepareOutputFile("EditTxn", "EditTxn.bim");
        iModel = StandaloneDb.createEmpty(fileName, {
            rootSubject: { name: "EditTxn" },
            enableTransactions: true,
        });
    });
    afterEach(() => {
        EditTxn.implicitWriteEnforcement = "allow";
        sinon.restore();
        if (iModel.isOpen)
            iModel.close();
        IModelJsFs.removeSync(fileName);
    });
    it("allows implicit writes before and after explicit txn scopes", () => {
        // Implicit IModelDb mutators should work before any explicit EditTxn starts.
        legacyWriteFileProperty(iModel, "legacy-before-start", "value");
        expect(iModel.queryFilePropertyString({ name: "legacy-before-start", namespace: "EditTxnTest" })).to.equal("value");
        // eslint-disable-next-line @typescript-eslint/no-deprecated
        iModel.abandonChanges();
        const txn = new TestEditTxn(iModel);
        txn.start();
        expect(txn.isActive).to.be.true;
        txn.writeFileProperty("cancelled", "value");
        txn.end("abandon");
        // After canceling, implicit writes should still work.
        expect(txn.isActive).to.be.false;
        legacyWriteFileProperty(iModel, "legacy-after-cancel", "value");
        expect(iModel.queryFilePropertyString({ name: "legacy-after-cancel", namespace: "EditTxnTest" })).to.equal("value");
        // eslint-disable-next-line @typescript-eslint/no-deprecated
        iModel.abandonChanges();
        expect(iModel.queryFilePropertyString({ name: "cancelled", namespace: "EditTxnTest" })).to.be.undefined;
        txn.start();
        txn.writeFileProperty("saved", "value");
        txn.end();
        // Committing also leaves the iModel ready for later implicit writes.
        expect(txn.isActive).to.be.false;
        expect(iModel.queryFilePropertyString({ name: "saved", namespace: "EditTxnTest" })).to.equal("value");
        legacyWriteFileProperty(iModel, "legacy-after-end", "value");
        expect(iModel.queryFilePropertyString({ name: "legacy-after-end", namespace: "EditTxnTest" })).to.equal("value");
    });
    it("enforces identical explicit transaction behavior across allow, log, and throw settings", () => {
        const settings = ["allow", "log", "throw"];
        for (const setting of settings) {
            EditTxn.implicitWriteEnforcement = setting;
            const txn = new TestEditTxn(iModel);
            const inactiveName = `inactive-${setting}`;
            const savedName = `saved-${setting}`;
            const inactiveAgainName = `inactive-again-${setting}`;
            // Inactive explicit txn writes must always fail, regardless of enforcement.
            expectEditTxnError(() => txn.writeFileProperty(inactiveName, "value"), "not-active");
            expectEditTxnError(() => txn.saveChanges(`save ${inactiveName}`), "not-active");
            expect(iModel.queryFilePropertyString({ name: inactiveName, namespace: "EditTxnTest" })).to.be.undefined;
            expectEditTxnError(() => txn.end("abandon"), "not-active");
            txn.start();
            txn.writeFileProperty(savedName, "value");
            txn.saveChanges(`save ${savedName}`);
            expect(txn.isActive).to.be.true;
            txn.end();
            expect(iModel.queryFilePropertyString({ name: savedName, namespace: "EditTxnTest" })).to.equal("value");
            expectEditTxnError(() => txn.writeFileProperty(inactiveAgainName, "value"), "not-active");
            expectEditTxnError(() => txn.saveChanges(`save ${inactiveAgainName}`), "not-active");
            expect(iModel.queryFilePropertyString({ name: inactiveAgainName, namespace: "EditTxnTest" })).to.be.undefined;
            expectEditTxnError(() => txn.end("abandon"), "not-active");
        }
    });
    it("logs implicit writes in log mode but still rejects inactive explicit writes", () => {
        EditTxn.implicitWriteEnforcement = "log";
        const logError = sinon.spy(Logger, "logError");
        const txn = new TestEditTxn(iModel);
        legacyWriteFileProperty(iModel, "implicit-log", "value");
        expect(iModel.queryFilePropertyString({ name: "implicit-log", namespace: "EditTxnTest" })).to.equal("value");
        expect(logError.called).to.be.true;
        expect(logError.firstCall.args[0]).to.equal("core-backend.IModelDb");
        expect(EditTxnError.isError(logError.firstCall.args[1], "implicit-txn-write-disallowed")).to.be.true;
        const callCount = logError.callCount;
        expectEditTxnError(() => txn.writeFileProperty("inactive-log", "value"), "not-active");
        expectEditTxnError(() => txn.saveChanges("save inactive log"), "not-active");
        expect(logError.callCount).to.equal(callCount);
        expectEditTxnError(() => txn.end("abandon"), "not-active");
    });
    it("rejects implicit writes in throw mode and still allows active explicit writes", () => {
        EditTxn.implicitWriteEnforcement = "throw";
        expectEditTxnError(() => legacyWriteFileProperty(iModel, "implicit-throw", "value"), "implicit-txn-write-disallowed");
        expect(iModel.queryFilePropertyString({ name: "implicit-throw", namespace: "EditTxnTest" })).to.be.undefined;
        const txn = new TestEditTxn(iModel);
        txn.start();
        txn.writeFileProperty("explicit-enforce", "value");
        txn.end();
        expect(iModel.queryFilePropertyString({ name: "explicit-enforce", namespace: "EditTxnTest" })).to.equal("value");
    });
    it("throws when started with unsaved changes", () => {
        // Unsaved changes made through the implicit surface block a new explicit EditTxn.
        legacyWriteFileProperty(iModel, "unsaved", "value");
        const txn = new TestEditTxn(iModel);
        expectEditTxnError(() => txn.start(), "unsaved-changes");
        // eslint-disable-next-line @typescript-eslint/no-deprecated
        iModel.abandonChanges();
        legacyWriteFileProperty(iModel, "legacy-after-abandon", "value");
        expect(iModel.queryFilePropertyString({ name: "legacy-after-abandon", namespace: "EditTxnTest" })).to.equal("value");
    });
    it("rejects starting an explicit txn while another explicit txn is active", () => {
        const first = new TestEditTxn(iModel);
        const second = new TestEditTxn(iModel);
        first.start();
        expect(first.isActive).to.be.true;
        // EditTxn.start() now requires the implicit txn to be active, so a second explicit txn cannot start yet.
        expectEditTxnError(() => second.start(), "already-active");
        expect(first.isActive).to.be.true;
        expect(second.isActive).to.be.false;
        first.end("abandon");
        expect(first.isActive).to.be.false;
    });
    it("allows deprecated mutating APIs while an explicit txn is active when enforcement is none", () => {
        const txn = new TestEditTxn(iModel);
        txn.start();
        // Deprecated IModelDb mutators route through the implicit txn and are allowed in compatibility mode.
        // eslint-disable-next-line @typescript-eslint/no-deprecated
        iModel.saveFileProperty({ name: "legacy", namespace: "EditTxnTest" }, "value");
        // eslint-disable-next-line @typescript-eslint/no-deprecated
        iModel.saveChanges("legacy save");
        expect(iModel.queryFilePropertyString({ name: "legacy", namespace: "EditTxnTest" })).to.equal("value");
        txn.writeFileProperty("explicit", "value");
        txn.saveChanges("explicit save");
        expect(iModel.queryFilePropertyString({ name: "explicit", namespace: "EditTxnTest" })).to.equal("value");
        txn.end("abandon");
        expect(txn.isActive).to.be.false;
    });
    it("allows starting a new explicit txn when only pending changes exist", () => {
        const first = new TestEditTxn(iModel);
        const second = new TestEditTxn(iModel);
        first.start();
        first.writeFileProperty("pending", "value");
        first.saveChanges("save pending");
        // Saved-but-unpushed changes are allowed; only unsaved local changes block start().
        expect(iModel.txns.hasPendingTxns).to.be.true;
        expect(iModel.txns.hasUnsavedChanges).to.be.false;
        first.end("abandon");
        expect(() => second.start()).to.not.throw();
        expect(first.isActive).to.be.false;
        expect(second.isActive).to.be.true;
        second.end("abandon");
        expect(second.isActive).to.be.false;
    });
    it("uses the default description unless save is given an override", () => {
        const txn = new EditTxn(iModel, "default description");
        txn.start();
        txn.saveFileProperty({ name: "default-description", namespace: "EditTxnTest" }, "value");
        txn.saveChanges();
        expect(iModel.txns.getLastSavedTxnProps()?.props.description).to.equal("default description");
        txn.saveFileProperty({ name: "override-description", namespace: "EditTxnTest" }, "value");
        txn.end("save", "override description");
        expect(iModel.txns.getLastSavedTxnProps()?.props.description).to.equal("override description");
    });
    it("reverseTxns abandons unsaved changes from the active explicit txn", () => {
        const txn = new TestEditTxn(iModel);
        txn.start();
        txn.writeFileProperty("reverse", "value");
        // Reversing txns abandons the active explicit txn's local changes, but the explicit txn remains active.
        iModel.txns.reverseTxns(1);
        expect(txn.isActive).to.be.true;
        expect(iModel.txns.hasUnsavedChanges).to.be.false;
        expect(iModel.queryFilePropertyString({ name: "reverse", namespace: "EditTxnTest" })).to.be.undefined;
        txn.end("abandon");
        expect(txn.isActive).to.be.false;
    });
    it("saves unsaved changes from the active explicit txn when the iModel closes", () => {
        const txn = new TestEditTxn(iModel);
        txn.start();
        txn.writeFileProperty("unsaved-on-close", "value");
        iModel.close();
        iModel = StandaloneDb.openFile(fileName, OpenMode.Readonly);
        expect(iModel.queryFilePropertyString({ name: "unsaved-on-close", namespace: "EditTxnTest" })).to.equal("value");
    });
    it("allows subclasses to abandon unsaved changes when the iModel closes", () => {
        const txn = new AbandonOnCloseEditTxn(iModel);
        txn.start();
        txn.writeFileProperty("saved-on-close", "value");
        iModel.close();
        iModel = StandaloneDb.openFile(fileName, OpenMode.Readonly);
        expect(iModel.queryFilePropertyString({ name: "saved-on-close", namespace: "EditTxnTest" })).to.be.undefined;
    });
});
//# sourceMappingURL=EditTxn.test.js.map