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