/*---------------------------------------------------------------------------------------------
* 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 { BeDuration } from "@itwin/core-bentley";
import { Matrix4d } from "@itwin/core-geometry";
import { IModelNative } from "../../internal/NativePlatform";
import { StandaloneDb } from "../../IModelDb";
import { IModelTestUtils } from "../IModelTestUtils";
import { _nativeDb } from "../../internal/Symbols";
describe("DgnDbWorker", () => {
    let imodel;
    function openIModel() {
        const rootSubject = { name: "DgnDbWorker tests", description: "DgnDbWorker tests" };
        imodel = StandaloneDb.createEmpty(IModelTestUtils.prepareOutputFile("DgnDbWorker", "DgnDbWorker.bim"), { rootSubject });
    }
    before(() => {
        openIModel();
    });
    after(() => {
        imodel.close();
    });
    class Worker {
        promise;
        _worker;
        constructor() {
            this._worker = new IModelNative.platform.TestWorker(imodel[_nativeDb]);
        }
        queue() { this.promise = this._worker.queue(); }
        cancel() { this._worker.cancel(); }
        setReady() { this._worker.setReady(); }
        setThrow() { this._worker.setThrow(); }
        get isCanceled() { return this._worker.isCanceled(); }
        get wasExecuted() { return this._worker.wasExecuted(); }
        get state() { return this._worker.getState(); }
        get wasQueued() { return 0 /* IModelJsNative.TestWorkerState.NotQueued */ !== this.state; }
        get isQueued() { return 1 /* IModelJsNative.TestWorkerState.Queued */ === this.state; }
        get isRunning() { return 3 /* IModelJsNative.TestWorkerState.Running */ === this.state; }
        get isError() { return 5 /* IModelJsNative.TestWorkerState.Error */ === this.state; }
        get isOk() { return 4 /* IModelJsNative.TestWorkerState.Ok */ === this.state; }
        get isSkipped() { return 2 /* IModelJsNative.TestWorkerState.Skipped */ === this.state; }
        get isAborted() { return 6 /* IModelJsNative.TestWorkerState.Aborted */ === this.state; }
    }
    async function waitUntil(condition) {
        if (condition())
            return;
        await new Promise((resolve) => setTimeout(resolve, 100));
        return waitUntil(condition);
    }
    it("executes asynchronously", async () => {
        const worker = new Worker();
        await BeDuration.wait(1000);
        expect(worker.wasQueued).to.be.false;
        expect(worker.isCanceled).to.be.false;
        worker.queue();
        expect(worker.isQueued || worker.isRunning).to.be.true;
        worker.setReady();
        await worker.promise;
        expect(worker.wasExecuted).to.be.true;
        expect(worker.isCanceled).to.be.false;
        expect(worker.isOk).to.be.true;
    });
    it("executes a maximum of 4 simultaneously", async () => {
        // Create 4 workers. They should all start executing.
        const first = [new Worker(), new Worker(), new Worker(), new Worker()];
        for (const worker of first)
            worker.queue();
        await waitUntil(() => first.every((x) => x.isRunning));
        // Queue up 2 more workers. They should not start executing until at least one of the first 4 resolves.
        const next = [new Worker(), new Worker()];
        for (const worker of next)
            worker.queue();
        await BeDuration.wait(1000);
        expect(first.every((x) => x.isRunning)).to.be.true;
        expect(next.every((x) => x.isQueued)).to.be.true;
        // Let the first worker resolve.
        first[0].setReady();
        await first[0].promise;
        expect(first[0].isOk).to.be.true;
        // Exactly one of the 2 workers remaining in the queue should start executing.
        await waitUntil(() => next.some((x) => x.isRunning));
        await BeDuration.wait(1000);
        expect(next.every((x) => x.isRunning)).to.be.false;
        // Clear the queue.
        const workers = first.concat(next);
        for (const worker of workers)
            worker.setReady();
        await Promise.all(workers.map((x) => x.promise)); // eslint-disable-line @typescript-eslint/promise-function-async
        expect(workers.every((x) => x.isOk)).to.be.true;
    });
    it("skips execution if canceled before execution begins", async () => {
        const worker = new Worker();
        worker.cancel();
        expect(worker.isCanceled).to.be.true;
        worker.queue();
        await assert.isRejected(worker.promise, "canceled");
        expect(worker.isSkipped).to.be.true;
        expect(worker.wasExecuted).to.be.false;
    });
    it("executes if canceled after execution begins", async () => {
        const worker = new Worker();
        worker.queue();
        await waitUntil(() => worker.isRunning);
        worker.cancel();
        worker.setReady();
        await worker.promise;
        expect(worker.isCanceled).to.be.true;
        expect(worker.isSkipped).to.be.false;
        expect(worker.wasExecuted).to.be.true;
    });
    it("throws", async () => {
        const worker = new Worker();
        worker.setThrow();
        worker.queue();
        await assert.isRejected(worker.promise, "throw");
        expect(worker.isCanceled).to.be.false;
        expect(worker.isError).to.be.true;
    });
    it("cancels all workers before iModel is closed", async () => {
        const resolve = new Worker();
        resolve.setReady();
        const reject = new Worker();
        reject.setThrow();
        // These 6 workers will never resolve nor reject explicitly.
        const cancel = [new Worker(), new Worker(), new Worker(), new Worker(), new Worker(), new Worker()];
        // Queue up all the workers.
        const workers = cancel.concat([resolve, reject]);
        for (const worker of workers)
            worker.queue();
        // Closing the iModel cancels all extant workers.
        imodel.close();
        openIModel();
        await expect(Promise.all(workers.map(async (x) => x.promise))).rejectedWith("canceled");
        expect(cancel.every((x) => x.isCanceled)).to.be.true;
        expect(cancel.every((x) => x.isAborted || x.isSkipped)).to.be.true;
        expect(resolve.isOk || resolve.isSkipped || resolve.isAborted).to.be.true;
        expect(reject.isError || reject.isSkipped).to.be.true;
    });
    it("cancels a snap request", async () => {
        // Block the worker thread pool with 4 workers so that our snap cannot complete before they complete.
        const blockers = [new Worker(), new Worker(), new Worker(), new Worker()];
        blockers.forEach((w) => w.queue());
        const sessionId = "0x222";
        const snap = imodel.requestSnap(sessionId, {
            testPoint: { x: 1, y: 2, z: 3 },
            closePoint: { x: 1, y: 2, z: 3 },
            id: "0x111",
            worldToView: Matrix4d.createIdentity().toJSON(),
        });
        const toBeRejected = expect(snap).to.be.rejectedWith("aborted");
        imodel.cancelSnap(sessionId);
        // Clear the worker thread pool so the snap request (now canceled) can be processed.
        blockers.forEach((w) => w.setReady());
        await Promise.all(blockers.map(async (w) => w.promise));
        await toBeRejected;
    });
});
//# sourceMappingURL=DgnDbWorker.test.js.map