"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SingleUseTestCollection = void 0;
const tslib_1 = require("tslib");
const ide_core_common_1 = require("@opensumi/ide-core-common");
const testCollection_1 = require("@opensumi/ide-testing/lib/common/testCollection");
const testId_1 = require("@opensumi/ide-testing/lib/common/testId");
const Convert = tslib_1.__importStar(require("../converter"));
const testApi_1 = require("./testApi");
/**
 * Maintains tests created and registered for a single set of hierarchies
 * for a workspace or document.
 * @private
 */
class SingleUseTestCollection extends ide_core_common_1.Disposable {
    constructor(controllerId) {
        super();
        this.controllerId = controllerId;
        this.debounceSendDiff = this.registerDispose(new ide_core_common_1.RunOnceScheduler(() => this.flushDiff(), 200));
        this.diffOpEmitter = this.registerDispose(new ide_core_common_1.Emitter());
        this.root = new testApi_1.TestItemRootImpl(this.controllerId, this.controllerId);
        this.tree = new Map();
        this.tags = new Map();
        this.debug = (0, ide_core_common_1.getDebugLogger)();
        this.diff = [];
        /**
         * Fires when an operation happens that should result in a diff.
         */
        this.onDidGenerateDiff = this.diffOpEmitter.event;
        this.root.canResolveChildren = true;
        this.upsertItem(this.root, undefined);
    }
    /**
     * Handler used for expanding test items.
     */
    set resolveHandler(handler) {
        this._resolveHandler = handler;
        for (const test of this.tree.values()) {
            this.updateExpandability(test);
        }
    }
    /**
     * Gets a diff of all changes that have been made, and clears the diff queue.
     */
    collectDiff() {
        const diff = this.diff;
        this.diff = [];
        return diff;
    }
    /**
     * Pushes a new diff entry onto the collected diff list.
     */
    pushDiff(diff) {
        // Try to merge updates, since they're invoked per-property
        const last = this.diff[this.diff.length - 1];
        if (last && diff[0] === 1 /* TestDiffOpType.Update */) {
            if (last[0] === 1 /* TestDiffOpType.Update */ && last[1].extId === diff[1].extId) {
                (0, testCollection_1.applyTestItemUpdate)(last[1], diff[1]);
                return;
            }
            if (last[0] === 0 /* TestDiffOpType.Add */ && last[1].item.extId === diff[1].extId) {
                (0, testCollection_1.applyTestItemUpdate)(last[1], diff[1]);
                return;
            }
        }
        this.diff.push(diff);
        if (!this.debounceSendDiff.isScheduled()) {
            this.debounceSendDiff.schedule();
        }
    }
    /**
     * Expands the test and the given number of `levels` of children. If levels
     * is < 0, then all children will be expanded. If it's 0, then only this
     * item will be expanded.
     */
    expand(testId, levels) {
        var _a;
        const internal = this.tree.get(testId);
        if (!internal) {
            return;
        }
        if (internal.expandLevels === undefined || levels > internal.expandLevels) {
            internal.expandLevels = levels;
        }
        // try to avoid awaiting things if the provider returns synchronously in
        // order to keep everything in a single diff and DOM update.
        if (internal.expand === 1 /* TestItemExpandState.Expandable */) {
            const r = this.resolveChildren(internal);
            return !r.isOpen()
                ? r.wait().then(() => this.expandChildren(internal, levels - 1))
                : this.expandChildren(internal, levels - 1);
        }
        else if (internal.expand === 3 /* TestItemExpandState.Expanded */) {
            return ((_a = internal.resolveBarrier) === null || _a === void 0 ? void 0 : _a.isOpen()) === false
                ? internal.resolveBarrier.wait().then(() => this.expandChildren(internal, levels - 1))
                : this.expandChildren(internal, levels - 1);
        }
    }
    dispose() {
        for (const item of this.tree.values()) {
            (0, testApi_1.getPrivateApiFor)(item.actual).listener = undefined;
        }
        this.tree.clear();
        this.diff = [];
        super.dispose();
    }
    onTestItemEvent(internal, evt) {
        switch (evt.op) {
            case 2 /* ExtHostTestItemEventOp.Invalidated */:
                this.pushDiff([4 /* TestDiffOpType.Retire */, internal.fullId.toString()]);
                break;
            case 1 /* ExtHostTestItemEventOp.RemoveChild */:
                this.removeItem(testId_1.TestId.joinToString(internal.fullId, evt.id));
                break;
            case 0 /* ExtHostTestItemEventOp.Upsert */:
                this.upsertItem(evt.item, internal);
                break;
            case 4 /* ExtHostTestItemEventOp.Bulk */:
                for (const op of evt.ops) {
                    this.onTestItemEvent(internal, op);
                }
                break;
            case 3 /* ExtHostTestItemEventOp.SetProp */:
                {
                    const { key, value, previous } = evt;
                    const extId = internal.fullId.toString();
                    switch (key) {
                        case 'canResolveChildren':
                            this.updateExpandability(internal);
                            break;
                        case 'tags':
                            this.diffTagRefs(value, previous, extId);
                            break;
                        case 'range':
                            this.pushDiff([1 /* TestDiffOpType.Update */, { extId, item: { range: Convert.Range.from(value) } }]);
                            break;
                        case 'error':
                            this.pushDiff([
                                1 /* TestDiffOpType.Update */,
                                {
                                    extId,
                                    item: {
                                        error: Convert.MarkdownString.fromStrict(value) || null,
                                    },
                                },
                            ]);
                            break;
                        default:
                            this.pushDiff([1 /* TestDiffOpType.Update */, { extId, item: { [key]: value !== null && value !== void 0 ? value : null } }]);
                            break;
                    }
                }
                break;
            default:
                throw new Error('Unreachable');
        }
    }
    upsertItem(actual, parent) {
        if (!(actual instanceof testApi_1.TestItemImpl)) {
            throw new Error(`TestItems provided to the VS Code API must extend \`vscode.TestItem\`, but ${actual.id} did not`);
        }
        const fullId = testId_1.TestId.fromExtHostTestItem(actual, this.root.id, parent === null || parent === void 0 ? void 0 : parent.actual);
        // If this test item exists elsewhere in the tree already (exists at an
        // old ID with an existing parent), remove that old item.
        const privateApi = (0, testApi_1.getPrivateApiFor)(actual);
        if (privateApi.parent && privateApi.parent !== (parent === null || parent === void 0 ? void 0 : parent.actual)) {
            privateApi.parent.children.delete(actual.id);
        }
        let internal = this.tree.get(fullId.toString());
        // Case 1: a brand new item
        if (!internal) {
            internal = {
                fullId,
                actual,
                parent: parent ? fullId.parentId : null,
                expandLevels: (parent === null || parent === void 0 ? void 0 : parent.expandLevels /* intentionally undefined or 0 */) ? parent.expandLevels - 1 : undefined,
                expand: 0 /* TestItemExpandState.NotExpandable */, // updated by `connectItemAndChildren`
            };
            actual.tags.forEach(this.incrementTagRefs, this);
            this.tree.set(internal.fullId.toString(), internal);
            this.setItemParent(actual, parent);
            this.pushDiff([
                0 /* TestDiffOpType.Add */,
                {
                    parent: internal.parent && internal.parent.toString(),
                    controllerId: this.controllerId,
                    expand: internal.expand,
                    item: Convert.TestItem.from(actual),
                },
            ]);
            this.connectItemAndChildren(actual, internal, parent);
            return;
        }
        // Case 2: re-insertion of an existing item, no-op
        if (internal.actual === actual) {
            this.connectItem(actual, internal, parent); // re-connect in case the parent changed
            return; // no-op
        }
        // Case 3: upsert of an existing item by ID, with a new instance
        const oldChildren = internal.actual.children;
        const oldActual = internal.actual;
        const changedProps = (0, testApi_1.diffTestItems)(oldActual, actual);
        (0, testApi_1.getPrivateApiFor)(oldActual).listener = undefined;
        internal.actual = actual;
        internal.expand = 0 /* TestItemExpandState.NotExpandable */; // updated by `connectItemAndChildren`
        for (const [key, value] of changedProps) {
            this.onTestItemEvent(internal, {
                op: 3 /* ExtHostTestItemEventOp.SetProp */,
                key,
                value,
                previous: oldActual[key],
            });
        }
        this.connectItemAndChildren(actual, internal, parent);
        // Remove any orphaned children.
        for (const child of oldChildren) {
            if (!actual.children.get(child.id)) {
                this.removeItem(testId_1.TestId.joinToString(fullId, child.id));
            }
        }
    }
    diffTagRefs(newTags, oldTags, extId) {
        const toDelete = new Set(oldTags.map((t) => t.id));
        for (const tag of newTags) {
            if (!toDelete.delete(tag.id)) {
                this.incrementTagRefs(tag);
            }
        }
        this.pushDiff([
            1 /* TestDiffOpType.Update */,
            {
                extId,
                item: {
                    tags: newTags.map((v) => Convert.TestTag.namespace(this.controllerId, v.id)),
                },
            },
        ]);
        toDelete.forEach(this.decrementTagRefs, this);
    }
    incrementTagRefs(tag) {
        const existing = this.tags.get(tag.id);
        if (existing) {
            existing.refCount++;
        }
        else {
            this.tags.set(tag.id, { refCount: 1 });
            this.pushDiff([
                5 /* TestDiffOpType.AddTag */,
                {
                    id: Convert.TestTag.namespace(this.controllerId, tag.id),
                    ctrlLabel: this.root.label,
                },
            ]);
        }
    }
    decrementTagRefs(tagId) {
        const existing = this.tags.get(tagId);
        if (existing && !--existing.refCount) {
            this.tags.delete(tagId);
            this.pushDiff([6 /* TestDiffOpType.RemoveTag */, Convert.TestTag.namespace(this.controllerId, tagId)]);
        }
    }
    setItemParent(actual, parent) {
        (0, testApi_1.getPrivateApiFor)(actual).parent = parent && parent.actual !== this.root ? parent.actual : undefined;
    }
    connectItem(actual, internal, parent) {
        this.setItemParent(actual, parent);
        const api = (0, testApi_1.getPrivateApiFor)(actual);
        api.parent = parent === null || parent === void 0 ? void 0 : parent.actual;
        api.listener = (evt) => this.onTestItemEvent(internal, evt);
        this.updateExpandability(internal);
    }
    connectItemAndChildren(actual, internal, parent) {
        this.connectItem(actual, internal, parent);
        // Discover any existing children that might have already been added
        for (const child of actual.children) {
            this.upsertItem(child, internal);
        }
    }
    /**
     * Updates the `expand` state of the item. Should be called whenever the
     * resolved state of the item changes. Can automatically expand the item
     * if requested by a consumer.
     */
    updateExpandability(internal) {
        let newState;
        if (!this._resolveHandler) {
            newState = 0 /* TestItemExpandState.NotExpandable */;
        }
        else if (internal.resolveBarrier) {
            newState = internal.resolveBarrier.isOpen() ? 3 /* TestItemExpandState.Expanded */ : 2 /* TestItemExpandState.BusyExpanding */;
        }
        else {
            newState = internal.actual.canResolveChildren
                ? 1 /* TestItemExpandState.Expandable */
                : 0 /* TestItemExpandState.NotExpandable */;
        }
        if (newState === internal.expand) {
            return;
        }
        internal.expand = newState;
        this.pushDiff([1 /* TestDiffOpType.Update */, { extId: internal.fullId.toString(), expand: newState }]);
        if (newState === 1 /* TestItemExpandState.Expandable */ && internal.expandLevels !== undefined) {
            this.resolveChildren(internal);
        }
    }
    /**
     * Expands all children of the item, 'levels' deep. If levels is 0, only
     * the children will be expanded. If it's 1, the children and their children
     * will be expanded. If it's <0, it's a no-op.
     */
    expandChildren(internal, levels) {
        if (levels < 0) {
            return;
        }
        const expandRequests = [];
        for (const child of internal.actual.children) {
            const promise = this.expand(testId_1.TestId.joinToString(internal.fullId, child.id), levels);
            if ((0, ide_core_common_1.isThenable)(promise)) {
                expandRequests.push(promise);
            }
        }
        if (expandRequests.length) {
            return Promise.all(expandRequests).then(() => { });
        }
    }
    /**
     * Calls `discoverChildren` on the item, refreshing all its tests.
     */
    resolveChildren(internal) {
        if (internal.resolveBarrier) {
            return internal.resolveBarrier;
        }
        if (!this._resolveHandler) {
            const b = new ide_core_common_1.Barrier();
            b.open();
            return b;
        }
        internal.expand = 2 /* TestItemExpandState.BusyExpanding */;
        this.pushExpandStateUpdate(internal);
        const barrier = (internal.resolveBarrier = new ide_core_common_1.Barrier());
        const applyError = (err) => {
            this.debug.error(`Unhandled error in resolveHandler of test controller '${this.controllerId}'`);
            if (internal.actual !== this.root) {
                internal.actual.error = err.stack || err.message || String(err);
            }
        };
        // eslint-disable-next-line no-undef-init
        let r = undefined;
        try {
            r = this._resolveHandler(internal.actual === this.root ? undefined : internal.actual);
        }
        catch (err) {
            applyError(err);
        }
        if ((0, ide_core_common_1.isThenable)(r)) {
            r.catch(applyError).then(() => {
                barrier.open();
                this.updateExpandability(internal);
            });
        }
        else {
            barrier.open();
            this.updateExpandability(internal);
        }
        return internal.resolveBarrier;
    }
    pushExpandStateUpdate(internal) {
        this.pushDiff([1 /* TestDiffOpType.Update */, { extId: internal.fullId.toString(), expand: internal.expand }]);
    }
    removeItem(childId) {
        const childItem = this.tree.get(childId);
        if (!childItem) {
            throw new Error('attempting to remove non-existent child');
        }
        this.pushDiff([2 /* TestDiffOpType.Remove */, childId]);
        const queue = [childItem];
        while (queue.length) {
            const item = queue.pop();
            if (!item) {
                continue;
            }
            (0, testApi_1.getPrivateApiFor)(item.actual).listener = undefined;
            for (const tag of item.actual.tags) {
                this.decrementTagRefs(tag.id);
            }
            this.tree.delete(item.fullId.toString());
            for (const child of item.actual.children) {
                queue.push(this.tree.get(testId_1.TestId.joinToString(item.fullId, child.id)));
            }
        }
    }
    /**
     * Immediately emits any pending diffs on the collection.
     */
    flushDiff() {
        const diff = this.collectDiff();
        if (diff.length) {
            this.diffOpEmitter.fire(diff);
        }
    }
}
exports.SingleUseTestCollection = SingleUseTestCollection;
//# sourceMappingURL=testCollection.js.map