text: import Future from 'fibers/future'
  import assign from 'object.assign'
  
  const Fiber = require('fibers') // ToDo fix unit test to work with imports
  
  const SYNC_COMMANDS = ['domain', '_events', '_maxListeners', 'setMaxListeners', 'emit',
      'addListener', 'on', 'once', 'removeListener', 'removeAllListeners', 'listeners',
      'getMaxListeners', 'listenerCount', 'getPrototype']
  
  const STACKTRACE_FILTER = /((wdio-sync\/)*(build\/index.js|node_modules\/fibers)|- - - - -)/g
  const STACKTRACE_FILTER_FN = (e) => !e.match(STACKTRACE_FILTER)
  
  let commandIsRunning = false
  let forcePromises = false
  
  /**
   * helpers
   */
  const isAsync = function () {
      if (!global.browser || !global.browser.options) {
          return true
      }
  
      return global.browser.options.sync === false
  }
  
  const isElements = function (result) {
      return (
          typeof result.selector === 'string' &&
          Array.isArray(result.value) && result.value.length &&
          typeof result.value[0].ELEMENT !== 'undefined'
      )
  }
  
  const is$$ = function (result) {
      return Array.isArray(result) && !!result.length && !!result[0] && result[0].ELEMENT !== undefined
  }
  
  const sanitizeErrorMessage = function (e) {
      let stack = e.stack.split(/\n/g)
      let errorMsg = stack.shift()
      let cwd = process.cwd()
  
      /**
       * filter out stack traces to wdio-sync and fibers
       * and transform absolute path to relative
       */
      stack = stack.filter(STACKTRACE_FILTER_FN)
      stack = stack.map((e) => '    ' + e.replace(cwd + '/', '').trim())
  
      /**
       * error stack can be empty when test execution is aborted and
       * the application is not running
       */
      let errorLine = 'unknown error line'
      if (stack && stack.length) {
          errorLine = stack.shift().trim()
      }
  
      /**
       * correct error occurence
       */
      let lineToFix = stack[stack.length - 1]
      if (lineToFix && lineToFix.indexOf('index.js') > -1) {
          stack[stack.length - 1] = lineToFix.slice(0, lineToFix.indexOf('index.js')) + errorLine
      } else {
          stack.unshift('    ' + errorLine)
      }
  
      /**
       * add back error message
       */
      stack.unshift(errorMsg)
  
      return stack.join('\n')
  }
  
  // filter out arguments passed to specFn & hookFn, don't allow callbacks
  // as there is no need for user to call e.g. `done()`
  const filterSpecArgs = function (args) {
      return args.filter((arg) => typeof arg !== 'function')
  }
  
  /**
   * Helper method to execute a row of hooks with certain parameters.
   * It will return with a reject promise due to a design decision to not let hooks/service intefer the
   * actual test process.
   *
   * @param  {Function|Function[]} hooks  list of hooks
   * @param  {Object[]} args  list of parameter for hook functions
   * @return {Promise}  promise that gets resolved once all hooks finished running
   */
  const executeHooksWithArgs = (hooks = [], args) => {
      /**
       * make sure hooks are an array of functions
       */
      if (typeof hooks === 'function') {
          hooks = [hooks]
      }
  
      /**
       * make sure args is an array since we are calling apply
       */
      if (!Array.isArray(args)) {
          args = [args]
      }
  
      hooks = hooks.map((hook) => new Promise((resolve) => {
          let _commandIsRunning = commandIsRunning
          let result
  
          const execHook = () => {
              commandIsRunning = true
  
              try {
                  result = hook.apply(null, args)
              } catch (e) {
                  console.error(e.stack)
                  return resolve(e)
              } finally {
                  commandIsRunning = _commandIsRunning
              }
              if (result && typeof result.then === 'function') {
                  return result.then(resolve, (e) => {
                      console.error(e.stack)
                      resolve(e)
                  })
              }
  
              resolve(result)
          }
  
          /**
           * no need for fiber wrap in async mode
           */
          if (isAsync()) {
              return execHook()
          }
  
          /**
           * after command hooks require additional Fiber environment
           */
          return Fiber(execHook).run()
      }))
  
      return Promise.all(hooks)
  }
  
  /**
   * global function to wrap callbacks into Fiber context
   * @param  {Function} fn  function to wrap around
   * @return {Function}     wrapped around function
   */
  const wdioSync = global.wdioSync = function (fn, done) {
      return function (...args) {
          return Fiber(() => {
              const result = fn.apply(this, args)
  
              if (typeof done === 'function') {
                  done(result)
              }
          }).run()
      }
  }
  
  /**
   * wraps a function into a Fiber ready context to enable sync execution and hooks
   * @param  {Function}   fn             function to be executed
   * @param  {String}     commandName    name of that function
   * @param  {Function[]} beforeCommand  method to be executed before calling the actual function
   * @param  {Function[]} afterCommand   method to be executed after calling the actual function
   * @return {Function}   actual wrapped function
   */
  const wrapCommand = function (fn, commandName, beforeCommand, afterCommand) {
      if (isAsync()) {
          /**
           * async command wrap
           */
          return function (...commandArgs) {
              return fn.apply(this, commandArgs)
          }
      }
  
      /**
       * sync command wrap
       */
      return function (...commandArgs) {
          let future = new Future()
          let futureFailed = false
  
          if (forcePromises) {
              return fn.apply(this, commandArgs)
          }
  
          /**
           * don't execute [before/after]Command hook if a command was executed
           * in these hooks (otherwise we will get into an endless loop)
           */
          if (commandIsRunning) {
              let commandPromise = fn.apply(this, commandArgs)
  
              /**
               * if commandPromise is actually not a promise just return result
               */
              if (typeof commandPromise.then !== 'function') {
                  return commandPromise
              }
  
              /**
               * Try to execute with Fibers and fall back if can't.
               * This part is executed when we want to set a fiber context within a command (e.g. in waitUntil).
               */
              try {
                  commandPromise.then((commandResult) => {
                      /**
                       * extend protoype of result so people can call browser.element(...).click()
                       */
                      future.return(applyPrototype.call(this, commandResult))
                  }, future.throw.bind(future))
                  return future.wait()
              } catch (e) {
                  if (e.message === "Can't wait without a fiber") {
                      return commandPromise
                  }
                  throw e
              }
          }
  
          /**
           * commands that get executed during waitUntil and debug (repl mode) should always
           * handled synchronously, therefor prevent propagating lastResults between single calls
           */
          if (commandName !== 'waitUntil' && commandName !== 'debug') {
              commandIsRunning = true
          }
  
          let newInstance = this
          let lastCommandResult = this.lastResult
          let commandResult, commandError
          executeHooksWithArgs(beforeCommand, [commandName, commandArgs]).then(() => {
              /**
               * actual function was already executed in desired catch block
               */
              if (futureFailed) {
                  return
              }
  
              newInstance = fn.apply(this, commandArgs)
              return newInstance.then((result) => {
                  commandResult = result
                  return executeHooksWithArgs(afterCommand, [commandName, commandArgs, result])
              }, (e) => {
                  commandError = e
                  return executeHooksWithArgs(afterCommand, [commandName, commandArgs, null, e])
              }).then(() => {
                  commandIsRunning = false
  
                  if (commandError) {
                      return future.throw(commandError)
                  }
                  wrapCommands(newInstance, beforeCommand, afterCommand)
  
                  /**
                   * don't modify call result prototype
                   */
                  if (commandName === 'call' || commandName === 'reload') {
                      return future.return(commandResult)
                  }
  
                  /**
                   * reset lastResult for all element calls within waitUntil/waitFor commands
                   */
                  if (commandName.match(/^(waitUntil|waitFor)/i)) {
                      this.lastResult = lastCommandResult
                  }
  
                  return future.return(applyPrototype.call(newInstance, commandResult))
              })
          })
  
          /**
           * try to execute with Fibers and fall back if can't
           */
          try {
              return future.wait()
          } catch (e) {
              if (e.message === "Can't wait without a fiber") {
                  futureFailed = true
                  return fn.apply(this, commandArgs)
              }
  
              e.stack = sanitizeErrorMessage(e)
              throw e
          }
      }
  }
  
  /**
   * enhance result with instance prototype to enable command chaining
   * @param  {Object} result       command result
   * @param  {Object} helperScope  instance scope with prototype of already wrapped commands
   * @return {Object}              command result with enhanced prototype
   */
  const applyPrototype = function (result, helperScope) {
      /**
       * don't overload result for none objects, arrays and buffer
       */
      if (!result || typeof result !== 'object' || (Array.isArray(result) && !isElements(result) && !(is$$(result))) || Buffer.isBuffer(result)) {
          return result
      }
  
      const mapPrototype = (el) => {
          let newInstance = Object.setPrototypeOf(Object.create(el), Object.getPrototypeOf(this))
          return applyPrototype.call(newInstance, el, this)
      }
  
      /**
       * overload elements results
       */
      if (isElements(result)) {
          result.value = result.value.map((el, i) => {
              el.selector = result.selector
              el.value = { ELEMENT: el.ELEMENT }
              el.index = i
              return el
          }).map(mapPrototype)
      }
  
      /**
       * overload $$ result
       */
      if (is$$(result)) {
          return result.map(mapPrototype)
      }
  
      let prototype = {}
      let hasExtendedPrototype = false
      for (let commandName of Object.keys(Object.getPrototypeOf(this))) {
          if (result[commandName] || SYNC_COMMANDS.indexOf(commandName) > -1) {
              continue
          }
  
          this.lastResult = result
  
          /**
           * Prefer the helperScope if given which is only the case when we overload elements result.
           * We can't use the `this` prototype because its methods are not wrapped and command results
           * wouldn't be fiberised
           */
          prototype[commandName] = { value: (helperScope || this)[commandName].bind(this) }
          hasExtendedPrototype = true
      }
  
      if (hasExtendedPrototype) {
          let newResult = Object.create(result, prototype)
  
          /**
           * since status is a command we need to rename the property
           */
          if (typeof result.status !== 'undefined') {
              result._status = result.status
              delete result.status
          }
  
          result = assign(newResult, result)
      }
  
      return result
  }
  
  /**
   * wraps all WebdriverIO commands
   * @param  {Object}     instance       WebdriverIO client instance (browser)
   * @param  {Function[]} beforeCommand  before command hook
   * @param  {Function[]} afterCommand   after command hook
   */
  const wrapCommands = function (instance, beforeCommand, afterCommand) {
      const addCommand = instance.addCommand
  
      /**
       * if instance is a multibrowser instance make sure to wrap commands
       * of its instances too
       */
      if (instance.isMultiremote) {
          instance.getInstances().forEach((browserName) => {
              wrapCommands(global[browserName], beforeCommand, afterCommand)
          })
      }
  
      Object.keys(Object.getPrototypeOf(instance)).forEach((commandName) => {
          if (SYNC_COMMANDS.indexOf(commandName) > -1) {
              return
          }
  
          let origFn = instance[commandName]
          instance[commandName] = wrapCommand.call(instance, origFn, commandName, beforeCommand, afterCommand)
      })
  
      /**
       * no need to overwrite addCommand in async mode
       */
      if (isAsync()) {
          return
      }
  
      /**
       * Adding a command within fiber context doesn't require a special routine
       * since everything runs sync. There is no need to promisify the command.
       */
      instance.addCommand = function (fnName, fn, forceOverwrite) {
          let commandGroup = instance.getPrototype()
          let commandName = fnName
          let namespace
  
          if (typeof fn === 'string') {
              namespace = arguments[0]
              fnName = arguments[1]
              fn = arguments[2]
              forceOverwrite = arguments[3]
  
              switch (typeof commandGroup[namespace]) {
              case 'function':
                  throw new Error(`Command namespace "${namespace}" is used internally, and can't be overwritten!`)
              case 'undefined':
                  commandGroup[namespace] = {}
                  break
              }
  
              commandName = `${namespace}.${fnName}`
              commandGroup = commandGroup[namespace]
          }
  
          if (commandGroup[fnName] && !forceOverwrite) {
              throw new Error(`Command ${fnName} is already defined!`)
          }
  
          /**
           * If method name is async the user specifies that he wants to use bare promises to handle asynchronicity.
           * First use native addCommand in order to be able to chain with other native commands, then wrap new
           * command again to run it synchronous in the test method.
           * This will allow us to run async custom commands within sync custom commands in a sync way.
           */
          if (fn.name === 'async') {
              addCommand(fnName, function (...args) {
                  const state = forcePromises
                  forcePromises = true
                  let res = fn.apply(instance, args)
                  forcePromises = state
                  return res
              }, forceOverwrite)
              commandGroup[fnName] = wrapCommand.call(commandGroup, commandGroup[fnName], fnName, beforeCommand, afterCommand)
              return
          }
  
          /**
           * for all other cases we internally return a promise that is
           * finished once the Fiber wrapped custom function has finished
           * #functionalProgrammingWTF!
           */
          commandGroup[fnName] = function (...args) {
              return new Promise((resolve) => {
                  const state = forcePromises
                  forcePromises = false
                  wdioSync(fn, resolve).apply(this, args)
                  forcePromises = state
              })
          }
          instance[fnName] = wrapCommand.call(commandGroup, commandGroup[fnName], commandName, beforeCommand, afterCommand)
      }
  }
  
  /**
   * execute test or hook synchronously
   * @param  {Function} fn         spec or hook method
   * @param  {Number}   repeatTest number of retries
   * @return {Promise}             that gets resolved once test/hook is done or was retried enough
   */
  const executeSync = function (fn, repeatTest = 0, args = []) {
      /**
       * if a new hook gets executed we can assume that all commands should have finised
       * with exception of timeouts where `commandIsRunning` will never be reset but here
       */
      commandIsRunning = false
  
      return new Promise((resolve, reject) => {
          try {
              const res = fn.apply(this, args)
              resolve(res)
          } catch (e) {
              if (repeatTest) {
                  return resolve(executeSync(fn, --repeatTest, args))
              }
  
              /**
               * no need to modify stack if no stack available
               */
              if (!e.stack) {
                  return reject(e)
              }
  
              e.stack = e.stack.split('\n').filter(STACKTRACE_FILTER_FN).join('\n')
              reject(e)
          }
      })
  }
  
  /**
   * execute test or hook asynchronously
   * @param  {Function} fn         spec or hook method
   * @param  {Number}   repeatTest number of retries
   * @return {Promise}             that gets resolved once test/hook is done or was retried enough
   */
  const executeAsync = function (fn, repeatTest = 0, args = []) {
      let result, error
  
      /**
       * if a new hook gets executed we can assume that all commands should have finised
       * with exception of timeouts where `commandIsRunning` will never be reset but here
       */
      commandIsRunning = false
  
      try {
          result = fn.apply(this, args)
      } catch (e) {
          error = e
      }
  
      /**
       * handle errors that get thrown directly and are not cause by
       * rejected promises
       */
      if (error) {
          if (repeatTest) {
              return executeAsync(fn, --repeatTest, args)
          }
          return new Promise((resolve, reject) => reject(error))
      }
  
      /**
       * if we don't retry just return result
       */
      if (repeatTest === 0 || !result || typeof result.catch !== 'function') {
          return new Promise(resolve => resolve(result))
      }
  
      /**
       * handle promise response
       */
      return result.catch((e) => {
          if (repeatTest) {
              return executeAsync(fn, --repeatTest, args)
          }
  
          e.stack = e.stack.split('\n').filter(STACKTRACE_FILTER_FN).join('\n')
          return Promise.reject(e)
      })
  }
  
  /**
   * runs a hook within fibers context (if function name is not async)
   * it also executes before/after hook hook
   *
   * @param  {Function} hookFn      function that was passed to the framework hook
   * @param  {Function} origFn      original framework hook function
   * @param  {Function} before      before hook hook
   * @param  {Function} after       after hook hook
   * @param  {Number}   repeatTest  number of retries if hook fails
   * @return {Function}             wrapped framework hook function
   */
  const runHook = function (hookFn, origFn, before, after, repeatTest = 0) {
      const hookError = (hookName) => (e) => console.error(`Error in ${hookName}: ${e.stack}`)
  
      return origFn(function (...hookArgs) {
          // Print errors encountered in beforeHook and afterHook to console, but
          // don't propagate them to avoid failing the test. However, errors in
          // framework hook functions should fail the test, so propagate those.
          return executeHooksWithArgs(before).catch(hookError('beforeHook')).then(() => {
              /**
               * user wants handle async command using promises, no need to wrap in fiber context
               */
              if (isAsync() || hookFn.name === 'async') {
                  return executeAsync.call(this, hookFn, repeatTest, filterSpecArgs(hookArgs))
              }
  
              return new Promise(runSync.call(this, hookFn, repeatTest, filterSpecArgs(hookArgs)))
          }).then(() => {
              return executeHooksWithArgs(after).catch(hookError('afterHook'))
          })
      })
  }
  
  /**
   * runs a spec function (test function) within the fibers context
   * @param  {string}   specTitle   test description
   * @param  {Function} specFn      test function that got passed in from the user
   * @param  {Function} origFn      original framework test function
   * @param  {Number}   repeatTest  number of retries if test fails
   * @return {Function}             wrapped test function
   */
  const runSpec = function (specTitle, specFn, origFn, repeatTest = 0) {
      /**
       * user wants handle async command using promises, no need to wrap in fiber context
       */
      if (isAsync() || specFn.name === 'async') {
          return origFn(specTitle, function async (...specArgs) {
              return executeAsync.call(this, specFn, repeatTest, filterSpecArgs(specArgs))
          })
      }
  
      return origFn(specTitle, function (...specArgs) {
          return new Promise(runSync.call(this, specFn, repeatTest, filterSpecArgs(specArgs)))
      })
  }
  
  /**
   * run hook or spec via executeSync
   */
  function runSync (fn, repeatTest = 0, args = []) {
      return (resolve, reject) =>
          Fiber(() => executeSync.call(this, fn, repeatTest, args).then(() => resolve(), reject)).run()
  }
  
  /**
   * wraps hooks and test function of a framework within a fiber context
   * @param  {Function} origFn               original framework function
   * @param  {string[]} testInterfaceFnNames actual test functions for that framework
   * @return {Function}                      wrapped test/hook function
   */
  const wrapTestFunction = function (fnName, origFn, testInterfaceFnNames, before, after) {
      return function (...specArguments) {
          /**
           * Variadic arguments:
           * [title, fn], [title], [fn]
           * [title, fn, retryCnt], [title, retryCnt], [fn, retryCnt]
           */
          let retryCnt = typeof specArguments[specArguments.length - 1] === 'number' ? specArguments.pop() : 0
          const specFn = typeof specArguments[0] === 'function' ? specArguments.shift()
              : (typeof specArguments[1] === 'function' ? specArguments.pop() : undefined)
          const specTitle = specArguments[0]
  
          if (testInterfaceFnNames.indexOf(fnName) > -1) {
              if (specFn) return runSpec(specTitle, specFn, origFn, retryCnt)
  
              /**
               * if specFn is undefined we are dealing with a pending function
               */
              return origFn(specTitle)
          }
  
          return runHook(specFn, origFn, before, after, retryCnt)
      }
  }
  
  /**
   * Wraps global test function like `it` so that commands can run synchronouse
   *
   * The scope parameter is used in the qunit framework since all functions are bound to global.QUnit instead of global
   *
   * @param  {String[]} testInterfaceFnNames  command that runs specs
   * @param  {Function} before               before hook hook
   * @param  {Function} after                after hook hook
   * @param  {String}   fnName               test interface command to wrap
   * @param  {Object}   scope                the scope to run command from, defaults to global
   */
  const runInFiberContext = function (testInterfaceFnNames, before, after, fnName, scope = global) {
      const origFn = scope[fnName]
      scope[fnName] = wrapTestFunction(fnName, origFn, testInterfaceFnNames, before, after)
  
      /**
       * support it.skip for the Mocha framework
       */
      if (typeof origFn.skip === 'function') {
          scope[fnName].skip = origFn.skip
      }
  
      /**
       * wrap it.only for the Mocha framework
       */
      if (typeof origFn.only === 'function') {
          const origOnlyFn = origFn.only
          scope[fnName].only = wrapTestFunction(fnName + '.only', origOnlyFn, testInterfaceFnNames, before, after)
      }
  }
  
  export {
      wrapCommand,
      wrapCommands,
      runInFiberContext,
      executeHooksWithArgs,
      executeSync,
      executeAsync,
      wdioSync,
      is$$
  }
  