'use strict'
var path = require('path')
var test = require('tap').test
var fs = require('fs')
var rimraf = require('rimraf')
var mr = require('npm-registry-mock')
var Tacks = require('tacks')
var File = Tacks.File
var Symlink = Tacks.Symlink
var Dir = Tacks.Dir
var common = require('../common-tap.js')
var isWindows = require('../../lib/utils/is-windows.js')

var basedir = path.join(__dirname, path.basename(__filename, '.js'))
var testdir = path.join(basedir, 'testdir')
var cachedir = path.join(basedir, 'cache')
var globaldir = path.join(basedir, 'global')
var tmpdir = path.join(basedir, 'tmp')

var conf = {
  cwd: testdir,
  env: common.emptyEnv().extend({
    npm_config_cache: cachedir,
    npm_config_tmp: tmpdir,
    npm_config_prefix: globaldir,
    npm_config_registry: common.registry,
    npm_config_loglevel: 'error'
  }),
  stdio: [0, 'pipe', 2]
}
var confE = {
  cwd: testdir,
  env: conf.env,
  stdio: [0, 'pipe', 'pipe']
}

function readJson (file) {
  return JSON.parse(fs.readFileSync(file))
}

function isSymlink (t, file, message) {
  try {
    var info = fs.lstatSync(file)
    t.is(info.isSymbolicLink(), true, message)
  } catch (ex) {
    if (ex.code === 'ENOENT') {
      t.fail(message, {found: null, wanted: 'symlink', compare: 'fs.lstat(' + file + ')'})
    } else {
      t.fail(message, {found: ex, wanted: 'symlink', compare: 'fs.lstat(' + file + ')'})
    }
  }
}

function fileExists (t, file, message) {
  try {
    fs.statSync(file)
    t.pass(message)
  } catch (ex) {
    if (ex.code === 'ENOENT') {
      t.fail(message, {found: null, wanted: 'exists', compare: 'fs.stat(' + file + ')'})
    } else {
      t.fail(message, {found: ex, wanted: 'exists', compare: 'fs.stat(' + file + ')'})
    }
  }
}

function noFileExists (t, file, message) {
  try {
    fs.statSync(file)
    t.fail(message, {found: 'exists', wanted: 'not exists', compare: 'fs.stat(' + file + ')'})
  } catch (ex) {
    if (ex.code === 'ENOENT') {
      t.pass(message)
    } else {
      t.fail(message, {found: ex, wanted: 'not exists', compare: 'fs.stat(' + file + ')'})
    }
  }
}

var server
var testdirContent = {
  node_modules: Dir({}),
  pkga: Dir({
    'package.json': File({
      name: 'pkga',
      version: '1.0.0'
    })
  }),
  pkgb: Dir({
    'package.json': File({
      name: 'pkgb',
      version: '1.0.0'
    })
  }),
  pkgc: Dir({
    'package.json': File({
      name: 'pkgc',
      version: '1.0.0'
    })
  }),
  pkgd: Dir({
    'package.json': File({
      name: 'pkgd',
      version: '1.0.0'
    })
  })
}

var fixture
function setup () {
  fixture = new Tacks(Dir({
    cache: Dir(),
    global: Dir(),
    tmp: Dir(),
    testdir: Dir(testdirContent)
  }))
  cleanup()
  fixture.create(basedir)
}

function cleanup () {
  fixture.remove(basedir)
}

test('setup', function (t) {
  process.nextTick(function () {
    setup()
    mr({port: common.port, throwOnUnmatched: true}, function (err, s) {
      if (err) throw err
      server = s
      t.done()
    })
  })
})

var installOk = []
var slashes = [ 'unix', 'win' ]
slashes.forEach(function (os) {
  var slash = os === 'unix'
    ? function (ss) { return ss.replace(/\\/g, '/') }
    : function (ss) { return ss.replace(/\//g, '\\') }
  installOk.push(os + '-file-abs-f')
  testdirContent[os + '-file-abs-f'] = Dir({
    'package.json': File({
      name: os + '-file-abs-f',
      version: '1.0.0',
      dependencies: {
        pkga: 'file:' + slash(testdir + '/pkga')
      }
    })
  })
  installOk.push(os + '-file-abs-fff')
  testdirContent[os + '-file-abs-fff'] = Dir({
    'package.json': File({
      name: os + '-file-abs-fff',
      version: '1.0.0',
      dependencies: {
        pkga: 'file://' + slash(testdir + '/pkga')
      }
    })
  })
  installOk.push(os + '-file-rel')
  testdirContent[os + '-file-rel'] = Dir({
    'package.json': File({
      name: os + '-file-rel',
      version: '1.0.0',
      dependencies: {
        pkga: 'file:' + slash('../pkga')
      }
    })
  })
  installOk.push(os + '-file-rel-fffff')
  testdirContent[os + '-file-rel-fffff'] = Dir({
    'package.json': File({
      name: os + '-file-rel-fffff',
      version: '1.0.0',
      dependencies: {
        pkga: 'file:' + slash('/////../pkga')
      }
    })
  })
})

testdirContent['win-abs-drive-win'] = Dir({
  'package.json': File({
    name: 'win-abs-drive-win',
    version: '1.0.0',
    dependencies: {
      pkga: 'file:D:\\thing\\pkga'
    }
  })
})

testdirContent['win-abs-drive-unix'] = Dir({
  'package.json': File({
    name: 'win-abs-drive-unix',
    version: '1.0.0',
    dependencies: {
      pkga: 'file://D:/thing/pkga'
    }
  })
})

test('specifiers', function (t) {
  t.plan(installOk.length + 2)
  installOk.forEach(function (mod) {
    t.test(mod, function (t) {
      common.npm(['install', '--dry-run', '--json', 'file:' + mod], conf, function (err, code, stdout) {
        if (err) throw err
        t.is(code, 0, 'command ran ok')
        t.comment(stdout.trim())
        t.done()
      })
    })
  })
  slashes.forEach(function (os) {
    t.test('win-abs-drive-' + os, function (t) {
      common.npm(['install', '--dry-run', '--json', 'file:win-abs-drive-' + os], confE, function (err, code, stdout, stderr) {
        if (err) throw err
        t.not(code, 0, 'command errored ok')
        t.comment(stderr.trim())
        if (isWindows) {
          t.test('verify failure of file-not-found or wrong drive letter on windows')
        } else {
          var result = JSON.parse(stdout)
          t.is(result.error && result.error.code, 'EWINDOWSPATH', 'verify failure due to windows paths not supported on non-Windows')
        }

        t.done()
      })
    })
  })
})
testdirContent['mkdirp'] = Dir({
  'package.json': File({
    name: 'mkdirp',
    version: '9.9.9'
  })
})
testdirContent['example'] = Dir({
  'minimist': Dir({
    'package.json': File({
      name: 'minimist',
      version: '9.9.9'
    })
  })
})
testdirContent['wordwrap'] = Dir({
})
testdirContent['prefer-pkg'] = Dir({
  'package.json': File({
    name: 'perfer-pkg',
    version: '1.0.0',
    dependencies: {
      'mkdirp': 'latest'
    }
  }),
  'latest': Dir({
    'package.json': File({
      name: 'mkdirp',
      version: '9.9.9'
    })
  })
})

test('ambiguity', function (t) {
  t.plan(5)
  t.test('arg: looks like package name, is dir', function (t) {
    common.npm(['install', 'mkdirp', '--dry-run', '--json'], conf, function (err, code, stdout) {
      if (err) throw err
      t.is(code, 0, 'command ran ok')
      const result = JSON.parse(stdout.trim())
      t.like(result, {added: [{name: 'mkdirp', version: '9.9.9'}]}, 'got local dir')
      t.done()
    })
  })
  t.test('arg: looks like package name, is package', function (t) {
    common.npm(['install', 'wordwrap', '--dry-run', '--json'], conf, function (err, code, stdout) {
      if (err) throw err
      t.is(code, 0, 'command ran ok')
      const result = JSON.parse(stdout.trim())
      t.like(result, {added: [{name: 'wordwrap', version: '0.0.2'}]}, 'even with local dir w/o package.json, got global')
      t.done()
    })
  })
  t.test('arg: looks like github repo, is dir', function (t) {
    common.npm(['install', 'example/minimist', '--dry-run', '--json'], conf, function (err, code, stdout) {
      if (err) throw err
      t.is(code, 0, 'command ran ok')
      const result = JSON.parse(stdout.trim())
      t.like(result, {added: [{name: 'minimist', version: '9.9.9'}]}, 'got local dir')
      t.done()
    })
  })
  t.test('package: looks like tag, has dir, use tag', function (t) {
    common.npm(['install', '--dry-run', '--json'], {cwd: path.join(testdir, 'prefer-pkg'), env: conf.env}, function (err, code, stdout) {
      if (err) throw err
      t.is(code, 0, 'command ran ok')
      const result = JSON.parse(stdout.trim())
      t.like(result, {added: [{name: 'mkdirp', version: '0.3.5'}]}, 'got local dir')
      t.done()
    })
  })

  t.test('test ambiguity for github repos')
})
testdirContent['existing-matches'] = Dir({
  'package.json': File({
    name: 'existing-matches',
    version: '1.0.0',
    dependencies: {
      'pkga': 'file:../pkga',
      'pkgb': 'file:' + testdir + '/pkgb',
      'pkgc': 'file:../pkgc',
      'pkgd': 'file:' + testdir + '/pkgd'
    }
  }),
  'node_modules': Dir({
    'pkga': Symlink('../../pkga'),
    'pkgd': Symlink('../../pkgd')
  })
})

test('existing install matches', function (t) {
  t.plan(1)
  // have to make these by hand because tacks doesn't support absolute paths in symlinks
  fs.symlinkSync(testdir + '/pkgb', testdir + '/existing-matches/node_modules/pkgb', 'junction')
  fs.symlinkSync(testdir + '/pkgc', testdir + '/existing-matches/node_modules/pkgc', 'junction')
  t.test('relative symlink counts as match of relative symlink in package.json', function (t) {
    common.npm(['ls', '--json'], {cwd: path.join(testdir, 'existing-matches'), env: conf.env}, function (err, code, stdout) {
      if (err) throw err
      t.is(code, 0, 'command ran ok')
      if (stdout) t.comment(stdout.trim())
      t.test('specifically test that output is valid')
      // relative symlink counts as match of relative symlink in package.json (pkga)
      // relative symlink counts as match of abs symlink in package.json (pkgc)
      // abs symlink counts as match of relative symlink in package.json (pkgb)
      // abs symlink counts as match of abs symlink in package.json (pkgd)
      t.done()
    })
  })
})
var ibdir = testdir + '/install-behavior'
testdirContent['ib-out'] = Dir({
  'package.json': File({
    name: 'ib-out',
    version: '1.0.0',
    dependencies: {
      'minimist': '*'
    }
  })
})

testdirContent['install-behavior'] = Dir({
  'package.json': File({
    name: 'install-behavior',
    version: '1.0.0'
  }),
  'ib-in': Dir({
    'package.json': File({
      name: 'ib-in',
      version: '1.0.0',
      dependencies: {
        'mkdirp': '*'
      }
    })
  }),
  'noext': File(new Buffer(
    '1f8b08000000000000032b484cce4e4c4fd52f80d07a59c5f9790c540606' +
    '06066626260a20dadccc144c1b1841f86000923334363037343536343732' +
    '633000728c0c80f2d4760836505a5c925804740aa5e640bca200a78708a8' +
    '56ca4bcc4d55b252cacb4fad2851d251502a4b2d2acecccf030a19ea19e8' +
    '1928d5720db41b47c1281805a36014501f00005012007200080000',
    'hex'
  )),
  'tarball-1.0.0.tgz': File(new Buffer(
    '1f8b08000000000000032b484cce4e4c4fd52f80d07a59c5f9790c540606' +
    '06066626260a20dadccc144c1b1841f8606062a6c060686c606e686a6c68' +
    '666ec26000e480e5a9ed106ca0b4b824b108e8144acd817845014e0f1150' +
    'ad9497989baa64a5040c85a4c49c1c251d05a5b2d4a2e2ccfc3ca0a0a19e' +
    '819e81522dd740bb72148c8251300a4601b50100473dd15800080000',
    'hex'
  )),
  'not-module': Dir({}),
  'test-preinstall': Dir({
    'preinstall.js': File('console.log("CWD:" + process.cwd())'),
    'package.json': File({
      name: 'test-preinstall',
      version: '1.0.0',
      scripts: {
        'preinstall': 'node ' + ibdir + '/test-preinstall/preinstall.js'
      }
    })
  })
})

test('install behavior', function (t) {
  var ibconf = {cwd: ibdir, env: conf.env.extend({npm_config_loglevel: 'silent'}), stdio: conf.stdio}
  t.plan(7)
  t.test('transitive deps for in-larger-module cases', function (t) {
    common.npm(['install', 'file:ib-in'], ibconf, function (err, code, stdout) {
      if (err) throw err
      t.is(code, 0, 'command ran ok')
      if (stdout) t.comment(stdout.trim())
      fileExists(t, ibdir + '/node_modules/mkdirp', 'transitive dep flattened')
      isSymlink(t, ibdir + '/node_modules/ib-in', 'dep is symlink')
      noFileExists(t, ibdir + '/ib-in/node_modules/mkdirp', 'transitive dep not nested')
      t.done()
    })
  })
  t.test('transitive deps for out-of-larger module cases', function (t) {
    common.npm(['install', 'file:../ib-out'], ibconf, function (err, code, stdout) {
      if (err) throw err
      t.is(code, 0, 'command ran ok')
      if (stdout) t.comment(stdout.trim())
      noFileExists(t, ibdir + '/node_modules/minimist', 'transitive dep not flattened')
      fileExists(t, testdir + '/ib-out/node_modules/minimist', 'transitive dep nested')
      t.done()
    })
  })
  t.test('transitive deps for out-of-larger module cases w/ --preserve-symlinks', function (t) {
    rimraf.sync(ibdir + '/node_modules')
    rimraf.sync(testdir + '/ib-out/node_modules')
    var env = conf.env.extend({NODE_PRESERVE_SYMLINKS: '1'})
    common.npm(['install', 'file:../ib-out'], {cwd: ibdir, env: env}, function (err, code, stdout) {
      if (err) throw err
      t.is(code, 0, 'command ran ok')
      if (stdout) t.comment(stdout.trim())
      fileExists(t, ibdir + '/node_modules/minimist', 'transitive dep flattened')
      noFileExists(t, testdir + '/ib-out/node_modules/minimist', 'transitive dep not nested')
      t.done()
    })
  })
  t.test('tar/tgz/tar.gz should install a tarball', function (t) {
    common.npm(['install', 'file:tarball-1.0.0.tgz'], ibconf, function (err, code, stdout) {
      if (err) throw err
      t.is(code, 0, 'command ran ok')
      if (stdout) t.comment(stdout.trim())
      fileExists(t, ibdir + '/node_modules/tarball')
      t.done()
    })
  })
  t.test('non tar/tgz/tar.gz files should give good error message', function (t) {
    common.npm(['install', 'file:noext', '--json'], ibconf, function (err, code, stdout) {
      if (err) throw err
      t.not(code, 0, 'do not install files w/o extensions')
      noFileExists(t, ibdir + '/node_modules/noext')
      var result = JSON.parse(stdout)
      t.like(result, {error: {code: 'ENOLOCAL'}}, 'new type of error')
      t.done()
    })
  })
  t.test('directories without package.json should give good error message', function (t) {
    common.npm(['install', 'file:not-module', '--json'], ibconf, function (err, code, stdout) {
      if (err) throw err
      t.not(code, 0, 'error on dir w/o module')
      var result = JSON.parse(stdout)
      t.like(result, {error: {code: 'ENOLOCAL'}}, 'new type of error')
      t.done()
    })
  })
  t.test('verify preinstall step runs after finalize, such that cwd is as expected', function (t) {
    common.npm(['install', 'file:test-preinstall'], ibconf, function (err, code, stdout) {
      if (err) throw err
      t.is(code, 0, 'command ran ok')
      isSymlink(t, `${ibdir}/node_modules/test-preinstall`, 'installed as symlink')
      t.notLike(stdout, /CWD:.*[.]staging/, 'cwd should not be in staging')

      t.test('verify that env is as expected')
      t.done()
    })
  })
})
var sbdir = testdir + '/save-behavior'
testdirContent['save-behavior'] = Dir({
  'package.json': File({
    name: 'save-behavior',
    version: '1.0.0'
  }),
  'npm-shrinkwrap.json': File({
    name: 'save-behavior',
    version: '1.0.0',
    dependencies: {}
  }),
  'transitive': Dir({
    'package.json': File({
      name: 'transitive',
      version: '1.0.0',
      dependencies: {
        'pkgc': 'file:../../pkgc'
      }
    })
  })
})
testdirContent['sb-transitive'] = Dir({
  'package.json': File({
    name: 'sb-transitive',
    version: '1.0.0',
    dependencies: {
      'sbta': 'file:sbta'
    }
  }),
  'sbta': Dir({
    'package.json': File({
      name: 'sbta',
      version: '1.0.0'
    })
  })
})
var sbdirp = testdir + '/save-behavior-pre'
testdirContent['save-behavior-pre'] = Dir({
  'package.json': File({
    name: 'save-behavior',
    version: '1.0.0'
  }),
  'npm-shrinkwrap.json': File({
    name: 'save-behavior',
    version: '1.0.0',
    dependencies: {}
  })
})
testdirContent['sb-transitive-preserve'] = Dir({
  'package.json': File({
    name: 'sb-transitive-preserve',
    version: '1.0.0',
    dependencies: {
      'sbtb': 'file:sbtb'
    }
  }),
  'sbtb': Dir({
    'package.json': File({
      name: 'sbtb',
      version: '1.0.0'
    })
  })
})
test('save behavior', function (t) {
  t.plan(6)
  var sbconf = {cwd: sbdir, env: conf.env, stdio: conf.stdio}
  t.test('to package.json and npm-shrinkwrap.json w/ abs', function (t) {
    common.npm(['install', '--save', 'file:' + testdir + '/pkga'], sbconf, function (err, code, stdout) {
      if (err) throw err
      t.is(code, 0, 'command ran ok')
      var pjson = readJson(sbdir + '/package.json')
      var shrinkwrap = readJson(sbdir + '/npm-shrinkwrap.json')
      t.is(pjson.dependencies.pkga, 'file:../pkga', 'package.json')
      var sdep = shrinkwrap.dependencies
      t.like(sdep, {pkga: {version: 'file:../pkga', resolved: null}}, 'npm-shrinkwrap.json')
      t.done()
    })
  })
  t.test('to package.json and npm-shrinkwrap.json w/ drive abs')
  t.test('to package.json and npm-shrinkwrap.json w/ rel', function (t) {
    common.npm(['install', '--save', 'file:../pkgb'], sbconf, function (err, code, stdout) {
      if (err) throw err
      t.is(code, 0, 'command ran ok')
      var pjson = readJson(sbdir + '/package.json')
      var shrinkwrap = readJson(sbdir + '/npm-shrinkwrap.json')
      t.is(pjson.dependencies.pkgb, 'file:../pkgb', 'package.json')
      var sdep = shrinkwrap.dependencies
      t.like(sdep, {pkgb: {version: 'file:../pkgb', resolved: null}}, 'npm-shrinkwrap.json')
      t.done()
    })
  })
  t.test('internal transitive dependencies of shrinkwraps', function (t) {
    common.npm(['install', '--save', 'file:transitive'], sbconf, function (err, code, stdout) {
      if (err) throw err
      t.is(code, 0, 'command ran ok')
      var pjson = readJson(sbdir + '/package.json')
      var shrinkwrap = readJson(sbdir + '/npm-shrinkwrap.json')
      t.is(pjson.dependencies.transitive, 'file:transitive', 'package.json')
      var sdep = shrinkwrap.dependencies.transitive || {}
      var tdep = shrinkwrap.dependencies.pkgc
      t.is(sdep.version, 'file:transitive', 'npm-shrinkwrap.json direct dep')
      t.is(tdep.version, 'file:../pkgc', 'npm-shrinkwrap.json transitive dep')
      t.done()
    })
  })
  t.test('external transitive dependencies of shrinkwraps', function (t) {
    common.npm(['install', '--save', 'file:../sb-transitive'], sbconf, function (err, code, stdout) {
      if (err) throw err
      t.is(code, 0, 'command ran ok')
      var pjson = readJson(sbdir + '/package.json')
      var shrinkwrap = readJson(sbdir + '/npm-shrinkwrap.json')
      var deps = pjson.dependencies || {}
      t.is(deps['sb-transitive'], 'file:../sb-transitive', 'package.json')
      var sdep = shrinkwrap.dependencies['sb-transitive'] || {}
      var tdep = sdep.dependencies.sbta
      t.like(tdep, {bundled: true, version: 'file:../sb-transitive/sbta'}, 'npm-shrinkwrap.json transitive dep')
      t.like(sdep, {bundled: null, version: 'file:../sb-transitive'}, 'npm-shrinkwrap.json direct dep')
      t.done()
    })
  })
  t.test('external transitive dependencies of shrinkwraps > preserve symlinks', function (t) {
    var preserveEnv = conf.env.extend({NODE_PRESERVE_SYMLINKS: '1'})
    var preserveConf = {cwd: sbdirp, env: preserveEnv, stdio: conf.stdio}
    common.npm(['install', '--save', 'file:../sb-transitive-preserve'], preserveConf, function (err, code, stdout) {
      if (err) throw err
      t.is(code, 0, 'command ran ok')
      var pjson = readJson(sbdirp + '/package.json')
      var shrinkwrap = readJson(sbdirp + '/npm-shrinkwrap.json')
      t.is(pjson.dependencies['sb-transitive-preserve'], 'file:../sb-transitive-preserve', 'package.json')
      var sdep = shrinkwrap.dependencies['sb-transitive-preserve'] || {}
      var tdep = shrinkwrap.dependencies.sbtb
      t.like(sdep, {bundled: null, version: 'file:../sb-transitive-preserve'}, 'npm-shrinkwrap.json direct dep')
      t.like(tdep, {bundled: null, version: 'file:../sb-transitive-preserve/sbtb'}, 'npm-shrinkwrap.json transitive dep')
      t.done()
    })
  })
})

test('removal', function (t) {
  t.plan(3)
  t.test('should remove the symlink')
  t.test('should not remove the transitive deps if it was not a `link:` type specifier.')
  t.test("should not remove transitive deps if it's outside the package and --preserver-symlinks isn't set")
})

test('misc', function (t) {
  t.plan(3)
  t.test('listing: should look right, not include version')
  t.test('outdated: show LOCAL for wanted / latest')
  t.test('update: if specifier exists, do nothing. otherwise as if `npm install`.')
})

test('cleanup', function (t) {
  server.close()
  cleanup()
  t.done()
})
