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

const packument = spec => {
  const mocks = {
    cat: {
      name: 'cat',
      'dist-tags': {
        latest: '1.0.1',
      },
      versions: {
        '1.0.1': {
          version: '1.0.1',
          dependencies: {
            dog: '2.0.0',
          },
        },
      },
    },
    chai: {
      name: 'chai',
      'dist-tags': {
        latest: '1.0.1',
      },
      versions: {
        '1.0.1': {
          version: '1.0.1',
        },
      },
    },
    dog: {
      name: 'dog',
      'dist-tags': {
        latest: '2.0.0',
      },
      versions: {
        '1.0.1': {
          version: '1.0.1',
        },
        '2.0.0': {
          version: '2.0.0',
        },
      },
    },
    theta: {
      name: 'theta',
      'dist-tags': {
        latest: '1.0.1',
      },
      versions: {
        '1.0.1': {
          version: '1.0.1',
        },
      },
    },
  }

  if (spec.name === 'eta') {
    throw new Error('There is an error with this package.')
  }

  if (!mocks[spec.name]) {
    const err = new Error()
    err.code = 'E404'
    throw err
  }

  return mocks[spec.name]
}

let logs
const output = (msg) => {
  logs = `${logs}\n${msg}`
}

const globalDir = t.testdir({
  node_modules: {
    cat: {
      'package.json': JSON.stringify({
        name: 'cat',
        version: '1.0.0',
      }, null, 2),
    },
  },
})

const outdated = (dir, opts) => {
  logs = ''
  const Outdated = t.mock('../../../lib/commands/outdated.js', {
    pacote: {
      packument,
    },
  })
  if (opts.config && opts.config.omit) {
    opts.flatOptions = {
      omit: opts.config.omit,
      ...opts.flatOptions,
    }
    delete opts.config.omit
  }
  const npm = mockNpm({
    ...opts,
    localPrefix: dir,
    prefix: dir,
    flatOptions: {
      workspacesEnabled: true,
      omit: [],
      ...opts.flatOptions,
    },
    globalDir: `${globalDir}/node_modules`,
    output,
  })
  return new Outdated(npm)
}

t.beforeEach(() => logs = '')

const { exitCode } = process

t.afterEach(() => process.exitCode = exitCode)

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)

t.test('should display outdated deps', t => {
  const testDir = t.testdir({
    'package.json': JSON.stringify({
      name: 'delta',
      version: '1.0.0',
      dependencies: {
        cat: '^1.0.0',
        dog: '^1.0.0',
        theta: '^1.0.0',
      },
      devDependencies: {
        zeta: '^1.0.0',
      },
      optionalDependencies: {
        lorem: '^1.0.0',
      },
      peerDependencies: {
        chai: '^1.0.0',
      },
    }, null, 2),
    node_modules: {
      cat: {
        'package.json': JSON.stringify({
          name: 'cat',
          version: '1.0.0',
          dependencies: {
            dog: '2.0.0',
          },
        }, null, 2),
        node_modules: {
          dog: {
            'package.json': JSON.stringify({
              name: 'dog',
              version: '2.0.0',
            }, null, 2),
          },
        },
      },
      chai: {
        'package.json': JSON.stringify({
          name: 'chai',
          version: '1.0.0',
        }, null, 2),
      },
      dog: {
        'package.json': JSON.stringify({
          name: 'dog',
          version: '1.0.1',
        }, null, 2),
      },
      zeta: {
        'package.json': JSON.stringify({
          name: 'zeta',
          version: '1.0.0',
        }, null, 2),
      },
    },
  })

  t.test('outdated global', async t => {
    await outdated(null, {
      config: { global: true },
    }).exec([])
    t.equal(process.exitCode, 1)
    t.matchSnapshot(logs)
  })

  t.test('outdated', async t => {
    await outdated(testDir, {
      config: {
        global: false,
      },
      color: true,
    }).exec([])
    t.equal(process.exitCode, 1)
    t.matchSnapshot(logs)
  })

  t.test('outdated --omit=dev', async t => {
    await outdated(testDir, {
      config: {
        global: false,
        omit: ['dev'],
      },
      color: true,
    }).exec([])
    t.equal(process.exitCode, 1)
    t.matchSnapshot(logs)
  })

  t.test('outdated --omit=dev --omit=peer', async t => {
    await outdated(testDir, {
      config: {
        global: false,
        omit: ['dev', 'peer'],
      },
      color: true,
    }).exec([])
    t.equal(process.exitCode, 1)
    t.matchSnapshot(logs)
  })

  t.test('outdated --omit=prod', async t => {
    await outdated(testDir, {
      config: {
        global: false,
        omit: ['prod'],
      },
      color: true,
    }).exec([])
    t.equal(process.exitCode, 1)
    t.matchSnapshot(logs)
  })

  t.test('outdated --long', async t => {
    await outdated(testDir, {
      config: {
        global: false,
        long: true,
      },
    }).exec([])
    t.equal(process.exitCode, 1)
    t.matchSnapshot(logs)
  })

  t.test('outdated --json', async t => {
    await outdated(testDir, {
      config: {
        global: false,
        json: true,
      },
    }).exec([])
    t.equal(process.exitCode, 1)
    t.matchSnapshot(logs)
  })

  t.test('outdated --json --long', async t => {
    await outdated(testDir, {
      config: {
        global: false,
        json: true,
        long: true,
      },
    }).exec([])
    t.equal(process.exitCode, 1)
    t.matchSnapshot(logs)
  })

  t.test('outdated --parseable', async t => {
    await outdated(testDir, {
      config: {
        global: false,
        parseable: true,
      },
    }).exec([])
    t.equal(process.exitCode, 1)
    t.matchSnapshot(logs)
  })

  t.test('outdated --parseable --long', async t => {
    await outdated(testDir, {
      config: {
        global: false,
        parseable: true,
        long: true,
      },
    }).exec([])
    t.equal(process.exitCode, 1)
    t.matchSnapshot(logs)
  })

  t.test('outdated --all', async t => {
    await outdated(testDir, {
      config: {
        all: true,
      },
    }).exec([])
    t.equal(process.exitCode, 1)
    t.matchSnapshot(logs)
  })

  t.test('outdated specific dep', async t => {
    await outdated(testDir, {
      config: {
        global: false,
      },
    }).exec(['cat'])
    t.equal(process.exitCode, 1)
    t.matchSnapshot(logs)
  })

  t.end()
})

t.test('should return if no outdated deps', async t => {
  const testDir = t.testdir({
    'package.json': JSON.stringify({
      name: 'delta',
      version: '1.0.0',
      dependencies: {
        cat: '^1.0.0',
      },
    }, null, 2),
    node_modules: {
      cat: {
        'package.json': JSON.stringify({
          name: 'cat',
          version: '1.0.1',
        }, null, 2),
      },
    },
  })

  await outdated(testDir, {
    global: false,
  }).exec([])
  t.equal(logs.length, 0, 'no logs')
})

t.test('throws if error with a dep', async t => {
  const testDir = t.testdir({
    'package.json': JSON.stringify({
      name: 'delta',
      version: '1.0.0',
      dependencies: {
        eta: '^1.0.0',
      },
    }, null, 2),
    node_modules: {
      eta: {
        'package.json': JSON.stringify({
          name: 'eta',
          version: '1.0.1',
        }, null, 2),
      },
    },
  })

  await t.rejects(
    outdated(testDir, {
      global: false,
    }).exec([]),
    'There is an error with this package.'
  )
})

t.test('should skip missing non-prod deps', async t => {
  const testDir = t.testdir({
    'package.json': JSON.stringify({
      name: 'delta',
      version: '1.0.0',
      devDependencies: {
        chai: '^1.0.0',
      },
    }, null, 2),
    node_modules: {},
  })

  await outdated(testDir, {
    global: false,
  }).exec([])
  t.equal(logs.length, 0, 'no logs')
})

t.test('should skip invalid pkg ranges', async t => {
  const testDir = t.testdir({
    'package.json': JSON.stringify({
      name: 'delta',
      version: '1.0.0',
      dependencies: {
        cat: '>=^2',
      },
    }, null, 2),
    node_modules: {
      cat: {
        'package.json': JSON.stringify({
          name: 'cat',
          version: '1.0.0',
        }, null, 2),
      },
    },
  })

  await outdated(testDir, {}).exec([])
  t.equal(logs.length, 0, 'no logs')
})

t.test('should skip git specs', async t => {
  const testDir = t.testdir({
    'package.json': JSON.stringify({
      name: 'delta',
      version: '1.0.0',
      dependencies: {
        cat: 'github:username/foo',
      },
    }, null, 2),
    node_modules: {
      cat: {
        'package.json': JSON.stringify({
          name: 'cat',
          version: '1.0.0',
        }, null, 2),
      },
    },
  })

  await outdated(testDir, {}).exec([])
  t.equal(logs.length, 0, 'no logs')
})

t.test('workspaces', async t => {
  const testDir = t.testdir({
    'package.json': JSON.stringify({
      name: 'workspaces-project',
      version: '1.0.0',
      workspaces: ['packages/*'],
      dependencies: {
        dog: '^1.0.0',
      },
    }),
    node_modules: {
      a: t.fixture('symlink', '../packages/a'),
      b: t.fixture('symlink', '../packages/b'),
      c: t.fixture('symlink', '../packages/c'),
      cat: {
        'package.json': JSON.stringify({
          name: 'cat',
          version: '1.0.0',
          dependencies: {
            dog: '2.0.0',
          },
        }),
        node_modules: {
          dog: {
            'package.json': JSON.stringify({
              name: 'dog',
              version: '2.0.0',
            }),
          },
        },
      },
      chai: {
        'package.json': JSON.stringify({
          name: 'chai',
          version: '1.0.0',
        }),
      },
      dog: {
        'package.json': JSON.stringify({
          name: 'dog',
          version: '1.0.1',
        }),
      },
      foo: {
        'package.json': JSON.stringify({
          name: 'foo',
          version: '1.0.0',
          dependencies: {
            chai: '^1.0.0',
          },
        }),
      },
      zeta: {
        'package.json': JSON.stringify({
          name: 'zeta',
          version: '1.0.0',
        }),
      },
    },
    packages: {
      a: {
        'package.json': JSON.stringify({
          name: 'a',
          version: '1.0.0',
          dependencies: {
            b: '^1.0.0',
            cat: '^1.0.0',
            foo: '^1.0.0',
          },
        }),
      },
      b: {
        'package.json': JSON.stringify({
          name: 'b',
          version: '1.0.0',
          dependencies: {
            zeta: '^1.0.0',
          },
        }),
      },
      c: {
        'package.json': JSON.stringify({
          name: 'c',
          version: '1.0.0',
          dependencies: {
            theta: '^1.0.0',
          },
        }),
      },
    },
  })

  await outdated(testDir, {}).exec([])

  t.matchSnapshot(logs, 'should display ws outdated deps human output')
  t.equal(process.exitCode, 1)

  await outdated(testDir, {
    flatOptions: {
      workspacesEnabled: false,
    },
  }).exec([])

  // TODO: This should display dog, but doesn't because arborist filters
  // workspace deps even if they're also root deps
  // This will be fixed in a future arborist version
  t.matchSnapshot(logs, 'should display only root outdated when ws disabled')

  await outdated(testDir, {
    config: {
      json: true,
    },
  }).exec([])
  t.matchSnapshot(logs, 'should display ws outdated deps json output')
  t.equal(process.exitCode, 1)

  await outdated(testDir, {
    config: {
      parseable: true,
    },
  }).exec([])

  t.matchSnapshot(logs, 'should display ws outdated deps parseable output')
  t.equal(process.exitCode, 1)

  await outdated(testDir, {
    config: {
      all: true,
    },
  }).exec([])

  t.matchSnapshot(logs, 'should display all dependencies')
  t.equal(process.exitCode, 1)

  await outdated(testDir, {
    color: true,
  }).exec([])

  t.matchSnapshot(logs, 'should highlight ws in dependend by section')
  t.equal(process.exitCode, 1)

  await outdated(testDir, {}).execWorkspaces([], ['a'])
  t.matchSnapshot(logs, 'should display results filtered by ws')
  t.equal(process.exitCode, 1)

  await outdated(testDir, {
    config: {
      json: true,
    },
  }).execWorkspaces([], ['a'])
  t.matchSnapshot(logs, 'should display json results filtered by ws')
  t.equal(process.exitCode, 1)

  await outdated(testDir, {
    config: {
      parseable: true,
    },
  }).execWorkspaces([], ['a'])
  t.matchSnapshot(logs, 'should display parseable results filtered by ws')
  t.equal(process.exitCode, 1)

  await outdated(testDir, {
    config: {
      all: true,
    },
  }).execWorkspaces([], ['a'])

  t.matchSnapshot(logs,
    'should display nested deps when filtering by ws and using --all')
  t.equal(process.exitCode, 1)

  await outdated(testDir, {}).execWorkspaces([], ['b'])
  t.matchSnapshot(logs,
    'should display no results if ws has no deps to display')

  await outdated(testDir, {}).execWorkspaces([], ['c'])
  t.matchSnapshot(logs,
    'should display missing deps when filtering by ws')
})

t.test('aliases', async t => {
  const testDir = t.testdir({
    'package.json': JSON.stringify({
      name: 'display-aliases',
      version: '1.0.0',
      dependencies: {
        cat: 'npm:dog@latest',
      },
    }),
    node_modules: {
      cat: {
        'package.json': JSON.stringify({
          name: 'dog',
          version: '1.0.0',
        }),
      },
    },
  })

  await outdated(testDir, {}).exec([])

  t.matchSnapshot(logs, 'should display aliased outdated dep output')
  t.equal(process.exitCode, 1)
})
