'use strict';

Object.defineProperty(exports, "__esModule", {
    value: true
});

var _getIterator2 = require('babel-runtime/core-js/get-iterator');

var _getIterator3 = _interopRequireDefault(_getIterator2);

var _keys = require('babel-runtime/core-js/object/keys');

var _keys2 = _interopRequireDefault(_keys);

var _typeof2 = require('babel-runtime/helpers/typeof');

var _typeof3 = _interopRequireDefault(_typeof2);

var _promise = require('babel-runtime/core-js/promise');

var _promise2 = _interopRequireDefault(_promise);

var _getPrototypeOf = require('babel-runtime/core-js/object/get-prototype-of');

var _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf);

var _create = require('babel-runtime/core-js/object/create');

var _create2 = _interopRequireDefault(_create);

var _q = require('q');

var _q2 = _interopRequireDefault(_q);

var _fs = require('fs');

var _fs2 = _interopRequireDefault(_fs);

var _path = require('path');

var _path2 = _interopRequireDefault(_path);

var _deepmerge = require('deepmerge');

var _deepmerge2 = _interopRequireDefault(_deepmerge);

var _mkdirp = require('mkdirp');

var _mkdirp2 = _interopRequireDefault(_mkdirp);

var _events = require('events');

var _events2 = _interopRequireDefault(_events);

var _RequestHandler = require('./utils/RequestHandler');

var _RequestHandler2 = _interopRequireDefault(_RequestHandler);

var _ErrorHandler = require('./utils/ErrorHandler');

var _Logger = require('./utils/Logger');

var _Logger2 = _interopRequireDefault(_Logger);

var _safeExecute = require('./helpers/safeExecute');

var _safeExecute2 = _interopRequireDefault(_safeExecute);

var _sanitize = require('./helpers/sanitize');

var _sanitize2 = _interopRequireDefault(_sanitize);

var _mobileDetector2 = require('./helpers/mobileDetector');

var _mobileDetector3 = _interopRequireDefault(_mobileDetector2);

var _detectSeleniumBackend = require('./helpers/detectSeleniumBackend');

var _detectSeleniumBackend2 = _interopRequireDefault(_detectSeleniumBackend);

var _errorHandler = require('./helpers/errorHandler');

var _errorHandler2 = _interopRequireDefault(_errorHandler);

var _hasElementResultHelper = require('./helpers/hasElementResultHelper');

var _hasElementResultHelper2 = _interopRequireDefault(_hasElementResultHelper);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

var INTERNAL_EVENTS = ['init', 'command', 'error', 'result', 'end', 'screenshot'];
var PROMISE_FUNCTIONS = ['then', 'catch', 'finally'];

var EventEmitter = _events2.default.EventEmitter;

/**
 * WebdriverIO v4
 */
var WebdriverIO = function WebdriverIO(args, modifier) {
    var prototype = (0, _create2.default)(Object.prototype);
    var rawCommands = {};
    var eventHandler = new EventEmitter();
    var fulFilledPromise = (0, _q2.default)();
    var stacktrace = [];
    var commandList = [];

    var EVENTHANDLER_FUNCTIONS = (0, _getPrototypeOf2.default)(eventHandler);

    /**
     * merge default options with given user options
     */
    var options = (0, _deepmerge2.default)({
        protocol: 'http',
        waitforTimeout: 500,
        waitforInterval: 500,
        coloredLogs: true,
        logLevel: 'silent',
        baseUrl: null,
        onError: [],
        connectionRetryTimeout: 90000,
        connectionRetryCount: 3
    }, typeof args !== 'string' ? args : {});

    /**
     * define Selenium backend given on user options
     */
    options = (0, _deepmerge2.default)((0, _detectSeleniumBackend2.default)(args), options);

    /**
     * only set globals we wouldn't get otherwise
     */
    if (!process.env.WEBDRIVERIO_COLORED_LOGS) {
        process.env.WEBDRIVERIO_COLORED_LOGS = options.coloredLogs;
    }

    var logger = new _Logger2.default(options, eventHandler);
    var requestHandler = new _RequestHandler2.default(options, eventHandler, logger);

    /**
     * assign instance to existing session
     */
    if (typeof args === 'string') {
        requestHandler.sessionID = args;
    }

    /**
     * sanitize error handler
     */
    if (!Array.isArray(options.onError)) {
        options.onError = [options.onError];
    }
    options.onError = options.onError.filter(function (fn) {
        return typeof fn === 'function';
    });

    var desiredCapabilities = (0, _deepmerge2.default)({
        javascriptEnabled: true,
        locationContextEnabled: true,
        handlesAlerts: true,
        rotatable: true
    }, options.desiredCapabilities || {});

    var _mobileDetector = (0, _mobileDetector3.default)(desiredCapabilities);

    var isMobile = _mobileDetector.isMobile;
    var isIOS = _mobileDetector.isIOS;
    var isAndroid = _mobileDetector.isAndroid;

    /**
     * if no caps are specified fall back to firefox
     */

    if (!desiredCapabilities.browserName && !isMobile) {
        desiredCapabilities.browserName = 'firefox';
    }

    if (!isMobile && typeof desiredCapabilities.loggingPrefs === 'undefined') {
        desiredCapabilities.loggingPrefs = {
            browser: 'ALL',
            driver: 'ALL'
        };
    }

    var resolve = function resolve(result, onFulfilled, onRejected, context) {
        var _this = this;

        if (typeof result === 'function') {
            this.isExecuted = true;
            result = result.call(this);
        }

        /**
         * run error handler if command fails
         */
        if (result instanceof Error) {
            (function () {
                var _result = result;

                _this.defer.resolve(_promise2.default.all(_errorHandler2.default.map(function (fn) {
                    return fn.call(context, result);
                })).then(function (res) {
                    var handlerResponses = res.filter(function (r) {
                        return typeof r !== 'undefined';
                    });

                    /**
                     * if no handler was triggered trough actual error
                     */
                    if (handlerResponses.length === 0) {
                        return callErrorHandlerAndReject.call(context, _result, onRejected);
                    }

                    return onFulfilled.call(context, handlerResponses[0]);
                }, function (e) {
                    return callErrorHandlerAndReject.call(context, e, onRejected);
                }));
            })();
        } else {
            this.defer.resolve(result);
        }

        return this.promise;
    };

    /**
     * middleware to call on error handler in wdio mode
     */
    var callErrorHandlerAndReject = function callErrorHandlerAndReject(err, onRejected) {
        var _this2 = this;

        /**
         * only call error handler if there is any and if error has bubbled up
         */
        if (!this || this.depth !== 0 || options.onError.length === 0) {
            return reject.call(this, err, onRejected);
        }

        return new _promise2.default(function (resolve, reject) {
            return _promise2.default.all(options.onError.map(function (fn) {
                if (!global.wdioSync) {
                    return fn.call(_this2, err);
                }

                return new _promise2.default(function (resolve) {
                    return global.wdioSync(fn, resolve).call(_this2, err);
                });
            })).then(resolve, reject);
        }).then(function () {
            return reject.call(_this2, err, onRejected);
        });
    };

    /**
     * By using finally in our next method we omit the duty to throw an exception at some
     * point. To avoid propagating rejected promises until everything crashes silently we
     * check if the last and current promise got rejected. If so we can throw the error.
     */
    var reject = function reject(err, onRejected) {
        if (!options.isWDIO && !options.screenshotOnReject && typeof onRejected === 'function') {
            delete err.screenshot;
            return onRejected(err);
        }

        var onRejectedSafe = function onRejectedSafe(err) {
            if (typeof onRejected === 'function') {
                onRejected(err);
            }
        };

        if (!this && !options.screenshotOnReject) {
            onRejectedSafe(err);
            throw err;
        }

        if (this && this.depth !== 0) {
            onRejectedSafe(err);
            return this.promise;
        }

        var shouldTakeScreenshot = options.screenshotOnReject || typeof options.screenshotPath === 'string';
        if (!shouldTakeScreenshot || err.shotTaken || insideCommand('screenshot', this)) {
            return fail(err, onRejected);
        }

        err.shotTaken = true;
        return takeScreenshot(err).catch(function (e) {
            return logger.log('\tFailed to take screenshot on reject:', e);
        }).then(function () {
            return fail(err, onRejected);
        });
    };

    function insideCommand(command, unit) {
        var commands = unit && unit.commandList;
        return commands && commands[commands.length - 1].name === command;
    }

    function takeScreenshot(err) {
        var client = unit();
        var failDate = new Date();

        // don't take a new screenshot if we already got one from Selenium
        var getScreenshot = typeof err.screenshot === 'string' ? function () {
            return err.screenshot;
        } : function () {
            return rawCommands.screenshot.call(client).then(function (res) {
                return res.value;
            });
        };

        return _q2.default.fcall(getScreenshot).then(function (screenshot) {
            if (options.screenshotOnReject) {
                err.screenshot = screenshot;
            }

            if (typeof options.screenshotPath === 'string') {
                var filename = saveScreenshotSync(screenshot, failDate);
                client.emit('screenshot', { data: screenshot, filename: filename });
            }
        });
    }

    function saveScreenshotSync(screenshot, date) {
        var screenshotPath = _path2.default.isAbsolute(options.screenshotPath) ? options.screenshotPath : _path2.default.join(process.cwd(), options.screenshotPath);

        /**
        * create directory if not existing
        */
        try {
            _fs2.default.statSync(screenshotPath);
        } catch (e) {
            _mkdirp2.default.sync(screenshotPath);
        }

        var capId = _sanitize2.default.caps(desiredCapabilities);
        var timestamp = date.toJSON().replace(/:/g, '-');
        var filename = 'ERROR_' + capId + '_' + timestamp + '.png';
        var filePath = _path2.default.join(screenshotPath, filename);

        _fs2.default.writeFileSync(filePath, new Buffer(screenshot, 'base64'));
        logger.log('\tSaved screenshot: ' + filename);

        return filename;
    }

    function fail(e, onRejected) {
        if (!e.stack) {
            e = new Error(e);
        }

        var stack = stacktrace.slice().map(function (trace) {
            return '    at ' + trace;
        });
        e.stack = e.name + ': ' + e.message + '\n' + stack.reverse().join('\n');

        /**
         * the waitUntil command can execute a lot of functions until it resolves
         * to keep the stacktrace sane we just shrink down the stacktrack and
         * only keep waitUntil in it
         */
        if (e.name === 'WaitUntilTimeoutError') {
            stack = e.stack.split('\n');
            stack.splice(1, stack.length - 2);
            e.stack = stack.join('\n');
        }

        /**
         * ToDo useful feature for standalone mode:
         * option that if true causes script to throw exception if command fails:
         *
         * process.nextTick(() => {
         *     throw e
         * })
         */

        if (typeof onRejected !== 'function') {
            throw e;
        }

        return onRejected(e);
    }

    /**
     * WebdriverIO Monad
     */
    function unit(lastPromise) {
        var client = (0, _create2.default)(prototype);
        var defer = _q2.default.defer();
        var promise = defer.promise;

        client.defer = defer;
        client.promise = promise;
        client.lastPromise = lastPromise || fulFilledPromise;

        client.desiredCapabilities = desiredCapabilities;
        client.requestHandler = requestHandler;
        client.logger = logger;
        client.options = options;
        client.commandList = commandList;

        client.isMobile = isMobile;
        client.isIOS = isIOS;
        client.isAndroid = isAndroid;

        /**
         * actual bind function
         */
        client.next = function (func, args, name) {
            var _this3 = this;

            /**
             * use finally to propagate rejected promises up the chain
             */
            return this.lastPromise.then(function (val) {
                /**
                 * store command into command list so `getHistory` can return it
                 */
                commandList.push({ name: name, args: args });

                /**
                 * allow user to leave out selector argument if they have already queried an element before
                 */
                var lastResult = val || _this3.lastResult;
                if ((0, _hasElementResultHelper2.default)(lastResult) && args.length < func.length && func.toString().indexOf('function ' + name + '(selector') === 0) {
                    if (lastResult.selector && name === 'waitForExist') {
                        _this3.lastResult = null;
                        args.unshift(lastResult.selector);
                    } else {
                        args.unshift(null);
                    }
                }

                return resolve.call(_this3, (0, _safeExecute2.default)(func, args));
            }, function (e) {
                /**
                 * this will get reached only in standalone mode if the command
                 * fails and doesn't get followed by a then or catch method
                 */
                return resolve.call(_this3, e, null, null, { depth: 0 });
            });
        };

        client.finally = function (fn) {
            var _this4 = this;

            var client = unit(this.promise.finally(function () {
                return resolve.call(client, (0, _safeExecute2.default)(fn, []).bind(_this4));
            }));
            return client;
        };

        client.call = function (fn) {
            var _this5 = this;

            var client = unit(this.promise.done(function () {
                return resolve.call(client, (0, _safeExecute2.default)(fn, []).bind(_this5));
            }));
            return client;
        };

        client.then = function (onFulfilled, onRejected) {
            var _this6 = this;

            if (typeof onFulfilled !== 'function' && typeof onRejected !== 'function') {
                return this;
            }

            /**
             * execute then function in context of the new instance
             * but resolve result with this
             */
            var client = unit(this.promise.then(function () {
                for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
                    args[_key] = arguments[_key];
                }

                /**
                 * store result in commandList
                 */
                if (commandList.length) {
                    commandList[commandList.length - 1].result = args[0];
                }

                /**
                 * resolve command
                 */
                return resolve.call(client, (0, _safeExecute2.default)(onFulfilled, args).bind(_this6));
            }, function (e) {
                var result = (0, _safeExecute2.default)(onRejected, [e]).bind(_this6);

                /**
                 * handle error once command was bubbled up the command chain
                 */
                if (_this6.depth === 0) {
                    result = e;
                }

                return resolve.call(client, result, onFulfilled, onRejected, _this6);
            }));

            return client;
        };

        client.catch = function (onRejected) {
            return this.then(undefined, onRejected);
        };

        client.inspect = function () {
            return this.promise.inspect();
        };

        /**
         * internal helper method to handle command results
         *
         * @param  {Promise[]} promises  list of promises
         * @param  {Boolean}   option    if true extract value property from selenium result
         */
        client.unify = function (promises, option) {
            option = option || {};
            promises = Array.isArray(promises) ? promises : [promises];

            return _promise2.default.all(promises)
            /**
             * extract value property from result if desired
             */
            .then(function (result) {
                if (!option.extractValue || !Array.isArray(result)) {
                    return result;
                }

                return result.map(function (res) {
                    return res.value && typeof res.value === 'string' ? res.value.trim() : res.value;
                });

                /**
                 * sanitize result for better assertion
                 */
            }).then(function (result) {
                if (Array.isArray(result) && result.length === 1) {
                    result = result[0];
                }

                if (option.lowercase && typeof result === 'string') {
                    result = result.toLowerCase();
                }

                return result;
            });
        };

        /**
         * addCommand handler for async or standalone mode
         * @param {String}   fnName         function name
         * @param {Function} fn             function
         * @param {Boolean}  forceOverwrite if true it can overwrite existing commands
         */
        client.addCommand = function (fnName, fn, forceOverwrite) {
            if (typeof fn === 'string') {
                var namespace = arguments[0];
                fnName = arguments[1];
                fn = arguments[2];
                forceOverwrite = arguments[3];

                switch ((0, _typeof3.default)(client[namespace])) {
                    case 'function':
                        throw new _ErrorHandler.RuntimeError('Command namespace "' + namespace + '" is used internally, and can\'t be overwritten!');
                    case 'object':
                        if (client[namespace][fnName] && !forceOverwrite) {
                            throw new _ErrorHandler.RuntimeError('Command "' + fnName + '" is already defined!');
                        }
                        break;
                }
                return unit.lift(namespace, fnName, fn);
            }

            if (client[fnName] && !forceOverwrite) {
                throw new _ErrorHandler.RuntimeError('Command "' + fnName + '" is already defined!');
            }
            return unit.lift(fnName, fn);
        };

        client.getPrototype = function () {
            return prototype;
        };

        client.transferPromiseness = function (target, promise) {
            /**
             * transfer WebdriverIO commands
             */
            var clientFunctions = (0, _keys2.default)(prototype);
            var functionsToTranfer = clientFunctions.concat(PROMISE_FUNCTIONS);

            var _iteratorNormalCompletion = true;
            var _didIteratorError = false;
            var _iteratorError = undefined;

            try {
                for (var _iterator = (0, _getIterator3.default)(functionsToTranfer), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
                    var fnName = _step.value;

                    if (typeof promise[fnName] === 'function') {
                        target[fnName] = promise[fnName].bind(promise);
                    }
                }
            } catch (err) {
                _didIteratorError = true;
                _iteratorError = err;
            } finally {
                try {
                    if (!_iteratorNormalCompletion && _iterator.return) {
                        _iterator.return();
                    }
                } finally {
                    if (_didIteratorError) {
                        throw _iteratorError;
                    }
                }
            }
        };

        if (typeof modifier === 'function') {
            client = modifier(client, options);
        }

        return client;
    }

    /**
     * enhance base monad prototype with methods
     */
    unit.lift = function (name, func) {
        var commandGroup = prototype;

        if (typeof func === 'string') {
            var namespace = arguments[0];
            name = arguments[1];
            func = arguments[2];

            if (!prototype[namespace]) {
                prototype[namespace] = {};
            }

            commandGroup = prototype[namespace];
        }

        rawCommands[name] = func;

        commandGroup[name] = function () {
            var nextPromise = this.promise;

            /**
             * commands executed inside commands don't have to wait
             * on any promise
             */
            if (this.isExecuted) {
                nextPromise = this.lastPromise;
            }

            var client = unit(nextPromise);

            /**
             * catch stack to find information about where the command that causes
             * the error was used (stack line 2) and only save it when it was not
             * within WebdriverIO context
             */
            var stack = new Error().stack;
            var lineInTest = stack.split('\n').slice(2, 3).join('\n');
            var fileAndPosition = lineInTest.slice(lineInTest.indexOf('(') + 1, lineInTest.indexOf(')'));
            var atCommand = lineInTest.trim().slice(3).split(' ')[0];

            atCommand = atCommand.slice(atCommand.lastIndexOf('.') + 1);

            for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
                args[_key2] = arguments[_key2];
            }

            var trace = name + '(' + _sanitize2.default.args(args) + ') - ' + fileAndPosition.slice(fileAndPosition.lastIndexOf('/') + 1);
            if ((0, _keys2.default)(prototype).indexOf(atCommand) === -1 && atCommand !== 'exports') {
                stacktrace = [trace];
            } else {
                /**
                 * save trace for nested commands
                 */
                stacktrace.push(trace);
            }

            /**
             * determine execution depth:
             * This little tweak helps us to determine whether the command was executed
             * by the test script or by another command. With that we can make sure
             * that errors are getting thrown once they bubbled up the command chain.
             */
            client.depth = stack.split('\n').filter(function (line) {
                return !!line.match(/\/lib\/(commands|protocol)\/(\w+)\.js/);
            }).length;

            /**
             * queue command
             */
            client.name = name;
            client.lastResult = this.lastResult;
            client.next(func, args, name);
            return client;
        };

        return unit;
    };

    /**
     * register event emitter
     */

    var _loop = function _loop(eventCommand) {
        prototype[eventCommand] = function () {
            for (var _len3 = arguments.length, args = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
                args[_key3] = arguments[_key3];
            }

            /**
             * custom commands needs to get emitted and registered in order
             * to prevent race conditions
             */
            if (INTERNAL_EVENTS.indexOf(args[0]) === -1) {
                return this.finally(function () {
                    return eventHandler[eventCommand].apply(eventHandler, args);
                });
            }

            eventHandler[eventCommand].apply(eventHandler, args);
            return this;
        };
    };

    for (var eventCommand in EVENTHANDLER_FUNCTIONS) {
        _loop(eventCommand);
    }

    return unit;
};

exports.default = WebdriverIO;
module.exports = exports['default'];
