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 && !opts.runOnInsecureOrigins))) {

  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;
  }
}
