(function() {
    "use strict";
    var idCounter = 0;

    function getInViewCenterPoint(rect) {
        var left = Math.max(0, rect.left);
        var right = Math.min(window.innerWidth, rect.right);
        var top = Math.max(0, rect.top);
        var bottom = Math.min(window.innerHeight, rect.bottom);

        var x = 0.5 * (left + right);
        var y = 0.5 * (top + bottom);

        return [x, y];
    }

    function getPointerInteractablePaintTree(element) {
        if (!window.document.contains(element)) {
            return [];
        }

        var rectangles = element.getClientRects();

        if (rectangles.length === 0) {
            return [];
        }

        var centerPoint = getInViewCenterPoint(rectangles[0]);

        if ("elementsFromPoint" in document) {
            return document.elementsFromPoint(centerPoint[0], centerPoint[1]);
        } else if ("msElementsFromPoint" in document) {
            var rv = document.msElementsFromPoint(centerPoint[0], centerPoint[1]);
            return Array.prototype.slice.call(rv ? rv : []);
        } else {
            throw new Error("document.elementsFromPoint unsupported");
        }
    }

    function inView(element) {
        var pointerInteractablePaintTree = getPointerInteractablePaintTree(element);
        return pointerInteractablePaintTree.indexOf(element) !== -1;
    }


    /**
     * @namespace
     */
    window.test_driver = {
        /**
         * Trigger user interaction in order to grant additional privileges to
         * a provided function.
         *
         * https://html.spec.whatwg.org/#triggered-by-user-activation
         *
         * @param {String} intent - a description of the action which much be
         *                          triggered by user interaction
         * @param {Function} action - code requiring escalated privileges
         *
         * @returns {Promise} fulfilled following user interaction and
         *                    execution of the provided `action` function;
         *                    rejected if interaction fails or the provided
         *                    function throws an error
         */
        bless: function(intent, action) {
            var button = document.createElement("button");
            button.innerHTML = "This test requires user interaction.<br />" +
                "Please click here to allow " + intent + ".";
            button.id = "wpt-test-driver-bless-" + (idCounter += 1);
            const elem = document.body || document.documentElement;
            elem.appendChild(button);

            return new Promise(function(resolve, reject) {
                    button.addEventListener("click", resolve);

                    test_driver.click(button).catch(reject);
                }).then(function() {
                    button.remove();

                    if (typeof action === "function") {
                        return action();
                    }
                });
        },

        /**
         * Triggers a user-initiated click
         *
         * This matches the behaviour of the {@link
         * https://w3c.github.io/webdriver/#element-click|WebDriver
         * Element Click command}.
         *
         * @param {Element} element - element to be clicked
         * @returns {Promise} fulfilled after click occurs, or rejected in
         *                    the cases the WebDriver command errors
         */
        click: function(element) {
            if (window.top !== window) {
                return Promise.reject(new Error("can only click in top-level window"));
            }

            if (!window.document.contains(element)) {
                return Promise.reject(new Error("element in different document or shadow tree"));
            }

            if (!inView(element)) {
                element.scrollIntoView({behavior: "instant",
                                        block: "end",
                                        inline: "nearest"});
            }

            var pointerInteractablePaintTree = getPointerInteractablePaintTree(element);
            if (pointerInteractablePaintTree.length === 0 ||
                !element.contains(pointerInteractablePaintTree[0])) {
                return Promise.reject(new Error("element click intercepted error"));
            }

            var rect = element.getClientRects()[0];
            var centerPoint = getInViewCenterPoint(rect);
            return window.test_driver_internal.click(element,
                                                     {x: centerPoint[0],
                                                      y: centerPoint[1]});
        },

        /**
         * Send keys to an element
         *
         * This matches the behaviour of the {@link
         * https://w3c.github.io/webdriver/#element-send-keys|WebDriver
         * Send Keys command}.
         *
         * @param {Element} element - element to send keys to
         * @param {String} keys - keys to send to the element
         * @returns {Promise} fulfilled after keys are sent, or rejected in
         *                    the cases the WebDriver command errors
         */
        send_keys: function(element, keys) {
            if (window.top !== window) {
                return Promise.reject(new Error("can only send keys in top-level window"));
            }

            if (!window.document.contains(element)) {
                return Promise.reject(new Error("element in different document or shadow tree"));
            }

            if (!inView(element)) {
                element.scrollIntoView({behavior: "instant",
                                        block: "end",
                                        inline: "nearest"});
            }

            var pointerInteractablePaintTree = getPointerInteractablePaintTree(element);
            if (pointerInteractablePaintTree.length === 0 ||
                !element.contains(pointerInteractablePaintTree[0])) {
                return Promise.reject(new Error("element send_keys intercepted error"));
            }

            return window.test_driver_internal.send_keys(element, keys);
        },

        /**
         * Freeze the current page
         *
         * The freeze function transitions the page from the HIDDEN state to
         * the FROZEN state as described in {@link
         * https://github.com/WICG/page-lifecycle/blob/master/README.md|Lifecycle API
         * for Web Pages}
         *
         * @returns {Promise} fulfilled after the freeze request is sent, or rejected
         *                    in case the WebDriver command errors
         */
        freeze: function() {
            return window.test_driver_internal.freeze();
        },

        /**
         * Send a sequence of actions
         *
         * This function sends a sequence of actions to the top level window
         * to perform. It is modeled after the behaviour of {@link
         * https://w3c.github.io/webdriver/#actions|WebDriver Actions Command}
         *
         * @param {Array} actions - an array of actions. The format is the same as the actions
                                    property of the WebDriver command {@link
                                    https://w3c.github.io/webdriver/#perform-actions|Perform
                                    Actions} command. Each element is an object representing an
                                    input source and each input source itself has an actions
                                    property detailing the behaviour of that source at each timestep
                                    (or tick). Authors are not expected to construct the actions
                                    sequence by hand, but to use the builder api provided in
                                    testdriver-actions.js
         * @returns {Promise} fufiled after the actions are performed, or rejected in
         *                    the cases the WebDriver command errors
         */
        action_sequence: function(actions) {
            return window.test_driver_internal.action_sequence(actions);
        },

        /**
         * Generates a test report on the current page
         *
         * The generate_test_report function generates a report (to be observed
         * by ReportingObserver) for testing purposes, as described in
         * {@link https://w3c.github.io/reporting/#generate-test-report-command}
         *
         * @returns {Promise} fulfilled after the report is generated, or
         *                    rejected if the report generation fails
         */
        generate_test_report: function(message) {
            return window.test_driver_internal.generate_test_report(message);
        },

        /**
         * Sets the state of a permission
         *
         * This function simulates a user setting a permission into a particular state as described
         * in {@link https://w3c.github.io/permissions/#set-permission-command}
         *
         * @param {Object} descriptor - a [PermissionDescriptor]{@link
         *                              https://w3c.github.io/permissions/#dictdef-permissiondescriptor}
         *                              object
         * @param {String} state - the state of the permission
         * @param {boolean} one_realm - Optional. Whether the permission applies to only one realm
         *
         * The above params are used to create a [PermissionSetParameters]{@link
         * https://w3c.github.io/permissions/#dictdef-permissionsetparameters} object
         *
         * @returns {Promise} fulfilled after the permission is set, or rejected if setting the
         *                    permission fails
         */
        set_permission: function(descriptor, state, one_realm) {
            let permission_params = {
              descriptor,
              state,
              oneRealm: one_realm,
            };
            return window.test_driver_internal.set_permission(permission_params);
        },

        /**
         * Creates a virtual authenticator
         *
         * This function creates a virtual authenticator for use with the U2F
         * and WebAuthn APIs as described in {@link
         * https://w3c.github.io/webauthn/#sctn-automation-add-virtual-authenticator}
         *
         * @param {Object} config - an [Authenticator Configuration]{@link
         *                          https://w3c.github.io/webauthn/#authenticator-configuration}
         *                          object
         * @returns {Promise} fulfilled after the authenticator is added, or
         *                    rejected in the cases the WebDriver command
         *                    errors. Returns the ID of the authenticator
         */
        add_virtual_authenticator: function(config) {
            return window.test_driver_internal.add_virtual_authenticator(config);
        },

        /**
         * Removes a virtual authenticator
         *
         * This function removes a virtual authenticator that has been created
         * by add_virtual_authenticator
         * https://w3c.github.io/webauthn/#sctn-automation-remove-virtual-authenticator
         *
         * @param {String} authenticator_id - the ID of the authenticator to be
         *                                    removed.
         *
         * @returns {Promise} fulfilled after the authenticator is removed, or
         *                    rejected in the cases the WebDriver command
         *                    errors
         */
        remove_virtual_authenticator: function(authenticator_id) {
            return window.test_driver_internal.remove_virtual_authenticator(authenticator_id);
        },

        /**
         * Adds a credential to a virtual authenticator
         *
         * https://w3c.github.io/webauthn/#sctn-automation-add-credential
         *
         * @param {String} authenticator_id - the ID of the authenticator
         * @param {Object} credential - A [Credential Parameters]{@link
         *                              https://w3c.github.io/webauthn/#credential-parameters}
         *                              object
         *
         * @returns {Promise} fulfilled after the credential is added, or
         *                    rejected in the cases the WebDriver command
         *                    errors
         */
        add_credential: function(authenticator_id, credential) {
            return window.test_driver_internal.add_credential(authenticator_id, credential);
        },

        /**
         * Gets all the credentials stored in an authenticator
         *
         * This function retrieves all the credentials (added via the U2F API,
         * WebAuthn, or the add_credential function) stored in a virtual
         * authenticator
         * https://w3c.github.io/webauthn/#sctn-automation-get-credentials
         *
         * @param {String} authenticator_id - the ID of the authenticator
         *
         * @returns {Promise} fulfilled after the credentials are returned, or
         *                    rejected in the cases the WebDriver command
         *                    errors. Returns an array of [Credential
         *                    Parameters]{@link
         *                    https://w3c.github.io/webauthn/#credential-parameters}
         */
        get_credentials: function(authenticator_id) {
            return window.test_driver_internal.get_credentials(authenticator_id);
        },

        /**
         * Remove a credential stored in an authenticator
         *
         * https://w3c.github.io/webauthn/#sctn-automation-remove-credential
         *
         * @param {String} authenticator_id - the ID of the authenticator
         * @param {String} credential_id - the ID of the credential
         *
         * @returns {Promise} fulfilled after the credential is removed, or
         *                    rejected in the cases the WebDriver command
         *                    errors.
         */
        remove_credential: function(authenticator_id, credential_id) {
            return window.test_driver_internal.remove_credential(authenticator_id, credential_id);
        },

        /**
         * Removes all the credentials stored in a virtual authenticator
         *
         * https://w3c.github.io/webauthn/#sctn-automation-remove-all-credentials
         *
         * @param {String} authenticator_id - the ID of the authenticator
         *
         * @returns {Promise} fulfilled after the credentials are removed, or
         *                    rejected in the cases the WebDriver command
         *                    errors.
         */
        remove_all_credentials: function(authenticator_id) {
            return window.test_driver_internal.remove_all_credentials(authenticator_id);
        },

        /**
         * Sets the User Verified flag on an authenticator
         *
         * Sets whether requests requiring user verification will succeed or
         * fail on a given virtual authenticator
         * https://w3c.github.io/webauthn/#sctn-automation-set-user-verified
         *
         * @param {String} authenticator_id - the ID of the authenticator
         * @param {boolean} uv - the User Verified flag
         */
        set_user_verified: function(authenticator_id, uv) {
            return window.test_driver_internal.set_user_verified(authenticator_id, uv);
        },

        /**
         * Sets the storage access rule for an origin when embedded
         * in a third-party context.
         *
         * {@link https://privacycg.github.io/storage-access/#set-storage-access-command}
         *
         * @param {String} origin - A third-party origin to block or allow.
         *                          May be "*" to indicate all origins.
         * @param {String} embedding_origin - an embedding (first-party) origin
         *                                    on which {origin}'s access should
         *                                    be blocked or allowed.
         *                                    May be "*" to indicate all origins.
         * @param {String} state - The storage access setting.
         *                         Must be either "allowed" or "blocked".
         *
         * @returns {Promise} Fulfilled after the storage access rule has been
         *                    set, or rejected if setting the rule fails.
         */
        set_storage_access: function(origin, embedding_origin, state) {
            if (state !== "allowed" && state !== "blocked") {
                throw new Error("storage access status must be 'allowed' or 'blocked'");
            }
            const blocked = state === "blocked";
            return window.test_driver_internal.set_storage_access(origin, embedding_origin, blocked);
        },
    };

    window.test_driver_internal = {
        /**
         * This flag should be set to `true` by any code which implements the
         * internal methods defined below for automation purposes. Doing so
         * allows the library to signal failure immediately when an automated
         * implementation of one of the methods is not available.
         */
        in_automation: false,

        /**
         * Waits for a user-initiated click
         *
         * @param {Element} element - element to be clicked
         * @param {{x: number, y: number} coords - viewport coordinates to click at
         * @returns {Promise} fulfilled after click occurs
         */
        click: function(element, coords) {
            if (this.in_automation) {
                return Promise.reject(new Error('Not implemented'));
            }

            return new Promise(function(resolve, reject) {
                element.addEventListener("click", resolve);
            });
        },

        /**
         * Waits for an element to receive a series of key presses
         *
         * @param {Element} element - element which should receve key presses
         * @param {String} keys - keys to expect
         * @returns {Promise} fulfilled after keys are received or rejected if
         *                    an incorrect key sequence is received
         */
        send_keys: function(element, keys) {
            if (this.in_automation) {
                return Promise.reject(new Error('Not implemented'));
            }

            return new Promise(function(resolve, reject) {
                var seen = "";

                function remove() {
                    element.removeEventListener("keydown", onKeyDown);
                }

                function onKeyDown(event) {
                    if (event.key.length > 1) {
                        return;
                    }

                    seen += event.key;

                    if (keys.indexOf(seen) !== 0) {
                        reject(new Error("Unexpected key sequence: " + seen));
                        remove();
                    } else if (seen === keys) {
                        resolve();
                        remove();
                    }
                }

                element.addEventListener("keydown", onKeyDown);
            });
        },

        /**
         * Freeze the current page
         *
         * @returns {Promise} fulfilled after freeze request is sent, otherwise
         * it gets rejected
         */
        freeze: function() {
            return Promise.reject(new Error("unimplemented"));
        },

        /**
         * Send a sequence of pointer actions
         *
         * @returns {Promise} fufilled after actions are sent, rejected if any actions
         *                    fail
         */
        action_sequence: function(actions) {
            return Promise.reject(new Error("unimplemented"));
        },

        /**
         * Generates a test report on the current page
         *
         * @param {String} message - the message to be contained in the report
         * @returns {Promise} fulfilled after the report is generated, or
         *                    rejected if the report generation fails
         */
        generate_test_report: function(message) {
            return Promise.reject(new Error("unimplemented"));
        },


        /**
         * Sets the state of a permission
         *
         * This function simulates a user setting a permission into a particular state as described
         * in {@link https://w3c.github.io/permissions/#set-permission-command}
         *
         * @param {Object} permission_params - a [PermissionSetParameters]{@lint
         *                                     https://w3c.github.io/permissions/#dictdef-permissionsetparameters}
         *                                     object
         * @returns {Promise} fulfilled after the permission is set, or rejected if setting the
         *                    permission fails
         */
        set_permission: function(permission_params) {
            return Promise.reject(new Error("unimplemented"));
        },

        /**
         * Creates a virtual authenticator
         *
         * @param {Object} config - the authenticator configuration
         * @returns {Promise} fulfilled after the authenticator is added, or
         *                    rejected in the cases the WebDriver command
         *                    errors.
         */
        add_virtual_authenticator: function(config) {
            return Promise.reject(new Error("unimplemented"));
        },

        /**
         * Removes a virtual authenticator
         *
         * @param {String} authenticator_id - the ID of the authenticator to be
         *                                    removed.
         *
         * @returns {Promise} fulfilled after the authenticator is removed, or
         *                    rejected in the cases the WebDriver command
         *                    errors
         */
        remove_virtual_authenticator: function(authenticator_id) {
            return Promise.reject(new Error("unimplemented"));
        },

        /**
         * Adds a credential to a virtual authenticator
         *
         * @param {String} authenticator_id - the ID of the authenticator
         * @param {Object} credential - A [Credential Parameters]{@link
         *                              https://w3c.github.io/webauthn/#credential-parameters}
         *                              object
         *
         * @returns {Promise} fulfilled after the credential is added, or
         *                    rejected in the cases the WebDriver command
         *                    errors
         *
         */
        add_credential: function(authenticator_id, credential) {
            return Promise.reject(new Error("unimplemented"));
        },

        /**
         * Gets all the credentials stored in an authenticator
         *
         * @param {String} authenticator_id - the ID of the authenticator
         *
         * @returns {Promise} fulfilled after the credentials are returned, or
         *                    rejected in the cases the WebDriver command
         *                    errors. Returns an array of [Credential
         *                    Parameters]{@link
         *                    https://w3c.github.io/webauthn/#credential-parameters}
         *
         */
        get_credentials: function(authenticator_id) {
            return Promise.reject(new Error("unimplemented"));
        },

        /**
         * Remove a credential stored in an authenticator
         *
         * @param {String} authenticator_id - the ID of the authenticator
         * @param {String} credential_id - the ID of the credential
         *
         * @returns {Promise} fulfilled after the credential is removed, or
         *                    rejected in the cases the WebDriver command
         *                    errors.
         *
         */
        remove_credential: function(authenticator_id, credential_id) {
            return Promise.reject(new Error("unimplemented"));
        },

        /**
         * Removes all the credentials stored in a virtual authenticator
         *
         * @param {String} authenticator_id - the ID of the authenticator
         *
         * @returns {Promise} fulfilled after the credentials are removed, or
         *                    rejected in the cases the WebDriver command
         *                    errors.
         *
         */
        remove_all_credentials: function(authenticator_id) {
            return Promise.reject(new Error("unimplemented"));
        },

        /**
         * Sets the User Verified flag on an authenticator
         *
         * @param {String} authenticator_id - the ID of the authenticator
         * @param {boolean} uv - the User Verified flag
         *
         */
        set_user_verified: function(authenticator_id, uv) {
            return Promise.reject(new Error("unimplemented"));
        },

        /**
         * Sets the storage access policy for a third-party origin when loaded
         * in the current first party context
         */
        set_storage_access: function(origin, embedding_origin, blocked) {
            return Promise.reject(new Error("unimplemented"));
        },
    };
})();
