const t = require('tap')
const MockRegistry = require('@npmcli/mock-registry')
const _mockNpm = require('../../fixtures/mock-npm')
const { cleanCwd } = require('../../fixtures/clean-snapshot')

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

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',
        },
      },
    },
    timed: {
      name: 'timed',
      'dist-tags': {
        latest: '2.0.0',
      },
      versions: {
        '1.0.0': {
          version: '1.0.0',
        },
        '2.0.0': {
          version: '2.0.0',
        },
      },
      time: {
        '1.0.0': '2020-01-01T00:00:00.000Z',
        '2.0.0': '2020-06-01T00:00:00.000Z',
      },
    },
  }

  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]
}

const fixtures = {
  global: {
    node_modules: {
      cat: {
        'package.json': JSON.stringify({
          name: 'cat',
          version: '1.0.0',
        }, null, 2),
      },
    },
  },
  local: {
    '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),
      },
    },
  },
  workspaces: {
    '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'),
      d: t.fixture('symlink', '../packages/d'),
      e: t.fixture('symlink', '../packages/e'),
      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',
          },
        }),
      },
      d: {
        'package.json': JSON.stringify({
          name: 'd',
          version: '1.0.0',
          dependencies: {
            theta: '^1.0.0',
          },
        }),
      },
      e: {
        'package.json': JSON.stringify({
          name: 'e',
          version: '1.0.0',
          dependencies: {
            theta: '^1.0.0',
          },
        }),
      },
    },
  },
}

const mockNpm = async (t, { prefixDir, ...opts } = {}) => {
  const res = await _mockNpm(t, {
    command: 'outdated',
    mocks: {
      pacote: {
        packument,
      },
    },
    ...opts,
    prefixDir,
  })

  // this is not currently used, but ensures that no requests are
  // hitting the registry.
  // XXX: the pacote mock should be replaced with mock registry calls
  const registry = new MockRegistry({
    tap: t,
    registry: res.npm.config.get('registry'),
    strict: true,
  })

  return {
    ...res,
    registry,
  }
}

t.test('should display outdated deps', async t => {
  await t.test('outdated global', async t => {
    const { outdated, joinedOutput } = await mockNpm(t, {
      globalPrefixDir: fixtures.global,
      config: { global: true },
    })
    await outdated.exec([])
    t.equal(process.exitCode, 1)
    t.matchSnapshot(joinedOutput())
  })

  await t.test('outdated', async t => {
    const { outdated, joinedOutput } = await mockNpm(t, {
      prefixDir: fixtures.local,
      config: {
        color: 'always',
      },
    })
    await outdated.exec([])
    t.equal(process.exitCode, 1)
    t.matchSnapshot(joinedOutput())
  })

  await t.test('outdated --omit=dev', async t => {
    const { outdated, joinedOutput } = await mockNpm(t, {
      prefixDir: fixtures.local,
      config: {
        omit: ['dev'],
        color: 'always',
      },
    })
    await outdated.exec([])
    t.equal(process.exitCode, 1)
    t.matchSnapshot(joinedOutput())
  })

  await t.test('outdated --omit=dev --omit=peer', async t => {
    const { outdated, joinedOutput } = await mockNpm(t, {
      prefixDir: fixtures.local,
      config: {
        omit: ['dev', 'peer'],
        color: 'always',
      },
    })
    await outdated.exec([])
    t.equal(process.exitCode, 1)
    t.matchSnapshot(joinedOutput())
  })

  await t.test('outdated --omit=prod', async t => {
    const { outdated, joinedOutput } = await mockNpm(t, {
      prefixDir: fixtures.local,
      config: {
        omit: ['prod'],
        color: 'always',
      },
    })
    await outdated.exec([])
    t.equal(process.exitCode, 1)
    t.matchSnapshot(joinedOutput())
  })

  await t.test('outdated --long', async t => {
    const { outdated, joinedOutput } = await mockNpm(t, {
      prefixDir: fixtures.local,
      config: {
        long: true,
      },
    })
    await outdated.exec([])
    t.equal(process.exitCode, 1)
    t.matchSnapshot(joinedOutput())
  })

  await t.test('outdated --json', async t => {
    const { outdated, joinedOutput } = await mockNpm(t, {
      prefixDir: fixtures.local,
      config: {
        json: true,
      },
    })
    await outdated.exec([])
    t.equal(process.exitCode, 1)
    t.matchSnapshot(joinedOutput())
  })

  await t.test('outdated --json --long', async t => {
    const { outdated, joinedOutput } = await mockNpm(t, {
      prefixDir: fixtures.local,
      config: {
        json: true,
        long: true,
      },
    })
    await outdated.exec([])
    t.equal(process.exitCode, 1)
    t.matchSnapshot(joinedOutput())
  })

  await t.test('outdated --parseable', async t => {
    const { outdated, joinedOutput } = await mockNpm(t, {
      prefixDir: fixtures.local,
      config: {
        parseable: true,
      },
    })
    await outdated.exec([])
    t.equal(process.exitCode, 1)
    t.matchSnapshot(joinedOutput())
  })

  await t.test('outdated --parseable --long', async t => {
    const { outdated, joinedOutput } = await mockNpm(t, {
      prefixDir: fixtures.local,
      config: {
        parseable: true,
        long: true,
      },
    })
    await outdated.exec([])
    t.equal(process.exitCode, 1)
    t.matchSnapshot(joinedOutput())
  })

  await t.test('outdated --all', async t => {
    const { outdated, joinedOutput } = await mockNpm(t, {
      prefixDir: fixtures.local,
      config: {
        all: true,
      },
    })
    await outdated.exec([])
    t.equal(process.exitCode, 1)
    t.matchSnapshot(joinedOutput())
  })

  await t.test('outdated specific dep', async t => {
    const { outdated, joinedOutput } = await mockNpm(t, {
      prefixDir: fixtures.local,
    })
    await outdated.exec(['cat'])
    t.equal(process.exitCode, 1)
    t.matchSnapshot(joinedOutput())
  })
})

t.test('should return if no outdated deps', async t => {
  const 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),
      },
    },
  }

  const { outdated, joinedOutput } = await mockNpm(t, {
    prefixDir: testDir,

  })
  await outdated.exec([])
  t.equal(joinedOutput(), '', 'no logs')
})

t.test('throws if error with a dep', async t => {
  const 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),
      },
    },
  }

  const { outdated } = await mockNpm(t, {
    prefixDir: testDir,
  })

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

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

  const { outdated, joinedOutput } = await mockNpm(t, {
    prefixDir: testDir,
  })

  await outdated.exec([])

  t.equal(joinedOutput(), '', 'no logs')
})

t.test('should skip invalid pkg ranges', async t => {
  const 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),
      },
    },
  }

  const { outdated, joinedOutput } = await mockNpm(t, {
    prefixDir: testDir,
  })
  await outdated.exec([])
  t.equal(joinedOutput(), '', 'no logs')
})

t.test('should skip git specs', async t => {
  const 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),
      },
    },
  }

  const { outdated, joinedOutput } = await mockNpm(t, {
    prefixDir: testDir,
  })
  await outdated.exec([])
  t.equal(joinedOutput(), '', 'no logs')
})

t.test('workspaces', async t => {
  const mockWorkspaces = async (t, { exitCode = 1, ...config } = {}) => {
    const { outdated, joinedOutput } = await mockNpm(t, {
      prefixDir: fixtures.workspaces,
      config,
    })

    await outdated.exec([])

    t.matchSnapshot(joinedOutput(), 'output')
    t.equal(process.exitCode, exitCode ?? undefined)
  }

  await t.test('should display ws outdated deps human output', t =>
    mockWorkspaces(t))

  // 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
  await t.test('should display only root outdated when ws disabled', t =>
    mockWorkspaces(t, { workspaces: false, exitCode: null }))

  await t.test('should display ws outdated deps json output', t =>
    mockWorkspaces(t, { json: true }))

  await t.test('should display ws outdated deps parseable output', t =>
    mockWorkspaces(t, { parseable: true }))

  await t.test('should display all dependencies', t =>
    mockWorkspaces(t, { all: true }))

  await t.test('should highlight ws in depended by section', t =>
    mockWorkspaces(t, { color: 'always' }))

  await t.test('should display results filtered by ws', t =>
    mockWorkspaces(t, { workspace: 'a' }))

  await t.test('should display json results filtered by ws', t =>
    mockWorkspaces(t, { json: true, workspace: 'a' }))

  await t.test('should display parseable results filtered by ws', t =>
    mockWorkspaces(t, { parseable: true, workspace: 'a' }))

  await t.test('should display nested deps when filtering by ws and using --all', t =>
    mockWorkspaces(t, { all: true, workspace: 'a' }))

  await t.test('should display no results if ws has no deps to display', t =>
    mockWorkspaces(t, { workspace: 'b', exitCode: null }))

  await t.test('should display missing deps when filtering by ws', t =>
    mockWorkspaces(t, { workspace: 'c', exitCode: 1 }))
})

t.test('aliases', async t => {
  const 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',
        }),
      },
    },
  }

  const { outdated, joinedOutput } = await mockNpm(t, {
    prefixDir: testDir,
  })
  await outdated.exec([])

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

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

  const { outdated, joinedOutput } = await mockNpm(t, {
    prefixDir: testDir,
  })
  await outdated.exec([])

  t.matchSnapshot(
    joinedOutput(),
    'should display aliased outdated dep output with correct wanted values'
  )
  t.equal(process.exitCode, 1)
})

t.test('dependent location', async t => {
  const testDir = {
    'package.json': JSON.stringify({
      name: 'similar-name',
      version: '1.0.0',
      workspaces: ['a', 'nest/a'],
    }),
    a: {
      'package.json': JSON.stringify({
        name: 'a',
        version: '1.0.0',
        dependencies: {
          dog: '^1.0.0',
        },
      }),
    },
    nest: {
      a: {
        'package.json': JSON.stringify({
          name: 'nest-a',
          version: '1.0.0',
          dependencies: {
            dog: '^1.0.0',
          },
        }),
      },
    },
    node_modules: {
      dog: {
        'package.json': JSON.stringify({
          name: 'dog',
          version: '1.0.0',
        }),
      },
      a: t.fixture('symlink', '../a'),
      'nest-a': t.fixture('symlink', '../nest/a'),
    },

  }
  t.test(`--long`, async t => {
    const { outdated, joinedOutput } = await mockNpm(t, {
      prefixDir: testDir,
      config: {
        long: true,
      },
    })
    await outdated.exec([])
    t.matchSnapshot(
      joinedOutput(),
      'should display dependent location when using --long'
    )
  })

  t.test('--long --json', async t => {
    const { outdated, joinedOutput } = await mockNpm(t, {
      prefixDir: testDir,
      config: {
        long: true,
        json: true,
      },
    })
    await outdated.exec([])
    t.matchSnapshot(
      joinedOutput(),
      'should display dependent location when using --long and --json'
    )
  })
})

t.test('min-release-age-exclude', async t => {
  const prefixDir = {
    'package.json': JSON.stringify({
      name: 'project',
      version: '1.0.0',
      dependencies: {
        timed: '^1.0.0',
      },
    }, null, 2),
    node_modules: {
      timed: {
        'package.json': JSON.stringify({
          name: 'timed',
          version: '1.0.0',
        }, null, 2),
      },
    },
  }

  await t.test('before hides the newer version', async t => {
    const { outdated, joinedOutput } = await mockNpm(t, {
      prefixDir,
      config: { before: new Date('2020-03-01') },
    })
    await outdated.exec([])
    t.notMatch(joinedOutput(), 'timed', 'newer version filtered out by before')
  })

  await t.test('exact-name exclude restores the newer version', async t => {
    const { outdated, joinedOutput } = await mockNpm(t, {
      prefixDir,
      config: {
        before: new Date('2020-03-01'),
        'min-release-age-exclude': ['timed'],
      },
    })
    await outdated.exec([])
    t.match(joinedOutput(), 'timed', 'excluded package is reported as outdated')
    t.match(joinedOutput(), '2.0.0', 'latest 2.0.0 is surfaced')
  })

  await t.test('glob exclude restores the newer version', async t => {
    const { outdated, joinedOutput } = await mockNpm(t, {
      prefixDir,
      config: {
        before: new Date('2020-03-01'),
        'min-release-age-exclude': ['tim*'],
      },
    })
    await outdated.exec([])
    t.match(joinedOutput(), '2.0.0', 'glob-excluded package shows newer version')
  })
})
