const npmlog = require('npmlog')
const procLog = require('../../lib/utils/proc-log-listener.js')
procLog.reset()

// In theory we shouldn't have to do this if all the tests were tearing down
// their listeners properly, we're still getting warnings even though
// perfStop() and procLog.reset() is in the teardown script.  This silences the
// warnings for now
require('events').defaultMaxListeners = Infinity

const realLog = {}
for (const level in npmlog.levels) {
  realLog[level] = npmlog[level]
}

const { title, execPath } = process

// Eventually this should default to having a prefix of an empty testdir, and
// awaiting npm.load() unless told not to (for npm tests for example).  Ideally
// the prefix of an empty dir is inferred rather than explicitly set
const RealMockNpm = (t, otherMocks = {}) => {
  const mock = {}
  mock.logs = []
  mock.outputs = []
  mock.joinedOutput = () => {
    return mock.outputs.map(o => o.join(' ')).join('\n')
  }
  mock.filteredLogs = title => mock.logs.filter(([t]) => t === title).map(([, , msg]) => msg)
  const Npm = t.mock('../../lib/npm.js', otherMocks)
  class MockNpm extends Npm {
    constructor () {
      super()
      for (const level in npmlog.levels) {
        npmlog[level] = (...msg) => {
          mock.logs.push([level, ...msg])

          const l = npmlog.level
          npmlog.level = 'silent'
          realLog[level](...msg)
          npmlog.level = l
        }
      }
      // npm.js tests need this restored to actually test this function!
      mock.npmOutput = this.output
      this.output = (...msg) => mock.outputs.push(msg)
    }
  }
  mock.Npm = MockNpm
  t.afterEach(() => {
    mock.outputs.length = 0
    mock.logs.length = 0
  })

  t.teardown(() => {
    process.removeAllListeners('time')
    process.removeAllListeners('timeEnd')
    npmlog.record.length = 0
    for (const level in npmlog.levels) {
      npmlog[level] = realLog[level]
    }
    procLog.reset()
    process.title = title
    process.execPath = execPath
    delete process.env.npm_command
    delete process.env.COLOR
  })

  return mock
}

const realConfig = require('../../lib/utils/config')

// Basic npm fixture that you can give a config object that acts like
// npm.config You still need a separate flatOptions. Tests should migrate to
// using the real npm mock above
class MockNpm {
  constructor (base = {}) {
    this._mockOutputs = []
    this.isMockNpm = true
    this.base = base

    const config = base.config || {}

    for (const attr in base) {
      if (attr !== 'config') {
        this[attr] = base[attr]
      }
    }

    this.flatOptions = base.flatOptions || {}
    this.config = {
      // for now just set `find` to what config.find should return
      // this works cause `find` is not an existing config entry
      find: (k) => ({ ...realConfig.defaults, ...config })[k],
      get: (k) => ({ ...realConfig.defaults, ...config })[k],
      set: (k, v) => config[k] = v,
      list: [{ ...realConfig.defaults, ...config }],
    }
    if (!this.log) {
      this.log = {
        clearProgress: () => {},
        disableProgress: () => {},
        enableProgress: () => {},
        http: () => {},
        info: () => {},
        levels: [],
        notice: () => {},
        pause: () => {},
        silly: () => {},
        verbose: () => {},
        warn: () => {},
      }
    }
  }

  output (...msg) {
    if (this.base.output) {
      return this.base.output(msg)
    }
    this._mockOutputs.push(msg)
  }
}

const FakeMockNpm = (base = {}) => {
  return new MockNpm(base)
}

module.exports = {
  fake: FakeMockNpm,
  real: RealMockNpm,
}
