"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.TestClientLogger = exports.createClientsAtInitialState = void 0;
const node_assert_1 = require("node:assert");
const internal_1 = require("@fluidframework/telemetry-utils/internal");
const constants_js_1 = require("../constants.js");
const mergeTreeDeltaCallback_js_1 = require("../mergeTreeDeltaCallback.js");
const mergeTreeNodeWalk_js_1 = require("../mergeTreeNodeWalk.js");
const mergeTreeNodes_js_1 = require("../mergeTreeNodes.js");
const ops_js_1 = require("../ops.js");
const properties_js_1 = require("../properties.js");
const segmentInfos_js_1 = require("../segmentInfos.js");
const textSegment_js_1 = require("../textSegment.js");
const testClient_js_1 = require("./testClient.js");
function getOpString(msg) {
    if (msg === undefined) {
        return "";
    }
    const op = msg.contents;
    const opType = op.type.toString();
    let opPos;
    if (op.type === ops_js_1.MergeTreeDeltaType.OBLITERATE_SIDED) {
        const pos1Side = op.type === ops_js_1.MergeTreeDeltaType.OBLITERATE_SIDED ? (op.pos1.before ? "[" : "(") : "";
        const pos2Side = op.type === ops_js_1.MergeTreeDeltaType.OBLITERATE_SIDED ? (op.pos2.before ? ")" : "]") : "";
        opPos = `@${pos1Side}${op.pos1.pos},${op.pos2.pos}${pos2Side}`;
    }
    else {
        opPos =
            // eslint-disable-next-line @typescript-eslint/dot-notation
            op?.["pos1"] === undefined
                ? ""
                : // eslint-disable-next-line @typescript-eslint/dot-notation
                    `@${op["pos1"]}${op["pos2"] === undefined ? "" : `,${op["pos2"]}`}`;
    }
    const seq = msg.sequenceNumber < 0 ? "L" : msg.sequenceNumber.toString();
    const ref = msg.referenceSequenceNumber.toString();
    const client = msg.clientId;
    return `${seq}:${ref}:${client}${opType}${opPos}`;
}
function arePropsEmpty(props) {
    return props === undefined || Object.entries(props).length === 0;
}
/**
 * Compare properties, allowing empty to match undefined
 */
function matchPropertiesHandleEmpty(a, b) {
    return (0, properties_js_1.matchProperties)(a, b) || (arePropsEmpty(a) && arePropsEmpty(b));
}
function createClientsAtInitialState(opts, ...clientIds) {
    const setup = (c) => {
        if (opts.initialState.length > 0) {
            c.insertTextLocal(0, opts.initialState);
            while (c.getText().includes("-")) {
                const index = c.getText().indexOf("-");
                c.removeRangeLocal(index, index + 1);
            }
        }
    };
    const all = [];
    const clients = {};
    for (const id of clientIds) {
        if (clients[id] === undefined) {
            const client = new testClient_js_1.TestClient(opts.options);
            clients[id] = client;
            all.push(client);
            setup(client);
            client.startOrUpdateCollaboration(id);
        }
    }
    return { ...clients, all };
}
exports.createClientsAtInitialState = createClientsAtInitialState;
class TestClientLogger {
    static toString(clients) {
        return (clients
            .map((c) => this.getSegString(c))
            // eslint-disable-next-line unicorn/no-array-reduce
            .reduce((pv, cv) => {
            pv[0] += `|${cv.acked.padEnd(cv.local.length, "")}`;
            pv[1] += `|${cv.local.padEnd(cv.acked.length, "")}`;
            return pv;
        }, ["", ""])
            .join("\n"));
    }
    /**
     * Unsubscribes this logger from its clients' events. Consider using this for tests with client lifetime
     * extending significantly past the logger's.
     */
    dispose() {
        for (const cb of this.disposeCallbacks) {
            cb();
        }
        this.disposeCallbacks.length = 0;
    }
    constructor(clients, title) {
        this.clients = clients;
        this.title = title;
        this.incrementalLog = false;
        this.paddings = [];
        this.roundLogLines = [];
        this.ackedLine = [];
        this.localLine = [];
        this.disposeCallbacks = [];
        const logHeaders = [];
        for (const [i, c] of clients.entries()) {
            logHeaders.push("op", `client ${c.longClientId}`);
            const callback = (deltaArgs) => {
                if (this.lastDeltaArgs?.sequencedMessage !== deltaArgs?.sequencedMessage ||
                    this.lastDeltaArgs?.op !== deltaArgs?.op) {
                    this.addNewLogLine();
                    this.lastDeltaArgs = deltaArgs;
                }
                const clientLogIndex = i * 2;
                this.ackedLine[clientLogIndex] =
                    deltaArgs === undefined
                        ? ""
                        : getOpString(deltaArgs.sequencedMessage === undefined
                            ? c.makeOpMessage(deltaArgs.op)
                            : { ...deltaArgs.sequencedMessage, contents: deltaArgs.op });
                const segStrings = TestClientLogger.getSegString(c);
                this.ackedLine[clientLogIndex + 1] = segStrings.acked;
                this.localLine[clientLogIndex + 1] = segStrings.local;
                this.paddings[clientLogIndex] = Math.max(this.ackedLine[clientLogIndex].length, this.localLine[clientLogIndex].length, this.paddings[clientLogIndex]);
                this.paddings[clientLogIndex + 1] = Math.max(this.ackedLine[clientLogIndex + 1].length, this.localLine[clientLogIndex + 1].length, this.paddings[clientLogIndex + 1]);
            };
            const maintenanceCallback = (main, op) => {
                if (main.operation === mergeTreeDeltaCallback_js_1.MergeTreeMaintenanceType.ACKNOWLEDGED) {
                    callback(op);
                }
            };
            c.on("delta", callback);
            c.on("maintenance", maintenanceCallback);
            this.disposeCallbacks.push(() => {
                c.off("delta", callback);
                c.off("maintenance", maintenanceCallback);
            });
        }
        this.roundLogLines.push(logHeaders);
        for (const v of this.roundLogLines[0])
            this.paddings.push(v.length);
        this.addNewLogLine(); // capture initial state
    }
    addNewLogLine() {
        if (this.incrementalLog) {
            while (this.roundLogLines.length > 0) {
                const logLine = this.roundLogLines.shift();
                if (logLine?.some((c) => c.trim().length > 0)) {
                    console.log(logLine.map((v, i) => v.padEnd(this.paddings[i])).join(" | "));
                }
            }
        }
        this.ackedLine = [];
        this.localLine = [];
        for (const [clientLogIndex, cc] of this.clients.entries()) {
            const segStrings = TestClientLogger.getSegString(cc);
            this.ackedLine.push("", segStrings.acked);
            this.localLine.push("", segStrings.local);
            this.paddings[clientLogIndex] = Math.max(this.ackedLine[clientLogIndex].length, this.localLine[clientLogIndex].length, this.paddings[clientLogIndex]);
            this.paddings[clientLogIndex + 1] = Math.max(this.ackedLine[clientLogIndex + 1].length, this.localLine[clientLogIndex + 1].length, this.paddings[clientLogIndex + 1]);
        }
        this.roundLogLines.push(this.ackedLine, this.localLine);
    }
    validate(opts) {
        const baseText = opts?.baseText ?? this.clients[0].getText();
        const errorPrefix = opts?.errorPrefix ? `${opts?.errorPrefix}: ` : "";
        // cache all the properties of client 0 for faster look up
        const properties = Array.from({ length: this.clients[0].getLength() }).map((_, i) => this.clients[0].getPropertiesAtPosition(i));
        for (const c of this.clients) {
            if (opts?.baseText === undefined && c === this.clients[0]) {
                continue;
            }
            // ensure all clients have seen the same ops
            node_assert_1.strict.equal(c.getCurrentSeq(), this.clients[0].getCurrentSeq(), `${errorPrefix}${c.longClientId} current seq does not match client ${this.clients[0].longClientId}`);
            // Pre-check to avoid this.toString() in the string template
            if (c.getText() !== baseText) {
                node_assert_1.strict.equal(c.getText(), baseText, `${errorPrefix}\n${this.toString()}\nClient ${c.longClientId} does not match client ${opts?.baseText ? "baseText" : this.clients[0].longClientId}`);
            }
            if (c === this.clients[0]) {
                continue;
            }
            let pos = 0;
            (0, mergeTreeNodeWalk_js_1.depthFirstNodeWalk)(c.mergeTree.root, c.mergeTree.root.children[0], undefined, (seg) => {
                if (toMoveOrRemove(seg) === undefined) {
                    const segProps = seg.properties;
                    for (let i = 0; i < seg.cachedLength; i++) {
                        if (!matchPropertiesHandleEmpty(segProps, properties[pos + i])) {
                            node_assert_1.strict.deepStrictEqual(segProps, properties[pos + i], `${errorPrefix}\n${this.toString()}\nClient ${c.longClientId} does not match client ${this.clients[0].longClientId} properties at pos ${pos + i}`);
                        }
                    }
                    pos += seg.cachedLength;
                }
            });
        }
        if (opts?.clear === true) {
            this.roundLogLines.splice(1, this.roundLogLines.length);
            for (const [i, v] of this.roundLogLines[0].entries())
                this.paddings[i] = v.length;
            this.addNewLogLine(); // capture initial state
        }
        return baseText;
    }
    static validate(clients, title) {
        const logger = new TestClientLogger(clients, title);
        const result = logger.validate();
        logger.dispose();
        return result;
    }
    toString(excludeHeader = false) {
        let str = "";
        if (!excludeHeader) {
            str +=
                `_: Local State\n` +
                    `-: Deleted    ~:Deleted <= MinSeq\n` +
                    `*: Unacked Insert and Delete\n` +
                    `${this.clients[0].getCollabWindow().minSeq}: msn/offset\n` +
                    `Op format <seq>:<ref>:<client><type>@<side1><pos1>,<pos2><side2>\n` +
                    `sequence number represented as offset from msn. L means local.\n` +
                    `op types: 0) insert 1) remove 2) annotate 4) obliterate\n` +
                    `for obliterates: [] indicates that the range includes the position,\n` +
                    `and () indicates that the range excludes that position from the obliterate.\n`;
            if (this.title) {
                str += `${this.title}\n`;
            }
        }
        str += this.roundLogLines
            .filter((line) => line.some((c) => c.trim().length > 0))
            .map((line) => line.map((v, i) => v.padEnd(this.paddings[i])).join(" | "))
            .join("\n");
        return str;
    }
    addLogsToError(e) {
        if (e instanceof Error) {
            e.message += `\n${this.toString()}`;
            return e;
        }
        return new internal_1.LoggingError(`${e}\n${this.toString()}`);
    }
    static getSegString(client) {
        let acked = "";
        let local = "";
        const nodes = [...client.mergeTree.root.children];
        let parent = nodes[0]?.parent;
        while (nodes.length > 0) {
            const node = nodes.shift();
            if (node) {
                if (node.isLeaf()) {
                    if (node.parent !== parent) {
                        if (acked.length > 0) {
                            acked += " ";
                            local += " ";
                        }
                        parent = node.parent;
                    }
                    const text = textSegment_js_1.TextSegment.is(node) ? node.text : mergeTreeNodes_js_1.Marker.is(node) ? "¶" : undefined;
                    const insertionSeq = (0, segmentInfos_js_1.toInsertionInfo)(node)?.seq;
                    if (text !== undefined) {
                        const removedNode = toMoveOrRemove(node);
                        if (removedNode === undefined) {
                            if (insertionSeq === constants_js_1.UnassignedSequenceNumber) {
                                acked += "_".repeat(text.length);
                                local += text;
                            }
                            else {
                                acked += text;
                                local += " ".repeat(text.length);
                            }
                        }
                        else {
                            if (removedNode.seq === constants_js_1.UnassignedSequenceNumber) {
                                acked += "_".repeat(text.length);
                                local +=
                                    insertionSeq === constants_js_1.UnassignedSequenceNumber
                                        ? "*".repeat(text.length)
                                        : "-".repeat(text.length);
                            }
                            else {
                                const removedSymbol = (0, mergeTreeNodes_js_1.seqLTE)(removedNode.seq, client.getCollabWindow().minSeq)
                                    ? "~"
                                    : "-";
                                acked += removedSymbol.repeat(text.length);
                                local += " ".repeat(text.length);
                            }
                        }
                    }
                }
                else {
                    nodes.push(...node.children);
                }
            }
        }
        return { acked, local };
    }
}
exports.TestClientLogger = TestClientLogger;
function toMoveOrRemove(segment) {
    const mi = (0, segmentInfos_js_1.toMoveInfo)(segment);
    const ri = (0, segmentInfos_js_1.toRemovalInfo)(segment);
    if (mi !== undefined || ri !== undefined) {
        return {
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-non-null-asserted-optional-chain
            seq: mi?.movedSeq ?? ri?.removedSeq,
        };
    }
}
//# sourceMappingURL=testClientLogger.js.map