"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.ensureBlobsShared = exports.attachHandle = exports.waitHandlePayloadShared = exports.unpackHandle = exports.blobToText = exports.textToBlob = exports.getSummaryContentsWithFormatValidation = exports.simulateAttach = exports.createTestMaterial = exports.MockStorageAdapter = exports.MockBlobStorage = exports.getDedupedStorageId = exports.getDedupedStorageIdForString = exports.getSerializedBlobForString = exports.MIN_TTL = void 0;
const node_assert_1 = require("node:assert");
const client_utils_1 = require("@fluid-internal/client-utils");
const internal_1 = require("@fluidframework/container-definitions/internal");
const internal_2 = require("@fluidframework/driver-definitions/internal");
const internal_3 = require("@fluidframework/runtime-utils/internal");
const internal_4 = require("@fluidframework/telemetry-utils/internal");
const uuid_1 = require("uuid");
const index_js_1 = require("../../blobManager/index.js");
exports.MIN_TTL = 24 * 60 * 60; // same as ODSP
const getSerializedBlobForString = (str) => (0, client_utils_1.bufferToString)((0, exports.textToBlob)(str), "base64");
exports.getSerializedBlobForString = getSerializedBlobForString;
const getDedupedStorageIdForString = async (str) => (0, exports.getDedupedStorageId)((0, exports.textToBlob)(str));
exports.getDedupedStorageIdForString = getDedupedStorageIdForString;
const getDedupedStorageId = async (blob) => (0, client_utils_1.gitHashFile)(client_utils_1.IsoBuffer.from((0, client_utils_1.bufferToString)(blob, "base64"), "base64"));
exports.getDedupedStorageId = getDedupedStorageId;
class MockBlobStorage {
    get blobsReceived() {
        return this._blobsReceived;
    }
    get blobsCreated() {
        return this._blobsCreated;
    }
    constructor(dedupe) {
        this.dedupe = dedupe;
        this.defaultMinTTL = exports.MIN_TTL;
        this.blobs = new Map();
        this.pendingBlobs = [];
        this.events = (0, client_utils_1.createEmitter)();
        this._blobsReceived = 0;
        this._blobsCreated = 0;
        this._paused = false;
        this.pause = () => {
            this._paused = true;
        };
        this.unpause = () => {
            this._paused = false;
            this.createAll();
        };
        this.createBlob = async (blob) => {
            const id = this.dedupe ? await (0, exports.getDedupedStorageId)(blob) : this.blobs.size.toString();
            this.pendingBlobs.push([id, blob]);
            this._blobsReceived++;
            const blobCreatedP = new Promise((resolve, reject) => {
                const onBlobCreated = (_id, _minTTLOverride) => {
                    if (_id === id) {
                        this.events.off("blobCreated", onBlobCreated);
                        this.events.off("blobCreateFailed", onBlobCreateFailed);
                        resolve({ minTTLOverride: _minTTLOverride });
                    }
                };
                const onBlobCreateFailed = (_id, error) => {
                    if (_id === id) {
                        this.events.off("blobCreated", onBlobCreated);
                        this.events.off("blobCreateFailed", onBlobCreateFailed);
                        reject(error);
                    }
                };
                this.events.on("blobCreated", onBlobCreated);
                this.events.on("blobCreateFailed", onBlobCreateFailed);
            });
            this.events.emit("blobReceived");
            if (!this._paused) {
                this.createAll();
            }
            const { minTTLOverride } = await blobCreatedP;
            this._blobsCreated++;
            return { id, minTTLInSeconds: minTTLOverride ?? this.defaultMinTTL };
        };
        this.readBlob = async (id) => {
            const blob = this.blobs.get(id);
            (0, node_assert_1.strict)(blob !== undefined, `Couldn't find blob ${id}`);
            return blob;
        };
        /**
         * Waits until at least one blob is available to be created. This is useful to confirm
         * the BlobManager is awaiting the upload to complete.
         */
        this.waitBlobAvailable = async () => {
            if (this.pendingBlobs.length === 0) {
                return new Promise((resolve) => {
                    const onBlobReceived = () => {
                        resolve();
                        this.events.off("blobReceived", onBlobReceived);
                    };
                    this.events.on("blobReceived", onBlobReceived);
                });
            }
        };
        this.createOne = (createOptions) => {
            const { error, minTTLOverride } = createOptions ?? {};
            const next = this.pendingBlobs.shift();
            (0, node_assert_1.strict)(next !== undefined, "Tried processing, but none to process");
            const [id, blob] = next;
            if (error === undefined) {
                this.blobs.set(id, blob);
                this.events.emit("blobCreated", id, minTTLOverride);
            }
            else {
                this.events.emit("blobCreateFailed", id, error);
            }
        };
        this.waitCreateOne = async (createOptions) => {
            (0, node_assert_1.strict)(this._paused, "waitCreateOne is only available in paused mode to avoid conflicting with normal blob creation");
            await this.waitBlobAvailable();
            this.createOne(createOptions);
        };
        this.createAll = () => {
            while (this.pendingBlobs.length > 0) {
                this.createOne();
            }
        };
    }
}
exports.MockBlobStorage = MockBlobStorage;
class MockStorageAdapter {
    get blobsReceived() {
        return this.getCurrentStorage().blobsReceived;
    }
    get blobsCreated() {
        return this.getCurrentStorage().blobsCreated;
    }
    constructor(attached) {
        this.attached = attached;
        this.events = (0, client_utils_1.createEmitter)();
        this.detachedStorage = new MockBlobStorage(false);
        this.attachedStorage = new MockBlobStorage(true);
        this.pause = () => this.getCurrentStorage().pause();
        this.unpause = () => this.getCurrentStorage().unpause();
        this.onBlobCreated = (id, minTTLOverride) => this.events.emit("blobCreated", id, minTTLOverride);
        this.onBlobCreateFailed = (id, error) => this.events.emit("blobCreateFailed", id, error);
        this.onBlobReceived = () => this.events.emit("blobReceived");
        this.simulateAttach = async (patchRedirectTable) => {
            (0, node_assert_1.strict)(!this.attached, "Can't simulate attach twice");
            // At least under current patterns, detached storage should always create blobs immediately.
            (0, node_assert_1.strict)(this.detachedStorage.pendingBlobs.length === 0, "Detached storage has pending blobs");
            // This is simulating the behavior in the loader layer during container attach (see attachment.ts).
            // We upload all of the blobs that we're holding in detached storage to the real storage,
            // and then call BlobManager.patchRedirectTable() with a mapping of the prior detached storage
            // IDs to their respective real storage IDs.
            const detachedToAttachedMappings = await Promise.all([...this.detachedStorage.blobs].map(async ([detachedStorageId, blob]) => {
                return this.attachedStorage.createBlob(blob).then(({ id: attachedStorageId }) => {
                    return [detachedStorageId, attachedStorageId];
                });
            }));
            const redirectTable = new Map(detachedToAttachedMappings);
            patchRedirectTable(redirectTable);
            this.attached = true;
            this.detachedStorage.events.off("blobCreated", this.onBlobCreated);
            this.detachedStorage.events.off("blobCreateFailed", this.onBlobCreateFailed);
            this.detachedStorage.events.off("blobReceived", this.onBlobReceived);
            this.attachedStorage.events.on("blobCreated", this.onBlobCreated);
            this.attachedStorage.events.on("blobCreateFailed", this.onBlobCreateFailed);
            this.attachedStorage.events.on("blobReceived", this.onBlobReceived);
        };
        this.getCurrentStorage = () => this.attached ? this.attachedStorage : this.detachedStorage;
        this.createBlob = async (blob) => this.getCurrentStorage().createBlob(blob);
        this.readBlob = async (id) => this.getCurrentStorage().readBlob(id);
        this.waitBlobAvailable = async () => this.getCurrentStorage().waitBlobAvailable();
        this.createOne = (createOptions) => this.getCurrentStorage().createOne(createOptions);
        this.waitCreateOne = async (createOptions) => this.getCurrentStorage().waitCreateOne(createOptions);
        if (attached) {
            this.attachedStorage.events.on("blobCreated", this.onBlobCreated);
            this.attachedStorage.events.on("blobCreateFailed", this.onBlobCreateFailed);
            this.attachedStorage.events.on("blobReceived", this.onBlobReceived);
        }
        else {
            this.detachedStorage.events.on("blobCreated", this.onBlobCreated);
            this.detachedStorage.events.on("blobCreateFailed", this.onBlobCreateFailed);
            this.detachedStorage.events.on("blobReceived", this.onBlobReceived);
        }
    }
}
exports.MockStorageAdapter = MockStorageAdapter;
class MockOrderingService {
    constructor() {
        this.unprocessedMessages = [];
        this.events = (0, client_utils_1.createEmitter)();
        this._messagesReceived = 0;
        this._messagesSequenced = 0;
        this._paused = false;
        this.pause = () => {
            this._paused = true;
        };
        this.unpause = () => {
            this._paused = false;
            this.sequenceAll();
        };
        /**
         * Waits until at least one message is available to be sequenced. This is useful to confirm
         * the BlobManager is awaiting the attach message's ack.
         */
        this.waitMessageAvailable = async () => {
            if (this.unprocessedMessages.length === 0) {
                return new Promise((resolve) => {
                    const onMessageReceived = (message) => {
                        resolve();
                        this.events.off("messageReceived", onMessageReceived);
                    };
                    this.events.on("messageReceived", onMessageReceived);
                });
            }
        };
        this.sequenceOne = () => {
            const message = this.unprocessedMessages.shift();
            (0, node_assert_1.strict)(message !== undefined, "Tried sequencing, but none to sequence");
            this._messagesSequenced++;
            // BlobManager only checks the metadata, so this cast is good enough.
            this.events.emit("messageSequenced", message);
        };
        this.waitSequenceOne = async () => {
            (0, node_assert_1.strict)(this._paused, "waitSequenceOne is only available in paused mode to avoid conflicting with normal sequencing");
            await this.waitMessageAvailable();
            this.sequenceOne();
        };
        // Sequence all unprocessed messages. The events emitted can be used to drive normal processing scenarios.
        this.sequenceAll = () => {
            while (this.unprocessedMessages.length > 0) {
                this.sequenceOne();
            }
        };
        this.dropOne = () => {
            const message = this.unprocessedMessages.shift();
            (0, node_assert_1.strict)(message !== undefined, "Tried dropping, but none to drop");
            this.events.emit("messageDropped", message);
        };
        this.waitDropOne = async () => {
            (0, node_assert_1.strict)(this._paused, "waitDropOne is only available in paused mode to avoid conflicting with normal sequencing");
            await this.waitMessageAvailable();
            this.dropOne();
        };
        // Drop all unprocessed messages. The events emitted can be used to drive resubmit scenarios.
        this.dropAll = () => {
            // Only drop the current unprocessed messages, since this will trigger resubmit and we don't
            // necessarily want to drop those too.
            const numberToDrop = this.unprocessedMessages.length;
            for (let i = 0; i < numberToDrop; i++) {
                this.dropOne();
            }
        };
        this.sendBlobAttachMessage = (clientId, localId, remoteId) => {
            const message = {
                clientId,
                metadata: { localId, blobId: remoteId },
            };
            this.unprocessedMessages.push(message);
            this._messagesReceived++;
            this.events.emit("messageReceived", message);
            if (!this._paused) {
                this.sequenceAll();
            }
        };
    }
    get messagesReceived() {
        return this._messagesReceived;
    }
    get messagesSequenced() {
        return this._messagesSequenced;
    }
}
class MockGarbageCollector {
    constructor() {
        this.deletedBlobs = new Set();
        this.simulateBlobDeletion = (blobPath) => {
            this.deletedBlobs.add(blobPath);
        };
        this.isBlobDeleted = (blobPath) => {
            return this.deletedBlobs.has(blobPath);
        };
    }
}
class MockRuntime extends client_utils_1.TypedEventEmitter {
    get attachState() {
        return this._attachState;
    }
    set attachState(value) {
        this._attachState = value;
        if (this._attachState === internal_1.AttachState.Attached) {
            this.emit("attached");
        }
    }
    get isAttached() {
        return this._attachState === internal_1.AttachState.Attached;
    }
    constructor(baseLogger, attached) {
        super();
        this.baseLogger = baseLogger;
        this._attachState = internal_1.AttachState.Detached;
        this.disposed = false;
        this._attachState = attached ? internal_1.AttachState.Attached : internal_1.AttachState.Detached;
    }
}
const createTestMaterial = (overrides) => {
    const clientId = overrides?.clientId ?? (0, uuid_1.v4)();
    const attached = overrides?.attached ?? true;
    const mockBlobStorage = overrides?.mockBlobStorage ?? new MockStorageAdapter(attached);
    const mockOrderingService = overrides?.mockOrderingService ?? new MockOrderingService();
    const mockGarbageCollector = overrides?.mockGarbageCollector ?? new MockGarbageCollector();
    const mockLogger = overrides?.mockLogger ?? new internal_4.MockLogger();
    const mockRuntime = overrides?.mockRuntime ?? new MockRuntime(mockLogger, attached);
    const blobManagerLoadInfo = overrides?.blobManagerLoadInfo ?? {};
    const pendingBlobs = overrides?.pendingBlobs ?? undefined;
    const createBlobPayloadPending = overrides?.createBlobPayloadPending ?? false;
    const blobManager = new index_js_1.BlobManager({
        // The routeContext is only needed by the BlobHandles to determine isAttached, so this
        // cast is good enough
        routeContext: mockRuntime,
        blobManagerLoadInfo,
        storage: mockBlobStorage,
        sendBlobAttachMessage: (localId, storageId) => mockOrderingService.sendBlobAttachMessage(clientId, localId, storageId),
        blobRequested: () => undefined,
        isBlobDeleted: mockGarbageCollector.isBlobDeleted,
        runtime: mockRuntime,
        pendingBlobs,
        createBlobPayloadPending,
    });
    mockOrderingService.events.on("messageSequenced", (message) => {
        blobManager.processBlobAttachMessage(message, message.clientId === clientId);
    });
    mockOrderingService.events.on("messageDropped", (message) => {
        if (message.clientId === clientId) {
            blobManager.reSubmit(message.metadata);
        }
    });
    return {
        clientId,
        attached,
        mockBlobStorage,
        mockOrderingService,
        mockGarbageCollector,
        mockLogger,
        mockRuntime,
        blobManagerLoadInfo,
        pendingBlobs,
        createBlobPayloadPending,
        blobManager,
    };
};
exports.createTestMaterial = createTestMaterial;
const simulateAttach = async (storage, runtime, blobManager) => {
    (0, node_assert_1.strict)(runtime.attachState === internal_1.AttachState.Detached, "Container must be detached");
    await storage.simulateAttach(blobManager.patchRedirectTable);
    (0, node_assert_1.strict)(runtime.attachState === internal_1.AttachState.Detached, "Container must be detached");
    // Blob storage transfer and redirect table set happens before the runtime transitions to Attaching.
    runtime.attachState = internal_1.AttachState.Attaching;
    // TODO: Probably want to test stuff between these states
    runtime.attachState = internal_1.AttachState.Attached;
};
exports.simulateAttach = simulateAttach;
const getSummaryContentsWithFormatValidation = (blobManager) => {
    const summary = blobManager.summarize();
    let ids;
    let redirectTable;
    for (const [key, summaryObject] of Object.entries(summary.summary.tree)) {
        if (summaryObject.type === internal_2.SummaryType.Attachment) {
            ids ??= [];
            ids.push(summaryObject.id);
        }
        else {
            node_assert_1.strict.strictEqual(key, index_js_1.redirectTableBlobName);
            (0, node_assert_1.strict)(summaryObject.type === internal_2.SummaryType.Blob);
            (0, node_assert_1.strict)(typeof summaryObject.content === "string");
            redirectTable = [
                ...new Map(JSON.parse(summaryObject.content)).entries(),
            ];
        }
    }
    return { ids, redirectTable };
};
exports.getSummaryContentsWithFormatValidation = getSummaryContentsWithFormatValidation;
const textToBlob = (text) => {
    const encoder = new TextEncoder();
    return encoder.encode(text).buffer;
};
exports.textToBlob = textToBlob;
const blobToText = (blob) => {
    const decoder = new TextDecoder();
    return decoder.decode(blob);
};
exports.blobToText = blobToText;
const unpackHandle = (handle) => {
    const internalHandle = (0, internal_3.toFluidHandleInternal)(handle);
    const pathParts = internalHandle.absolutePath.split("/");
    return {
        absolutePath: internalHandle.absolutePath,
        localId: pathParts[2],
        payloadPending: (0, internal_3.isFluidHandleInternalPayloadPending)(internalHandle),
    };
};
exports.unpackHandle = unpackHandle;
const waitHandlePayloadShared = async (handle) => {
    if ((0, internal_3.isLocalFluidHandle)(handle) && handle.payloadState !== "shared") {
        return new Promise((resolve, reject) => {
            const onPayloadShared = () => {
                resolve();
                handle.events.off("payloadShared", onPayloadShared);
                handle.events.off("payloadShareFailed", onPayloadShareFailed);
            };
            const onPayloadShareFailed = (error) => {
                // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
                reject(error);
                handle.events.off("payloadShared", onPayloadShared);
                handle.events.off("payloadShareFailed", onPayloadShareFailed);
            };
            handle.events.on("payloadShared", onPayloadShared);
            handle.events.on("payloadShareFailed", onPayloadShareFailed);
        });
    }
};
exports.waitHandlePayloadShared = waitHandlePayloadShared;
const attachHandle = (handle) => {
    const internalHandle = (0, internal_3.toFluidHandleInternal)(handle);
    if (!internalHandle.isAttached) {
        internalHandle.attachGraph();
    }
};
exports.attachHandle = attachHandle;
const ensureBlobsShared = async (handles) => {
    return Promise.all(handles.map(async (handle) => {
        (0, exports.attachHandle)(handle);
        return (0, exports.waitHandlePayloadShared)(handle);
    }));
};
exports.ensureBlobsShared = ensureBlobsShared;
//# sourceMappingURL=blobTestUtils.js.map