"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.getStats = exports.createRevertDriver = exports.TestClient = exports.specToSegment = void 0;
const node_assert_1 = require("node:assert");
const client_utils_1 = require("@fluid-internal/client-utils");
const stochastic_test_utils_1 = require("@fluid-private/stochastic-test-utils");
const internal_1 = require("@fluidframework/core-utils/internal");
const internal_2 = require("@fluidframework/driver-definitions/internal");
const internal_3 = require("@fluidframework/telemetry-utils/internal");
const internal_4 = require("@fluidframework/test-runtime-utils/internal");
const MergeTreeTextHelper_js_1 = require("../MergeTreeTextHelper.js");
const client_js_1 = require("../client.js");
const constants_js_1 = require("../constants.js");
const mergeTree_js_1 = require("../mergeTree.js");
const mergeTreeNodeWalk_js_1 = require("../mergeTreeNodeWalk.js");
const mergeTreeNodes_js_1 = require("../mergeTreeNodes.js");
const opBuilder_js_1 = require("../opBuilder.js");
const ops_js_1 = require("../ops.js");
const perspective_js_1 = require("../perspective.js");
const referencePositions_js_1 = require("../referencePositions.js");
const segmentInfos_js_1 = require("../segmentInfos.js");
const snapshotlegacy_js_1 = require("../snapshotlegacy.js");
const textSegment_js_1 = require("../textSegment.js");
const testSerializer_js_1 = require("./testSerializer.js");
const testUtils_js_1 = require("./testUtils.js");
function specToSegment(spec) {
    const maybeText = textSegment_js_1.TextSegment.fromJSONObject(spec);
    if (maybeText) {
        return maybeText;
    }
    const maybeMarker = mergeTreeNodes_js_1.Marker.fromJSONObject(spec);
    if (maybeMarker) {
        return maybeMarker;
    }
    throw new Error(`Unrecognized IJSONSegment type: '${JSON.stringify(spec)}'`);
}
exports.specToSegment = specToSegment;
const random = (0, stochastic_test_utils_1.makeRandom)(0xdeadbeef, 0xfeedbed);
function opStampToString(stamp) {
    return stamp.seq === constants_js_1.UnassignedSequenceNumber ? `L${stamp.localSeq}` : `${stamp.seq}`;
}
class TestClient extends client_js_1.Client {
    static async createFromClientSnapshot(client1, newLongClientId) {
        const snapshot = new snapshotlegacy_js_1.SnapshotLegacy(client1.mergeTree, (0, internal_3.createChildLogger)({ namespace: "fluid:snapshot" }));
        snapshot.extractSync();
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        const summaryTree = snapshot.emit([], TestClient.serializer, undefined).summary;
        return TestClient.createFromSummary(summaryTree, newLongClientId, client1.specToSegment, client1.mergeTree.options);
    }
    static async createFromSnapshot(snapshotTree, newLongClientId, specToSeg, options) {
        return TestClient.createFromStorage(new internal_4.MockStorage(snapshotTree), newLongClientId, specToSeg, options);
    }
    static async createFromSummary(summaryTree, newLongClientId, specToSeg, options) {
        return TestClient.createFromStorage(internal_4.MockStorage.createFromSummary(summaryTree), newLongClientId, specToSeg, options);
    }
    static async createFromStorage(storage, newLongClientId, specToSeg, options) {
        const client2 = new TestClient(options, specToSeg);
        const { catchupOpsP } = await client2.load({
            logger: client2.logger,
            clientId: newLongClientId,
        }, storage, TestClient.serializer);
        await catchupOpsP;
        return client2;
    }
    constructor(options, specToSeg = specToSegment, getMinInFlightRefSeq = () => undefined) {
        super(specToSeg, (0, internal_3.createChildLogger)({ namespace: "fluid:testClient" }), options, getMinInFlightRefSeq);
        this.measureOps = false;
        this.accumTime = 0;
        this.accumWindowTime = 0;
        this.accumWindow = 0;
        this.accumOps = 0;
        this.maxWindowTime = 0;
        this.checkQ = new internal_1.DoublyLinkedList();
        this.q = new internal_1.DoublyLinkedList();
        this.mergeTree = this._mergeTree;
        this.textHelper = new MergeTreeTextHelper_js_1.MergeTreeTextHelper(this.mergeTree);
        // Validate by default
        this.on("delta", (o, d) => {
            // assert.notEqual(d.deltaSegments.length, 0);
            for (const s of d.deltaSegments) {
                if (d.operation === ops_js_1.MergeTreeDeltaType.INSERT) {
                    (0, segmentInfos_js_1.assertMergeNode)(s.segment);
                }
            }
        });
    }
    getText(start, end) {
        return this.textHelper.getText(this.mergeTree.localPerspective, "", start, end);
    }
    enqueueTestString() {
        this.checkQ.push(this.getText());
    }
    getMessageCount() {
        return this.q.length;
    }
    enqueueMsg(msg) {
        this.q.push(msg);
    }
    dequeueMsg() {
        return this.q.shift()?.data;
    }
    applyMessages(msgCount) {
        let currMsgCount = msgCount;
        while (currMsgCount > 0) {
            const msg = this.dequeueMsg();
            if (msg) {
                this.applyMsg(msg);
            }
            else {
                break;
            }
            currMsgCount--;
        }
        return true;
    }
    insertTextLocal(pos, text, props) {
        const segment = textSegment_js_1.TextSegment.make(text, props);
        return this.insertSegmentLocal(pos, segment);
    }
    insertTextRemote(pos, text, props, seq, refSeq, longClientId) {
        const segment = textSegment_js_1.TextSegment.make(text, props);
        this.applyMsg(this.makeOpMessage((0, opBuilder_js_1.createInsertSegmentOp)(pos, segment), seq, refSeq, longClientId));
    }
    removeRangeRemote(start, end, seq, refSeq, longClientId) {
        this.applyMsg(this.makeOpMessage((0, opBuilder_js_1.createRemoveRangeOp)(start, end), seq, refSeq, longClientId));
    }
    obliterateRangeRemote(start, end, seq, refSeq, longClientId) {
        this.applyMsg(this.makeOpMessage((0, opBuilder_js_1.createObliterateRangeOp)(start, end), seq, refSeq, longClientId));
    }
    annotateRangeRemote(start, end, props, seq, refSeq, longClientId) {
        this.applyMsg(this.makeOpMessage((0, opBuilder_js_1.createAnnotateRangeOp)(start, end, props), seq, refSeq, longClientId));
    }
    insertMarkerLocal(pos, behaviors, props) {
        const segment = mergeTreeNodes_js_1.Marker.make(behaviors, props);
        return this.insertSegmentLocal(pos, segment);
    }
    insertMarkerRemote(pos, markerDef, props, seq, refSeq, longClientId) {
        const segment = mergeTreeNodes_js_1.Marker.make(markerDef.refType ?? ops_js_1.ReferenceType.Tile, props);
        this.applyMsg(this.makeOpMessage((0, opBuilder_js_1.createInsertSegmentOp)(pos, segment), seq, refSeq, longClientId));
    }
    relText(clientId, refSeq) {
        return `cli: ${this.getLongClientId(clientId)} refSeq: ${refSeq}: ${this.textHelper.getText(new perspective_js_1.PriorPerspective(refSeq, clientId))}`;
    }
    makeOpMessage(op, seq = constants_js_1.UnassignedSequenceNumber, refSeq = this.getCurrentSeq(), longClientId, minSeqNumber = 0) {
        if (op === undefined) {
            throw new Error("op cannot be undefined");
        }
        const msg = {
            clientId: longClientId ?? this.longClientId ?? "",
            clientSequenceNumber: 1,
            contents: op,
            metadata: undefined,
            minimumSequenceNumber: minSeqNumber,
            referenceSequenceNumber: refSeq,
            sequenceNumber: seq,
            timestamp: Date.now(),
            traces: [],
            type: internal_2.MessageType.Operation,
        };
        return msg;
    }
    validate() {
        (0, node_assert_1.strict)((0, testUtils_js_1.nodeOrdinalsHaveIntegrity)(this.mergeTree.root));
    }
    searchFromPos(pos, target) {
        let start = pos;
        let chunk = "";
        while (start < this.getLength()) {
            chunk = this.getText(start, start + TestClient.searchChunkSize);
            const result = chunk.match(target);
            if (result?.index) {
                return { text: result[0], pos: result.index + start };
            }
            start += TestClient.searchChunkSize;
        }
    }
    findRandomWord() {
        const len = this.getLength();
        const pos = random.integer(0, len);
        const nextWord = this.searchFromPos(pos, /\s\w+\b/);
        return nextWord;
    }
    debugDumpTree(tree) {
        // want the segment's content and the state of insert/remove
        const test = [];
        (0, mergeTreeNodeWalk_js_1.walkAllChildSegments)(tree.root, (segment) => {
            const prefixes = [];
            (0, segmentInfos_js_1.assertInserted)(segment);
            prefixes.push(opStampToString(segment.insert));
            if ((0, segmentInfos_js_1.isRemoved)(segment)) {
                prefixes.push(opStampToString(segment.removes[0]));
            }
            // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
            test.push(`${prefixes.join(",")}:${segment.text}`);
        });
        return test;
    }
    /**
     * Rebases a (local) position from the perspective `{ seq: seqNumberFrom, localSeq }` to the perspective
     * of the current sequence number. This is desirable when rebasing operations for reconnection. Perform
     * slow-path computations in this function without leveraging the merge-tree's structure
     */
    rebasePosition(pos, seqNumberFrom, localSeq) {
        let segment;
        let posAccumulated = 0;
        let offset = pos;
        const perspective = new perspective_js_1.LocalReconnectingPerspective(seqNumberFrom, this.getCollabWindow().clientId, localSeq);
        (0, mergeTreeNodeWalk_js_1.walkAllChildSegments)(this.mergeTree.root, (seg) => {
            (0, segmentInfos_js_1.assertInserted)(seg);
            segment = seg;
            if (perspective.isSegmentPresent(seg)) {
                posAccumulated += seg.cachedLength;
                if (offset >= seg.cachedLength) {
                    offset -= seg.cachedLength;
                }
            }
            // Keep going while we've yet to reach the segment at the desired position
            return posAccumulated <= pos;
        });
        (0, node_assert_1.strict)(segment !== undefined, "No segment found");
        const segoff = (0, mergeTree_js_1.getSlideToSegoff)({ segment, offset }, undefined, perspective);
        if (segoff === undefined) {
            return referencePositions_js_1.DetachedReferencePosition;
        }
        return this.findReconnectionPosition(segoff.segment, localSeq) + segoff.offset;
    }
    findReconnectionPosition(segment, localSeq) {
        const fasterComputedPosition = super.findReconnectionPosition(segment, localSeq);
        const perspective = new perspective_js_1.LocalReconnectingPerspective(Number.MAX_SAFE_INTEGER, this.getCollabWindow().clientId, localSeq);
        let segmentPosition = 0;
        /*
            Walk the segments up to the current segment, and calculate its
            position taking into account local segments that were modified,
            after the current segment.
        */
        (0, mergeTreeNodeWalk_js_1.walkAllChildSegments)(this.mergeTree.root, (seg) => {
            // If we've found the desired segment, terminate the walk and return 'segmentPosition'.
            if (seg === segment) {
                return false;
            }
            // Otherwise, advance segmentPosition if the segment is visible at the given perspective.
            if (perspective.isSegmentPresent(seg)) {
                segmentPosition += seg.cachedLength;
            }
            return true;
        });
        node_assert_1.strict.equal(fasterComputedPosition, segmentPosition, "Expected fast-path computation to match result from walk all segments");
        return segmentPosition;
    }
    /**
     * Validates segments either all have attribution information or none of them.
     * If no segment has attribution information, returns undefined.
     *
     * @param channel - Attribution channel name to request information from.
     * @returns an array of all attribution seq#s from the current perspective.
     * The `i`th entry of the array is the attribution key for the character at position `i`.
     */
    getAllAttributionSeqs(channel) {
        const seqs = [];
        this.walkAllSegments((segment) => {
            for (let i = 0; i < segment.cachedLength; i++) {
                const key = segment.attribution?.getAtOffset(i, channel);
                seqs.push(key?.type === "op" ? key.seq : key);
            }
            return true;
        });
        return seqs;
    }
    peekPendingSegmentGroups(count = 1) {
        return super.peekPendingSegmentGroups(count);
    }
    /**
     * Override and add some test only metrics
     */
    applyMsg(msg, local = false) {
        let traceStart;
        if (this.measureOps) {
            traceStart = client_utils_1.Trace.start();
        }
        super.applyMsg(msg, local);
        if (traceStart) {
            this.accumTime += elapsedMicroseconds(traceStart);
            this.accumOps++;
            this.accumWindow += this.getCurrentSeq() - this.getCollabWindow().minSeq;
        }
    }
    /**
     * Override and add some test only metrics
     */
    updateMinSeq(minSeq) {
        let trace;
        if (this.measureOps) {
            trace = client_utils_1.Trace.start();
        }
        super.updateMinSeq(minSeq);
        if (trace) {
            const elapsed = elapsedMicroseconds(trace);
            this.accumWindowTime += elapsed;
            if (elapsed > this.maxWindowTime) {
                this.maxWindowTime = elapsed;
            }
        }
    }
    slowSearchForMarker(startPos, markerLabel, forwards = true) {
        let foundMarker;
        const { segment } = this.getContainingSegment(startPos) ?? {};
        (0, mergeTreeNodes_js_1.assertSegmentLeaf)(segment);
        if (mergeTreeNodes_js_1.Marker.is(segment)) {
            if ((0, referencePositions_js_1.refHasTileLabel)(segment, markerLabel)) {
                foundMarker = segment;
            }
        }
        else {
            if (forwards) {
                (0, mergeTreeNodeWalk_js_1.forwardExcursion)(segment, (seg) => {
                    if (mergeTreeNodes_js_1.Marker.is(seg) && (0, referencePositions_js_1.refHasTileLabel)(seg, markerLabel)) {
                        foundMarker = seg;
                        return false;
                    }
                });
            }
            else {
                (0, mergeTreeNodeWalk_js_1.backwardExcursion)(segment, (seg) => {
                    if (mergeTreeNodes_js_1.Marker.is(seg) && (0, referencePositions_js_1.refHasTileLabel)(seg, markerLabel)) {
                        foundMarker = seg;
                        return false;
                    }
                });
            }
        }
        return foundMarker;
    }
}
exports.TestClient = TestClient;
TestClient.searchChunkSize = 256;
TestClient.serializer = new testSerializer_js_1.TestSerializer();
/**
 * Used for in-memory testing.  This will queue a reference string for each client message.
 */
TestClient.useCheckQ = false;
function elapsedMicroseconds(trace) {
    return trace.trace().duration * 1000;
}
const createRevertDriver = (client) => {
    return {
        removeRange(start, end) {
            const op = client.removeRangeLocal(start, end);
            this.submitOpCallback?.(op);
        },
        annotateRange(start, end, props) {
            const op = client.annotateRangeLocal(start, end, props);
            this.submitOpCallback?.(op);
        },
        insertFromSpec(pos, spec) {
            const op = client.insertSegmentLocal(pos, client.specToSegment(spec));
            this.submitOpCallback?.(op);
        },
    };
};
exports.createRevertDriver = createRevertDriver;
function getStats(tree) {
    const nodeGetStats = (block) => {
        const stats = {
            maxHeight: 0,
            nodeCount: 0,
            leafCount: 0,
            removedLeafCount: 0,
            liveCount: 0,
            histo: [],
        };
        for (let k = 0; k < mergeTreeNodes_js_1.MaxNodesInBlock; k++) {
            stats.histo[k] = 0;
        }
        for (let i = 0; i < block.childCount; i++) {
            const child = block.children[i];
            let height = 1;
            if (child.isLeaf()) {
                stats.leafCount++;
                const segment = child;
                if ((0, segmentInfos_js_1.isRemoved)(segment)) {
                    stats.removedLeafCount++;
                }
            }
            else {
                const childStats = nodeGetStats(child);
                height = 1 + childStats.maxHeight;
                stats.nodeCount += childStats.nodeCount;
                stats.leafCount += childStats.leafCount;
                stats.removedLeafCount += childStats.removedLeafCount;
                stats.liveCount += childStats.liveCount;
                for (let j = 0; j < mergeTreeNodes_js_1.MaxNodesInBlock; j++) {
                    stats.histo[j] += childStats.histo[j];
                }
            }
            if (height > stats.maxHeight) {
                stats.maxHeight = height;
            }
        }
        stats.histo[block.childCount]++;
        stats.nodeCount++;
        stats.liveCount += block.childCount;
        return stats;
    };
    const rootStats = nodeGetStats(tree.root);
    return rootStats;
}
exports.getStats = getStats;
//# sourceMappingURL=testClient.js.map