const t = require('tap')
const path = require('path')

// make a bunch of stuff consistent for snapshots

process.getuid = () => 69
process.getgid = () => 420

Object.defineProperty(process, 'arch', {
  value: 'x64',
  configurable: true,
})

const { resolve } = require('path')
const npm = require('../../../lib/npm.js')
const CACHE = '/some/cache/dir'
npm.config = {
  flat: {
    color: false,
  },
  loaded: false,
  localPrefix: '/some/prefix/dir',
  get: key => {
    if (key === 'cache')
      return CACHE
    else if (key === 'node-version')
      return '99.99.99'
    else if (key === 'global')
      return false
    else
      throw new Error('unexpected config lookup: ' + key)
  },
}

npm.version = '123.69.420-npm'
Object.defineProperty(process, 'version', {
  value: '123.69.420-node',
  configurable: true,
})

const npmlog = require('npmlog')
const verboseLogs = []
npmlog.verbose = (...message) => {
  verboseLogs.push(message)
}

const EXPLAIN_CALLED = []
const mocks = {
  '../../../lib/utils/explain-eresolve.js': {
    report: (...args) => {
      EXPLAIN_CALLED.push(args)
      return 'explanation'
    },
  },
  // XXX ???
  get '../../../lib/utils/is-windows.js' () {
    return process.platform === 'win32'
  },
}
let errorMessage = t.mock('../../../lib/utils/error-message.js', { ...mocks })

const beWindows = () => {
  Object.defineProperty(process, 'platform', {
    value: 'win32',
    configurable: true,
  })
  errorMessage = t.mock('../../../lib/utils/error-message.js', { ...mocks })
}

const bePosix = () => {
  Object.defineProperty(process, 'platform', {
    value: 'posix',
    configurable: true,
  })
  errorMessage = t.mock('../../../lib/utils/error-message.js', { ...mocks })
}

t.test('just simple messages', t => {
  npm.command = 'audit'
  const codes = [
    'ENOAUDIT',
    'ENOLOCK',
    'ECONNREFUSED',
    'ENOGIT',
    'EPUBLISHCONFLICT',
    'EISGIT',
    'EEXIST',
    'ENEEDAUTH',
    'ECONNRESET',
    'ENOTFOUND',
    'ETIMEDOUT',
    'EAI_FAIL',
    'EBADENGINE',
    'ENOSPC',
    'EROFS',
    'ENOENT',
    'EMISSINGARG',
    'EUNKNOWNTYPE',
    'EINVALIDTYPE',
    'ETOOMANYARGS',
    'ETARGET',
    'E403',
    'ERR_SOCKET_TIMEOUT',
  ]
  t.plan(codes.length)
  codes.forEach(code => {
    const path = '/some/path'
    const pkgid = 'some@package'
    const file = '/some/file'
    const stack = 'dummy stack trace'
    const er = Object.assign(new Error('foo'), {
      code,
      path,
      pkgid,
      file,
      stack,
    })
    t.matchSnapshot(errorMessage(er, npm))
  })
})

t.test('replace message/stack sensistive info', t => {
  npm.command = 'audit'
  const path = '/some/path'
  const pkgid = 'some@package'
  const file = '/some/file'
  const stack = 'dummy stack trace at https://user:pass@registry.npmjs.org/'
  const message = 'Error at registry: https://user:pass@registry.npmjs.org/'
  const er = Object.assign(new Error(message), {
    code: 'ENOAUDIT',
    path,
    pkgid,
    file,
    stack,
  })
  t.matchSnapshot(errorMessage(er, npm))
  t.end()
})

t.test('bad engine with config loaded', t => {
  npm.config.loaded = true
  t.teardown(() => {
    npm.config.loaded = false
  })
  const path = '/some/path'
  const pkgid = 'some@package'
  const file = '/some/file'
  const stack = 'dummy stack trace'
  const er = Object.assign(new Error('foo'), {
    code: 'EBADENGINE',
    path,
    pkgid,
    file,
    stack,
  })
  t.matchSnapshot(errorMessage(er, npm))
  t.end()
})

t.test('enoent without a file', t => {
  const path = '/some/path'
  const pkgid = 'some@package'
  const stack = 'dummy stack trace'
  const er = Object.assign(new Error('foo'), {
    code: 'ENOENT',
    path,
    pkgid,
    stack,
  })
  t.matchSnapshot(errorMessage(er, npm))
  t.end()
})

t.test('enolock without a command', t => {
  npm.command = null
  const path = '/some/path'
  const pkgid = 'some@package'
  const file = '/some/file'
  const stack = 'dummy stack trace'
  const er = Object.assign(new Error('foo'), {
    code: 'ENOLOCK',
    path,
    pkgid,
    file,
    stack,
  })
  t.matchSnapshot(errorMessage(er, npm))
  t.end()
})

t.test('default message', t => {
  t.matchSnapshot(errorMessage(new Error('error object'), npm))
  t.matchSnapshot(errorMessage('error string'), npm)
  t.matchSnapshot(errorMessage(Object.assign(new Error('cmd err'), {
    cmd: 'some command',
    signal: 'SIGYOLO',
    args: ['a', 'r', 'g', 's'],
    stdout: 'stdout',
    stderr: 'stderr',
  }), npm))
  t.end()
})

t.test('args are cleaned', t => {
  t.matchSnapshot(errorMessage(Object.assign(new Error('cmd err'), {
    cmd: 'some command',
    signal: 'SIGYOLO',
    args: ['a', 'r', 'g', 's', 'https://evil:password@npmjs.org'],
    stdout: 'stdout',
    stderr: 'stderr',
  }), npm))
  t.end()
})

t.test('eacces/eperm', t => {
  const runTest = (windows, loaded, cachePath, cacheDest) => t => {
    if (windows)
      beWindows()
    else
      bePosix()

    npm.config.loaded = loaded
    const path = `${cachePath ? CACHE : '/not/cache/dir'}/path`
    const dest = `${cacheDest ? CACHE : '/not/cache/dir'}/dest`
    const er = Object.assign(new Error('whoopsie'), {
      code: 'EACCES',
      path,
      dest,
      stack: 'dummy stack trace',
    })
    verboseLogs.length = 0
    t.matchSnapshot(errorMessage(er, npm))
    t.matchSnapshot(verboseLogs)
    t.end()
    verboseLogs.length = 0
  }

  for (const windows of [true, false]) {
    for (const loaded of [true, false]) {
      for (const cachePath of [true, false]) {
        for (const cacheDest of [true, false]) {
          const m = JSON.stringify({windows, loaded, cachePath, cacheDest})
          t.test(m, runTest(windows, loaded, cachePath, cacheDest))
        }
      }
    }
  }
  t.end()
})

t.test('json parse', t => {
  t.test('merge conflict in package.json', t => {
    const dir = t.testdir({
      'package.json': `
{
  "array": [
<<<<<<< HEAD
    100,
    {
      "foo": "baz"
    },
||||||| merged common ancestors
    1,
=======
    111,
    1,
    2,
    3,
    {
      "foo": "bar"
    },
>>>>>>> a
    1
  ],
  "a": {
    "b": {
<<<<<<< HEAD
      "c": {
        "x": "bbbb"
      }
||||||| merged common ancestors
      "c": {
        "x": "aaaa"
      }
=======
      "c": "xxxx"
>>>>>>> a
    }
  }
}
`,
    })
    const { prefix } = npm
    const { argv } = process
    t.teardown(() => {
      Object.defineProperty(npm, 'prefix', {
        value: prefix,
        configurable: true,
      })
      process.argv = argv
    })
    Object.defineProperty(npm, 'prefix', { value: dir, configurable: true })
    process.argv = ['arg', 'v']
    t.matchSnapshot(errorMessage(Object.assign(new Error('conflicted'), {
      code: 'EJSONPARSE',
      path: resolve(dir, 'package.json'),
    }), npm))
    t.end()
  })

  t.test('just regular bad json in package.json', t => {
    const dir = t.testdir({
      'package.json': 'not even slightly json',
    })
    const { prefix } = npm
    const { argv } = process
    t.teardown(() => {
      Object.defineProperty(npm, 'prefix', {
        value: prefix,
        configurable: true,
      })
      process.argv = argv
    })
    Object.defineProperty(npm, 'prefix', { value: dir, configurable: true })
    process.argv = ['arg', 'v']
    t.matchSnapshot(errorMessage(Object.assign(new Error('not json'), {
      code: 'EJSONPARSE',
      path: resolve(dir, 'package.json'),
    }), npm))
    t.end()
  })

  t.test('json somewhere else', t => {
    const dir = t.testdir({
      'blerg.json': 'not even slightly json',
    })
    const { argv } = process
    t.teardown(() => {
      process.argv = argv
    })
    process.argv = ['arg', 'v']
    t.matchSnapshot(errorMessage(Object.assign(new Error('not json'), {
      code: 'EJSONPARSE',
      path: `${dir}/blerg.json`,
    }), npm))
    t.end()
  })

  t.end()
})

t.test('eotp/e401', t => {
  t.test('401, no auth headers', t => {
    t.matchSnapshot(errorMessage(Object.assign(new Error('nope'), {
      code: 'E401',
    }), npm))
    t.end()
  })

  t.test('401, no message', t => {
    t.matchSnapshot(errorMessage({
      code: 'E401',
    }, npm))
    t.end()
  })

  t.test('one-time pass challenge code', t => {
    t.matchSnapshot(errorMessage(Object.assign(new Error('nope'), {
      code: 'EOTP',
    }), npm))
    t.end()
  })

  t.test('one-time pass challenge message', t => {
    const message = 'one-time pass'
    t.matchSnapshot(errorMessage(Object.assign(new Error(message), {
      code: 'E401',
    }), npm))
    t.end()
  })

  t.test('www-authenticate challenges', t => {
    const auths = [
      'Bearer realm=do, charset="UTF-8", challenge="yourself"',
      'Basic realm=by, charset="UTF-8", challenge="your friends"',
      'PickACardAnyCard realm=friday, charset="UTF-8"',
      'WashYourHands, charset="UTF-8"',
    ]
    t.plan(auths.length)
    for (const auth of auths) {
      t.test(auth, t => {
        const er = Object.assign(new Error('challenge!'), {
          headers: {
            'www-authenticate': [auth],
          },
          code: 'E401',
        })
        t.matchSnapshot(errorMessage(er, npm))
        t.end()
      })
    }
  })

  t.end()
})

t.test('404', t => {
  t.test('no package id', t => {
    const er = Object.assign(new Error('404 not found'), { code: 'E404' })
    t.matchSnapshot(errorMessage(er, npm))
    t.end()
  })
  t.test('you should publish it', t => {
    const er = Object.assign(new Error('404 not found'), {
      pkgid: 'yolo',
      code: 'E404',
    })
    t.matchSnapshot(errorMessage(er, npm))
    t.end()
  })
  t.test('name with warning', t => {
    const er = Object.assign(new Error('404 not found'), {
      pkgid: new Array(215).fill('x').join(''),
      code: 'E404',
    })
    t.matchSnapshot(errorMessage(er, npm))
    t.end()
  })
  t.test('name with error', t => {
    const er = Object.assign(new Error('404 not found'), {
      pkgid: 'node_modules',
      code: 'E404',
    })
    t.matchSnapshot(errorMessage(er, npm))
    t.end()
  })
  t.test('cleans sensitive info from package id', t => {
    const er = Object.assign(new Error('404 not found'), {
      pkgid: 'http://evil:password@npmjs.org/not-found',
      code: 'E404',
    })
    t.matchSnapshot(errorMessage(er, npm))
    t.end()
  })
  t.end()
})

t.test('bad platform', t => {
  t.test('string os/arch', t => {
    const er = Object.assign(new Error('a bad plat'), {
      pkgid: 'lodash@1.0.0',
      current: {
        os: 'posix',
        cpu: 'x64',
      },
      required: {
        os: '!yours',
        cpu: 'x420',
      },
      code: 'EBADPLATFORM',
    })
    t.matchSnapshot(errorMessage(er, npm))
    t.end()
  })
  t.test('array os/arch', t => {
    const er = Object.assign(new Error('a bad plat'), {
      pkgid: 'lodash@1.0.0',
      current: {
        os: 'posix',
        cpu: 'x64',
      },
      required: {
        os: ['!yours', 'mine'],
        cpu: ['x420', 'x69'],
      },
      code: 'EBADPLATFORM',
    })
    t.matchSnapshot(errorMessage(er, npm))
    t.end()
  })

  t.end()
})

t.test('explain ERESOLVE errors', t => {
  const er = Object.assign(new Error('could not resolve'), {
    code: 'ERESOLVE',
  })
  t.matchSnapshot(errorMessage(er, npm))
  t.match(EXPLAIN_CALLED, [[
    er,
    false,
    path.resolve(npm.cache, 'eresolve-report.txt'),
  ]])
  t.end()
})
