const fs = require('fs')
const { installedBrowsers, info, warn, error, chalk, execa } = require('@vue/cli-shared-utils')

module.exports = (api, options) => {
  api.registerCommand('test:e2e', {
    description: 'run end-to-end tests with nightwatch',
    usage: 'vue-cli-service test:e2e [options]',
    options: {
      '--url': 'run end-to-end tests against given url instead of auto-starting dev server',
      '--config': 'use custom nightwatch config file (overrides internals)',
      '--headless': 'use chrome or firefox in headless mode',
      '--parallel': 'enable parallel mode via test workers (only available in chromedriver)',
      '--use-selenium': 'use Selenium standalone server instead of chromedriver or geckodriver',
      '-e, --env': 'specify comma-delimited browser envs to run in (default: chrome)',
      '-t, --test': 'specify a test to run by name',
      '-f, --filter': 'glob to filter tests by filename'
    },
    details:
      `All Nightwatch CLI options are also supported.\n` +
      chalk.yellow(`https://nightwatchjs.org/guide/running-tests/#command-line-options`)
  }, (args, rawArgs) => {
    if (args.env && args.env.includes('firefox')) {
      try {
        require('geckodriver')
      } catch (e) {
        error(`To run e2e tests in Firefox, you need to install ${chalk.yellow.bold('geckodriver')} first.`)
        process.exit(1)
      }
    }
    if (installedBrowsers.chrome) {
      const userVersion = installedBrowsers.chrome
      const driverVersion = require('chromedriver').version

      const userMajor = userVersion.match(/^(\d+)\./)[1]
      const driverMajor = driverVersion.match(/^(\d+)\./)[1]

      if (userMajor !== driverMajor) {
        warn(`Local ${chalk.cyan.bold('Chrome')} version is ${chalk.cyan.bold(userMajor)}, but the installed ${chalk.cyan.bold('chromedriver')} is for version ${chalk.cyan.bold(driverMajor)}.`)
        warn(`There may be incompatibilities between them.`)
        warn(`Please update your ${chalk.cyan.bold('chromedriver')} dependency to match the ${chalk.cyan.bold('Chrome')} version.`)
      }
    }

    // remove args
    ;['url', 'mode'].forEach(toRemove => removeArg(rawArgs, toRemove))
    // remove flags
    ;['headless', 'use-selenium', 'parallel'].forEach(toRemove => removeArg(rawArgs, toRemove, 0))

    return Promise.all([
      startDevServer(args, api),
      loadNightwatchConfig(rawArgs, api)
    ]).then((results) => {
      const { server, url } = results[0]
      let content = args.headless ? 'in headless mode' : ''
      if (args.parallel) {
        content += ' with concurrency'
      }

      info(`Running end-to-end tests ${content}...`)

      // expose dev server url to tests
      process.env.VUE_DEV_SERVER_URL = url

      if (rawArgs.indexOf('--env') === -1 && rawArgs.indexOf('-e') === -1) {
        rawArgs.push('--env', 'chrome')
      }

      if (args['use-selenium']) {
        process.env.VUE_NIGHTWATCH_USE_SELENIUM = '1'
      }

      if (args.headless) {
        process.env.VUE_NIGHTWATCH_HEADLESS = '1'
      }

      if (args.parallel) {
        process.env.VUE_NIGHTWATCH_CONCURRENT = '1'
      }

      const nightWatchBinPath = require.resolve('nightwatch/bin/nightwatch')
      const runner = execa(nightWatchBinPath, rawArgs, { stdio: 'inherit' })
      if (server) {
        runner.on('exit', () => server.close())
        runner.on('error', () => server.close())
      }

      if (process.env.VUE_CLI_TEST) {
        runner.on('exit', code => {
          process.exit(code)
        })
      }

      return runner
    })
  })
}

module.exports.defaultModes = {
  'test:e2e': 'production'
}

function startDevServer (args, api) {
  const { url } = args

  if (url) {
    return Promise.resolve({ url })
  }

  return api.service.run('serve')
}

async function loadNightwatchConfig (rawArgs, api) {
  if (rawArgs.indexOf('--config') === -1) {
    // expose user options to config file
    let userOptions
    const configFiles = [
      'nightwatch.config.js',
      'nightwatch.conf.js',
      'nightwatch.json'
    ].map((entry) => api.resolve(entry))

    const userOptionsPath = await findAsync(configFiles, fileExists)

    if (userOptionsPath) {
      userOptions = require(userOptionsPath)
    }

    process.env.VUE_NIGHTWATCH_USER_OPTIONS = JSON.stringify(userOptions || {})

    rawArgs.push('--config', require.resolve('./nightwatch.config.js'))
  }
}

async function findAsync (arr, callback) {
  while (arr.length) {
    const item = arr.shift()
    const result = await callback(item)

    if (result) {
      return item
    }
  }

  return false
}

async function fileExists (path) {
  try {
    const stats = await checkPath(path)

    return stats.isFile()
  } catch (err) {
    if (err.code === 'ENOENT') {
      return false
    }

    throw err
  }
}

function checkPath (source) {
  return new Promise(function (resolve, reject) {
    fs.stat(source, function (err, stat) {
      if (err) {
        return reject(err)
      }

      resolve(stat)
    })
  })
}

function removeArg (rawArgs, argToRemove, offset = 1) {
  const matchRE = new RegExp(`^--${argToRemove}`)
  const equalRE = new RegExp(`^--${argToRemove}=`)
  const i = rawArgs.findIndex(arg => matchRE.test(arg))
  if (i > -1) {
    rawArgs.splice(i, offset + (equalRE.test(rawArgs[i]) ? 0 : 1))
  }
}
