// https://github.com/berstend/puppeteer-extra/blob/c44c8bb0224c6bba2554017bfb9d7a1d0119f92f/packages/puppeteer-extra-plugin-stealth/evasions/chrome.runtime/index.js

(runOnInsecureOrigins) => {
  const STATIC_DATA = {
    "OnInstalledReason": {
      "CHROME_UPDATE": "chrome_update",
      "INSTALL": "install",
      "SHARED_MODULE_UPDATE": "shared_module_update",
      "UPDATE": "update"
    },
    "OnRestartRequiredReason": {
      "APP_UPDATE": "app_update",
      "OS_UPDATE": "os_update",
      "PERIODIC": "periodic"
    },
    "PlatformArch": {
      "ARM": "arm",
      "ARM64": "arm64",
      "MIPS": "mips",
      "MIPS64": "mips64",
      "X86_32": "x86-32",
      "X86_64": "x86-64"
    },
    "PlatformNaclArch": {
      "ARM": "arm",
      "MIPS": "mips",
      "MIPS64": "mips64",
      "X86_32": "x86-32",
      "X86_64": "x86-64"
    },
    "PlatformOs": {
      "ANDROID": "android",
      "CROS": "cros",
      "LINUX": "linux",
      "MAC": "mac",
      "OPENBSD": "openbsd",
      "WIN": "win"
    },
    "RequestUpdateCheckStatus": {
      "NO_UPDATE": "no_update",
      "THROTTLED": "throttled",
      "UPDATE_AVAILABLE": "update_available"
    }
  }
  if (!window.chrome) {
    // Use the exact property descriptor found in headful Chrome
    // fetch it via `Object.getOwnPropertyDescriptor(window, 'chrome')`
    Object.defineProperty(window, 'chrome', {
      writable: true,
      enumerable: true,
      configurable: false, // note!
      value: {} // We'll extend that later
    })
  }

  // That means we're running headful and don't need to mock anything
  const existsAlready = 'runtime' in window.chrome
  // `chrome.runtime` is only exposed on secure origins
  const isNotSecure = !window.location.protocol.startsWith('https')
  if (existsAlready || (isNotSecure && !runOnInsecureOrigins)) {
    return // Nothing to do here
  }

  window.chrome.runtime = {
    // There's a bunch of static data in that property which doesn't seem to change,
    // we should periodically check for updates: `JSON.stringify(window.chrome.runtime, null, 2)`
    ...STATIC_DATA,
    // `chrome.runtime.id` is extension related and returns undefined in Chrome
    get id() {
      return undefined
    },
    // These two require more sophisticated mocks
    connect: null,
    sendMessage: null
  }

  const makeCustomRuntimeErrors = (preamble, method, extensionId) => ({
    NoMatchingSignature: new TypeError(
      preamble + `No matching signature.`
    ),
    MustSpecifyExtensionID: new TypeError(
      preamble +
      `${method} called from a webpage must specify an Extension ID (string) for its first argument.`
    ),
    InvalidExtensionID: new TypeError(
      preamble + `Invalid extension id: '${extensionId}'`
    )
  })

  // Valid Extension IDs are 32 characters in length and use the letter `a` to `p`:
  // https://source.chromium.org/chromium/chromium/src/+/master:components/crx_file/id_util.cc;drc=14a055ccb17e8c8d5d437fe080faba4c6f07beac;l=90
  const isValidExtensionID = str =>
    str.length === 32 && str.toLowerCase().match(/^[a-p]+$/)

  /** Mock `chrome.runtime.sendMessage` */
  const sendMessageHandler = {
    apply: function (target, ctx, args) {
      const [extensionId, options, responseCallback] = args || []

      // Define custom errors
      const errorPreamble = `Error in invocation of runtime.sendMessage(optional string extensionId, any message, optional object options, optional function responseCallback): `
      const Errors = makeCustomRuntimeErrors(
        errorPreamble,
        `chrome.runtime.sendMessage()`,
        extensionId
      )

      // Check if the call signature looks ok
      const noArguments = args.length === 0
      const tooManyArguments = args.length > 4
      const incorrectOptions = options && typeof options !== 'object'
      const incorrectResponseCallback =
        responseCallback && typeof responseCallback !== 'function'
      if (
        noArguments ||
        tooManyArguments ||
        incorrectOptions ||
        incorrectResponseCallback
      ) {
        throw Errors.NoMatchingSignature
      }

      // At least 2 arguments are required before we even validate the extension ID
      if (args.length < 2) {
        throw Errors.MustSpecifyExtensionID
      }

      // Now let's make sure we got a string as extension ID
      if (typeof extensionId !== 'string') {
        throw Errors.NoMatchingSignature
      }

      if (!isValidExtensionID(extensionId)) {
        throw Errors.InvalidExtensionID
      }

      return undefined // Normal behavior
    }
  }
  utils.mockWithProxy(
    window.chrome.runtime,
    'sendMessage',
    function sendMessage() { },
    sendMessageHandler
  )

  /**
   * Mock `chrome.runtime.connect`
   *
   * @see https://developer.chrome.com/apps/runtime#method-connect
   */
  const connectHandler = {
    apply: function (target, ctx, args) {
      const [extensionId, connectInfo] = args || []

      // Define custom errors
      const errorPreamble = `Error in invocation of runtime.connect(optional string extensionId, optional object connectInfo): `
      const Errors = makeCustomRuntimeErrors(
        errorPreamble,
        `chrome.runtime.connect()`,
        extensionId
      )

      // Behavior differs a bit from sendMessage:
      const noArguments = args.length === 0
      const emptyStringArgument = args.length === 1 && extensionId === ''
      if (noArguments || emptyStringArgument) {
        throw Errors.MustSpecifyExtensionID
      }

      const tooManyArguments = args.length > 2
      const incorrectConnectInfoType =
        connectInfo && typeof connectInfo !== 'object'

      if (tooManyArguments || incorrectConnectInfoType) {
        throw Errors.NoMatchingSignature
      }

      const extensionIdIsString = typeof extensionId === 'string'
      if (extensionIdIsString && extensionId === '') {
        throw Errors.MustSpecifyExtensionID
      }
      if (extensionIdIsString && !isValidExtensionID(extensionId)) {
        throw Errors.InvalidExtensionID
      }

      // There's another edge-case here: extensionId is optional so we might find a connectInfo object as first param, which we need to validate
      const validateConnectInfo = ci => {
        // More than a first param connectInfo as been provided
        if (args.length > 1) {
          throw Errors.NoMatchingSignature
        }
        // An empty connectInfo has been provided
        if (Object.keys(ci).length === 0) {
          throw Errors.MustSpecifyExtensionID
        }
        // Loop over all connectInfo props an check them
        Object.entries(ci).forEach(([k, v]) => {
          const isExpected = ['name', 'includeTlsChannelId'].includes(k)
          if (!isExpected) {
            throw new TypeError(
              errorPreamble + `Unexpected property: '${k}'.`
            )
          }
          const MismatchError = (propName, expected, found) =>
            TypeError(
              errorPreamble +
              `Error at property '${propName}': Invalid type: expected ${expected}, found ${found}.`
            )
          if (k === 'name' && typeof v !== 'string') {
            throw MismatchError(k, 'string', typeof v)
          }
          if (k === 'includeTlsChannelId' && typeof v !== 'boolean') {
            throw MismatchError(k, 'boolean', typeof v)
          }
        })
      }
      if (typeof extensionId === 'object') {
        validateConnectInfo(extensionId)
        throw Errors.MustSpecifyExtensionID
      }

      // Unfortunately even when the connect fails Chrome will return an object with methods we need to mock as well
      return utils.patchToStringNested(makeConnectResponse())
    }
  }
  utils.mockWithProxy(
    window.chrome.runtime,
    'connect',
    function connect() { },
    connectHandler
  )

  function makeConnectResponse() {
    const onSomething = () => ({
      addListener: function addListener() { },
      dispatch: function dispatch() { },
      hasListener: function hasListener() { },
      hasListeners: function hasListeners() {
        return false
      },
      removeListener: function removeListener() { }
    })

    const response = {
      name: '',
      sender: undefined,
      disconnect: function disconnect() { },
      onDisconnect: onSomething(),
      onMessage: onSomething(),
      postMessage: function postMessage() {
        if (!arguments.length) {
          throw new TypeError(`Insufficient number of arguments.`)
        }
        throw new Error(`Attempting to use a disconnected port object`)
      }
    }
    return response
  }
}
