/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { strict as assert } from "node:assert";
import { Trace } from "@fluid-internal/client-utils";
import { makeRandom } from "@fluid-private/stochastic-test-utils";
import { MessageType, } from "@fluidframework/driver-definitions/internal";
import { createChildLogger } from "@fluidframework/telemetry-utils/internal";
import { MockStorage } from "@fluidframework/test-runtime-utils/internal";
import { MergeTreeTextHelper } from "../MergeTreeTextHelper.js";
import { Client } from "../client.js";
import { DoublyLinkedList } from "../collections/index.js";
import { UnassignedSequenceNumber } from "../constants.js";
import { getSlideToSegoff } from "../mergeTree.js";
import { backwardExcursion, forwardExcursion, walkAllChildSegments, } from "../mergeTreeNodeWalk.js";
import { Marker, MaxNodesInBlock, assertSegmentLeaf, } from "../mergeTreeNodes.js";
import { createAnnotateRangeOp, createInsertSegmentOp, createObliterateRangeOp, createRemoveRangeOp, } from "../opBuilder.js";
import { MergeTreeDeltaType, ReferenceType, } from "../ops.js";
import { LocalReconnectingPerspective, PriorPerspective } from "../perspective.js";
import { DetachedReferencePosition, refHasTileLabel } from "../referencePositions.js";
import { assertInserted, assertMergeNode, isRemoved } from "../segmentInfos.js";
import { SnapshotLegacy } from "../snapshotlegacy.js";
import { TextSegment } from "../textSegment.js";
import { TestSerializer } from "./testSerializer.js";
import { nodeOrdinalsHaveIntegrity } from "./testUtils.js";
export function specToSegment(spec) {
    const maybeText = TextSegment.fromJSONObject(spec);
    if (maybeText) {
        return maybeText;
    }
    const maybeMarker = Marker.fromJSONObject(spec);
    if (maybeMarker) {
        return maybeMarker;
    }
    throw new Error(`Unrecognized IJSONSegment type: '${JSON.stringify(spec)}'`);
}
const random = makeRandom(0xdeadbeef, 0xfeedbed);
function opStampToString(stamp) {
    return stamp.seq === UnassignedSequenceNumber ? `L${stamp.localSeq}` : `${stamp.seq}`;
}
export class TestClient extends Client {
    static async createFromClientSnapshot(client1, newLongClientId) {
        const snapshot = new SnapshotLegacy(client1.mergeTree, 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 MockStorage(snapshotTree), newLongClientId, specToSeg, options);
    }
    static async createFromSummary(summaryTree, newLongClientId, specToSeg, options) {
        return TestClient.createFromStorage(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, 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 DoublyLinkedList();
        this.q = new DoublyLinkedList();
        this.mergeTree = this._mergeTree;
        this.textHelper = new 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 === MergeTreeDeltaType.INSERT) {
                    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.make(text, props);
        return this.insertSegmentLocal(pos, segment);
    }
    insertTextRemote(pos, text, props, seq, refSeq, longClientId) {
        const segment = TextSegment.make(text, props);
        this.applyMsg(this.makeOpMessage(createInsertSegmentOp(pos, segment), seq, refSeq, longClientId));
    }
    removeRangeRemote(start, end, seq, refSeq, longClientId) {
        this.applyMsg(this.makeOpMessage(createRemoveRangeOp(start, end), seq, refSeq, longClientId));
    }
    obliterateRangeRemote(start, end, seq, refSeq, longClientId) {
        this.applyMsg(this.makeOpMessage(createObliterateRangeOp(start, end), seq, refSeq, longClientId));
    }
    annotateRangeRemote(start, end, props, seq, refSeq, longClientId) {
        this.applyMsg(this.makeOpMessage(createAnnotateRangeOp(start, end, props), seq, refSeq, longClientId));
    }
    insertMarkerLocal(pos, behaviors, props) {
        const segment = Marker.make(behaviors, props);
        return this.insertSegmentLocal(pos, segment);
    }
    insertMarkerRemote(pos, markerDef, props, seq, refSeq, longClientId) {
        const segment = Marker.make(markerDef.refType ?? ReferenceType.Tile, props);
        this.applyMsg(this.makeOpMessage(createInsertSegmentOp(pos, segment), seq, refSeq, longClientId));
    }
    relText(clientId, refSeq) {
        return `cli: ${this.getLongClientId(clientId)} refSeq: ${refSeq}: ${this.textHelper.getText(new PriorPerspective(refSeq, clientId))}`;
    }
    makeOpMessage(op, seq = 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: MessageType.Operation,
        };
        return msg;
    }
    validate() {
        assert(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 = [];
        walkAllChildSegments(tree.root, (segment) => {
            const prefixes = [];
            assertInserted(segment);
            prefixes.push(opStampToString(segment.insert));
            if (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 LocalReconnectingPerspective(seqNumberFrom, this.getCollabWindow().clientId, localSeq);
        walkAllChildSegments(this.mergeTree.root, (seg) => {
            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;
        });
        assert(segment !== undefined, "No segment found");
        const segoff = getSlideToSegoff({ segment, offset }, undefined, perspective) ?? segment;
        if (segoff.segment === undefined || segoff.offset === undefined) {
            return DetachedReferencePosition;
        }
        return this.findReconnectionPosition(segoff.segment, localSeq) + segoff.offset;
    }
    findReconnectionPosition(segment, localSeq) {
        const fasterComputedPosition = super.findReconnectionPosition(segment, localSeq);
        const perspective = new 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.
        */
        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;
        });
        assert.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 = 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 = 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);
        assertSegmentLeaf(segment);
        if (Marker.is(segment)) {
            if (refHasTileLabel(segment, markerLabel)) {
                foundMarker = segment;
            }
        }
        else {
            if (forwards) {
                forwardExcursion(segment, (seg) => {
                    if (Marker.is(seg) && refHasTileLabel(seg, markerLabel)) {
                        foundMarker = seg;
                        return false;
                    }
                });
            }
            else {
                backwardExcursion(segment, (seg) => {
                    if (Marker.is(seg) && refHasTileLabel(seg, markerLabel)) {
                        foundMarker = seg;
                        return false;
                    }
                });
            }
        }
        return foundMarker;
    }
}
TestClient.searchChunkSize = 256;
TestClient.serializer = new 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;
}
export 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);
        },
    };
};
export function getStats(tree) {
    const nodeGetStats = (block) => {
        const stats = {
            maxHeight: 0,
            nodeCount: 0,
            leafCount: 0,
            removedLeafCount: 0,
            liveCount: 0,
            histo: [],
        };
        for (let k = 0; k < 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 (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 < 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;
}
//# sourceMappingURL=testClient.js.map