const t = require('tap')
const { load: _loadMockNpm } = require('../../fixtures/mock-npm.js')

t.cleanSnapshot = str => str
  .replace(/(published ).*?( ago)/g, '$1{TIME}$2')

// run the same as tap does when running directly with node
process.stdout.columns = undefined

// 3 days. its never yesterday and never a week ago
const yesterday = new Date(Date.now() - 1000 * 60 * 60 * 24 * 3)

const packument = (nv, opts) => {
  if (!opts.fullMetadata) {
    throw new Error('must fetch fullMetadata')
  }

  if (!opts.preferOnline) {
    throw new Error('must fetch with preferOnline')
  }

  const mocks = {
    red: {
      _id: 'red@1.0.1',
      name: 'red',
      'dist-tags': {
        '1.0.1': {},
      },
      time: {
        unpublished: {
          time: '2012-12-20T00:00:00.000Z',
        },
      },
    },
    blue: {
      _id: 'blue',
      name: 'blue',
      'dist-tags': {
        v1: '1.0.0',
        next: '1.0.1',
        prev: '1.0.0',
        latest: '1.0.0',
        a: '1.0.0',
        c: '1.0.0',
        b: '1.0.0',
        d: '1.0.0',
        f: '1.0.1',
        g: '1.0.1',
        h: '1.0.1',
        e: '1.0.1',
        z: '1.0.0',
        x: '1.0.1',
        y: '1.0.0',
      },
      time: {
        '1.0.0': yesterday,
        '1.0.1': '2012-12-20T00:00:00.000Z',
      },
      versions: {
        '1.0.0': {
          name: 'blue',
          version: '1.0.0',
          dist: {
            shasum: '123',
            tarball: 'http://hm.blue.com/1.0.0.tgz',
            fileCount: 1,
          },
        },
        '1.0.1': {
          name: 'blue',
          version: '1.0.1',
          dist: {
            shasum: '124',
            tarball: 'http://hm.blue.com/1.0.1.tgz',
            integrity: '---',
            fileCount: 1,
            unpackedSize: 1000,
          },
        },
      },
    },
    // package with no time attribute
    gray: {
      _id: 'gray',
      name: 'gray',
      'dist-tags': {
        latest: '1.1.0',
        beta: '1.2.0-beta',
        alpha: '1.2.0-alpha',
        old: '1.0.0',
        stable: '1.1.0',
      },
      versions: {
        '1.1.0': {
          name: 'gray',
          version: '1.1.0',
          dist: {
            shasum: 'b',
            tarball: 'http://gray/1.1.0.tgz',
            fileCount: 1,
          },
        },
      },
    },
    cyan: {
      _npmUser: {
        name: 'claudia',
        email: 'claudia@cyan.com',
      },
      name: 'cyan',
      'dist-tags': {
        latest: '1.0.0',
      },
      versions: {
        '1.0.0': {
          version: '1.0.0',
          name: 'cyan',
          dist: {
            shasum: '123',
            tarball: 'http://hm.cyan.com/1.0.0.tgz',
            integrity: '---',
            fileCount: 1,
            unpackedSize: 1000000,
          },
        },
        '1.0.1': {},
      },
    },
    'cyan-oidc': {
      _npmUser: {
        name: 'claudia',
        email: 'claudia@cyan.com',
        trustedPublisher: {
          id: 'github',
          oidcConfigId: 'oidc:a0e127d0-8d66-45d0-8264-e4f8372c7249',
        },
      },
      name: 'cyan',
      'dist-tags': {
        latest: '1.0.0',
      },
      versions: {
        '1.0.0': {
          version: '1.0.0',
          name: 'cyan',
          dist: {
            shasum: '123',
            tarball: 'http://hm.cyan.com/1.0.0.tgz',
            integrity: '---',
            fileCount: 1,
            unpackedSize: 1000000,
          },
        },
        '1.0.1': {},
      },
    },
    brown: {
      name: 'brown',
    },
    yellow: {
      _id: 'yellow',
      name: 'yellow',
      author: {
        name: 'foo',
        email: 'foo@yellow.com',
        twitter: 'foo',
      },
      empty: '',
      readme: 'a very useful readme',
      versions: {
        '1.0.0': {
          version: '1.0.0',
          author: 'claudia',
          readme: 'a very useful readme',
          maintainers: [
            { name: 'claudia', email: 'c@yellow.com', twitter: 'cyellow' },
            { name: 'isaacs', email: 'i@yellow.com', twitter: 'iyellow' },
          ],
        },
        '1.0.1': {
          version: '1.0.1',
          author: 'claudia',
        },
        '1.0.2': {
          version: '1.0.2',
          author: 'claudia',
        },
      },
    },
    purple: {
      name: 'purple',
      versions: {
        '1.0.0': {
          foo: 1,
          maintainers: [
            { name: 'claudia' },
          ],
        },
        '1.0.1': {},
      },
    },
    green: {
      _id: 'green',
      name: 'green',
      'dist-tags': {
        latest: '1.0.0',
      },
      maintainers: [
        { name: 'claudia', email: 'c@yellow.com', twitter: 'cyellow' },
        { name: 'isaacs', email: 'i@yellow.com', twitter: 'iyellow' },
      ],
      keywords: ['colors', 'green', 'crayola'],
      versions: {
        '1.0.0': {
          _id: 'green',
          version: '1.0.0',
          description: 'green is a very important color',
          bugs: {
            url: 'http://bugs.green.com',
          },
          deprecated: true,
          repository: {
            url: 'http://repository.green.com',
          },
          license: { type: 'ACME' },
          bin: {
            green: 'bin/green.js',
          },
          dependencies: {
            red: '1.0.0',
            yellow: '1.0.0',
          },
          dist: {
            shasum: '123',
            tarball: 'http://hm.green.com/1.0.0.tgz',
            integrity: '---',
            fileCount: 1,
            unpackedSize: 1000000000,
          },
        },
        '1.0.1': {},
      },
    },
    black: {
      name: 'black',
      'dist-tags': {
        latest: '1.0.0',
      },
      versions: {
        '1.0.0': {
          version: '1.0.0',
          bugs: 'http://bugs.black.com',
          license: {},
          dependencies: (() => {
            const deps = {}
            for (let i = 0; i < 25; i++) {
              deps[i] = '1.0.0'
            }

            return deps
          })(),
          dist: {
            shasum: '123',
            tarball: 'http://hm.black.com/1.0.0.tgz',
            integrity: '---',
            fileCount: 1,
            unpackedSize: 1,
          },
        },
        '1.0.1': {},
      },
    },
    pink: {
      name: 'pink',
      'dist-tags': {
        latest: '1.0.0',
      },
      versions: {
        '1.0.0': {
          version: '1.0.0',
          maintainers: [
            { name: 'claudia', url: 'http://c.pink.com' },
            { name: 'isaacs', url: 'http://i.pink.com' },
          ],
          repository: 'http://repository.pink.com',
          license: {},
          dist: {
            shasum: '123',
            tarball: 'http://hm.pink.com/1.0.0.tgz',
            integrity: '---',
            fileCount: 1,
            unpackedSize: 1,
          },
        },
        '1.0.1': {},
      },
    },
    orange: {
      name: 'orange',
      'dist-tags': {
        latest: '1.0.0',
      },
      versions: {
        '1.0.0': {
          version: '1.0.0',
          homepage: 'http://hm.orange.com',
          license: {},
          dist: {
            shasum: '123',
            tarball: 'http://hm.orange.com/1.0.0.tgz',
            integrity: '---',
            fileCount: 1,
            unpackedSize: 1,
          },
        },
        '1.0.1': {},
        '100000000000000000.0.0': {
        },
      },
    },
    'single-version': {
      _id: 'single-version',
      name: 'single-version',
      'dist-tags': {
        latest: '1.0.0',
      },
      versions: {
        '1.0.0': {
          name: 'single-version',
          version: '1.0.0',
          dist: {
            shasum: '123',
            tarball: 'http://hm.single-version.com/1.0.0.tgz',
            fileCount: 1,
          },
        },
      },
    },
    // this is a packaged named error which will conflict with
    // the error key in json output
    error: {
      _id: 'error',
      name: 'error',
      'dist-tags': {
        latest: '1.0.0',
      },
      versions: {
        '1.0.0': {
          name: 'error',
          version: '1.0.0',
          dist: {
            shasum: '123',
            tarball: 'http://hm.error.com/1.0.0.tgz',
            fileCount: 1,
          },
        },
      },
    },
  }

  if (nv.type === 'git') {
    return mocks[nv.hosted.project]
  }

  if (nv.raw === './blue') {
    return mocks.blue
  }

  if (mocks[nv.name]) {
    return mocks[nv.name]
  }

  if (nv.name === 'unknown-error') {
    throw new Error('Unknown error')
  }

  throw Object.assign(new Error('404'), { code: 'E404' })
}

const loadMockNpm = async function (t, opts = {}) {
  return _loadMockNpm(t, {
    command: 'view',
    mocks: {
      pacote: {
        packument,
      },
    },
    ...opts,
    config: {
      color: 'always',
      ...opts.config,
    },
  })
}

t.test('package from git', async t => {
  const { view, joinedOutput } = await loadMockNpm(t, { config: { unicode: false } })
  await view.exec(['https://github.com/npm/green'])
  t.matchSnapshot(joinedOutput())
})

t.test('deprecated package with license, bugs, repository and other fields', async t => {
  const { view, joinedOutput } = await loadMockNpm(t, { config: { unicode: false } })
  await view.exec(['green@1.0.0'])
  t.matchSnapshot(joinedOutput())
})

t.test('deprecated package with unicode', async t => {
  const { view, joinedOutput } = await loadMockNpm(t, { config: { unicode: true } })
  await view.exec(['green@1.0.0'])
  t.matchSnapshot(joinedOutput())
})

t.test('package with more than 25 deps', async t => {
  const { view, joinedOutput } = await loadMockNpm(t, { config: { unicode: false } })
  await view.exec(['black@1.0.0'])
  t.matchSnapshot(joinedOutput())
})

t.test('package with maintainers info as object', async t => {
  const { view, joinedOutput } = await loadMockNpm(t, { config: { unicode: false } })
  await view.exec(['pink@1.0.0'])
  t.matchSnapshot(joinedOutput())
})

t.test('package with homepage', async t => {
  const { view, joinedOutput } = await loadMockNpm(t, { config: { unicode: false } })
  await view.exec(['orange@1.0.0'])
  t.matchSnapshot(joinedOutput())
})

t.test('package with invalid version', async t => {
  const { view, joinedOutput } = await loadMockNpm(t, { config: { unicode: false } })
  await view.exec(['orange', 'versions'])
  t.matchSnapshot(joinedOutput())
})

t.test('package with no versions', async t => {
  const { view, joinedOutput } = await loadMockNpm(t, { config: { unicode: false } })
  await view.exec(['brown'])
  t.equal(joinedOutput(), '', 'no info to display')
})

t.test('package with no repo or homepage', async t => {
  const { view, joinedOutput } = await loadMockNpm(t, { config: { unicode: false } })
  await view.exec(['blue@1.0.0'])
  t.matchSnapshot(joinedOutput())
})

t.test('package with semver range', async t => {
  const { view, joinedOutput } = await loadMockNpm(t, { config: { unicode: false } })
  await view.exec(['blue@^1.0.0'])
  t.matchSnapshot(joinedOutput())
})

t.test('package with no modified time', async t => {
  const { view, joinedOutput } = await loadMockNpm(t, { config: { unicode: false } })
  await view.exec(['cyan@1.0.0'])
  t.matchSnapshot(joinedOutput())
})

t.test('package with --json and semver range', async t => {
  const { view, joinedOutput } = await loadMockNpm(t, { config: { json: true } })
  await view.exec(['cyan@^1.0.0'])
  t.matchSnapshot(joinedOutput())
})

t.test('package with --json and single-match semver range preserves array output', async t => {
  const { view, joinedOutput } = await loadMockNpm(t, { config: { json: true } })
  await view.exec(['single-version@^1'])
  const parsed = JSON.parse(joinedOutput())
  t.ok(Array.isArray(parsed), 'preserves the top-level array for semver ranges')
  t.equal(parsed.length, 1, 'returns the single matching version in an array')
  t.match(parsed[0], {
    name: 'single-version',
    version: '1.0.0',
    dist: {
      shasum: '123',
      tarball: 'http://hm.single-version.com/1.0.0.tgz',
      fileCount: 1,
    },
  }, 'returns the expected package data')
})

t.test('package field with --json and single-match semver range preserves array output', async t => {
  const { view, joinedOutput } = await loadMockNpm(t, { config: { json: true } })
  await view.exec(['single-version@^1', 'version'])
  t.strictSame(JSON.parse(joinedOutput()), ['1.0.0'], 'does not unwrap single field matches for semver ranges')
})

t.test('package with _npmUser.trustedPublisher shows cleaned up property with --json', async t => {
  const { view, joinedOutput } = await loadMockNpm(t, { config: { json: true } })
  await view.exec(['cyan-oidc@^1.0.0'])
  t.match(joinedOutput(), /claudia <claudia@cyan.com>/, 'uses oidc trustedPublisher info for _npmUser')
})

t.test('package with --json and no versions', async t => {
  const { view, joinedOutput } = await loadMockNpm(t, { config: { json: true } })
  await view.exec(['brown'])
  t.equal(joinedOutput(), '', 'no info to display')
})

t.test('package with --json and single string arg', async t => {
  const { view, joinedOutput } = await loadMockNpm(t, { config: { json: true } })
  await view.exec(['blue', 'dist-tags.latest'])
  t.strictSame(JSON.parse(joinedOutput()), ['1.0.0'], 'returns single string value as array')
})

t.test('package with single version', async t => {
  t.test('full json', async t => {
    const { view, joinedOutput } = await loadMockNpm(t, { config: { json: true } })
    await view.exec(['single-version'])
    t.matchSnapshot(joinedOutput())
  })

  t.test('json and versions arg', async t => {
    const { view, joinedOutput } = await loadMockNpm(t, { config: { json: true } })
    await view.exec(['single-version', 'versions'])
    const parsed = JSON.parse(joinedOutput())
    t.strictSame(parsed, [['1.0.0']], 'does not unwrap single item arrays in json')
  })

  t.test('no json and versions arg', async t => {
    const { view, joinedOutput } = await loadMockNpm(t, { config: { json: false } })
    await view.exec(['single-version', 'versions'])
    t.strictSame(joinedOutput(), '1.0.0', 'unwraps single item arrays in basic mode')
  })
})

t.test('package in cwd', async t => {
  const prefixDir = {
    'package.json': JSON.stringify({
      name: 'blue',
      version: '1.0.0',
    }, null, 2),
  }

  t.test('specific version', async t => {
    const { view, joinedOutput } = await loadMockNpm(t, { prefixDir })
    await view.exec(['.@1.0.0'])
    t.matchSnapshot(joinedOutput())
  })

  t.test('non-specific version', async t => {
    const { view, joinedOutput } = await loadMockNpm(t, { prefixDir })
    await view.exec(['.'])
    t.matchSnapshot(joinedOutput())
  })

  t.test('directory', async t => {
    const { view, joinedOutput } = await loadMockNpm(t, { prefixDir })
    await view.exec(['./blue'])
    t.matchSnapshot(joinedOutput())
  })
})

t.test('specific field names', async t => {
  const { view, joinedOutput, clearOutput } = await loadMockNpm(t, { config: { color: false } })
  t.afterEach(() => clearOutput())

  t.test('readme', async t => {
    await view.exec(['yellow@1.0.0', 'readme'])
    t.matchSnapshot(joinedOutput())
  })

  t.test('several fields', async t => {
    await view.exec(['yellow@1.0.0', 'name', 'version', 'foo[bar]'])
    t.matchSnapshot(joinedOutput())
  })

  t.test('several fields with several versions', async t => {
    await view.exec(['yellow@1.x.x', 'author'])
    t.matchSnapshot(joinedOutput())
  })

  t.test('nested field with brackets', async t => {
    await view.exec(['orange@1.0.0', 'dist[shasum]'])
    t.matchSnapshot(joinedOutput())
  })

  t.test('maintainers with email', async t => {
    await view.exec(['yellow@1.0.0', 'maintainers', 'name'])
    t.matchSnapshot(joinedOutput())
  })

  t.test('maintainers with url', async t => {
    await view.exec(['pink@1.0.0', 'maintainers'])
    t.matchSnapshot(joinedOutput())
  })

  t.test('unknown nested field ', async t => {
    await view.exec(['yellow@1.0.0', 'dist.foobar'])
    t.equal(joinedOutput(), '', 'no info to display')
  })

  t.test('array field - 1 element', async t => {
    await view.exec(['purple@1.0.0', 'maintainers.name'])
    t.matchSnapshot(joinedOutput())
  })

  t.test('array field - 2 elements', async t => {
    await view.exec(['yellow@1.x.x', 'maintainers.name'])
    t.matchSnapshot(joinedOutput())
  })

  t.test('fields with empty values', async t => {
    await view.exec(['yellow', 'empty'])
    t.matchSnapshot(joinedOutput())
  })
})

t.test('throw error if global mode', async t => {
  const { npm } = await loadMockNpm(t, { config: { global: true } })
  await t.rejects(
    npm.exec('view', []),
    /Cannot use view command in global mode./
  )
})

t.test('throw ENOENT error if package.json missing', async t => {
  const { npm } = await loadMockNpm(t)
  await t.rejects(
    npm.exec('view', []),
    { code: 'ENOENT' }
  )
})

t.test('throw error if package.json has no name', async t => {
  const { npm } = await loadMockNpm(t, {
    prefixDir: {
      'package.json': '{}',
    },
  })
  await t.rejects(
    npm.exec('view', []),
    /Invalid package.json, no "name" field/
  )
})

t.test('throws when unpublished', async t => {
  const { npm } = await loadMockNpm(t)
  await t.rejects(
    npm.exec('view', ['red']),
    { code: 'E404', pkgid: 'red@1.0.1', message: 'Unpublished on 2012-12-20T00:00:00.000Z' }
  )
})

t.test('throws when version not matched', async t => {
  const { npm } = await loadMockNpm(t)
  await t.rejects(
    npm.exec('view', ['blue@2.0.0']),
    { code: 'E404', pkgid: 'blue@2.0.0', message: 'No match found for version 2.0.0' }
  )
})

t.test('workspaces', async t => {
  const prefixDir = {
    'package.json': JSON.stringify({
      name: 'workspaces-test-package',
      version: '1.2.3',
      workspaces: ['test-workspace-a', 'test-workspace-b'],
    }),
    'test-workspace-a': {
      'package.json': JSON.stringify({
        name: 'green',
        version: '1.2.3',
      }),
    },
    'test-workspace-b': {
      'package.json': JSON.stringify({
        name: 'orange',
        version: '1.2.3',
      }),
    },
  }

  const prefixDir404 = {
    'test-workspace-b': {
      'package.json': JSON.stringify({
        name: 'missing-package',
        version: '1.2.3',
      }),
    },
  }

  const prefixDirError = {
    'test-workspace-b': {
      'package.json': JSON.stringify({
        name: 'unknown-error',
        version: '1.2.3',
      }),
    },
  }

  const prefixPackageNamedError = {
    'test-workspace-a': {
      'package.json': JSON.stringify({
        name: 'error',
        version: '1.2.3',
      }),
    },
  }

  t.test('all workspaces', async t => {
    const { view, joinedOutput } = await loadMockNpm(t, {
      prefixDir,
      config: { unicode: false, workspaces: true },
    })
    await view.exec([])
    t.matchSnapshot(joinedOutput())
  })

  t.test('one specific workspace', async t => {
    const { view, joinedOutput } = await loadMockNpm(t, {
      prefixDir,
      config: { unicode: false, workspace: ['green'] },
    })
    await view.exec([])
    t.matchSnapshot(joinedOutput())
  })

  t.test('all workspaces --json', async t => {
    const { view, joinedOutput } = await loadMockNpm(t, {
      prefixDir,
      config: { unicode: false, workspaces: true, json: true },
    })
    await view.exec([])
    t.matchSnapshot(joinedOutput())
  })

  t.test('all workspaces single field', async t => {
    const { view, joinedOutput } = await loadMockNpm(t, {
      prefixDir,
      config: { unicode: false, workspaces: true },
    })
    await view.exec(['.', 'name'])
    t.matchSnapshot(joinedOutput())
  })

  t.test('all workspaces nonexistent field', async t => {
    const { view, joinedOutput } = await loadMockNpm(t, {
      prefixDir,
      config: { unicode: false, workspaces: true },
    })
    await view.exec(['.', 'foo'])
    t.matchSnapshot(joinedOutput())
  })

  t.test('all workspaces nonexistent field --json', async t => {
    const { view, joinedOutput } = await loadMockNpm(t, {
      prefixDir,
      config: { unicode: false, workspaces: true, json: true },
    })
    await view.exec(['.', 'foo'])
    t.matchSnapshot(joinedOutput())
  })

  t.test('all workspaces single field --json', async t => {
    const { view, joinedOutput } = await loadMockNpm(t, {
      prefixDir,
      config: { unicode: false, workspaces: true, json: true },
    })
    await view.exec(['.', 'name'])
    t.matchSnapshot(joinedOutput())
  })

  t.test('single workspace --json', async t => {
    const { view, joinedOutput } = await loadMockNpm(t, {
      prefixDir,
      config: { unicode: false, workspace: ['green'], json: true },
    })
    await view.exec([])
    t.matchSnapshot(joinedOutput())
  })

  t.test('remote package name', async t => {
    const { view, logs, joinedOutput } = await loadMockNpm(t, {
      prefixDir,
      config: { unicode: false, workspaces: true },
    })
    await view.exec(['pink'])
    t.matchSnapshot(joinedOutput())
    t.matchSnapshot(logs.warn, 'should have warning of ignoring workspaces')
  })

  t.test('404 workspaces', async t => {
    t.test('basic', async t => {
      const { view, joinedFullOutput } = await loadMockNpm(t, {
        prefixDir: { ...prefixDir, ...prefixDir404 },
        config: { workspaces: true, loglevel: 'error' },
      })
      await view.exec([])
      t.matchSnapshot(joinedFullOutput())
      t.equal(process.exitCode, 1)
    })

    t.test('json', async t => {
      const { view, joinedFullOutput } = await loadMockNpm(t, {
        prefixDir: { ...prefixDir, ...prefixDir404 },
        config: { workspaces: true, json: true, loglevel: 'error' },
      })
      await view.exec([])
      t.matchSnapshot(joinedFullOutput())
      t.equal(process.exitCode, 1)
    })

    t.test('json with package named error', async t => {
      const { view, joinedFullOutput } = await loadMockNpm(t, {
        prefixDir: { ...prefixDir, ...prefixDir404, ...prefixPackageNamedError },
        config: { workspaces: true, json: true, loglevel: 'warn' },
      })
      await view.exec([])
      t.matchSnapshot(joinedFullOutput())
      t.equal(process.exitCode, 1)
    })

    t.test('non-404 error rejects', async t => {
      const { view, joinedFullOutput } = await loadMockNpm(t, {
        prefixDir: { ...prefixDir, ...prefixDirError },
        config: { workspaces: true, loglevel: 'error' },
      })
      await t.rejects(view.exec([]))
      t.matchSnapshot(joinedFullOutput())
    })

    t.test('non-404 error rejects with single arg', async t => {
      const { view, joinedFullOutput } = await loadMockNpm(t, {
        prefixDir: { ...prefixDir, ...prefixDirError },
        config: { workspaces: true, loglevel: 'error' },
      })
      await t.rejects(view.exec(['.', 'version']))
      t.matchSnapshot(joinedFullOutput())
    })
  })
})

t.test('completion', async t => {
  const { view } = await loadMockNpm(t, { command: 'view' })
  const res = await view.completion({
    conf: { argv: { remain: ['npm', 'view', 'green@1.0.0'] } },
  })
  t.ok(res, 'returns back fields')
})

t.test('no package completion', async t => {
  const { view } = await loadMockNpm(t, { command: 'view' })
  const res = await view.completion({ conf: { argv: { remain: ['npm', 'view'] } } })
  t.notOk(res, 'there is no package completion')
  t.end()
})

t.test('allow-git=root, package with multiple dist‑tags and no time', async t => {
  const { view, joinedOutput } = await loadMockNpm(t, { config: { unicode: false, 'allow-git': 'root' } })
  await view.exec(['https://github.com/npm/gray'])
  t.matchSnapshot(joinedOutput())
})

t.test('allow-git=none', async t => {
  const { view } = await loadMockNpm(t, { config: { 'allow-git': 'none' }, mocks: {} })
  await t.rejects(view.exec(['npm/npm']), {
    code: 'EALLOWGIT',
    package: 'github:npm/npm',
    message: 'Fetching packages of type "git" have been disabled',
  })
})
