'use strict'

const { test } = require('tap')
const requireInject = require('require-inject')

const version = '1.0.0'
const funding = {
  type: 'individual',
  url: 'http://example.com/donate'
}

const maintainerOwnsAllDeps = {
  'package.json': JSON.stringify({
    name: 'maintainer-owns-all-deps',
    version,
    funding,
    dependencies: {
      'dep-foo': '*',
      'dep-bar': '*'
    }
  }),
  node_modules: {
    'dep-foo': {
      'package.json': JSON.stringify({
        name: 'dep-foo',
        version,
        funding,
        dependencies: {
          'dep-sub-foo': '*'
        }
      }),
      node_modules: {
        'dep-sub-foo': {
          'package.json': JSON.stringify({
            name: 'dep-sub-foo',
            version,
            funding
          })
        }
      }
    },
    'dep-bar': {
      'package.json': JSON.stringify({
        name: 'dep-bar',
        version,
        funding
      })
    }
  }
}

const nestedNoFundingPackages = {
  'package.json': JSON.stringify({
    name: 'nested-no-funding-packages',
    version,
    dependencies: {
      foo: '*'
    },
    devDependencies: {
      lorem: '*'
    }
  }),
  node_modules: {
    foo: {
      'package.json': JSON.stringify({
        name: 'foo',
        version,
        dependencies: {
          bar: '*'
        }
      }),
      node_modules: {
        bar: {
          'package.json': JSON.stringify({
            name: 'bar',
            version,
            funding
          }),
          node_modules: {
            'sub-bar': {
              'package.json': JSON.stringify({
                name: 'sub-bar',
                version,
                funding: 'https://example.com/sponsor'
              })
            }
          }
        }
      }
    },
    lorem: {
      'package.json': JSON.stringify({
        name: 'lorem',
        version,
        funding: {
          url: 'https://example.com/lorem'
        }
      })
    }
  }
}

const nestedMultipleFundingPackages = {
  'package.json': JSON.stringify({
    name: 'nested-multiple-funding-packages',
    version,
    funding: [
      'https://one.example.com',
      'https://two.example.com'
    ],
    dependencies: {
      foo: '*'
    },
    devDependencies: {
      bar: '*'
    }
  }),
  node_modules: {
    foo: {
      'package.json': JSON.stringify({
        name: 'foo',
        version,
        funding: [
          'http://example.com',
          { url: 'http://sponsors.example.com/me' },
          'http://collective.example.com'
        ]
      })
    },
    bar: {
      'package.json': JSON.stringify({
        name: 'bar',
        version,
        funding: [
          'http://collective.example.com',
          { url: 'http://sponsors.example.com/you' }
        ]
      })
    }
  }
}

const conflictingFundingPackages = {
  'package.json': JSON.stringify({
    name: 'conflicting-funding-packages',
    version,
    dependencies: {
      foo: '1.0.0'
    },
    devDependencies: {
      bar: '1.0.0'
    }
  }),
  node_modules: {
    foo: {
      'package.json': JSON.stringify({
        name: 'foo',
        version: '1.0.0',
        funding: 'http://example.com/1'
      })
    },
    bar: {
      node_modules: {
        foo: {
          'package.json': JSON.stringify({
            name: 'foo',
            version: '2.0.0',
            funding: 'http://example.com/2'
          })
        }
      },
      'package.json': JSON.stringify({
        name: 'bar',
        version: '1.0.0',
        dependencies: {
          foo: '2.0.0'
        }
      })
    }
  }
}

let result = ''
let printUrl = ''
const _flatOptions = {
  color: false,
  json: false,
  global: false,
  prefix: undefined,
  unicode: false,
  which: undefined
}
const openUrl = (url, msg, cb) => {
  if (url === 'http://npmjs.org') {
    cb(new Error('ERROR'))
    return
  }
  if (_flatOptions.json) {
    printUrl = JSON.stringify({
      title: msg,
      url: url
    })
  } else {
    printUrl = `${msg}:\n  ${url}`
  }
  cb()
}
const fund = requireInject('../../lib/fund.js', {
  '../../lib/npm.js': {
    flatOptions: _flatOptions,
    get prefix () { return _flatOptions.prefix }
  },
  '../../lib/utils/open-url.js': openUrl,
  '../../lib/utils/output.js': msg => { result += msg + '\n' },
  pacote: {
    manifest: (arg) => arg.name === 'ntl'
      ? Promise.resolve({
        funding: 'http://example.com/pacote'
      })
      : Promise.reject(new Error('ERROR'))
  }
})

test('fund with no package containing funding', t => {
  _flatOptions.prefix = t.testdir({
    'package.json': JSON.stringify({
      name: 'no-funding-package',
      version: '0.0.0'
    })
  })

  fund([], (err) => {
    t.ifError(err, 'should not error out')
    t.matchSnapshot(result, 'should print empty funding info')
    result = ''
    t.end()
  })
})

test('fund in which same maintainer owns all its deps', t => {
  _flatOptions.prefix = t.testdir(maintainerOwnsAllDeps)

  fund([], (err) => {
    t.ifError(err, 'should not error out')
    t.matchSnapshot(result, 'should print stack packages together')
    result = ''
    t.end()
  })
})

test('fund in which same maintainer owns all its deps, using --json option', t => {
  _flatOptions.json = true
  _flatOptions.prefix = t.testdir(maintainerOwnsAllDeps)

  fund([], (err) => {
    t.ifError(err, 'should not error out')
    t.deepEqual(
      JSON.parse(result),
      {
        length: 3,
        name: 'maintainer-owns-all-deps',
        version: '1.0.0',
        funding: { type: 'individual', url: 'http://example.com/donate' },
        dependencies: {
          'dep-bar': {
            version: '1.0.0',
            funding: { type: 'individual', url: 'http://example.com/donate' }
          },
          'dep-foo': {
            version: '1.0.0',
            funding: { type: 'individual', url: 'http://example.com/donate' },
            dependencies: {
              'dep-sub-foo': {
                version: '1.0.0',
                funding: { type: 'individual', url: 'http://example.com/donate' }
              }
            }
          }
        }
      },
      'should print stack packages together'
    )

    result = ''
    _flatOptions.json = false
    t.end()
  })
})

test('fund containing multi-level nested deps with no funding', t => {
  _flatOptions.prefix = t.testdir(nestedNoFundingPackages)

  fund([], (err) => {
    t.ifError(err, 'should not error out')
    t.matchSnapshot(
      result,
      'should omit dependencies with no funding declared'
    )

    result = ''
    t.end()
  })
})

test('fund containing multi-level nested deps with no funding, using --json option', t => {
  _flatOptions.prefix = t.testdir(nestedNoFundingPackages)
  _flatOptions.json = true

  fund([], (err) => {
    t.ifError(err, 'should not error out')
    t.deepEqual(
      JSON.parse(result),
      {
        length: 2,
        name: 'nested-no-funding-packages',
        version: '1.0.0',
        dependencies: {
          lorem: {
            version: '1.0.0',
            funding: { url: 'https://example.com/lorem' }
          },
          bar: {
            version: '1.0.0',
            funding: { type: 'individual', url: 'http://example.com/donate' }
          }
        }
      },
      'should omit dependencies with no funding declared in json output'
    )

    result = ''
    _flatOptions.json = false
    t.end()
  })
})

test('fund containing multi-level nested deps with no funding, using --json option', t => {
  _flatOptions.prefix = t.testdir(nestedMultipleFundingPackages)
  _flatOptions.json = true

  fund([], (err) => {
    t.ifError(err, 'should not error out')
    t.deepEqual(
      JSON.parse(result),
      {
        length: 2,
        name: 'nested-multiple-funding-packages',
        version: '1.0.0',
        funding: [
          {
            url: 'https://one.example.com'
          },
          {
            url: 'https://two.example.com'
          }
        ],
        dependencies: {
          bar: {
            version: '1.0.0',
            funding: [
              {
                url: 'http://collective.example.com'
              },
              {
                url: 'http://sponsors.example.com/you'
              }
            ]
          },
          foo: {
            version: '1.0.0',
            funding: [
              {
                url: 'http://example.com'
              },
              {
                url: 'http://sponsors.example.com/me'
              },
              {
                url: 'http://collective.example.com'
              }
            ]
          }
        }
      },
      'should list multiple funding entries in json output'
    )

    result = ''
    _flatOptions.json = false
    t.end()
  })
})

test('fund does not support global', t => {
  _flatOptions.prefix = t.testdir({})
  _flatOptions.global = true

  fund([], (err) => {
    t.match(err.code, 'EFUNDGLOBAL', 'should throw EFUNDGLOBAL error')

    result = ''
    _flatOptions.global = false
    t.end()
  })
})

test('fund using package argument', t => {
  _flatOptions.prefix = t.testdir(maintainerOwnsAllDeps)

  fund(['.'], (err) => {
    t.ifError(err, 'should not error out')
    t.matchSnapshot(printUrl, 'should open funding url')

    printUrl = ''
    t.end()
  })
})

test('fund does not support global, using --json option', t => {
  _flatOptions.prefix = t.testdir({})
  _flatOptions.global = true
  _flatOptions.json = true

  fund([], (err) => {
    t.equal(err.code, 'EFUNDGLOBAL', 'should use EFUNDGLOBAL error code')
    t.equal(
      err.message,
      '`npm fund` does not support global packages',
      'should use expected error msg'
    )

    _flatOptions.global = false
    _flatOptions.json = false
    t.end()
  })
})

test('fund using string shorthand', t => {
  _flatOptions.prefix = t.testdir({
    'package.json': JSON.stringify({
      name: 'funding-string-shorthand',
      version: '0.0.0',
      funding: 'https://example.com/sponsor'
    })
  })

  fund(['.'], (err) => {
    t.ifError(err, 'should not error out')
    t.matchSnapshot(printUrl, 'should open string-only url')

    printUrl = ''
    t.end()
  })
})

test('fund using nested packages with multiple sources', t => {
  _flatOptions.prefix = t.testdir(nestedMultipleFundingPackages)

  fund(['.'], (err) => {
    t.ifError(err, 'should not error out')
    t.matchSnapshot(result, 'should prompt with all available URLs')

    result = ''
    t.end()
  })
})

test('fund using symlink ref', t => {
  _flatOptions.prefix = t.testdir({
    'package.json': JSON.stringify({
      name: 'using-symlink-ref',
      version: '1.0.0'
    }),
    a: {
      'package.json': JSON.stringify({
        name: 'a',
        version: '1.0.0',
        funding: 'http://example.com/a'
      })
    },
    node_modules: {
      a: t.fixture('symlink', '../a')
    }
  })

  // using symlinked ref
  fund(['./node_modules/a'], (err) => {
    t.ifError(err, 'should not error out')
    t.match(
      printUrl,
      'http://example.com/a',
      'should retrieve funding url from symlink'
    )

    printUrl = ''

    // using target ref
    fund(['./a'], (err) => {
      t.ifError(err, 'should not error out')

      t.match(
        printUrl,
        'http://example.com/a',
        'should retrieve funding url from symlink target'
      )

      printUrl = ''
      result = ''
      t.end()
    })
  })
})

test('fund using data from actual tree', t => {
  _flatOptions.prefix = t.testdir({
    'package.json': JSON.stringify({
      name: 'using-actual-tree',
      version: '1.0.0'
    }),
    node_modules: {
      a: {
        'package.json': JSON.stringify({
          name: 'a',
          version: '1.0.0',
          funding: 'http://example.com/a'
        })
      },
      b: {
        'package.json': JSON.stringify({
          name: 'a',
          version: '1.0.0',
          funding: 'http://example.com/b'
        }),
        node_modules: {
          a: {
            'package.json': JSON.stringify({
              name: 'a',
              version: '1.0.1',
              funding: 'http://example.com/_AAA'
            })
          }
        }
      }
    }
  })

  // using symlinked ref
  fund(['a'], (err) => {
    t.ifError(err, 'should not error out')
    t.match(
      printUrl,
      'http://example.com/_AAA',
      'should retrieve fund info from actual tree, using greatest version found'
    )

    printUrl = ''
    t.end()
  })
})

test('fund using nested packages with multiple sources, with a source number', t => {
  _flatOptions.prefix = t.testdir(nestedMultipleFundingPackages)
  _flatOptions.which = '1'

  fund(['.'], (err) => {
    t.ifError(err, 'should not error out')
    t.matchSnapshot(printUrl, 'should open the numbered URL')

    _flatOptions.which = undefined
    printUrl = ''
    t.end()
  })
})

test('fund using pkg name while having conflicting versions', t => {
  _flatOptions.prefix = t.testdir(conflictingFundingPackages)
  _flatOptions.which = '1'

  fund(['foo'], (err) => {
    t.ifError(err, 'should not error out')
    t.matchSnapshot(printUrl, 'should open greatest version')

    printUrl = ''
    t.end()
  })
})

test('fund using package argument with no browser, using --json option', t => {
  _flatOptions.prefix = t.testdir(maintainerOwnsAllDeps)
  _flatOptions.json = true

  fund(['.'], (err) => {
    t.ifError(err, 'should not error out')
    t.deepEqual(
      JSON.parse(printUrl),
      {
        title: 'individual funding available at the following URL',
        url: 'http://example.com/donate'
      },
      'should open funding url using json output'
    )

    _flatOptions.json = false
    printUrl = ''
    t.end()
  })
})

test('fund using package info fetch from registry', t => {
  _flatOptions.prefix = t.testdir({})

  fund(['ntl'], (err) => {
    t.ifError(err, 'should not error out')
    t.match(
      printUrl,
      /http:\/\/example.com\/pacote/,
      'should open funding url that was loaded from registry manifest'
    )

    printUrl = ''
    t.end()
  })
})

test('fund tries to use package info fetch from registry but registry has nothing', t => {
  _flatOptions.prefix = t.testdir({})

  fund(['foo'], (err) => {
    t.equal(err.code, 'ENOFUND', 'should have ENOFUND error code')
    t.equal(
      err.message,
      'No valid funding method available for: foo',
      'should have no valid funding message'
    )

    printUrl = ''
    t.end()
  })
})

test('fund but target module has no funding info', t => {
  _flatOptions.prefix = t.testdir(nestedNoFundingPackages)

  fund(['foo'], (err) => {
    t.equal(err.code, 'ENOFUND', 'should have ENOFUND error code')
    t.equal(
      err.message,
      'No valid funding method available for: foo',
      'should have no valid funding message'
    )

    result = ''
    t.end()
  })
})

test('fund using bad which value', t => {
  _flatOptions.prefix = t.testdir(nestedMultipleFundingPackages)
  _flatOptions.which = 3

  fund(['bar'], (err) => {
    t.equal(err.code, 'EFUNDNUMBER', 'should have EFUNDNUMBER error code')
    t.equal(
      err.message,
      '`npm fund [<@scope>/]<pkg> [--which=fundingSourceNumber]` must be given a positive integer',
      'should have bad which option error message'
    )

    _flatOptions.which = undefined
    result = ''
    t.end()
  })
})

test('fund pkg missing version number', t => {
  _flatOptions.prefix = t.testdir({
    'package.json': JSON.stringify({
      name: 'foo',
      funding: 'http://example.com/foo'
    })
  })

  fund([], (err) => {
    t.ifError(err, 'should not error out')
    t.matchSnapshot(result, 'should print name only')
    result = ''
    t.end()
  })
})

test('fund a package throws on openUrl', t => {
  _flatOptions.prefix = t.testdir({
    'package.json': JSON.stringify({
      name: 'foo',
      version: '1.0.0',
      funding: 'http://npmjs.org'
    })
  })

  fund(['.'], (err) => {
    t.equal(err.message, 'ERROR', 'should throw unknown error')
    result = ''
    t.end()
  })
})

test('fund a package with type and multiple sources', t => {
  _flatOptions.prefix = t.testdir({
    'package.json': JSON.stringify({
      name: 'foo',
      funding: [
        {
          type: 'Foo',
          url: 'http://example.com/foo'
        },
        {
          type: 'Lorem',
          url: 'http://example.com/foo-lorem'
        }
      ]
    })
  })

  fund(['.'], (err) => {
    t.ifError(err, 'should not error out')
    t.matchSnapshot(result, 'should print prompt select message')

    result = ''
    t.end()
  })
})

test('fund colors', t => {
  _flatOptions.prefix = t.testdir({
    'package.json': JSON.stringify({
      name: 'test-fund-colors',
      version: '1.0.0',
      dependencies: {
        a: '^1.0.0',
        b: '^1.0.0',
        c: '^1.0.0'
      }
    }),
    node_modules: {
      a: {
        'package.json': JSON.stringify({
          name: 'a',
          version: '1.0.0',
          funding: 'http://example.com/a'
        })
      },
      b: {
        'package.json': JSON.stringify({
          name: 'b',
          version: '1.0.0',
          funding: 'http://example.com/b',
          dependencies: {
            d: '^1.0.0',
            e: '^1.0.0'
          }
        })
      },
      c: {
        'package.json': JSON.stringify({
          name: 'c',
          version: '1.0.0',
          funding: 'http://example.com/b'
        })
      },
      d: {
        'package.json': JSON.stringify({
          name: 'd',
          version: '1.0.0',
          funding: 'http://example.com/d'
        })
      },
      e: {
        'package.json': JSON.stringify({
          name: 'e',
          version: '1.0.0',
          funding: 'http://example.com/e'
        })
      }
    }
  })
  _flatOptions.color = true

  fund([], (err) => {
    t.ifError(err, 'should not error out')
    t.matchSnapshot(result, 'should print output with color info')

    result = ''
    _flatOptions.color = false
    t.end()
  })
})

test('sub dep with fund info and a parent with no funding info', t => {
  _flatOptions.prefix = t.testdir({
    'package.json': JSON.stringify({
      name: 'test-multiple-funding-sources',
      version: '1.0.0',
      dependencies: {
        a: '^1.0.0',
        b: '^1.0.0'
      }
    }),
    node_modules: {
      a: {
        'package.json': JSON.stringify({
          name: 'a',
          version: '1.0.0',
          dependencies: {
            c: '^1.0.0'
          }
        })
      },
      b: {
        'package.json': JSON.stringify({
          name: 'b',
          version: '1.0.0',
          funding: 'http://example.com/b'
        })
      },
      c: {
        'package.json': JSON.stringify({
          name: 'c',
          version: '1.0.0',
          funding: [
            'http://example.com/c',
            'http://example.com/c-other'
          ]
        })
      }
    }
  })

  fund([], (err) => {
    t.ifError(err, 'should not error out')
    t.matchSnapshot(result, 'should nest sub dep as child of root')

    result = ''
    t.end()
  })
})
