// Do not rely on package._fields, so that we don't throw
// false failures if a tree is generated by other clients.
// Only relies on child.resolved, which MAY come from
// client-specific package.json meta _fields, but most of
// the time will be pulled out of a lockfile

const semver = require('semver')
const npa = require('npm-package-arg')
const {relative} = require('path')
const fromPath = require('./from-path.js')

const depValid = (child, requested, requestor) => {
  // NB: we don't do much to verify 'tag' type requests.
  // Just verify that we got a remote resolution.  Presumably, it
  // came from a registry and was tagged at some point.

  if (typeof requested === 'string') {
    try {
      // tarball/dir must have resolved to the same tgz on disk, but for
      // file: deps that depend on other files/dirs, we must resolve the
      // location based on the *requestor* file/dir, not where it ends up.
      // '' is equivalent to '*'
      requested = npa.resolve(child.name, requested || '*', fromPath(requestor))
    } catch (er) {
      // Not invalid because the child doesn't match, but because
      // the spec itself is not supported.  Nothing would match,
      // so the edge is definitely not valid and never can be.
      er.dependency = child.name
      er.requested = requested
      requestor.errors.push(er)
      return false
    }
  }

  // if the lockfile is super old, or hand-modified,
  // then it's possible to hit this state.
  if (!requested) {
    const er = new Error('Invalid dependency specifier')
    er.dependency = child.name
    er.requested = requested
    requestor.errors.push(er)
    return false
  }

  switch (requested.type) {
    case 'range':
      if (requested.fetchSpec === '*') {
        return true
      }
      // fallthrough
    case 'version':
      // if it's a version or a range other than '*', semver it
      return semver.satisfies(child.version, requested.fetchSpec, true)

    case 'directory':
      // directory must be a link to the specified folder
      return !!child.isLink &&
        relative(child.realpath, requested.fetchSpec) === ''

    case 'file':
      return tarballValid(child, requested, requestor)

    case 'alias':
      // check that the alias target is valid
      return depValid(child, requested.subSpec, requestor)

    case 'tag':
      // if it's a tag, we just verify that it has a tarball resolution
      // presumably, it came from the registry and was tagged at some point
      return child.resolved && npa(child.resolved).type === 'remote'

    case 'remote':
      // verify that we got it from the desired location
      return child.resolved === requested.fetchSpec

    case 'git': {
      // if it's a git type, verify that they're the same repo
      //
      // if it specifies a definite commit, then it must have the
      // same commit to be considered the same repo
      //
      // if it has a #semver:<range> specifier, verify that the
      // version in the package is in the semver range
      const resRepo = npa(child.resolved || '')
      const resHost = resRepo.hosted
      const reqHost = requested.hosted
      const reqCommit = /^[a-fA-F0-9]{40}$/.test(requested.gitCommittish || '')
      const nc = { noCommittish: !reqCommit }
      const sameRepo =
        resHost ? reqHost && reqHost.ssh(nc) === resHost.ssh(nc)
        : resRepo.fetchSpec === requested.fetchSpec

      return !sameRepo ? false
        : !requested.gitRange ? true
        : semver.satisfies(child.package.version, requested.gitRange, {
          loose: true,
        })
    }

    default: // unpossible, just being cautious
      break
  }

  const er = new Error('Unsupported dependency type')
  er.dependency = child.name
  er.requested = requested
  requestor.errors.push(er)
  return false
}

const tarballValid = (child, requested, requestor) => {
  if (child.isLink) {
    return false
  }

  if (child.resolved) {
    return child.resolved.replace(/\\/g, '/') === `file:${requested.fetchSpec.replace(/\\/g, '/')}`
  }

  // if we have a legacy mutated package.json file.  we can't be 100%
  // sure that it resolved to the same file, but if it was the same
  // request, that's a pretty good indicator of sameness.
  if (child.package._requested) {
    return child.package._requested.saveSpec === requested.saveSpec
  }

  // ok, we're probably dealing with some legacy cruft here, not much
  // we can do at this point unfortunately.
  return false
}

module.exports = (child, requested, accept, requestor) =>
  depValid(child, requested, requestor) ||
  (typeof accept === 'string' ? depValid(child, accept, requestor) : false)
