/**
 * Copyright 2025 Google LLC.
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { InvalidArgumentException, UnsupportedOperationException, } from '../../../protocol/protocol.js';
export class EmulationProcessor {
    #userContextStorage;
    #browsingContextStorage;
    #contextConfigStorage;
    constructor(browsingContextStorage, userContextStorage, contextConfigStorage) {
        this.#userContextStorage = userContextStorage;
        this.#browsingContextStorage = browsingContextStorage;
        this.#contextConfigStorage = contextConfigStorage;
    }
    async setGeolocationOverride(params) {
        if ('coordinates' in params && 'error' in params) {
            // Unreachable. Handled by params parser.
            throw new InvalidArgumentException('Coordinates and error cannot be set at the same time');
        }
        let geolocation = null;
        if ('coordinates' in params) {
            if ((params.coordinates?.altitude ?? null) === null &&
                (params.coordinates?.altitudeAccuracy ?? null) !== null) {
                throw new InvalidArgumentException('Geolocation altitudeAccuracy can be set only with altitude');
            }
            geolocation = params.coordinates;
        }
        else if ('error' in params) {
            if (params.error.type !== 'positionUnavailable') {
                // Unreachable. Handled by params parser.
                throw new InvalidArgumentException(`Unknown geolocation error ${params.error.type}`);
            }
            geolocation = params.error;
        }
        else {
            // Unreachable. Handled by params parser.
            throw new InvalidArgumentException(`Coordinates or error should be set`);
        }
        const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts);
        for (const browsingContextId of params.contexts ?? []) {
            this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
                geolocation,
            });
        }
        for (const userContextId of params.userContexts ?? []) {
            this.#contextConfigStorage.updateUserContextConfig(userContextId, {
                geolocation,
            });
        }
        await Promise.all(browsingContexts.map(async (context) => {
            // Actual value can be different from the one in params, e.g. in case of already
            // existing more granular setting.
            const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
            await context.setGeolocationOverride(config.geolocation ?? null);
        }));
        return {};
    }
    async setLocaleOverride(params) {
        const locale = params.locale ?? null;
        if (locale !== null && !isValidLocale(locale)) {
            throw new InvalidArgumentException(`Invalid locale "${locale}"`);
        }
        const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts);
        for (const browsingContextId of params.contexts ?? []) {
            this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
                locale,
            });
        }
        for (const userContextId of params.userContexts ?? []) {
            this.#contextConfigStorage.updateUserContextConfig(userContextId, {
                locale,
            });
        }
        await Promise.all(browsingContexts.map(async (context) => {
            // Actual value can be different from the one in params, e.g. in case of already
            // existing more granular setting.
            const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
            await Promise.all([
                context.setLocaleOverride(config.locale ?? null),
                // Set `AcceptLanguage` to locale.
                context.setUserAgentAndAcceptLanguage(config.userAgent, config.locale, config.clientHints),
            ]);
        }));
        return {};
    }
    async setScriptingEnabled(params) {
        const scriptingEnabled = params.enabled;
        const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts);
        for (const browsingContextId of params.contexts ?? []) {
            this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
                scriptingEnabled,
            });
        }
        for (const userContextId of params.userContexts ?? []) {
            this.#contextConfigStorage.updateUserContextConfig(userContextId, {
                scriptingEnabled,
            });
        }
        await Promise.all(browsingContexts.map(async (context) => {
            // Actual value can be different from the one in params, e.g. in case of already
            // existing more granular setting.
            const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
            await context.setScriptingEnabled(config.scriptingEnabled ?? null);
        }));
        return {};
    }
    async setScreenOrientationOverride(params) {
        const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts);
        for (const browsingContextId of params.contexts ?? []) {
            this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
                screenOrientation: params.screenOrientation,
            });
        }
        for (const userContextId of params.userContexts ?? []) {
            this.#contextConfigStorage.updateUserContextConfig(userContextId, {
                screenOrientation: params.screenOrientation,
            });
        }
        await Promise.all(browsingContexts.map(async (context) => {
            // Actual value can be different from the one in params, e.g. in case of already
            // existing more granular setting.
            const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
            await context.setViewport(config.viewport ?? null, config.devicePixelRatio ?? null, config.screenOrientation ?? null);
        }));
        return {};
    }
    async setScreenSettingsOverride(params) {
        const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts);
        for (const browsingContextId of params.contexts ?? []) {
            this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
                screenArea: params.screenArea,
            });
        }
        for (const userContextId of params.userContexts ?? []) {
            this.#contextConfigStorage.updateUserContextConfig(userContextId, {
                screenArea: params.screenArea,
            });
        }
        await Promise.all(browsingContexts.map(async (context) => {
            // Actual value can be different from the one in params, e.g. in case of already
            // existing more granular setting.
            const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
            await context.setViewport(config.viewport ?? null, config.devicePixelRatio ?? null, config.screenOrientation ?? null);
        }));
        return {};
    }
    /**
     * Returns a list of top-level browsing contexts.
     */
    async #getRelatedTopLevelBrowsingContexts(browsingContextIds, userContextIds, allowGlobal = false) {
        if (browsingContextIds === undefined && userContextIds === undefined) {
            if (allowGlobal) {
                return this.#browsingContextStorage.getTopLevelContexts();
            }
            throw new InvalidArgumentException('Either user contexts or browsing contexts must be provided');
        }
        if (browsingContextIds !== undefined && userContextIds !== undefined) {
            throw new InvalidArgumentException('User contexts and browsing contexts are mutually exclusive');
        }
        const result = [];
        if (browsingContextIds === undefined) {
            // userContextIds !== undefined
            if (userContextIds.length === 0) {
                throw new InvalidArgumentException('user context should be provided');
            }
            // Verify that all user contexts exist.
            await this.#userContextStorage.verifyUserContextIdList(userContextIds);
            for (const userContextId of userContextIds) {
                const topLevelBrowsingContexts = this.#browsingContextStorage
                    .getTopLevelContexts()
                    .filter((browsingContext) => browsingContext.userContext === userContextId);
                result.push(...topLevelBrowsingContexts);
            }
        }
        else {
            if (browsingContextIds.length === 0) {
                throw new InvalidArgumentException('browsing context should be provided');
            }
            for (const browsingContextId of browsingContextIds) {
                const browsingContext = this.#browsingContextStorage.getContext(browsingContextId);
                if (!browsingContext.isTopLevelContext()) {
                    throw new InvalidArgumentException('The command is only supported on the top-level context');
                }
                result.push(browsingContext);
            }
        }
        // Remove duplicates. Compare `BrowsingContextImpl` by reference is correct here, as
        // `browsingContextStorage` returns the same instance for the same id.
        return [...new Set(result).values()];
    }
    async setTimezoneOverride(params) {
        let timezone = params.timezone ?? null;
        if (timezone !== null && !isValidTimezone(timezone)) {
            throw new InvalidArgumentException(`Invalid timezone "${timezone}"`);
        }
        if (timezone !== null && isTimeZoneOffsetString(timezone)) {
            // CDP supports offset timezone with `GMT` prefix.
            timezone = `GMT${timezone}`;
        }
        const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts);
        for (const browsingContextId of params.contexts ?? []) {
            this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
                timezone,
            });
        }
        for (const userContextId of params.userContexts ?? []) {
            this.#contextConfigStorage.updateUserContextConfig(userContextId, {
                timezone,
            });
        }
        await Promise.all(browsingContexts.map(async (context) => {
            // Actual value can be different from the one in params, e.g. in case of already
            // existing more granular setting.
            const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
            await context.setTimezoneOverride(config.timezone ?? null);
        }));
        return {};
    }
    async setTouchOverride(params) {
        const maxTouchPoints = params.maxTouchPoints;
        const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts, true);
        for (const browsingContextId of params.contexts ?? []) {
            this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
                maxTouchPoints,
            });
        }
        for (const userContextId of params.userContexts ?? []) {
            this.#contextConfigStorage.updateUserContextConfig(userContextId, {
                maxTouchPoints,
            });
        }
        if (params.contexts === undefined && params.userContexts === undefined) {
            this.#contextConfigStorage.updateGlobalConfig({
                maxTouchPoints,
            });
        }
        await Promise.all(browsingContexts.map(async (context) => {
            // Actual value can be different from the one in params, e.g. in case of already
            // existing more granular setting.
            const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
            await context.setTouchOverride(config.maxTouchPoints ?? null);
        }));
        return {};
    }
    async setUserAgentOverrideParams(params) {
        if (params.userAgent === '') {
            throw new UnsupportedOperationException('empty user agent string is not supported');
        }
        const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts, true);
        for (const browsingContextId of params.contexts ?? []) {
            this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
                userAgent: params.userAgent,
            });
        }
        for (const userContextId of params.userContexts ?? []) {
            this.#contextConfigStorage.updateUserContextConfig(userContextId, {
                userAgent: params.userAgent,
            });
        }
        if (params.contexts === undefined && params.userContexts === undefined) {
            this.#contextConfigStorage.updateGlobalConfig({
                userAgent: params.userAgent,
            });
        }
        await Promise.all(browsingContexts.map(async (context) => {
            // Actual value can be different from the one in params, e.g. in case of already
            // existing more granular setting.
            const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
            await context.setUserAgentAndAcceptLanguage(config.userAgent, config.locale, config.clientHints);
        }));
        return {};
    }
    async setClientHintsOverride(params) {
        const clientHints = params.clientHints ?? null;
        // Get all relevant contexts to update:
        // 1. Specific browsing contexts (if provided).
        // 2. All contexts for specific user contexts (if provided).
        // 3. All top-level contexts (if global).
        const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts, true);
        for (const browsingContextId of params.contexts ?? []) {
            this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
                clientHints,
            });
        }
        for (const userContextId of params.userContexts ?? []) {
            this.#contextConfigStorage.updateUserContextConfig(userContextId, {
                clientHints,
            });
        }
        if (params.contexts === undefined && params.userContexts === undefined) {
            this.#contextConfigStorage.updateGlobalConfig({
                clientHints,
            });
        }
        await Promise.all(browsingContexts.map(async (context) => {
            // Actual value can be different from the one in params, e.g. in case of already
            // existing more granular setting.
            const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
            await context.setUserAgentAndAcceptLanguage(config.userAgent, config.locale, config.clientHints);
        }));
        return {};
    }
    async setNetworkConditions(params) {
        const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts, true);
        for (const browsingContextId of params.contexts ?? []) {
            this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
                emulatedNetworkConditions: params.networkConditions,
            });
        }
        for (const userContextId of params.userContexts ?? []) {
            this.#contextConfigStorage.updateUserContextConfig(userContextId, {
                emulatedNetworkConditions: params.networkConditions,
            });
        }
        if (params.contexts === undefined && params.userContexts === undefined) {
            this.#contextConfigStorage.updateGlobalConfig({
                emulatedNetworkConditions: params.networkConditions,
            });
        }
        if (params.networkConditions !== null &&
            params.networkConditions.type !== 'offline') {
            throw new UnsupportedOperationException(`Unsupported network conditions ${params.networkConditions.type}`);
        }
        await Promise.all(browsingContexts.map(async (context) => {
            // Actual value can be different from the one in params, e.g. in case of already
            // existing more granular setting.
            const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
            await context.setEmulatedNetworkConditions(config.emulatedNetworkConditions ?? null);
        }));
        return {};
    }
}
// Export for testing.
export function isValidLocale(locale) {
    try {
        new Intl.Locale(locale);
        return true;
    }
    catch (e) {
        if (e instanceof RangeError) {
            return false;
        }
        // Re-throw other errors
        throw e;
    }
}
// Export for testing.
export function isValidTimezone(timezone) {
    try {
        Intl.DateTimeFormat(undefined, { timeZone: timezone });
        return true;
    }
    catch (e) {
        if (e instanceof RangeError) {
            return false;
        }
        // Re-throw other errors
        throw e;
    }
}
// Export for testing.
export function isTimeZoneOffsetString(timezone) {
    return /^[+-](?:2[0-3]|[01]\d)(?::[0-5]\d)?$/.test(timezone);
}
//# sourceMappingURL=EmulationProcessor.js.map