/* eslint-disable no-extend-native */
/* eslint-disable no-global-assign */
const t = require('tap')
const EventEmitter = require('events')
const os = require('os')
const fs = require('fs')
const path = require('path')

const { real: mockNpm } = require('../../fixtures/mock-npm')

// generic error to be used in tests
const err = Object.assign(new Error('ERROR'), { code: 'ERROR' })
err.stack = 'Error: ERROR'

const redactCwd = (path) => {
  const normalizePath = p => p
    .replace(/\\+/g, '/')
    .replace(/\r\n/g, '\n')
  return normalizePath(path)
    .replace(new RegExp(normalizePath(process.cwd()), 'g'), '{CWD}')
}

t.cleanSnapshot = (str) => redactCwd(str)

const cacheFolder = t.testdir({})
const logFile = path.resolve(cacheFolder, '_logs', 'expecteddate-debug.log')
const timingFile = path.resolve(cacheFolder, '_timing.json')

const { Npm } = mockNpm(t, {
  '../../package.json': {
    version: '1.0.0',
  },
})
const npm = new Npm()

t.before(async () => {
  await npm.load()
  npm.config.set('cache', cacheFolder)
})

// cut off process from script so that it won't quit the test runner
// while trying to run through the myriad of cases.  need to make it
// have all the functions signal-exit relies on so that it doesn't
// nerf itself, thinking global.process is broken or gone.
const _process = process
process = Object.assign(
  new EventEmitter(),
  {
    argv: ['/node', ..._process.argv.slice(1)],
    cwd: _process.cwd,
    env: _process.env,
    version: 'v1.0.0',
    exit: (code) => {
      process.exitCode = code || process.exitCode || 0
      process.emit('exit', process.exitCode)
    },
    stdout: { write (_, cb) {
      cb()
    } },
    stderr: { write () {} },
    hrtime: _process.hrtime,
    kill: () => {},
    reallyExit: (code) => process.exit(code),
    pid: 123456,
  }
)

const osType = os.type
const osRelease = os.release
// overrides OS type/release for cross platform snapshots
os.type = () => 'Foo'
os.release = () => '1.0.0'

// generates logfile name with mocked date
const _toISOString = Date.prototype.toISOString
Date.prototype.toISOString = () => 'expecteddate'

const consoleError = console.error
const errors = []
console.error = (err) => {
  errors.push(err)
}
t.teardown(() => {
  os.type = osType
  os.release = osRelease
  // needs to put process back in its place in order for tap to exit properly
  process = _process
  Date.prototype.toISOString = _toISOString
  console.error = consoleError
})

t.afterEach(() => {
  errors.length = 0
  npm.log.level = 'silent'
  // clear out the 'A complete log' message
  npm.log.record.length = 0
  delete process.exitCode
})

const mocks = {
  '../../../lib/utils/error-message.js': (err) => ({
    ...err,
    summary: [['ERR', err.message]],
    detail: [['ERR', err.message]],
  }),
}

const exitHandler = t.mock('../../../lib/utils/exit-handler.js', mocks)
exitHandler.setNpm(npm)

t.test('exit handler never called - loglevel silent', (t) => {
  npm.log.level = 'silent'
  process.emit('exit', 1)
  const logData = fs.readFileSync(logFile, 'utf8')
  t.match(logData, 'Exit handler never called!')
  t.match(errors, [''], 'logs one empty string to console.error')
  t.end()
})

t.test('exit handler never called - loglevel notice', (t) => {
  npm.log.level = 'notice'
  process.emit('exit', 1)
  const logData = fs.readFileSync(logFile, 'utf8')
  t.match(logData, 'Exit handler never called!')
  t.match(errors, ['', ''], 'logs two empty strings to console.error')
  t.end()
})

t.test('handles unknown error', (t) => {
  t.plan(2)

  npm.log.level = 'notice'

  process.once('timeEnd', (msg) => {
    t.equal(msg, 'npm', 'should trigger timeEnd for npm')
  })

  exitHandler(err)
  const logData = fs.readFileSync(logFile, 'utf8')
  t.matchSnapshot(
    logData,
    'should have expected log contents for unknown error'
  )
  t.end()
})

t.test('fail to write logfile', (t) => {
  t.plan(1)

  t.teardown(() => {
    npm.config.set('cache', cacheFolder)
  })

  const badDir = t.testdir({
    _logs: 'is a file',
  })

  npm.config.set('cache', badDir)

  t.doesNotThrow(
    () => exitHandler(err),
    'should not throw on cache write failure'
  )
})

t.test('console.log output using --json', (t) => {
  t.plan(1)

  npm.config.set('json', true)
  t.teardown(() => {
    npm.config.set('json', false)
  })

  exitHandler(new Error('Error: EBADTHING Something happened'))
  t.same(
    JSON.parse(errors[0]),
    {
      error: {
        code: 'EBADTHING', // should default error code to E[A-Z]+
        summary: 'Error: EBADTHING Something happened',
        detail: 'Error: EBADTHING Something happened',
      },
    },
    'should output expected json output'
  )
})

t.test('throw a non-error obj', (t) => {
  t.plan(2)

  const weirdError = {
    code: 'ESOMETHING',
    message: 'foo bar',
  }

  process.once('exit', code => {
    t.equal(code, 1, 'exits with exitCode 1')
  })
  exitHandler(weirdError)
  t.match(
    npm.log.record.find(r => r.level === 'error'),
    { message: 'foo bar' }
  )
})

t.test('throw a string error', (t) => {
  t.plan(2)
  const error = 'foo bar'

  process.once('exit', code => {
    t.equal(code, 1, 'exits with exitCode 1')
  })
  exitHandler(error)
  t.match(
    npm.log.record.find(r => r.level === 'error'),
    { message: 'foo bar' }
  )
})

t.test('update notification', (t) => {
  const updateMsg = 'you should update npm!'
  npm.updateNotification = updateMsg
  npm.log.level = 'silent'

  t.teardown(() => {
    delete npm.updateNotification
  })

  exitHandler()
  t.match(
    npm.log.record.find(r => r.level === 'notice'),
    { message: 'you should update npm!' }
  )
  t.end()
})

t.test('npm.config not ready', (t) => {
  t.plan(1)

  const { Npm: Unloaded } = mockNpm(t)
  const unloaded = new Unloaded()

  t.teardown(() => {
    exitHandler.setNpm(npm)
  })

  exitHandler.setNpm(unloaded)

  exitHandler()
  t.match(
    errors[0],
    /Error: Exit prior to config file resolving./,
    'should exit with config error msg'
  )
  t.end()
})

t.test('timing', (t) => {
  npm.config.set('timing', true)

  t.teardown(() => {
    fs.unlinkSync(timingFile)
    npm.config.set('timing', false)
  })

  exitHandler()
  const timingData = JSON.parse(fs.readFileSync(timingFile, 'utf8'))
  t.match(timingData, { version: '1.0.0', 'config:load:defaults': Number })
  t.end()
})

t.test('timing - with error', (t) => {
  npm.config.set('timing', true)

  t.teardown(() => {
    fs.unlinkSync(timingFile)
    npm.config.set('timing', false)
  })

  exitHandler(err)
  const timingData = JSON.parse(fs.readFileSync(timingFile, 'utf8'))
  t.match(timingData, { version: '1.0.0', 'config:load:defaults': Number })
  t.end()
})

t.test('uses code from errno', (t) => {
  t.plan(1)

  process.once('exit', code => {
    t.equal(code, 127, 'should set exitCode from errno')
  })
  exitHandler(Object.assign(
    new Error('Error with errno'),
    {
      errno: 127,
    }
  ))
})

t.test('uses code from number', (t) => {
  t.plan(1)

  process.once('exit', code => {
    t.equal(code, 404, 'should set exitCode from a number')
  })
  exitHandler(Object.assign(
    new Error('Error with code type number'),
    {
      code: 404,
    }
  ))
})

t.test('call exitHandler with no error', (t) => {
  t.plan(1)
  process.once('exit', code => {
    t.equal(code, 0, 'should end up with exitCode 0 (default)')
  })
  exitHandler()
})

t.test('defaults to log error msg if stack is missing', (t) => {
  const { Npm: Unloaded } = mockNpm(t)
  const unloaded = new Unloaded()

  t.teardown(() => {
    exitHandler.setNpm(npm)
  })

  exitHandler.setNpm(unloaded)
  const noStackErr = Object.assign(
    new Error('Error with no stack'),
    {
      code: 'ENOSTACK',
      errno: 127,
    }
  )
  delete noStackErr.stack

  exitHandler(noStackErr)
  t.equal(errors[0], 'Error with no stack', 'should use error msg')
  t.end()
})

t.test('exits uncleanly when only emitting exit event', (t) => {
  t.plan(2)

  npm.log.level = 'silent'
  process.emit('exit')
  const logData = fs.readFileSync(logFile, 'utf8')
  t.match(logData, 'Exit handler never called!')
  t.match(process.exitCode, 1, 'exitCode coerced to 1')
  t.end()
})

t.test('do no fancy handling for shellouts', t => {
  const { command } = npm
  const LOG_RECORD = []
  npm.command = 'exec'

  t.teardown(() => {
    npm.command = command
  })
  t.beforeEach(() => LOG_RECORD.length = 0)

  const loudNoises = () => npm.log.record
    .filter(({ level }) => ['warn', 'error'].includes(level))

  t.test('shellout with a numeric error code', t => {
    t.plan(2)
    process.once('exit', code => {
      t.equal(code, 5, 'got expected exit code')
    })
    exitHandler(Object.assign(new Error(), { code: 5 }))
    t.strictSame(loudNoises(), [], 'no noisy warnings')
  })

  t.test('shellout without a numeric error code (something in npm)', t => {
    t.plan(2)
    process.once('exit', code => {
      t.equal(code, 1, 'got expected exit code')
    })
    exitHandler(Object.assign(new Error(), { code: 'banana stand' }))
    // should log some warnings and errors, because something weird happened
    t.strictNotSame(loudNoises(), [], 'bring the noise')
    t.end()
  })

  t.test('shellout with code=0 (extra weird?)', t => {
    t.plan(2)
    process.once('exit', code => {
      t.equal(code, 1, 'got expected exit code')
    })
    exitHandler(Object.assign(new Error(), { code: 0 }))
    t.strictNotSame(loudNoises(), [], 'bring the noise')
  })

  t.end()
})
