function checkVisibility(selector) {
    let selectorMatches = document.querySelectorAll(selector);
    let element = selectorMatches[0];

    if (selectorMatches.length > 1)
        throw new Error(`Multiple matches of ${selector} found`);

    let isVisible = false;
    if (element) {
        let eventHandler = event => {
            event.preventDefault();
            isVisible = true;
        };
        element.addEventListener('mouseover', eventHandler);
        let rect = element.getBoundingClientRect();
        let x = rect.left + rect.width / 2;
        let y = rect.top + rect.height / 2;
        let elementInCenter = document.elementFromPoint(x, y);
        let elementInTopLeft = document.elementFromPoint(rect.left, rect.top);
        let elementInBottomRight = document.elementFromPoint(rect.right, rect.bottom);

        let e = new MouseEvent('mouseover', {
            view: window,
            bubbles: true,
            cancelable: true,
        });

        if (elementInCenter)
            elementInCenter.dispatchEvent(e);

        if (elementInTopLeft)
            elementInTopLeft.dispatchEvent(e);

        if (elementInBottomRight)
            elementInBottomRight.dispatchEvent(e);

        element.removeEventListener('mouseover', eventHandler);
    }
    return isVisible;
}

let actions = {
    clickIfExists: async function(selector) {
        let exists;
        try {
            exists = await this.waitForSelector(selector, {timeout: 500});
        } catch (error) {
            exists = false;
        }
        if (exists) await this.waitToClick(selector);
        return exists;
    },

    expectURL: async function(expectedHash) {
        try {
            await this.waitForFunction(expectedHash => {
                return document.location.hash.includes(expectedHash);
            }, {}, expectedHash);
        } catch (error) {
            throw new Error(`Failed to reach URL containing: ${expectedHash}`);
        }
        await this.waitForSpinnerLoad();
        return true;
    },

    doLogin: async function(userName, password = 'nightmare') {
        let state = await this.getState();

        if (state != 'login') {
            try {
                await this.gotoState('login');
            } catch (err) {
                let dialog = await this.evaluate(
                    () => document.querySelector('button[response="accept"]'));

                if (dialog)
                    await this.waitToClick('button[response="accept"]');
                else
                    throw err;
            }
        }

        await this.waitForState('login');

        await this.waitForSelector(`vn-login vn-textfield[ng-model="$ctrl.user"]`, {visible: true});
        await this.clearInput(`vn-login vn-textfield[ng-model="$ctrl.user"]`);
        await this.write(`vn-login vn-textfield[ng-model="$ctrl.user"]`, userName);
        await this.clearInput(`vn-login vn-textfield[ng-model="$ctrl.password"]`);
        await this.write(`vn-login vn-textfield[ng-model="$ctrl.password"]`, password);
        await this.waitToClick('vn-login button[type=submit]');
    },

    login: async function(userName) {
        await this.doLogin(userName);
        await this.waitForState('home');
        await this.addStyleTag({
            content: `
                *,
                *::after,
                *::before {
                    transition-delay: 0s !important;
                    transition-duration: 0s !important;
                    animation-delay: -0.0001s !important;
                    animation-duration: 0s !important;
                    animation-play-state: paused !important;
                    caret-color: transparent !important;
                }`
        });
    },

    selectModule: async function(moduleName) {
        let state = `${moduleName}.index`;
        await this.waitToClick(`vn-home a[ui-sref="${state}"]`);
        await this.waitForState(state);
    },

    loginAndModule: async function(userName, moduleName) {
        await this.login(userName);
        await this.selectModule(moduleName);
    },

    getState: async function() {
        return this.evaluate(() => {
            let $state = angular.element(document.body).injector().get('$state');
            return $state.current.name;
        });
    },

    gotoState: async function(state, params) {
        await this.evaluate((state, params) => {
            let $state = angular.element(document.body).injector().get('$state');
            return $state.go(state, params);
        }, state, params);
        await this.waitForSpinnerLoad();
    },

    waitForState: async function(state) {
        await this.waitForFunction(state => {
            let $state = angular.element(document.body).injector().get('$state');
            return !$state.transition && $state.is(state);
        }, {}, state);
        await this.waitForFunction(() => {
            return angular.element(() => {
                return true;
            });
        });
        await this.waitForSpinnerLoad();
    },

    waitForTransition: async function() {
        await this.waitForFunction(() => {
            const $state = angular.element(document.body).injector().get('$state');
            return !$state.transition;
        });
        await this.waitForSpinnerLoad();
    },

    accessToSection: async function(state, name = 'Others') {
        await this.waitForSelector('vn-left-menu');
        let nested = await this.evaluate(state => {
            return document.querySelector(`vn-left-menu li li > a[ui-sref="${state}"]`) != null;
        }, state);

        if (nested) {
            let selector = `vn-left-menu li[name="${name}"]`;
            await this.evaluate(selector => {
                document.querySelector(selector).scrollIntoViewIfNeeded();
            }, selector);
            await this.waitToClick(selector);
            await this.waitForSelector('vn-left-menu .expanded');
        }

        await this.evaluate(state => {
            let navButton = document.querySelector(`vn-left-menu li > a[ui-sref="${state}"]`);
            navButton.scrollIntoViewIfNeeded();
            return navButton.click();
        }, state);

        await this.waitForState(state);
    },

    reloadSection: async function(state) {
        await this.click('vn-icon[icon="launch"]');
        await this.accessToSection(state);
    },

    forceReloadSection: async function(sectionRoute) {
        await this.waitToClick('vn-icon[icon="launch"]');
        await this.waitToClick('button[response="accept"]');
        await this.waitForSelector('vn-card.summary');
        await this.waitToClick(`vn-left-menu li > a[ui-sref="${sectionRoute}"]`);
    },

    doSearch: async function(searchValue) {
        await this.clearInput('vn-searchbar');
        if (searchValue)
            await this.write('vn-searchbar', searchValue);

        await this.waitToClick('vn-searchbar vn-icon[icon="search"]');
        await this.waitForTransition();
    },

    accessToSearchResult: async function(searchValue) {
        await this.doSearch(searchValue);
        await this.waitForSelector('.vn-descriptor');
    },

    getProperty: async function(selector, property) {
        return this.evaluate((selector, property) => {
            return document.querySelector(selector)[property].replace(/\s+/g, ' ').trim();
        }, selector, property);
    },

    getClassName: async function(selector) {
        const element = await this.$(selector);
        const handle = await element.getProperty('className');
        return handle.jsonValue();
    },

    waitPropertyLength: async function(selector, property, minLength) {
        await this.waitForFunction((selector, property, minLength) => {
            const element = document.querySelector(selector);
            const isValidElement = element && element[property] != null && element[property] !== '';
            return isValidElement && element[property].length >= minLength;
        }, {}, selector, property, minLength);
        return this.getProperty(selector, property);
    },

    expectPropertyValue: async function(selector, property, value) {
        let builtSelector = selector;
        if (property != 'innerText')
            builtSelector = await this.selectorFormater(selector);

        try {
            return this.waitForFunction((selector, property, value) => {
                const element = document.querySelector(selector);
                return element[property] == value;
            }, {}, builtSelector, property, value);
        } catch (error) {
            throw new Error(`${value} wasn't the value of ${builtSelector}, ${error}`);
        }
    },

    waitToGetProperty: async function(selector, property) {
        let builtSelector = selector;
        if (selector.includes('vn-input-file') || property != 'innerText')
            builtSelector = await this.selectorFormater(selector);

        try {
            await this.waitForFunction((selector, property) => {
                const element = document.querySelector(selector);

                return element && element[property] != null && element[property] !== '';
            }, {}, builtSelector, property);
            return this.getProperty(builtSelector, property);
        } catch (error) {
            throw new Error(`couldn't get property: ${property} of ${builtSelector}, ${error}`);
        }
    },

    write: async function(selector, text) {
        let builtSelector = await this.selectorFormater(selector);
        await this.waitForSelector(selector);
        await this.type(builtSelector, text);
        await this.waitForTextInField(selector, text);
    },

    overwrite: async function(selector, text) {
        await this.clearInput(selector);
        await this.write(selector, text);
    },

    waitToClick: async function(selector) {
        await this.waitForSelector(selector);
        await this.waitForFunction(checkVisibility, {}, selector);

        return this.click(selector);
    },

    writeOnEditableTD: async function(selector, text) {
        let builtSelector = await this.selectorFormater(selector);
        await this.waitToClick(selector);
        await this.waitForSelector(builtSelector, {visible: true});
        await this.type(builtSelector, text);
        await this.keyboard.press('Enter');
    },

    focusElement: async function(selector) {
        await this.waitForSelector(selector);
        return this.evaluate(selector => {
            let element = document.querySelector(selector);
            element.focus();
        }, selector);
    },

    isVisible: async function(selector) {
        await this.waitForSelector(selector);
        return this.evaluate(checkVisibility, selector);
    },

    waitImgLoad: async function(selector) {
        await this.waitForSelector(selector);
        return this.waitForFunction(selector => {
            const imageReady = document.querySelector(selector).complete;
            return imageReady;
        }, {}, selector);
    },

    countElement: async function(selector) {
        return this.evaluate(selector => {
            return document.querySelectorAll(selector).length;
        }, selector);
    },

    waitForNumberOfElements: async function(selector, count) {
        try {
            await this.waitForFunction((selector, count) => {
                return document.querySelectorAll(selector).length == count;
            }, {}, selector, count);
        } catch (error) {
            const amount = await this.countElement(selector);
            throw new Error(`actual amount of elements was: ${amount} instead of ${count}, ${error}`);
        }
    },

    waitForClassNotPresent: async function(selector, className) {
        await this.waitForSelector(selector);
        return this.waitForFunction((selector, className) => {
            if (!document.querySelector(selector).classList.contains(className))
                return true;
        }, {}, selector, className);
    },

    waitForClassPresent: async function(selector, className) {
        await this.waitForSelector(selector);
        return this.waitForFunction((elementSelector, targetClass) => {
            if (document.querySelector(elementSelector).classList.contains(targetClass))
                return true;
        }, {}, selector, className);
    },

    waitForTextInElement: async function(selector, text) {
        await this.waitForFunction((selector, text) => {
            if (document.querySelector(selector)) {
                const innerText = document.querySelector(selector).innerText.toLowerCase();
                const expectedText = text.toLowerCase();
                if (innerText.includes(expectedText))
                    return innerText;
            }
        }, {}, selector, text);
    },

    waitForTextInField: async function(selector, text) {
        const builtSelector = await this.selectorFormater(selector);
        const expectedValue = text.toLowerCase();

        try {
            await this.waitForFunction((selector, text) => {
                const element = document.querySelector(selector);
                if (element) {
                    const value = element.value.toLowerCase();
                    if (value.includes(text))
                        return true;
                }
            }, {}, builtSelector, expectedValue);
        } catch (error) {
            throw new Error(`${text} wasn't the value of ${builtSelector}, ${error}`);
        }
    },

    selectorFormater: function(selector) {
        if (selector.includes('vn-textarea'))
            return `${selector} textarea`;

        if (selector.includes('vn-input-file'))
            return `${selector} section`;

        return `${selector} input`;
    },

    waitForInnerText: async function(selector) {
        await this.waitForSelector(selector, {});
        await this.waitForFunction(selector => {
            const innerText = document.querySelector(selector).innerText;
            return innerText != null && innerText != '';
        }, {}, selector);
        return this.evaluate(selector => {
            return document.querySelector(selector).innerText;
        }, selector);
    },

    waitForEmptyInnerText: async function(selector) {
        return this.waitFunction(selector => {
            return document.querySelector(selector).innerText == '';
        }, selector);
    },

    hideSnackbar: async function() {
        // Holds up for the snackbar to be visible for a small period of time.
        if (process.env.E2E_DEBUG)
            await this.waitForTimeout(300);

        await this.evaluate(() => {
            let hideButton = document
                .querySelector('vn-snackbar .shape.shown button');
            if (hideButton)
                return hideButton.click();
        });
        await this.waitForSelector('vn-snackbar .shape.shown', {hidden: true});
    },

    waitForSnackbar: async function() {
        const selector = 'vn-snackbar .shape.shown';
        await this.waitForSelector(selector);

        const message = await this.evaluate(selector => {
            const shape = document.querySelector(selector);
            const message = {
                text: shape.querySelector('.text').innerText
            };

            const types = ['error', 'success'];
            for (let type of types) {
                if (shape.classList.contains(type)) {
                    message.type = type;
                    break;
                }
            }

            return message;
        }, selector);

        message.isSuccess = message.type == 'success';

        await this.hideSnackbar();
        return message;
    },

    pickDate: async function(selector, date) {
        date = date || Date.vnNew();

        const timeZoneOffset = date.getTimezoneOffset() * 60000;
        const localDate = (new Date(date.getTime() - timeZoneOffset))
            .toISOString().substr(0, 10);

        await this.waitForSelector(selector);
        await this.evaluate((selector, localDate) => {
            let input = document.querySelector(selector).$ctrl.input;
            input.value = localDate;
            input.dispatchEvent(new Event('change'));
        }, selector, localDate);
    },

    pickTime: async function(selector, time) {
        await this.waitForSelector(selector);
        await this.evaluate((selector, time) => {
            let input = document.querySelector(selector).$ctrl.input;
            input.value = time;
            input.dispatchEvent(new Event('change'));
        }, selector, time);
    },

    clearTextarea: async function(selector) {
        await this.waitForSelector(selector, {visible: true});
        await this.evaluate(inputSelector => {
            return document.querySelector(`${inputSelector} textarea`).value = '';
        }, selector);
    },

    autocompleteSearch: async function(selector, searchValue) {
        let builtSelector = await this.selectorFormater(selector);

        await this.waitToClick(selector);
        await this.write('.vn-drop-down.shown vn-textfield', searchValue);

        try {
            await this.waitForFunction((selector, searchValue) => {
                let element = document
                    .querySelector(`${selector} vn-drop-down`).$ctrl.content
                    .querySelector('li.active');
                if (element)
                    return element.innerText.toLowerCase().includes(searchValue.toLowerCase());
            }, {}, selector, searchValue);
        } catch (error) {
            let inputValue = await this.evaluate(() => {
                return document.querySelector('.vn-drop-down.shown vn-textfield input').value;
            });
            throw new Error(`${builtSelector} value is ${inputValue}! ${error}`);
        }
        await this.keyboard.press('Enter');
        await this.waitForFunction((selector, searchValue) => {
            return document.querySelector(selector).value.toLowerCase()
                .includes(searchValue.toLowerCase());
        }, {}, builtSelector, searchValue);

        await this.waitForSelector('.vn-drop-down', {hidden: true});
    },

    checkboxState: async function(selector) {
        await this.waitForSelector(selector);
        const value = await this.getInputValue(selector);
        switch (value) {
        case null:
            return 'intermediate';
        case true:
            return 'checked';
        default:
            return 'unchecked';
        }
    },

    isDisabled: async function(selector) {
        await this.waitForSelector(selector);
        return this.evaluate(selector => {
            let element = document.querySelector(selector);
            return element.$ctrl.disabled;
        }, selector);
    },

    waitForStylePresent: async function(selector, property, value) {
        return this.waitForFunction((selector, property, value) => {
            const element = document.querySelector(selector);
            return element.style[property] == value;
        }, {}, selector, property, value);
    },

    waitForSpinnerLoad: async function() {
        await this.waitForSelector('vn-topbar vn-spinner', {hidden: true});
    },

    waitForWatcherData: async function(selector) {
        await this.waitForSelector(selector);
        await this.waitForFunction(selector => {
            let watcher = document.querySelector(selector);
            let orgData = watcher.$ctrl.orgData;
            return !angular.equals({}, orgData) && orgData != null;
        }, {}, selector);
        await this.waitForSpinnerLoad();
    },

    waitForMutation: async function(selector, type) {
        try {
            await this.evaluate((selector, type) => {
                return new Promise(resolve => {
                    const config = {attributes: true, childList: true, subtree: true};
                    const target = document.querySelector(selector);

                    const onEnd = function(mutationsList, observer) {
                        resolve();

                        observer.disconnect();
                    };
                    const observer = new MutationObserver(onEnd);
                    observer.expectedType = type;

                    observer.observe(target, config);
                });
            }, selector, type);
        } catch (error) {
            throw new Error(`failed to wait for mutation type: ${type}`);
        }
    },

    waitForTransitionEnd: async function(selector) {
        await this.evaluate(selector => {
            return new Promise(resolve => {
                const transition = document.querySelector(selector);
                const onEnd = function() {
                    transition.removeEventListener('transitionend', onEnd);
                    resolve();
                };
                transition.addEventListener('transitionend', onEnd);
            });
        }, selector);
    },

    closePopup: async function() {
        await Promise.all([
            this.keyboard.press('Escape'),
            this.waitForSelector('.vn-popup', {hidden: true}),
        ]);
    },

    respondToDialog: async function(response) {
        await this.waitForSelector('.vn-dialog.shown');
        const firstCount = await this.evaluate(text => {
            const dialogs = document.querySelectorAll('.vn-dialog');
            const dialogOnTop = dialogs[dialogs.length - 1];
            const button = dialogOnTop.querySelector(`div.buttons [response="${text}"]`);
            button.click();
            return dialogs.length;
        }, response);

        await this.waitForFunction(firstCount => {
            const dialogs = document.querySelectorAll('.vn-dialog');
            return dialogs.length < firstCount;
        }, {}, firstCount);
    },

    waitForContentLoaded: async function() {
        await this.waitForSpinnerLoad();
    },

    async getInputValue(selector) {
        return this.evaluate(selector => {
            const input = document.querySelector(selector);
            return input.$ctrl.field;
        }, selector);
    },

    async getValue(selector) {
        return await this.waitToGetProperty(selector, 'value');
    },

    async innerText(selector) {
        const element = await this.$(selector);
        const handle = await element.getProperty('innerText');
        return handle.jsonValue();
    },

    async setInput(selector, value) {
        const input = await this.$(selector);
        const tagName = (await input.evaluate(e => e.tagName)).toLowerCase();

        switch (tagName) {
        case 'vn-textfield':
        case 'vn-datalist':
        case 'vn-input-number':
            await this.clearInput(selector);
            if (value)
                await this.write(selector, value.toString());
            break;
        case 'vn-autocomplete':
            if (value)
                await this.autocompleteSearch(selector, value.toString());
            else
                await this.clearInput(selector);
            break;
        case 'vn-date-picker':
            if (value)
                await this.pickDate(selector, value);
            else
                await this.clearInput(selector);
            break;
        case 'vn-input-time':
            if (value)
                await this.pickTime(selector, value);
            else
                await this.clearInput(selector);
            break;
        case 'vn-check':
            for (let i = 0; i < 3; i++) {
                if (await this.getInput(selector) == value) break;
                await this.click(selector);
            }
            break;
        }
    },

    async getInput(selector) {
        const input = await this.$(selector);
        const tagName = (await input.evaluate(e => e.tagName)).toLowerCase();
        let el;
        let value;

        switch (tagName) {
        case 'vn-textfield':
        case 'vn-autocomplete':
        case 'vn-input-time':
        case 'vn-datalist':
            el = await input.$('input');
            value = await el.getProperty('value');
            return value.jsonValue();
        case 'vn-check':
        case 'vn-input-number':
            return await this.getInputValue(selector);
        case 'vn-textarea':
            el = await input.$('textarea');
            value = await el.getProperty('value');
            return value.jsonValue();
        case 'vn-date-picker':
            el = await input.$('input');
            value = await el.getProperty('value');
            if (value) {
                const date = new Date(await value.jsonValue());
                date.setUTCHours(0, 0, 0, 0);
                return date;
            } else
                return null;
        default:
            value = await this.innerText(selector);
            return value.jsonValue();
        }
    },

    async clearInput(selector) {
        await this.waitForSelector(selector);

        let field = await this.evaluate(selector => {
            return document.querySelector(`${selector} input`).closest('.vn-field').$ctrl.field;
        }, selector);

        if ((field != null && field != '') || field == '0') {
            let coords = await this.evaluate(selector => {
                let rect = document.querySelector(selector).getBoundingClientRect();
                return {x: rect.x + (rect.width / 2), y: rect.y + (rect.height / 2), width: rect.width};
            }, selector);
            await this.mouse.move(coords.x, coords.y);
            await this.waitForSelector(`${selector} [icon="clear"]`, {visible: true});
            await this.waitToClick(`${selector} [icon="clear"]`);
        }

        await this.evaluate(selector => {
            return document.querySelector(`${selector} input`).closest('.vn-field').$ctrl.field == '';
        }, selector);
    },

    async fetchForm(selector, inputNames) {
        const values = {};
        for (const inputName of inputNames)
            values[inputName] = await this.getInput(`${selector} [vn-name="${inputName}"]`);
        return values;
    },

    async fillForm(selector, values) {
        for (const inputName in values)
            await this.setInput(`${selector} [vn-name="${inputName}"]`, values[inputName]);
    },

    async sendForm(selector, values) {
        if (values) await this.fillForm(selector, values);
        await this.click(`${selector} button[type=submit]`);
        return await this.waitForSnackbar();
    }
};

export function extendPage(page) {
    for (let name in actions) {
        page[name] = async(...args) => {
            try {
                return await actions[name].apply(page, args);
            } catch (err) {
                let stringArgs = args
                    .map(i => typeof i == 'function' ? 'Function' : `'${i}'`)
                    .join(', ');
                const myErr = new Error(`${err.message}\n  at Page.${name}(${stringArgs})`);
                myErr.stack = err.stack;
                throw myErr;
            }
        };
    }

    page.wait = page.waitFor;

    return page;
}

export default actions;