"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 assert_1 = require("assert");
const stochastic_test_utils_1 = require("@fluid-private/stochastic-test-utils");
const telemetry_utils_1 = require("@fluidframework/telemetry-utils");
const protocol_definitions_1 = require("@fluidframework/protocol-definitions");
const test_runtime_utils_1 = require("@fluidframework/test-runtime-utils");
const client_utils_1 = require("@fluid-internal/client-utils");
const client_1 = require("../client.cjs");
const collections_1 = require("../collections/index.cjs");
const constants_1 = require("../constants.cjs");
const mergeTreeNodes_1 = require("../mergeTreeNodes.cjs");
const opBuilder_1 = require("../opBuilder.cjs");
const ops_1 = require("../ops.cjs");
const snapshotlegacy_1 = require("../snapshotlegacy.cjs");
const textSegment_1 = require("../textSegment.cjs");
const mergeTree_1 = require("../mergeTree.cjs");
const MergeTreeTextHelper_1 = require("../MergeTreeTextHelper.cjs");
const mergeTreeNodeWalk_1 = require("../mergeTreeNodeWalk.cjs");
const referencePositions_1 = require("../referencePositions.cjs");
const testSerializer_1 = require("./testSerializer.cjs");
const testUtils_1 = require("./testUtils.cjs");
function specToSegment(spec) {
    const maybeText = textSegment_1.TextSegment.fromJSONObject(spec);
    if (maybeText) {
        return maybeText;
    }
    const maybeMarker = mergeTreeNodes_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);
class TestClient extends client_1.Client {
    static async createFromClientSnapshot(client1, newLongClientId) {
        const snapshot = new snapshotlegacy_1.SnapshotLegacy(client1.mergeTree, (0, telemetry_utils_1.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 test_runtime_utils_1.MockStorage(snapshotTree), newLongClientId, specToSeg, options);
    }
    static async createFromSummary(summaryTree, newLongClientId, specToSeg, options) {
        return TestClient.createFromStorage(test_runtime_utils_1.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) {
        super(specToSeg, (0, telemetry_utils_1.createChildLogger)({ namespace: "fluid:testClient" }), options);
        this.measureOps = false;
        this.accumTime = 0;
        this.accumWindowTime = 0;
        this.accumWindow = 0;
        this.accumOps = 0;
        this.maxWindowTime = 0;
        this.checkQ = new collections_1.DoublyLinkedList();
        this.q = new collections_1.DoublyLinkedList();
        this.mergeTree = this._mergeTree;
        this.textHelper = new MergeTreeTextHelper_1.MergeTreeTextHelper(this.mergeTree);
        // Validate by default
        this.on("delta", (o, d) => {
            // assert.notEqual(d.deltaSegments.length, 0);
            d.deltaSegments.forEach((s) => {
                if (d.operation === ops_1.MergeTreeDeltaType.INSERT) {
                    const seg = s.segment;
                    assert_1.strict.notEqual(seg.parent, undefined);
                }
            });
        });
    }
    /**
     * @internal
     */
    obliterateRange({ start, end, refSeq, clientId, seq, overwrite = false, opArgs, }) {
        this.mergeTree.obliterateRange(start, end, refSeq, clientId, seq, overwrite, opArgs);
    }
    getText(start, end) {
        return this.textHelper.getText(this.getCurrentSeq(), this.getClientId(), "", 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 = new textSegment_1.TextSegment(text);
        if (props) {
            segment.addProperties(props);
        }
        return this.insertSegmentLocal(pos, segment);
    }
    insertTextRemote(pos, text, props, seq, refSeq, longClientId) {
        const segment = new textSegment_1.TextSegment(text);
        if (props) {
            segment.addProperties(props);
        }
        this.applyMsg(this.makeOpMessage((0, opBuilder_1.createInsertSegmentOp)(pos, segment), seq, refSeq, longClientId));
    }
    removeRangeRemote(start, end, seq, refSeq, longClientId) {
        this.applyMsg(this.makeOpMessage((0, opBuilder_1.createRemoveRangeOp)(start, end), seq, refSeq, longClientId));
    }
    annotateRangeRemote(start, end, props, seq, refSeq, longClientId) {
        this.applyMsg(this.makeOpMessage((0, opBuilder_1.createAnnotateRangeOp)(start, end, props), seq, refSeq, longClientId));
    }
    insertMarkerLocal(pos, behaviors, props) {
        const segment = new mergeTreeNodes_1.Marker(behaviors);
        if (props) {
            segment.addProperties(props);
        }
        return this.insertSegmentLocal(pos, segment);
    }
    insertMarkerRemote(pos, markerDef, props, seq, refSeq, longClientId) {
        const segment = new mergeTreeNodes_1.Marker(markerDef.refType ?? ops_1.ReferenceType.Tile);
        if (props) {
            segment.addProperties(props);
        }
        this.applyMsg(this.makeOpMessage((0, opBuilder_1.createInsertSegmentOp)(pos, segment), seq, refSeq, longClientId));
    }
    relText(clientId, refSeq) {
        return `cli: ${this.getLongClientId(clientId)} refSeq: ${refSeq}: ${this.textHelper.getText(refSeq, clientId)}`;
    }
    makeOpMessage(op, seq = constants_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: protocol_definitions_1.MessageType.Operation,
        };
        return msg;
    }
    validate() {
        (0, assert_1.strict)((0, testUtils_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_1.walkAllChildSegments)(tree.root, (segment) => {
            const prefixes = [];
            prefixes.push(segment.seq !== constants_1.UnassignedSequenceNumber ? segment.seq : `L${segment.localSeq}`);
            if (segment.removedSeq !== undefined) {
                prefixes.push(segment.removedSeq !== constants_1.UnassignedSequenceNumber
                    ? segment.removedSeq
                    : `L${segment.localRemovedSeq}`);
            }
            test.push(`${prefixes.join(",")}:${segment.text}`);
        });
    }
    /**
     * 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 isInsertedInView = (seg) => (seg.seq !== undefined &&
            seg.seq !== constants_1.UnassignedSequenceNumber &&
            seg.seq <= seqNumberFrom) ||
            (seg.localSeq !== undefined && seg.localSeq <= localSeq);
        const isRemovedFromView = ({ removedSeq, localRemovedSeq }) => (removedSeq !== undefined &&
            removedSeq !== constants_1.UnassignedSequenceNumber &&
            removedSeq <= seqNumberFrom) ||
            (localRemovedSeq !== undefined && localRemovedSeq <= localSeq);
        (0, mergeTreeNodeWalk_1.walkAllChildSegments)(this.mergeTree.root, (seg) => {
            (0, assert_1.strict)(seg.seq !== undefined || seg.localSeq !== undefined, "either seq or localSeq should be defined");
            segment = seg;
            if (isInsertedInView(seg) && !isRemovedFromView(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, assert_1.strict)(segment !== undefined, "No segment found");
        const segoff = (0, mergeTree_1.getSlideToSegoff)({ segment, offset }) ?? segment;
        if (segoff.segment === undefined || segoff.offset === undefined) {
            return referencePositions_1.DetachedReferencePosition;
        }
        return this.findReconnectionPosition(segoff.segment, localSeq) + segoff.offset;
    }
    findReconnectionPosition(segment, localSeq) {
        const fasterComputedPosition = super.findReconnectionPosition(segment, localSeq);
        let segmentPosition = 0;
        const isInsertedInView = (seg) => seg.localSeq === undefined || seg.localSeq <= localSeq;
        const isRemovedFromView = ({ removedSeq, localRemovedSeq }) => removedSeq !== undefined &&
            (removedSeq !== constants_1.UnassignedSequenceNumber ||
                (localRemovedSeq !== undefined && localRemovedSeq <= localSeq));
        const isMovedFromView = ({ movedSeq, localMovedSeq }) => movedSeq !== undefined &&
            (movedSeq !== constants_1.UnassignedSequenceNumber ||
                (localMovedSeq !== undefined && localMovedSeq <= localSeq));
        /*
            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_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 has been inserted and not removed
            // with respect to the given 'localSeq'.
            //
            // Note that all ACKed / remote ops are applied and we only need concern ourself with
            // determining if locally pending ops fall before/after the given 'localSeq'.
            if (isInsertedInView(seg) && !isRemovedFromView(seg) && !isMovedFromView(seg)) {
                segmentPosition += seg.cachedLength;
            }
            return true;
        });
        assert_1.strict.equal(fasterComputedPosition, segmentPosition, "Expected fast-path computation to match result from walk all segments");
        return segmentPosition;
    }
    /**
     * @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`.
     * Validates segments either all have attribution information or none of them.
     * If no segment has attribution information, returns undefined.
     */
    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;
    }
    /**
     * 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);
        const segWithParent = segment;
        if (mergeTreeNodes_1.Marker.is(segWithParent)) {
            if ((0, referencePositions_1.refHasTileLabel)(segWithParent, markerLabel)) {
                foundMarker = segWithParent;
            }
        }
        else {
            if (forwards) {
                (0, mergeTreeNodeWalk_1.forwardExcursion)(segWithParent, (seg) => {
                    if (mergeTreeNodes_1.Marker.is(seg)) {
                        if ((0, referencePositions_1.refHasTileLabel)(seg, markerLabel)) {
                            foundMarker = seg;
                            return false;
                        }
                    }
                });
            }
            else {
                (0, mergeTreeNodeWalk_1.backwardExcursion)(segWithParent, (seg) => {
                    if (mergeTreeNodes_1.Marker.is(seg)) {
                        if ((0, referencePositions_1.refHasTileLabel)(seg, markerLabel)) {
                            foundMarker = seg;
                            return false;
                        }
                    }
                });
            }
        }
        return foundMarker;
    }
}
exports.TestClient = TestClient;
TestClient.searchChunkSize = 256;
TestClient.serializer = new testSerializer_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_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()) {
                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_1.MaxNodesInBlock; j++) {
                    stats.histo[j] += childStats.histo[j];
                }
            }
            else {
                stats.leafCount++;
                const segment = child;
                if (segment.removedSeq !== undefined) {
                    stats.removedLeafCount++;
                }
            }
            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.cjs.map