"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
const node_assert_1 = require("node:assert");
const container_definitions_1 = require("@fluidframework/container-definitions");
const internal_1 = require("@fluidframework/container-definitions/internal");
const internal_2 = require("@fluidframework/telemetry-utils/internal");
const internal_3 = require("@fluidframework/test-runtime-utils/internal");
const containerRuntime_js_1 = require("../containerRuntime.js");
const dataStoreRegistry_js_1 = require("../dataStoreRegistry.js");
const testExtensionId = "test:extension";
const extensionCompatibilityDetails = {
    generation: 1,
    version: "1.0.0",
    capabilities: new Set([]),
};
class TestExtension {
    constructor(host) {
        this.compatibility = extensionCompatibilityDetails;
        this.extension = this;
        this.interface = {
            get connectedToService() {
                return host.getJoinedStatus() !== "disconnected";
            },
            events: host.events,
        };
    }
    handleVersionOrCapabilitiesMismatch(_thisExistingInstantiation, _newCompatibilityRequest) {
        throw new Error("compat mismatch with TestExtension is not expected");
    }
    onNewUse() {
        // No-op
    }
}
class TestExtensionFactoryClass {
    constructor() {
        this.hostRequirements = {
            minSupportedGeneration: 1,
            requiredFeatures: [],
        };
        this.instanceExpectations = extensionCompatibilityDetails;
    }
    resolvePriorInstantiation(existingEntry) {
        throw new Error("compat mismatch with TestExtension is not expected");
    }
    instantiateExtension(host) {
        return new TestExtension(host);
    }
    [Symbol.hasInstance](instance) {
        return instance instanceof TestExtension;
    }
}
const TestExtensionFactory = new TestExtensionFactoryClass();
class MockContext {
    constructor(isReadonly = false) {
        this.deltaManager = new internal_3.MockDeltaManager();
        this.quorum = new internal_3.MockQuorumClients();
        this.storage = {};
        this.baseSnapshot = undefined;
        this.options = {};
        this.loader = {};
        this.clientDetails = { capabilities: { interactive: true } };
        this.scope = {};
        this.id = "mockId";
        this.taggedLogger = new internal_2.MockLogger();
        this.updateDirtyContainerState = () => { };
        this.getLoadedFromVersion = () => undefined;
        this.submitBatchFn = () => 1;
        this.submitSummaryFn = () => 1;
        this.submitSignalFn = () => { };
        this.submitFn = () => 1;
        this.disposeFn = () => { };
        this.closeFn = () => { };
        this.getAbsoluteUrl = async () => "mockUrl";
        this.getConnectionState = () => this.connectionState;
        this.audience = new internal_3.MockAudience();
        // signalAudience is optional to allow simulating older loader (create mock, then delete this property)
        this.signalAudience = new internal_3.MockAudience();
        // State for testing purposes
        this.connectionState = internal_1.ConnectionState.Disconnected;
        this.isReadonly = isReadonly;
    }
    get clientId() {
        return this.audience.getSelf()?.clientId;
    }
    get connected() {
        return this.connectionState === internal_1.ConnectionState.Connected;
    }
    get attachState() {
        return container_definitions_1.AttachState.Attached;
    }
    setConnectionState(connectionState, clientId) {
        this.connectionState = connectionState;
        if (clientId !== undefined) {
            this.signalAudience?.setCurrentClientId(clientId);
            if (connectionState !== internal_1.ConnectionState.CatchingUp) {
                this.audience.setCurrentClientId(clientId);
            }
        }
    }
}
async function createRuntimeWithMockContext(isReadonly = false) {
    const context = new MockContext(isReadonly);
    const { runtime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
        context,
        registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
        existing: false,
        provideEntryPoint: async () => ({}),
    });
    return { runtime, context };
}
async function createRuntimeWithoutConnectionState(isReadonly = false) {
    const context = new MockContext(isReadonly);
    // Delete the getConnectionState method before creating the runtime
    delete context.getConnectionState;
    delete context.signalAudience;
    const { runtime } = await containerRuntime_js_1.ContainerRuntime.loadRuntime2({
        context,
        registry: new dataStoreRegistry_js_1.FluidDataStoreRegistry([]),
        existing: false,
        provideEntryPoint: async () => ({}),
    });
    return { runtime, context };
}
function updateConnectionState(runtime, context, connectionState, clientId) {
    context.setConnectionState(connectionState, clientId);
    switch (connectionState) {
        case internal_1.ConnectionState.EstablishingConnection: {
            runtime.setConnectionStatus({
                connectionState,
                canSendOps: false,
                readonly: context.isReadonly,
            });
            break;
        }
        case internal_1.ConnectionState.CatchingUp: {
            (0, node_assert_1.strict)(clientId !== undefined, "clientId must be provided when catching up");
            runtime.setConnectionStatus({
                connectionState,
                pendingClientConnectionId: clientId,
                canSendOps: false,
                readonly: context.isReadonly,
            });
            break;
        }
        case internal_1.ConnectionState.Connected: {
            (0, node_assert_1.strict)(clientId !== undefined, "clientId must be provided when connected");
            runtime.setConnectionStatus({
                connectionState,
                clientConnectionId: clientId,
                canSendOps: !context.isReadonly,
                readonly: context.isReadonly,
            });
            break;
        }
        default: {
            runtime.setConnectionStatus({
                connectionState,
                canSendOps: false,
                readonly: context.isReadonly,
                priorPendingClientConnectionId: context.signalAudience?.getSelf()?.clientId,
                priorConnectedClientConnectionId: context.clientId,
            });
        }
    }
}
function setupExtensionEventListeners(extensionInterface) {
    const events = [];
    extensionInterface.events.on("joined", ({ clientId, canWrite }) => {
        events.push({ type: "joined", clientId, canWrite });
    });
    extensionInterface.events.on("disconnected", () => {
        events.push({ type: "disconnected" });
    });
    extensionInterface.events.on("operabilityChanged", (canWrite) => {
        events.push({ type: "operabilityChanged", canWrite });
    });
    return events;
}
describe("Runtime", () => {
    let runtime;
    let context;
    describe("Container Runtime", () => {
        describe("Container Extension", () => {
            let extension;
            beforeEach(async () => {
                const setup = await createRuntimeWithMockContext();
                runtime = setup.runtime;
                context = setup.context;
                extension = runtime.acquireExtension(testExtensionId, TestExtensionFactory);
            });
            describe("connection status", () => {
                it("should return true when context is `Connected`", async () => {
                    updateConnectionState(runtime, context, internal_1.ConnectionState.Connected, "mockClientId");
                    node_assert_1.strict.strictEqual(extension.connectedToService, true, "Extension should be connected");
                });
                it("should return false when context is `CatchingUp`", async () => {
                    updateConnectionState(runtime, context, internal_1.ConnectionState.CatchingUp, "mockClientId");
                    node_assert_1.strict.strictEqual(extension.connectedToService, true, "Extension should be connected during CatchingUp state");
                });
                it("should return false when context is `Disconnected`", async () => {
                    updateConnectionState(runtime, context, internal_1.ConnectionState.Disconnected);
                    node_assert_1.strict.strictEqual(extension.connectedToService, false, "Extension should not be connected when disconnected");
                });
                it("should return false when context is `EstablishingConnection`", async () => {
                    updateConnectionState(runtime, context, internal_1.ConnectionState.EstablishingConnection);
                    node_assert_1.strict.strictEqual(extension.connectedToService, false, "Extension should not be connected when establishing connection");
                });
                describe("fallback behavior when getConnectionState is undefined", () => {
                    let fallbackRuntime;
                    let fallbackContext;
                    let fallbackExtension;
                    beforeEach(async () => {
                        // Create runtime without getConnectionState to test fallback behavior
                        const setup = await createRuntimeWithoutConnectionState();
                        fallbackRuntime = setup.runtime;
                        fallbackContext = setup.context;
                        fallbackExtension = fallbackRuntime.acquireExtension(testExtensionId, TestExtensionFactory);
                    });
                    it("should fallback to canSendOps when context is `Connected`", async () => {
                        updateConnectionState(fallbackRuntime, fallbackContext, internal_1.ConnectionState.Connected, "mockClientId");
                        node_assert_1.strict.strictEqual(fallbackExtension.connectedToService, true, "Extension should fallback to runtime.connected when connectionState is undefined");
                    });
                    it("should fallback to canSendOps when context is `Disconnected`", async () => {
                        updateConnectionState(fallbackRuntime, fallbackContext, internal_1.ConnectionState.Disconnected);
                        node_assert_1.strict.strictEqual(fallbackExtension.connectedToService, false, "Extension should fallback to runtime.connected when connectionState is undefined and runtime is disconnected");
                    });
                });
            });
            describe("event handling", () => {
                let events;
                beforeEach(() => {
                    events = setupExtensionEventListeners(extension);
                });
                it("should handle dynamic connection state transitions - write client", async () => {
                    // Initially disconnected
                    node_assert_1.strict.strictEqual(extension.connectedToService, false, "Extension should initially be disconnected");
                    node_assert_1.strict.strictEqual(events.length, 0, "Should have no events initially");
                    // Transition to EstablishingConnection
                    updateConnectionState(runtime, context, internal_1.ConnectionState.EstablishingConnection);
                    node_assert_1.strict.strictEqual(extension.connectedToService, false, "Extension should remain disconnected during EstablishingConnection");
                    node_assert_1.strict.strictEqual(events.length, 0, "Should have no events during EstablishingConnection");
                    // Transition to CatchingUp
                    updateConnectionState(runtime, context, internal_1.ConnectionState.CatchingUp, "mockClientId");
                    node_assert_1.strict.strictEqual(events.length, 1, "Should have received one joined event");
                    node_assert_1.strict.deepStrictEqual(events[0], { type: "joined", clientId: "mockClientId", canWrite: false }, "First event should be joined for reading");
                    node_assert_1.strict.strictEqual(extension.connectedToService, true, "Extension should be connected during CatchingUp");
                    // Transition to Connected
                    updateConnectionState(runtime, context, internal_1.ConnectionState.Connected, "mockClientId");
                    node_assert_1.strict.strictEqual(events.length, 2, "Should have received two events total");
                    node_assert_1.strict.deepStrictEqual(events[1], { type: "operabilityChanged", canWrite: true }, "Second event should be operabilityChanged to can write");
                    node_assert_1.strict.strictEqual(extension.connectedToService, true, "Extension should be connected when fully Connected");
                    // Transition back to Disconnected
                    updateConnectionState(runtime, context, internal_1.ConnectionState.Disconnected);
                    node_assert_1.strict.strictEqual(events.length, 3, "Should have received three events total");
                    node_assert_1.strict.deepStrictEqual(events[2], { type: "disconnected" }, "Third event should be disconnected");
                    node_assert_1.strict.strictEqual(extension.connectedToService, false, "Extension should be disconnected after transition back to Disconnected");
                });
                it("should handle dynamic connection state transitions - read client", async () => {
                    // Create a read-only runtime setup
                    const readOnlySetup = await createRuntimeWithMockContext(true /* readonly */);
                    const readOnlyRuntime = readOnlySetup.runtime;
                    const readOnlyContext = readOnlySetup.context;
                    const readOnlyExtension = readOnlyRuntime.acquireExtension(testExtensionId, TestExtensionFactory);
                    const readOnlyEvents = setupExtensionEventListeners(readOnlyExtension);
                    // Initially disconnected
                    node_assert_1.strict.strictEqual(readOnlyExtension.connectedToService, false, "Extension should initially be disconnected");
                    node_assert_1.strict.strictEqual(readOnlyEvents.length, 0, "Should have no events initially");
                    // Transition to EstablishingConnection
                    updateConnectionState(readOnlyRuntime, readOnlyContext, internal_1.ConnectionState.EstablishingConnection);
                    node_assert_1.strict.strictEqual(readOnlyExtension.connectedToService, false, "Extension should remain disconnected during EstablishingConnection");
                    node_assert_1.strict.strictEqual(readOnlyEvents.length, 0, "Should have no events during EstablishingConnection");
                    // Transition to CatchingUp
                    updateConnectionState(readOnlyRuntime, readOnlyContext, internal_1.ConnectionState.CatchingUp, "mockClientId");
                    node_assert_1.strict.strictEqual(readOnlyExtension.connectedToService, true, "Extension should be connected during CatchingUp");
                    node_assert_1.strict.strictEqual(readOnlyEvents.length, 1, "Should have received one event: joined for reading only");
                    node_assert_1.strict.deepStrictEqual(readOnlyEvents[0], { type: "joined", clientId: "mockClientId", canWrite: false }, "Event should be joined for reading");
                    // Transition to Connected
                    updateConnectionState(readOnlyRuntime, readOnlyContext, internal_1.ConnectionState.Connected, "mockClientId");
                    node_assert_1.strict.strictEqual(readOnlyEvents.length, 1, "Should have received one event total");
                    node_assert_1.strict.strictEqual(readOnlyExtension.connectedToService, true, "Extension should be connected when fully Connected");
                    // Transition back to Disconnected
                    updateConnectionState(readOnlyRuntime, readOnlyContext, internal_1.ConnectionState.Disconnected);
                    node_assert_1.strict.strictEqual(readOnlyEvents.length, 2, "Should have received two events total");
                    node_assert_1.strict.deepStrictEqual(readOnlyEvents[1], { type: "disconnected" }, "Second event should be disconnected");
                    node_assert_1.strict.strictEqual(readOnlyExtension.connectedToService, false, "Extension should be disconnected after transition back to Disconnected");
                });
                it("should handle connection type changes", async () => {
                    updateConnectionState(runtime, context, internal_1.ConnectionState.Connected, "mockClientId");
                    // Should have initial joined event
                    node_assert_1.strict.strictEqual(events.length, 1, "Should have received initial joined event");
                    node_assert_1.strict.deepStrictEqual(events[0], { type: "joined", clientId: "mockClientId", canWrite: true }, "Initial event should be joined with write access");
                    // Act: forceReadonly
                    // forceReadonly would change the connection type of container
                    // This "readonly" event from deltaManager would then trigger
                    // setConnectionState to be called with canSendOps being false (Connected && !readonly)
                    runtime.setConnectionState(false, context.clientId);
                    updateConnectionState(runtime, context, internal_1.ConnectionState.Connected, "newMockClientId");
                    node_assert_1.strict.strictEqual(events.length, 3, "Should have received three events total: joined + two operabilityChanged");
                    node_assert_1.strict.deepStrictEqual(events[1], { type: "operabilityChanged", canWrite: false }, "Second event should indicate operability changed to read-only");
                    node_assert_1.strict.deepStrictEqual(events[2], { type: "operabilityChanged", canWrite: true }, "Third event should indicate operability changed to writable");
                });
                it("should still emit joined and disconnected events when getConnectionState is undefined", async () => {
                    // Create a new runtime without getConnectionState to test fallback behavior
                    const fallbackSetup = await createRuntimeWithoutConnectionState();
                    const fallbackRuntime = fallbackSetup.runtime;
                    const fallbackContext = fallbackSetup.context;
                    const fallbackExtension = fallbackRuntime.acquireExtension(testExtensionId, TestExtensionFactory);
                    const fallbackEvents = setupExtensionEventListeners(fallbackExtension);
                    // Connect
                    updateConnectionState(fallbackRuntime, fallbackContext, internal_1.ConnectionState.Connected, "mockClientId");
                    node_assert_1.strict.strictEqual(fallbackExtension.connectedToService, true, "Extension should be connected");
                    node_assert_1.strict.strictEqual(fallbackEvents.length, 1, "Should have received one joined event");
                    node_assert_1.strict.deepStrictEqual(fallbackEvents[0], { type: "joined", clientId: "mockClientId", canWrite: true }, "Event should be joined with canWrite = true when getConnectionState is undefined");
                    // Disconnect
                    updateConnectionState(fallbackRuntime, fallbackContext, internal_1.ConnectionState.Disconnected);
                    node_assert_1.strict.strictEqual(fallbackExtension.connectedToService, false, "Extension should be disconnected");
                    node_assert_1.strict.strictEqual(fallbackEvents.length, 2, "Should have received two events total");
                    node_assert_1.strict.deepStrictEqual(fallbackEvents[1], { type: "disconnected" }, "Second event should be disconnected");
                });
            });
        });
    });
});
//# sourceMappingURL=containerRuntime.extensions.spec.js.map