'use strict'

const BB = require('bluebird')

const common = BB.promisifyAll(require('../common-tap.js'))
const fs = BB.promisifyAll(require('fs'))
const mr = BB.promisify(require('npm-registry-mock'))
const path = require('path')
const rimraf = BB.promisify(require('rimraf'))
const Tacks = require('tacks')
const test = require('tap').test

const Dir = Tacks.Dir
const File = Tacks.File
const cacheDir = common.cache
const testDir = common.pkg

const EXEC_OPTS = {
  cwd: testDir,
  nodeExecPath: process.execPath
}

const PKG = {
  name: 'top',
  version: '1.2.3',
  scripts: {
    install: 'node -p process.env.npm_config_foo'
  },
  dependencies: {
    optimist: '0.6.0',
    clean: '2.1.6'
  }
}
let RAW_LOCKFILE
let SERVER
let TREE

function scrubFrom (tree) {
  // npm ci and npm i write different `from` fields for dependency deps. This
  // is fine any ok, but it messes with `t.deepEqual` comparisons.
  function _scrubFrom (deps) {
    Object.keys(deps).forEach((k) => {
      deps[k].from = ''
      if (deps[k].dependencies) { _scrubFrom(deps[k].dependencies) }
    })
  }
  tree.dependencies && _scrubFrom(tree.dependencies)
}

test('setup', () => {
  const fixture = new Tacks(Dir({
    'package.json': File(PKG)
  }))
  return Promise.all([rimraf(cacheDir), rimraf(testDir)]).then(() => {
    fixture.create(testDir)
    return mr({port: common.port})
  })
    .then((server) => {
      SERVER = server
      return common.npm([
        'install',
        '--registry', common.registry
      ], EXEC_OPTS)
    })
    .then(() => fs.readFileAsync(
      path.join(testDir, 'package-lock.json'),
      'utf8')
    )
    .then((lock) => {
      RAW_LOCKFILE = lock
    })
    .then(() => common.npm(['ls', '--json'], EXEC_OPTS))
    .then((ret) => {
      TREE = scrubFrom(JSON.parse(ret[1]))
    })
})

test('basic installation', (t) => {
  const fixture = new Tacks(Dir({
    'package.json': File(PKG),
    'package-lock.json': File(RAW_LOCKFILE)
  }))
  return rimraf(testDir)
    .then(() => fixture.create(testDir))
    .then(() => common.npm([
      'ci',
      '--foo=asdf',
      '--registry', common.registry,
      '--loglevel', 'warn'
    ], EXEC_OPTS))
    .then((ret) => {
      const code = ret[0]
      const stdout = ret[1]
      const stderr = ret[2]
      t.equal(code, 0, 'command completed without error')
      t.equal(stderr.trim(), '', 'no output on stderr')
      t.match(
        stdout.trim(),
        /\nasdf\nadded 6 packages in \d+(?:\.\d+)?s$/,
        'no warnings on stderr, and final output has right number of packages'
      )
      return fs.readdirAsync(path.join(testDir, 'node_modules'))
    })
    .then((modules) => {
      t.deepEqual(modules.sort(), [
        'async', 'checker', 'clean', 'minimist', 'optimist', 'wordwrap'
      ], 'packages installed')
      return BB.all(modules.map((mod) => {
        return fs.readFileAsync(
          path.join(testDir, 'node_modules', mod, 'package.json')
        )
          .then((f) => JSON.parse(f))
          .then((pkgjson) => {
            t.equal(pkgjson.name, mod, `${mod} package name correct`)
            t.match(
              pkgjson._integrity,
              /sha\d+-[a-z0-9=+/]+$/i,
              `${mod} pkgjson has _integrity`
            )
            t.match(
              pkgjson._resolved,
              new RegExp(`http.*/-/${mod}-${pkgjson.version}.tgz`),
              `${mod} pkgjson has correct _resolved`
            )
            t.match(
              pkgjson._from,
              new RegExp(`${mod}@.*`),
              `${mod} pkgjson has _from field`
            )
          })
      }))
    })
    .then(() => fs.readFileAsync(
      path.join(testDir, 'package-lock.json'),
      'utf8')
    )
    .then((lock) => t.equal(lock, RAW_LOCKFILE, 'package-lock.json unchanged'))
    .then(() => common.npm(['ls', '--json'], EXEC_OPTS))
    .then((ret) => {
      const lsResult = JSON.parse(ret[1])
      t.equal(ret[0], 0, 'ls exited successfully')
      t.deepEqual(scrubFrom(lsResult), TREE, 'tree matches one from `install`')
    })
})

test('supports npm-shrinkwrap.json as well', (t) => {
  const fixture = new Tacks(Dir({
    'package.json': File(PKG),
    'npm-shrinkwrap.json': File(RAW_LOCKFILE)
  }))
  return rimraf(testDir)
    .then(() => fixture.create(testDir))
    .then(() => common.npm([
      'ci',
      '--foo=asdf',
      '--registry', common.registry,
      '--loglevel', 'warn'
    ], EXEC_OPTS))
    .then((ret) => {
      const code = ret[0]
      const stdout = ret[1]
      const stderr = ret[2]
      t.equal(code, 0, 'command completed without error')
      t.equal(stderr.trim(), '', 'no output on stderr')
      t.match(
        stdout.trim(),
        /\nasdf\nadded 6 packages in \d+(?:\.\d+)?s$/,
        'no warnings on stderr, and final output has right number of packages'
      )
    })
    .then(() => common.npm(['ls', '--json'], EXEC_OPTS))
    .then((ret) => {
      t.equal(ret[0], 0, 'ls exited successfully')
      t.deepEqual(
        scrubFrom(JSON.parse(ret[1])),
        TREE,
        'tree matches one from `install`'
      )
    })
    .then(() => fs.readFileAsync(
      path.join(testDir, 'npm-shrinkwrap.json'),
      'utf8')
    )
    .then((lock) => t.equal(lock, RAW_LOCKFILE, 'npm-shrinkwrap.json unchanged'))
    .then(() => fs.readdirAsync(path.join(testDir)))
    .then((files) => t.notOk(
      files.some((f) => f === 'package-lock.json'),
      'no package-lock.json created'
    ))
})

test('removes existing node_modules/ before installing', (t) => {
  const fixture = new Tacks(Dir({
    'package.json': File(PKG),
    'package-lock.json': File(RAW_LOCKFILE),
    'node_modules': Dir({
      foo: Dir({
        'index.js': File('"hello world"')
      })
    })
  }))
  return rimraf(testDir)
    .then(() => fixture.create(testDir))
    .then(() => common.npm([
      'ci',
      '--foo=asdf',
      '--registry', common.registry,
      '--loglevel', 'warn'
    ], EXEC_OPTS))
    .then((ret) => {
      const code = ret[0]
      const stderr = ret[2]
      t.equal(code, 0, 'command completed without error')
      t.match(
        stderr.trim(),
        /^npm.*WARN.*removing existing node_modules/,
        'user warned that existing node_modules were removed'
      )
      return fs.readdirAsync(path.join(testDir, 'node_modules'))
    })
    .then((modules) => {
      t.deepEqual(modules.sort(), [
        'async', 'checker', 'clean', 'minimist', 'optimist', 'wordwrap'
      ], 'packages installed, with old node_modules dir gone')
    })
    .then(() => common.npm(['ls'], EXEC_OPTS))
    .then((ret) => t.equal(ret[0], 0, 'ls exited successfully'))
    .then(() => fs.readFileAsync(
      path.join(testDir, 'package-lock.json'),
      'utf8')
    )
    .then((lock) => t.equal(lock, RAW_LOCKFILE, 'package-lock.json unchanged'))
})

test('installs all package types correctly')

test('errors if package-lock.json missing', (t) => {
  const fixture = new Tacks(Dir({
    'package.json': File(PKG)
  }))
  return rimraf(testDir)
    .then(() => fixture.create(testDir))
    .then(() => common.npm([
      'ci',
      '--foo=asdf',
      '--registry', common.registry,
      '--loglevel', 'warn'
    ], EXEC_OPTS))
    .then((ret) => {
      const code = ret[0]
      const stdout = ret[1]
      const stderr = ret[2]
      t.equal(code, 1, 'command errored')
      t.equal(stdout.trim(), '', 'no output on stdout')
      t.match(
        stderr.trim(),
        /can only install packages with an existing package-lock/i,
        'user informed about the issue'
      )
      return fs.readdirAsync(path.join(testDir))
    })
    .then((dir) => {
      t.notOk(dir.some((f) => f === 'node_modules'), 'no node_modules installed')
      t.notOk(
        dir.some((f) => f === 'package-lock.json'),
        'no package-lock.json created'
      )
    })
})

test('errors if package-lock.json invalid', (t) => {
  const badJson = JSON.parse(RAW_LOCKFILE)
  delete badJson.dependencies.optimist
  const fixture = new Tacks(Dir({
    'package.json': File(PKG),
    'package-lock.json': File(badJson)
  }))
  return rimraf(testDir)
    .then(() => fixture.create(testDir))
    .then(() => common.npm([
      'ci',
      '--foo=asdf',
      '--registry', common.registry,
      '--loglevel', 'warn'
    ], EXEC_OPTS))
    .then((ret) => {
      const code = ret[0]
      const stdout = ret[1]
      const stderr = ret[2]
      t.equal(code, 1, 'command errored')
      t.equal(stdout.trim(), '', 'no output on stdout')
      t.match(
        stderr.trim(),
        /can only install packages when your package.json/i,
        'user informed about the issue'
      )
      return fs.readdirAsync(path.join(testDir))
    })
    .then((dir) => {
      t.notOk(dir.some((f) => f === 'node_modules'), 'no node_modules installed')
    })
    .then(() => fs.readFileAsync(
      path.join(testDir, 'package-lock.json'),
      'utf8')
    )
    .then((lock) => t.deepEqual(
      JSON.parse(lock),
      badJson,
      'bad package-lock.json left unchanged')
    )
})

test('correct cache location when using cache config', (t) => {
  const fixture = new Tacks(Dir({
    'package.json': File(PKG),
    'package-lock.json': File(RAW_LOCKFILE)
  }))
  return Promise.all([rimraf(cacheDir), rimraf(testDir)])
    .then(() => fixture.create(cacheDir))
    .then(() => fixture.create(testDir))
    .then(() => common.npm([
      'ci',
      `--cache=${cacheDir}`,
      '--foo=asdf',
      '--registry', common.registry,
      '--loglevel', 'warn'
    ], EXEC_OPTS))
    .then((ret) => {
      const code = ret[0]
      const stderr = ret[2]
      t.equal(code, 0, 'command completed without error')
      t.equal(stderr.trim(), '', 'no output on stderr')
      return fs.readdirAsync(path.join(cacheDir, '_cacache'))
    })
    .then((modules) => {
      t.ok(modules, 'should create _cacache folder')
      t.end()
    })
})

test('cleanup', () => {
  SERVER.close()
  return Promise.all([rimraf(cacheDir), rimraf(testDir)])
})
