// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import type * as ProtocolProxyApi from '../../generated/protocol-proxy-api.js';
import type * as Protocol from '../../generated/protocol.js';
import * as Common from '../common/common.js';
import * as Host from '../host/host.js';
import type * as Platform from '../platform/platform.js';

import {DebuggerModel, type FunctionDetails} from './DebuggerModel.js';
import {HeapProfilerModel} from './HeapProfilerModel.js';
import {
  RemoteFunction,
  RemoteObject,
  RemoteObjectImpl,
  RemoteObjectProperty,
  type ScopeRef,
  ScopeRemoteObject,
} from './RemoteObject.js';
import {SDKModel} from './SDKModel.js';
import {Capability, type Target, Type} from './Target.js';

export class RuntimeModel extends SDKModel<EventTypes> {
  readonly agent: ProtocolProxyApi.RuntimeApi;
  readonly #executionContextById = new Map<number, ExecutionContext>();
  #executionContextComparator: (arg0: ExecutionContext, arg1: ExecutionContext) => number = ExecutionContext.comparator;
  constructor(target: Target) {
    super(target);

    this.agent = target.runtimeAgent();
    this.target().registerRuntimeDispatcher(new RuntimeDispatcher(this));
    void this.agent.invoke_enable();

    const settings = this.target().targetManager().context.get(Common.Settings.Settings);
    if (settings.moduleSetting('custom-formatters').get()) {
      void this.agent.invoke_setCustomObjectFormatterEnabled({enabled: true});
    }

    settings.moduleSetting('custom-formatters').addChangeListener(this.customFormattersStateChanged.bind(this));
  }

  static isSideEffectFailure(response: Protocol.Runtime.EvaluateResponse|EvaluationResult): boolean {
    const exceptionDetails = 'exceptionDetails' in response && response.exceptionDetails;
    return Boolean(
        exceptionDetails &&
        exceptionDetails.exception?.description?.startsWith('EvalError: Possible side-effect in debug-evaluate'));
  }

  debuggerModel(): DebuggerModel {
    return this.target().model(DebuggerModel) as DebuggerModel;
  }

  heapProfilerModel(): HeapProfilerModel {
    return this.target().model(HeapProfilerModel) as HeapProfilerModel;
  }

  executionContexts(): ExecutionContext[] {
    return [...this.#executionContextById.values()].sort(this.executionContextComparator());
  }

  setExecutionContextComparator(comparator: (arg0: ExecutionContext, arg1: ExecutionContext) => number): void {
    this.#executionContextComparator = comparator;
  }

  /**
   * comparator
   */
  executionContextComparator(): (arg0: ExecutionContext, arg1: ExecutionContext) => number {
    return this.#executionContextComparator;
  }

  defaultExecutionContext(): ExecutionContext|null {
    for (const context of this.executionContexts()) {
      if (context.isDefault) {
        return context;
      }
    }
    return null;
  }

  executionContext(id: number): ExecutionContext|null {
    return this.#executionContextById.get(id) || null;
  }

  executionContextCreated(context: Protocol.Runtime.ExecutionContextDescription): void {
    const data = context.auxData || {isDefault: true};
    const executionContext = new ExecutionContext(
        this, context.id, context.uniqueId, context.name, context.origin as Platform.DevToolsPath.UrlString,
        data['isDefault'], data['frameId']);
    this.#executionContextById.set(executionContext.id, executionContext);
    this.dispatchEventToListeners(Events.ExecutionContextCreated, executionContext);
  }

  executionContextDestroyed(executionContextId: number): void {
    const executionContext = this.#executionContextById.get(executionContextId);
    if (!executionContext) {
      return;
    }
    this.debuggerModel().executionContextDestroyed(executionContext);
    this.#executionContextById.delete(executionContextId);
    this.dispatchEventToListeners(Events.ExecutionContextDestroyed, executionContext);
  }

  fireExecutionContextOrderChanged(): void {
    this.dispatchEventToListeners(Events.ExecutionContextOrderChanged, this);
  }

  executionContextsCleared(): void {
    this.debuggerModel().globalObjectCleared();
    const contexts = this.executionContexts();
    this.#executionContextById.clear();
    for (let i = 0; i < contexts.length; ++i) {
      this.dispatchEventToListeners(Events.ExecutionContextDestroyed, contexts[i]);
    }
  }

  createRemoteObject(payload: Protocol.Runtime.RemoteObject): RemoteObject {
    console.assert(typeof payload === 'object', 'Remote object payload should only be an object');
    return new RemoteObjectImpl(
        this, payload.objectId, payload.type, payload.subtype, payload.value, payload.unserializableValue,
        payload.description, payload.preview, payload.customPreview, payload.className);
  }

  createScopeRemoteObject(payload: Protocol.Runtime.RemoteObject, scopeRef: ScopeRef): RemoteObject {
    return new ScopeRemoteObject(
        this, payload.objectId, scopeRef, payload.type, payload.subtype, payload.value, payload.unserializableValue,
        payload.description, payload.preview);
  }

  createRemoteObjectFromPrimitiveValue(value: string|number|bigint|boolean|undefined): RemoteObject {
    const type = typeof value;
    let unserializableValue: string|undefined = undefined;
    const unserializableDescription = RemoteObject.unserializableDescription(value);
    if (unserializableDescription !== null) {
      unserializableValue = (unserializableDescription);
    }
    if (typeof unserializableValue !== 'undefined') {
      value = undefined;
    }
    return new RemoteObjectImpl(this, undefined, type, undefined, value, unserializableValue);
  }

  createRemotePropertyFromPrimitiveValue(name: string, value: string|number|boolean): RemoteObjectProperty {
    return new RemoteObjectProperty(name, this.createRemoteObjectFromPrimitiveValue(value));
  }

  discardConsoleEntries(): void {
    void this.agent.invoke_discardConsoleEntries();
  }

  releaseObjectGroup(objectGroup: string): void {
    void this.agent.invoke_releaseObjectGroup({objectGroup});
  }

  releaseEvaluationResult(result: EvaluationResult): void {
    if ('object' in result && result.object) {
      result.object.release();
    }
    if ('exceptionDetails' in result && result.exceptionDetails?.exception) {
      const exception = result.exceptionDetails.exception;
      const exceptionObject = this.createRemoteObject({type: exception.type, objectId: exception.objectId});
      exceptionObject.release();
    }
  }

  runIfWaitingForDebugger(): void {
    void this.agent.invoke_runIfWaitingForDebugger();
  }

  private customFormattersStateChanged({data: enabled}: Common.EventTarget.EventTargetEvent<boolean>): void {
    void this.agent.invoke_setCustomObjectFormatterEnabled({enabled});
  }

  async compileScript(
      expression: string, sourceURL: string, persistScript: boolean,
      executionContextId: Protocol.Runtime.ExecutionContextId): Promise<CompileScriptResult|null> {
    const response = await this.agent.invoke_compileScript({
      expression,
      sourceURL,
      persistScript,
      executionContextId,
    });

    if (response.getError()) {
      console.error(response.getError());
      return null;
    }
    return {scriptId: response.scriptId, exceptionDetails: response.exceptionDetails};
  }

  async runScript(
      scriptId: Protocol.Runtime.ScriptId, executionContextId: Protocol.Runtime.ExecutionContextId,
      objectGroup?: string, silent?: boolean, includeCommandLineAPI?: boolean, returnByValue?: boolean,
      generatePreview?: boolean, awaitPromise?: boolean): Promise<EvaluationResult> {
    const response = await this.agent.invoke_runScript({
      scriptId,
      executionContextId,
      objectGroup,
      silent,
      includeCommandLineAPI,
      returnByValue,
      generatePreview,
      awaitPromise,
    });

    const error = response.getError();
    if (error) {
      console.error(error);
      return {error};
    }
    return {object: this.createRemoteObject(response.result), exceptionDetails: response.exceptionDetails};
  }

  async queryObjects(prototype: RemoteObject): Promise<QueryObjectResult> {
    if (!prototype.objectId) {
      return {error: 'Prototype should be an Object.'};
    }
    const response =
        await this.agent.invoke_queryObjects({prototypeObjectId: prototype.objectId, objectGroup: 'console'});
    const error = response.getError();
    if (error) {
      console.error(error);
      return {error};
    }
    return {objects: this.createRemoteObject(response.objects)};
  }

  async isolateId(): Promise<string> {
    const response = await this.agent.invoke_getIsolateId();
    if (response.getError() || !response.id) {
      return this.target().id();
    }
    return response.id;
  }

  async heapUsage(): Promise<{
    usedSize: number,
    totalSize: number,
    // Available after V8 13.4. Node.js has not yet been released with this version of V8 yet.
    embedderHeapUsedSize?: number,
    backingStorageSize?: number,
  }|null> {
    const result = await this.agent.invoke_getHeapUsage();
    return result.getError() ? null : result;
  }

  inspectRequested(payload: Protocol.Runtime.RemoteObject, hints: unknown, executionContextId?: number): void {
    const object = this.createRemoteObject(payload);

    if (hints !== null && typeof hints === 'object') {
      if ('copyToClipboard' in hints && Boolean(hints.copyToClipboard)) {
        this.copyRequested(object);
        return;
      }

      if ('queryObjects' in hints && hints.queryObjects) {
        void this.queryObjectsRequested(object, executionContextId);
        return;
      }
    }

    if (object.isNode()) {
      const omitFocus = hints !== null && typeof hints === 'object' && 'omitFocus' in hints && Boolean(hints.omitFocus);
      void Common.Revealer.reveal(object, omitFocus).then(object.release.bind(object));
      return;
    }

    if (object.type === 'function') {
      void RemoteFunction.objectAsFunction(object).targetFunctionDetails().then(didGetDetails);
      return;
    }

    function didGetDetails(response: FunctionDetails|null): void {
      object.release();
      if (!response?.location) {
        return;
      }
      void Common.Revealer.reveal(response.location);
    }
    object.release();
  }

  async addBinding(event: Protocol.Runtime.AddBindingRequest): Promise<Protocol.ProtocolResponseWithError> {
    return await this.agent.invoke_addBinding(event);
  }

  async removeBinding(request: Protocol.Runtime.RemoveBindingRequest): Promise<Protocol.ProtocolResponseWithError> {
    return await this.agent.invoke_removeBinding(request);
  }

  bindingCalled(event: Protocol.Runtime.BindingCalledEvent): void {
    this.dispatchEventToListeners(Events.BindingCalled, event);
  }

  private copyRequested(object: RemoteObject): void {
    if (!object.objectId) {
      Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(
          object.unserializableValue() || (object.value as string));
      return;
    }

    const indent =
        this.target().targetManager().context.get(Common.Settings.Settings).moduleSetting('text-editor-indent').get();
    void object
        .callFunctionJSON(toStringForClipboard, [{
                            value: {
                              subtype: object.subtype,
                              indent,
                            },
                          }])
        .then(Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText.bind(
            Host.InspectorFrontendHost.InspectorFrontendHostInstance));

    function toStringForClipboard(this: Object, data: {
      subtype: string,
      indent: string,
    }): string|undefined {
      const subtype = data.subtype;
      const indent = data.indent;

      if (subtype === 'node') {
        return this instanceof Element ? this.outerHTML : undefined;
      }
      if (subtype && typeof this === 'undefined') {
        return String(subtype);
      }
      try {
        return JSON.stringify(this, null, indent);
      } catch {
        return String(this);
      }
    }
  }

  private async queryObjectsRequested(object: RemoteObject, executionContextId?: number): Promise<void> {
    const result = await this.queryObjects(object);
    object.release();
    if ('error' in result) {
      Common.Console.Console.instance().error(result.error);
      return;
    }
    this.dispatchEventToListeners(Events.QueryObjectRequested, {objects: result.objects, executionContextId});
  }

  static simpleTextFromException(exceptionDetails: Protocol.Runtime.ExceptionDetails): string {
    let text = exceptionDetails.text;
    if (exceptionDetails.exception?.description) {
      let description: string = exceptionDetails.exception.description;
      if (description.indexOf('\n') !== -1) {
        description = description.substring(0, description.indexOf('\n'));
      }
      text += ' ' + description;
    }
    return text;
  }

  exceptionThrown(timestamp: number, exceptionDetails: Protocol.Runtime.ExceptionDetails): void {
    const exceptionWithTimestamp = {timestamp, details: exceptionDetails};
    this.dispatchEventToListeners(Events.ExceptionThrown, exceptionWithTimestamp);
  }

  exceptionRevoked(exceptionId: number): void {
    this.dispatchEventToListeners(Events.ExceptionRevoked, exceptionId);
  }

  consoleAPICalled(
      type: Protocol.Runtime.ConsoleAPICalledEventType, args: Protocol.Runtime.RemoteObject[],
      executionContextId: number, timestamp: number, stackTrace?: Protocol.Runtime.StackTrace, context?: string): void {
    const consoleAPICall = {
      type,
      args,
      executionContextId,
      timestamp,
      stackTrace,
      context,
    };
    this.dispatchEventToListeners(Events.ConsoleAPICalled, consoleAPICall);
  }

  executionContextIdForScriptId(scriptId: string): number {
    const script = this.debuggerModel().scriptForId(scriptId);
    return script ? script.executionContextId : 0;
  }

  executionContextForStackTrace(stackTrace: Protocol.Runtime.StackTrace): number {
    let currentStackTrace: (Protocol.Runtime.StackTrace|null)|Protocol.Runtime.StackTrace = stackTrace;
    while (currentStackTrace && !currentStackTrace.callFrames.length) {
      currentStackTrace = currentStackTrace.parent || null;
    }
    if (!currentStackTrace?.callFrames.length) {
      return 0;
    }
    return this.executionContextIdForScriptId(currentStackTrace.callFrames[0].scriptId);
  }

  terminateExecution(): Promise<Protocol.ProtocolResponseWithError> {
    return this.agent.invoke_terminateExecution();
  }

  async getExceptionDetails(errorObjectId: Protocol.Runtime.RemoteObjectId):
      Promise<Protocol.Runtime.ExceptionDetails|undefined> {
    const response = await this.agent.invoke_getExceptionDetails({errorObjectId});
    if (response.getError()) {
      // This CDP method errors if called with non-Error object ids. Swallow that.
      return undefined;
    }
    return response.exceptionDetails;
  }
}

export enum Events {
  /* eslint-disable @typescript-eslint/naming-convention -- Used by web_tests. */
  BindingCalled = 'BindingCalled',
  ExecutionContextCreated = 'ExecutionContextCreated',
  ExecutionContextDestroyed = 'ExecutionContextDestroyed',
  ExecutionContextChanged = 'ExecutionContextChanged',
  ExecutionContextOrderChanged = 'ExecutionContextOrderChanged',
  ExceptionThrown = 'ExceptionThrown',
  ExceptionRevoked = 'ExceptionRevoked',
  ConsoleAPICalled = 'ConsoleAPICalled',
  QueryObjectRequested = 'QueryObjectRequested',
  /* eslint-enable @typescript-eslint/naming-convention */
}

export interface ConsoleAPICall {
  type: Protocol.Runtime.ConsoleAPICalledEventType;
  args: Protocol.Runtime.RemoteObject[];
  executionContextId: number;
  timestamp: number;
  stackTrace?: Protocol.Runtime.StackTrace;
  context?: string;
}

export interface ExceptionWithTimestamp {
  timestamp: number;
  details: Protocol.Runtime.ExceptionDetails;
}

export interface QueryObjectRequestedEvent {
  objects: RemoteObject;
  executionContextId?: number;
}

export interface EventTypes {
  [Events.BindingCalled]: Protocol.Runtime.BindingCalledEvent;
  [Events.ExecutionContextCreated]: ExecutionContext;
  [Events.ExecutionContextDestroyed]: ExecutionContext;
  [Events.ExecutionContextChanged]: ExecutionContext;
  [Events.ExecutionContextOrderChanged]: RuntimeModel;
  [Events.ExceptionThrown]: ExceptionWithTimestamp;
  [Events.ExceptionRevoked]: number;
  [Events.ConsoleAPICalled]: ConsoleAPICall;
  [Events.QueryObjectRequested]: QueryObjectRequestedEvent;
}

class RuntimeDispatcher implements ProtocolProxyApi.RuntimeDispatcher {
  readonly #runtimeModel: RuntimeModel;
  constructor(runtimeModel: RuntimeModel) {
    this.#runtimeModel = runtimeModel;
  }

  executionContextCreated({context}: Protocol.Runtime.ExecutionContextCreatedEvent): void {
    this.#runtimeModel.executionContextCreated(context);
  }

  executionContextDestroyed({executionContextId}: Protocol.Runtime.ExecutionContextDestroyedEvent): void {
    this.#runtimeModel.executionContextDestroyed(executionContextId);
  }

  executionContextsCleared(): void {
    this.#runtimeModel.executionContextsCleared();
  }

  exceptionThrown({timestamp, exceptionDetails}: Protocol.Runtime.ExceptionThrownEvent): void {
    this.#runtimeModel.exceptionThrown(timestamp, exceptionDetails);
  }

  exceptionRevoked({exceptionId}: Protocol.Runtime.ExceptionRevokedEvent): void {
    this.#runtimeModel.exceptionRevoked(exceptionId);
  }

  consoleAPICalled({type, args, executionContextId, timestamp, stackTrace, context}:
                       Protocol.Runtime.ConsoleAPICalledEvent): void {
    this.#runtimeModel.consoleAPICalled(type, args, executionContextId, timestamp, stackTrace, context);
  }

  inspectRequested({object, hints, executionContextId}: Protocol.Runtime.InspectRequestedEvent): void {
    this.#runtimeModel.inspectRequested(object, hints, executionContextId);
  }

  bindingCalled(event: Protocol.Runtime.BindingCalledEvent): void {
    this.#runtimeModel.bindingCalled(event);
  }
}

export class ExecutionContext {
  id: Protocol.Runtime.ExecutionContextId;
  uniqueId: string;
  name: string;
  #label: string|null;
  origin: Platform.DevToolsPath.UrlString;
  isDefault: boolean;
  runtimeModel: RuntimeModel;
  debuggerModel: DebuggerModel;
  frameId: Protocol.Page.FrameId|undefined;
  constructor(
      runtimeModel: RuntimeModel, id: Protocol.Runtime.ExecutionContextId, uniqueId: string, name: string,
      origin: Platform.DevToolsPath.UrlString, isDefault: boolean, frameId?: Protocol.Page.FrameId) {
    this.id = id;
    this.uniqueId = uniqueId;
    this.name = name;
    this.#label = null;
    this.origin = origin;
    this.isDefault = isDefault;
    this.runtimeModel = runtimeModel;
    this.debuggerModel = runtimeModel.debuggerModel();
    this.frameId = frameId;
    this.#setLabel('');
  }

  target(): Target {
    return this.runtimeModel.target();
  }

  static comparator(a: ExecutionContext, b: ExecutionContext): number {
    function targetWeight(target: Target): number {
      if (target.parentTarget()?.type() !== Type.FRAME) {
        return 5;
      }
      if (target.type() === Type.FRAME) {
        return 4;
      }
      if (target.type() === Type.ServiceWorker) {
        return 3;
      }
      if (target.type() === Type.Worker || target.type() === Type.SHARED_WORKER) {
        return 2;
      }
      return 1;
    }

    function targetPath(target: Target): Target[] {
      let currentTarget: (Target|null)|Target = target;
      const parents = [];
      while (currentTarget) {
        parents.push(currentTarget);
        currentTarget = currentTarget.parentTarget();
      }
      return parents.reverse();
    }

    const tagetsA = targetPath(a.target());
    const targetsB = targetPath(b.target());
    let targetA;
    let targetB;
    for (let i = 0;; i++) {
      if (!tagetsA[i] || !targetsB[i] || (tagetsA[i] !== targetsB[i])) {
        targetA = tagetsA[i];
        targetB = targetsB[i];
        break;
      }
    }
    if (!targetA && targetB) {
      return -1;
    }

    if (!targetB && targetA) {
      return 1;
    }

    if (targetA && targetB) {
      const weightDiff = targetWeight(targetA) - targetWeight(targetB);
      if (weightDiff) {
        return -weightDiff;
      }
      return targetA.id().localeCompare(targetB.id());
    }

    // Main world context should always go first.
    if (a.isDefault) {
      return -1;
    }
    if (b.isDefault) {
      return +1;
    }
    return a.name.localeCompare(b.name);
  }

  async evaluate(options: EvaluationOptions, userGesture: boolean, awaitPromise: boolean): Promise<EvaluationResult> {
    // FIXME: It will be moved to separate ExecutionContext.
    if (this.debuggerModel.selectedCallFrame()) {
      return await this.debuggerModel.evaluateOnSelectedCallFrame(options);
    }
    return await this.evaluateGlobal(options, userGesture, awaitPromise);
  }

  globalObject(objectGroup: string, generatePreview: boolean): Promise<EvaluationResult> {
    const evaluationOptions = {
      expression: 'this',
      objectGroup,
      includeCommandLineAPI: false,
      silent: true,
      returnByValue: false,
      generatePreview,
    };
    return this.evaluateGlobal((evaluationOptions as EvaluationOptions), false, false);
  }

  async callFunctionOn(options: CallFunctionOptions): Promise<EvaluationResult> {
    const response = await this.runtimeModel.agent.invoke_callFunctionOn({
      functionDeclaration: options.functionDeclaration,
      returnByValue: options.returnByValue,
      userGesture: options.userGesture,
      awaitPromise: options.awaitPromise,
      throwOnSideEffect: options.throwOnSideEffect,
      arguments: options.arguments,
      // Old back-ends don't know about uniqueContextId (and also don't generate
      // one), so fall back to contextId in that case (https://crbug.com/1192621).
      ...(this.uniqueId ? {uniqueContextId: this.uniqueId} : {contextId: this.id}),
    });

    const error = response.getError();
    if (error) {
      return {error};
    }
    return {object: this.runtimeModel.createRemoteObject(response.result), exceptionDetails: response.exceptionDetails};
  }

  private async evaluateGlobal(options: EvaluationOptions, userGesture: boolean, awaitPromise: boolean):
      Promise<EvaluationResult> {
    if (!options.expression) {
      // There is no expression, so the completion should happen against global properties.
      options.expression = 'this';
    }

    const response = await this.runtimeModel.agent.invoke_evaluate({
      expression: options.expression,
      objectGroup: options.objectGroup,
      includeCommandLineAPI: options.includeCommandLineAPI,
      silent: options.silent,
      returnByValue: options.returnByValue,
      generatePreview: options.generatePreview,
      userGesture,
      awaitPromise,
      throwOnSideEffect: options.throwOnSideEffect,
      timeout: options.timeout,
      disableBreaks: options.disableBreaks,
      replMode: options.replMode,
      allowUnsafeEvalBlockedByCSP: options.allowUnsafeEvalBlockedByCSP,
      // Old back-ends don't know about uniqueContextId (and also don't generate
      // one), so fall back to contextId in that case (https://crbug.com/1192621).
      ...(this.uniqueId ? {uniqueContextId: this.uniqueId} : {contextId: this.id}),
    });

    const error = response.getError();
    if (error) {
      console.error(error);
      return {error};
    }
    return {object: this.runtimeModel.createRemoteObject(response.result), exceptionDetails: response.exceptionDetails};
  }

  async globalLexicalScopeNames(): Promise<string[]|null> {
    const response = await this.runtimeModel.agent.invoke_globalLexicalScopeNames({executionContextId: this.id});
    return response.getError() ? [] : response.names;
  }

  label(): string|null {
    return this.#label;
  }

  setLabel(label: string): void {
    this.#setLabel(label);
    this.runtimeModel.dispatchEventToListeners(Events.ExecutionContextChanged, this);
  }

  #setLabel(label: string): void {
    if (label) {
      this.#label = label;
      return;
    }
    if (this.name) {
      this.#label = this.name;
      return;
    }
    const parsedUrl = Common.ParsedURL.ParsedURL.fromString(this.origin);
    this.#label = parsedUrl ? parsedUrl.lastPathComponentWithFragment() : '';
  }
}

SDKModel.register(RuntimeModel, {capabilities: Capability.JS, autostart: true});

export type EvaluationResult = {
  object: RemoteObject,
  exceptionDetails?: Protocol.Runtime.ExceptionDetails,
}|{
  error: string,
};

export interface CompileScriptResult {
  scriptId?: string;
  exceptionDetails?: Protocol.Runtime.ExceptionDetails;
}

export interface EvaluationOptions {
  expression: string;
  objectGroup?: string;
  includeCommandLineAPI?: boolean;
  silent?: boolean;
  returnByValue?: boolean;
  generatePreview?: boolean;
  throwOnSideEffect?: boolean;
  timeout?: number;
  disableBreaks?: boolean;
  replMode?: boolean;
  allowUnsafeEvalBlockedByCSP?: boolean;
  contextId?: number;
}

export interface CallFunctionOptions {
  functionDeclaration: string;
  returnByValue?: boolean;
  throwOnSideEffect?: boolean;
  allowUnsafeEvalBlockedByCSP?: boolean;
  arguments: Protocol.Runtime.CallArgument[];
  userGesture: boolean;
  awaitPromise: boolean;
}

export type QueryObjectResult = {
  objects: RemoteObject,
}|{error: string};
