"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.HubMock = void 0;
const path_1 = require("path");
const core_bentley_1 = require("@itwin/core-bentley");
const CheckpointManager_1 = require("../CheckpointManager");
const IModelHost_1 = require("../IModelHost");
const IModelJsFs_1 = require("../IModelJsFs");
const LocalHub_1 = require("../LocalHub");
const Symbols_1 = require("./Symbols");
const BriefcaseManager_1 = require("../BriefcaseManager");
const path = require("path");
function wasStarted(val) {
    if (undefined === val)
        throw new Error("Call HubMock.startup first");
}
function doDownload(args) {
    HubMock.findLocalHub(args.iModelId).downloadCheckpoint(args);
}
const mockCheckpoint = {
    mockAttach: (checkpoint) => {
        const targetFile = path.join(BriefcaseManager_1.BriefcaseManager.getBriefcaseBasePath(checkpoint.iModelId), `${checkpoint.changeset.index}.bim`);
        doDownload({ ...checkpoint, targetFile });
        return targetFile;
    },
    mockDownload: (request) => {
        doDownload({ ...request.checkpoint, targetFile: request.localFile });
    }
};
/**
 * Mocks iModelHub for testing creating Briefcases, downloading checkpoints, and simulating multiple users pushing and pulling changesets, etc.
 *
 * Generally, tests for apis that *create or modify* iModels can and should be mocked. Otherwise they:
 * - create tremendous load on the test servers when they run on programmer's desktops and in CI jobs
 * - waste network and data center resources (i.e. $$$s),
 * - interfere with other tests running on the same or other systems, and
 * - (far worse) are the source of test flakiness outside of the api being tested.
 *
 * This class can be used to create tests that do not require authentication, are synchronous,
 * are guaranteed to be self-contained (i.e. do not interfere with other tests running at the same time or later), and do not fail for reasons outside
 * of the control of the test itself. As a bonus, in addition to making tests more reliable, mocking IModelHub generally makes tests run *much* faster.
 *
 * On the other hand, tests that expect to find an existing iModels, checkpoints, changesets, etc. in IModelHub cannot be mocked. In that case, those tests
 * should be careful to NOT modify the data, since doing so causes interference with other tests running simultaneously. These tests should be limited to
 * low level testing of the core apis only.
 *
 * To initialize HubMock, call [[startup]] at the beginning of your test, usually in `describe.before`. Thereafter, all access to iModelHub for an iModel will be
 * directed to a [[LocalHub]] - your test code does not change. After the test(s) complete, call [[shutdown]] (usually in `describe.after`) to stop mocking IModelHub and clean
 * up any resources used by the test(s). If you want to mock a single test, call [[startup]] as the first line and [[shutdown]] as the last. If you wish to run the
 * test against a "real" IModelHub, you can simply comment off the call [[startup]], though in that case you should make sure the name of your
 * iModel is unique so your test won't collide with other tests (iModel name uniqueness is not necessary for mocked tests.)
 *
 * Mocked tests must always start by creating a new iModel via [[IModelHost[_hubAccess].createNewIModel]] with a `version0` iModel.
 * They use mock (aka "bogus") credentials for `AccessTokens`, which is fine since [[HubMock]] never accesses resources outside the current
 * computer.
 *
 * @note Only one HubMock at a time, *running in a single process*, may be active. The comments above about multiple simultaneous tests refer to tests
 * running on different computers, or on a single computer in multiple processes. All of those scenarios are problematic without mocking.
 *
 * @internal
 */
class HubMock {
    static mockRoot;
    static hubs = new Map();
    static _saveHubAccess;
    static _iTwinId;
    /** Determine whether a test us currently being run under HubMock */
    static get isValid() { return undefined !== this.mockRoot; }
    static get iTwinId() {
        wasStarted(this._iTwinId);
        return this._iTwinId;
    }
    /**
     * Begin mocking IModelHub access. After this call, all access to IModelHub will be directed to a [[LocalHub]].
     * @param mockName a unique name (e.g. "MyTest") for this HubMock to disambiguate tests when more than one is simultaneously active.
     * It is used to create a private directory used by the HubMock for a test. That directory is removed when [[shutdown]] is called.
     */
    static startup(mockName, outputDir) {
        if (this.isValid)
            throw new Error("Either a previous test did not call HubMock.shutdown() properly, or more than one test is simultaneously attempting to use HubMock, which is not allowed");
        this.hubs.clear();
        this.mockRoot = (0, path_1.join)(outputDir, "HubMock", mockName);
        IModelJsFs_1.IModelJsFs.recursiveMkDirSync(this.mockRoot);
        IModelJsFs_1.IModelJsFs.purgeDirSync(this.mockRoot);
        this._saveHubAccess = IModelHost_1.IModelHost[Symbols_1._getHubAccess]();
        IModelHost_1.IModelHost[Symbols_1._setHubAccess](this);
        HubMock._iTwinId = core_bentley_1.Guid.createValue(); // all iModels for this test get the same "iTwinId"
        CheckpointManager_1.V2CheckpointManager[Symbols_1._mockCheckpoint] = mockCheckpoint;
    }
    /** Stop a HubMock that was previously started with [[startup]]
     * @note this function throws an exception if any of the iModels used during the tests are left open.
     */
    static shutdown() {
        if (this.mockRoot === undefined)
            return;
        CheckpointManager_1.V2CheckpointManager[Symbols_1._mockCheckpoint] = undefined;
        HubMock._iTwinId = undefined;
        for (const hub of this.hubs)
            hub[1].cleanup();
        this.hubs.clear();
        IModelJsFs_1.IModelJsFs.purgeDirSync(this.mockRoot);
        IModelJsFs_1.IModelJsFs.removeSync(this.mockRoot);
        IModelHost_1.IModelHost[Symbols_1._setHubAccess](this._saveHubAccess);
        this.mockRoot = undefined;
    }
    static findLocalHub(iModelId) {
        const hub = this.hubs.get(iModelId);
        if (!hub)
            throw new Error(`local hub for iModel ${iModelId} not created`);
        return hub;
    }
    /** create a [[LocalHub]] for an iModel.  */
    static async createNewIModel(arg) {
        wasStarted(this.mockRoot);
        const props = { ...arg, iModelId: core_bentley_1.Guid.createValue() };
        const mock = new LocalHub_1.LocalHub((0, path_1.join)(this.mockRoot, props.iModelId), props);
        this.hubs.set(props.iModelId, mock);
        return props.iModelId;
    }
    /** remove the [[LocalHub]] for an iModel */
    static destroy(iModelId) {
        this.findLocalHub(iModelId).cleanup();
        this.hubs.delete(iModelId);
    }
    /** All methods below are mocks of the [[BackendHubAccess]] interface */
    static async getChangesetFromNamedVersion(arg) {
        return this.findLocalHub(arg.iModelId).findNamedVersion(arg.versionName);
    }
    static changesetIndexFromArg(arg) {
        return (undefined !== arg.changeset.index) ? arg.changeset.index : this.findLocalHub(arg.iModelId).getChangesetIndex(arg.changeset.id);
    }
    static async getChangesetFromVersion(arg) {
        const hub = this.findLocalHub(arg.iModelId);
        const version = arg.version;
        if (version.isFirst)
            return hub.getChangesetByIndex(0);
        const asOf = version.getAsOfChangeSet();
        if (asOf)
            return hub.getChangesetById(asOf);
        const versionName = version.getName();
        if (versionName)
            return hub.findNamedVersion(versionName);
        return hub.getLatestChangeset();
    }
    static async getLatestChangeset(arg) {
        return this.findLocalHub(arg.iModelId).getLatestChangeset();
    }
    static async getAccessToken(arg) {
        return arg.accessToken ?? await IModelHost_1.IModelHost.getAccessToken();
    }
    static async getMyBriefcaseIds(arg) {
        const accessToken = await this.getAccessToken(arg);
        return this.findLocalHub(arg.iModelId).getBriefcaseIds(accessToken);
    }
    static async acquireNewBriefcaseId(arg) {
        const accessToken = await this.getAccessToken(arg);
        return this.findLocalHub(arg.iModelId).acquireNewBriefcaseId(accessToken, arg.briefcaseAlias);
    }
    /** Release a briefcaseId. After this call it is illegal to generate changesets for the released briefcaseId. */
    static async releaseBriefcase(arg) {
        return this.findLocalHub(arg.iModelId).releaseBriefcaseId(arg.briefcaseId);
    }
    static async downloadChangeset(arg) {
        const changesetProps = this.findLocalHub(arg.iModelId).downloadChangeset({ index: this.changesetIndexFromArg(arg), targetDir: arg.targetDir });
        if (arg.progressCallback) {
            const totalSize = IModelJsFs_1.IModelJsFs.lstatSync(changesetProps.pathname)?.size;
            if (totalSize)
                await HubMock.mockProgressReporting(arg.progressCallback, totalSize);
        }
        return changesetProps;
    }
    static async downloadChangesets(arg) {
        const changesetProps = this.findLocalHub(arg.iModelId).downloadChangesets({ range: arg.range, targetDir: arg.targetDir });
        if (arg.progressCallback) {
            const totalSize = changesetProps.reduce((sum, props) => sum + (IModelJsFs_1.IModelJsFs.lstatSync(props.pathname)?.size ?? 0), 0);
            await HubMock.mockProgressReporting(arg.progressCallback, totalSize);
        }
        return changesetProps;
    }
    static async queryChangeset(arg) {
        return this.findLocalHub(arg.iModelId).getChangesetByIndex(this.changesetIndexFromArg(arg));
    }
    static async queryChangesets(arg) {
        return this.findLocalHub(arg.iModelId).queryChangesets(arg.range);
    }
    static async pushChangeset(arg) {
        return this.findLocalHub(arg.iModelId).addChangeset(arg.changesetProps);
    }
    static async queryV2Checkpoint(arg) {
        return {
            accountName: "none",
            sasToken: "none",
            containerId: core_bentley_1.Guid.createValue(),
            dbName: `${arg.changeset.index ?? 0}.bim`,
            storageType: "mock",
            isMock: true,
            checkpoint: arg,
        };
    }
    static async releaseAllLocks(arg) {
        const hub = this.findLocalHub(arg.iModelId);
        hub.releaseAllLocks({ briefcaseId: arg.briefcaseId, changesetIndex: hub.getIndexFromChangeset(arg.changeset) });
    }
    static async queryAllLocks(_arg) {
        return [];
    }
    static async acquireLocks(arg, locks) {
        this.findLocalHub(arg.iModelId).acquireLocks(locks, arg);
    }
    static async queryIModelByName(arg) {
        for (const hub of this.hubs) {
            const localHub = hub[1];
            if (localHub.iTwinId === arg.iTwinId && localHub.iModelName === arg.iModelName)
                return localHub.iModelId;
        }
        return undefined;
    }
    static async deleteIModel(arg) {
        return this.destroy(arg.iModelId);
    }
    static async mockProgressReporting(progressCallback, totalSize) {
        await new Promise((resolve, reject) => {
            let rejected = false;
            const mockProgress = (index) => {
                const bytesDownloaded = Math.floor(totalSize * (index / 4));
                if (!rejected && progressCallback(bytesDownloaded, totalSize) === CheckpointManager_1.ProgressStatus.Abort) {
                    rejected = true;
                    reject(new Error("AbortError"));
                }
            };
            mockProgress(1);
            setTimeout(() => mockProgress(2), 50);
            setTimeout(() => mockProgress(3), 100);
            setTimeout(() => {
                mockProgress(4);
                resolve(undefined);
            }, 150);
        });
    }
}
exports.HubMock = HubMock;
//# sourceMappingURL=HubMock.js.map