const _ = require('lodash')
const Promise = require('bluebird')
const path = require('path')
const cypressEx = require('@packages/example')
const { fs } = require('./util/fs')
const glob = require('./util/glob')
const cwd = require('./cwd')
const debug = require('debug')('cypress:server:scaffold')
const { isEmpty } = require('ramda')
const { isDefault } = require('./util/config')

const exampleFolderName = cypressEx.getFolderName()
const getExampleSpecsFullPaths = cypressEx.getPathToExamples()

const getPathFromIntegrationFolder = (file) => {
  return file.substring(file.indexOf('integration/') + 'integration/'.length)
}

const isDifferentNumberOfFiles = (files, exampleSpecs) => {
  return files.length !== exampleSpecs.length
}

const getExampleSpecs = () => {
  return getExampleSpecsFullPaths
  .then((fullPaths) => {
    // short paths relative to integration folder (i.e. examples/actions.spec.js)
    const shortPaths = _.map(fullPaths, (file) => {
      return getPathFromIntegrationFolder(file)
    })

    // index for quick lookup and for getting full path from short path
    const index = _.transform(shortPaths, (memo, spec, i) => {
      return memo[spec] = fullPaths[i]
    }, {})

    return { fullPaths, shortPaths, index }
  })
}

const getIndexedExample = (file, index) => {
  return index[getPathFromIntegrationFolder(file)]
}

const filesNamesAreDifferent = (files, index) => {
  return _.some(files, (file) => {
    return !getIndexedExample(file, index)
  })
}

const getFileSize = (file) => {
  return fs.statAsync(file).get('size')
}

const filesSizesAreSame = (files, index) => {
  return Promise.join(
    Promise.all(_.map(files, getFileSize)),
    Promise.all(_.map(files, (file) => {
      return getFileSize(getIndexedExample(file, index))
    })),
  )
  .spread((fileSizes, originalFileSizes) => {
    return _.every(fileSizes, (size, i) => {
      return size === originalFileSizes[i]
    })
  })
}

const componentTestingEnabled = (config) => {
  const componentTestingEnabled = _.get(config, 'resolved.testingType.value', 'e2e') === 'component'

  return componentTestingEnabled && !isDefault(config, 'componentFolder')
}

const isNewProject = (integrationFolder) => {
  // logic to determine if new project
  // 1. component testing is not enabled
  // 2. there are no files in 'integrationFolder'
  // 3. there is the same number of files in 'integrationFolder'
  // 4. the files are named the same as the example files
  // 5. the bytes of the files match the example files

  debug('determine if new project by globbing files in %o', { integrationFolder })

  // checks for file up to 3 levels deep
  return glob('{*,*/*,*/*/*}', { cwd: integrationFolder, realpath: true, nodir: true })
  .then((files) => {
    debug(`found ${files.length} files in folder ${integrationFolder}`)
    debug('determine if we should scaffold:')

    // TODO: add tests for this
    debug('- empty?', isEmpty(files))
    if (isEmpty(files)) {
      return true
    } // 1

    return getExampleSpecs()
    .then((exampleSpecs) => {
      const numFilesDifferent = isDifferentNumberOfFiles(files, exampleSpecs.shortPaths)

      debug('- different number of files?', numFilesDifferent)
      if (numFilesDifferent) {
        return false
      } // 2

      const filesNamesDifferent = filesNamesAreDifferent(files, exampleSpecs.index)

      debug('- different file names?', filesNamesDifferent)
      if (filesNamesDifferent) {
        return false
      } // 3

      return filesSizesAreSame(files, exampleSpecs.index)
    })
  }).then((sameSizes) => {
    debug('- same sizes?', sameSizes)

    return sameSizes
  })
}

module.exports = {
  isNewProject,

  integrationExampleName () {
    return exampleFolderName
  },

  integration (folder, config) {
    debug(`integration folder ${folder}`)

    // skip if user has explicitly set integrationFolder
    // or if user has set up component testing
    if (!isDefault(config, 'integrationFolder') || componentTestingEnabled(config)) {
      return Promise.resolve()
    }

    return this.verifyScaffolding(folder, () => {
      debug(`copying examples into ${folder}`)

      return getExampleSpecs()
      .then(({ fullPaths }) => {
        return Promise.all(_.map(fullPaths, (file) => {
          return this._copy(file, path.join(folder, exampleFolderName), config)
        }))
      })
    })
  },

  fixture (folder, config) {
    debug(`fixture folder ${folder}`)

    // skip if user has explicitly set fixturesFolder
    if (!config.fixturesFolder || !isDefault(config, 'fixturesFolder')) {
      return Promise.resolve()
    }

    return this.verifyScaffolding(folder, () => {
      debug(`copying example.json into ${folder}`)

      return this._copy(cypressEx.getPathToFixture(), folder, config)
    })
  },

  support (folder, config) {
    debug(`support folder ${folder}, support file ${config.supportFile}`)

    // skip if user has explicitly set supportFile
    if (!config.supportFile || !isDefault(config, 'supportFile')) {
      return Promise.resolve()
    }

    return this.verifyScaffolding(folder, () => {
      debug(`copying commands.js and index.js to ${folder}`)

      return cypressEx.getPathToSupportFiles()
      .then((supportFiles) => {
        return Promise.all(
          supportFiles.map((supportFilePath) => {
            return this._copy(supportFilePath, folder, config)
          }),
        )
      })
    })
  },

  plugins (folder, config) {
    debug(`plugins folder ${folder}`)

    // skip if user has explicitly set pluginsFile
    if (!config.pluginsFile || !isDefault(config, 'pluginsFile')) {
      return Promise.resolve()
    }

    return this.verifyScaffolding(folder, () => {
      debug(`copying index.js into ${folder}`)

      return this._copy(cypressEx.getPathToPlugins(), folder, config)
    })
  },

  _copy (file, folder, config) {
    // allow file to be relative or absolute
    const src = path.resolve(cwd('lib', 'scaffold'), file)
    const dest = path.join(folder, path.basename(file))

    return this._assertInFileTree(dest, config)
    .then(() => {
      return fs.copyAsync(src, dest)
    })
  },

  verifyScaffolding (folder, fn) {
    // we want to build out the folder + and example files
    // but only create the example files if the folder doesn't
    // exist
    //
    // this allows us to automatically insert the folder on existing
    // projects (whenever they are booted) but allows the user to delete
    // the file and not have it re-generated each time
    //
    // this is ideal because users who are upgrading to newer cypress version
    // will still get the files scaffolded but existing users won't be
    // annoyed by new example files coming into their projects unnecessarily
    // console.debug('-- verify', folder)
    debug(`verify scaffolding in ${folder}`)

    return fs.statAsync(folder)
    .then(() => {
      return debug(`folder ${folder} already exists`)
    }).catch(() => {
      debug(`missing folder ${folder}`)

      return fn.call(this)
    })
  },

  fileTree (config = {}) {
    // returns a tree-like structure of what files are scaffolded.
    // this should be updated any time we add, remove, or update the name
    // of a scaffolded file

    const getFilePath = (dir, name) => {
      return path.relative(config.projectRoot, path.join(dir, name))
    }

    return getExampleSpecs()
    .then((specs) => {
      let files = []

      if (!componentTestingEnabled(config)) {
        files = _.map(specs.shortPaths, (file) => {
          return getFilePath(config.integrationFolder, file)
        })
      }

      if (config.fixturesFolder) {
        files = files.concat([
          getFilePath(config.fixturesFolder, 'example.json'),
        ])
      }

      if (config.supportFolder && (config.supportFile !== false)) {
        files = files.concat([
          getFilePath(config.supportFolder, 'commands.js'),
          getFilePath(config.supportFolder, 'index.js'),
        ])
      }

      if (config.pluginsFile) {
        files = files.concat([
          getFilePath(path.dirname(config.pluginsFile), 'index.js'),
        ])
      }

      debug('scaffolded files %j', files)

      return this._fileListToTree(files)
    })
  },

  _fileListToTree (files) {
    // turns a list of file paths into a tree-like structure where
    // each entry has a name and children if it's a directory

    return _.reduce(files, (tree, file) => {
      let placeholder = tree
      const parts = file.split('/')

      _.each(parts, (part, index) => {
        let entry = _.find(placeholder, { name: part })

        if (!entry) {
          entry = { name: part }
          if (index < (parts.length - 1)) {
            // if it's not the last, it's a directory
            entry.children = []
          }

          placeholder.push(entry)
        }

        placeholder = entry.children
      })

      return tree
    }, [])
  },

  _assertInFileTree (filePath, config) {
    const relativeFilePath = path.relative(config.projectRoot, filePath)

    return this.fileTree(config)
    .then((fileTree) => {
      if (!this._inFileTree(fileTree, relativeFilePath)) {
        throw new Error(`You attempted to scaffold a file, '${relativeFilePath}', that's not in the scaffolded file tree. This is because you added, removed, or changed a scaffolded file. Make sure to update scaffold#fileTree to match your changes.`)
      }
    })
  },

  _inFileTree (fileTree, filePath) {
    let branch = fileTree
    const parts = filePath.split('/')

    for (let part of parts) {
      let found = _.find(branch, { name: part })

      if (found) {
        branch = found.children
      } else {
        return false
      }
    }

    return true
  },
}
