const _ = require('lodash')

const $errUtils = require('./error_utils')

const builtInCommands = [
  require('../cy/commands/actions/check'),
  require('../cy/commands/actions/click'),
  require('../cy/commands/actions/focus'),
  require('../cy/commands/actions/hover'),
  require('../cy/commands/actions/scroll'),
  require('../cy/commands/actions/select'),
  require('../cy/commands/actions/submit'),
  require('../cy/commands/actions/trigger'),
  require('../cy/commands/actions/type'),
  require('../cy/commands/agents'),
  require('../cy/commands/aliasing'),
  require('../cy/commands/angular'),
  require('../cy/commands/asserting'),
  require('../cy/commands/clock'),
  require('../cy/commands/commands'),
  require('../cy/commands/connectors'),
  require('../cy/commands/cookies'),
  require('../cy/commands/debugging'),
  require('../cy/commands/exec'),
  require('../cy/commands/files'),
  require('../cy/commands/fixtures'),
  require('../cy/commands/local_storage'),
  require('../cy/commands/location'),
  require('../cy/commands/misc'),
  require('../cy/commands/popups'),
  require('../cy/commands/navigation'),
  require('../cy/commands/querying'),
  require('../cy/commands/request'),
  require('../cy/commands/screenshot'),
  require('../cy/commands/task'),
  require('../cy/commands/traversals'),
  require('../cy/commands/waiting'),
  require('../cy/commands/window'),
  require('../cy/commands/xhr'),
  require('../cy/net-stubbing').addCommand,
]

const getTypeByPrevSubject = (prevSubject) => {
  if (prevSubject === 'optional') {
    return 'dual'
  }

  if (prevSubject) {
    return 'child'
  }

  return 'parent'
}

const create = (Cypress, cy, state, config) => {
  // create a single instance
  // of commands
  const commands = {}
  const commandBackups = {}

  const store = (obj) => {
    commands[obj.name] = obj

    return cy.addCommand(obj)
  }

  const storeOverride = (name, fn) => {
    // grab the original function if its been backed up
    // or grab it from the command store
    const original = commandBackups[name] || commands[name]

    if (!original) {
      $errUtils.throwErrByPath('miscellaneous.invalid_overwrite', {
        args: {
          name,
        },
      })
    }

    // store the backup again now
    commandBackups[name] = original

    const originalFn = (...args) => {
      const current = state('current')
      let storedArgs = args

      if (current.get('type') === 'child') {
        storedArgs = args.slice(1)
      }

      current.set('args', storedArgs)

      return original.fn(...args)
    }

    const overridden = _.clone(original)

    overridden.fn = function (...args) {
      args = [].concat(originalFn, args)

      return fn.apply(this, args)
    }

    return cy.addCommand(overridden)
  }

  const Commands = {
    _commands: commands, // for testing

    each (fn) {
      // perf loop
      for (let name in commands) {
        const command = commands[name]

        fn(command)
      }

      // prevent loop comprehension
      return null
    },

    addAllSync (obj) {
      // perf loop
      for (let name in obj) {
        const fn = obj[name]

        Commands.addSync(name, fn)
      }

      // prevent loop comprehension
      return null
    },

    addSync (name, fn) {
      return cy.addCommandSync(name, fn)
    },

    addAll (options = {}, obj) {
      if (!obj) {
        obj = options
        options = {}
      }

      // perf loop
      for (let name in obj) {
        const fn = obj[name]

        Commands.add(name, options, fn)
      }

      // prevent loop comprehension
      return null
    },

    add (name, options, fn) {
      if (_.isFunction(options)) {
        fn = options
        options = {}
      }

      const { prevSubject } = options

      // normalize type by how they validate their
      // previous subject (unless they're explicitly set)
      options.type = options.type ?? getTypeByPrevSubject(prevSubject)
      const type = options.type

      return store({
        name,
        fn,
        type,
        prevSubject,
      })
    },

    addChainer (obj) {
      // perp loop
      for (let name in obj) {
        const fn = obj[name]

        cy.addChainer(name, fn)
      }

      // prevent loop comprehension
      return null
    },

    overwrite (name, fn) {
      return storeOverride(name, fn)
    },
  }

  // perf loop
  for (let cmd of builtInCommands) {
    cmd(Commands, Cypress, cy, state, config)
  }

  return Commands
}

module.exports = {
  create,
}
