/* eslint-env mocha */
/* eslint max-nested-callbacks: ["error", 8] */

'use strict'

const chai = require('chai')
chai.use(require('dirty-chai'))
const expect = chai.expect
const parallel = require('async/parallel')
const _times = require('lodash.times')
const DelegatedPeerRouter = require('libp2p-delegated-peer-routing')
const sinon = require('sinon')
const nock = require('nock')

const createNode = require('./utils/create-node')

describe('.peerRouting', () => {
  describe('via the dht', () => {
    let nodeA
    let nodeB
    let nodeC
    let nodeD
    let nodeE

    before('create the outer ring of connections', (done) => {
      const tasks = _times(5, () => (cb) => {
        createNode('/ip4/0.0.0.0/tcp/0', (err, node) => {
          expect(err).to.not.exist()
          node.start((err) => cb(err, node))
        })
      })

      parallel(tasks, (err, nodes) => {
        expect(err).to.not.exist()
        nodeA = nodes[0]
        nodeB = nodes[1]
        nodeC = nodes[2]
        nodeD = nodes[3]
        nodeE = nodes[4]

        parallel([
          (cb) => nodeA.dial(nodeB.peerInfo, cb),
          (cb) => nodeB.dial(nodeC.peerInfo, cb),
          (cb) => nodeC.dial(nodeD.peerInfo, cb),
          (cb) => nodeD.dial(nodeE.peerInfo, cb),
          (cb) => nodeE.dial(nodeA.peerInfo, cb)
        ], (err) => {
          expect(err).to.not.exist()
          // Give the kbucket time to fill in the dht
          setTimeout(done, 250)
        })
      })
    })

    after((done) => {
      parallel([
        (cb) => nodeA.stop(cb),
        (cb) => nodeB.stop(cb),
        (cb) => nodeC.stop(cb),
        (cb) => nodeD.stop(cb),
        (cb) => nodeE.stop(cb)
      ], done)
    })

    it('should use the nodes dht', (done) => {
      const stub = sinon.stub(nodeA._dht, 'findPeer').callsFake(() => {
        stub.restore()
        done()
      })

      nodeA.peerRouting.findPeer()
    })

    describe('connected in an el ring', () => {
      it('should be able to find a peer we are not directly connected to', (done) => {
        parallel([
          (cb) => nodeA.dial(nodeC.peerInfo.id, cb),
          (cb) => nodeB.dial(nodeD.peerInfo.id, cb),
          (cb) => nodeC.dial(nodeE.peerInfo.id, cb)
        ], (err) => {
          if (err) throw err
          expect(err).to.not.exist()
          nodeB.peerRouting.findPeer(nodeE.peerInfo.id, (err, peerInfo) => {
            expect(err).to.not.exist()
            expect(nodeE.peerInfo.id.toB58String()).to.equal(peerInfo.id.toB58String())
            done()
          })
        })
      })
    })
  })

  describe('via a delegate', () => {
    let nodeA
    let delegate

    before((done) => {
      parallel([
        // Create the node using the delegate
        (cb) => {
          delegate = new DelegatedPeerRouter({
            host: 'ipfs.io',
            protocol: 'https',
            port: '443'
          })
          createNode('/ip4/0.0.0.0/tcp/0', {
            modules: {
              peerRouting: [ delegate ]
            },
            config: {
              dht: {
                enabled: false
              }
            }
          }, (err, node) => {
            expect(err).to.not.exist()
            nodeA = node
            nodeA.start(cb)
          })
        }
      ], done)
    })

    after((done) => nodeA.stop(done))
    afterEach(() => nock.cleanAll())

    it('should use the delegate router to find peers', (done) => {
      const stub = sinon.stub(delegate, 'findPeer').callsFake(() => {
        stub.restore()
        done()
      })
      nodeA.peerRouting.findPeer()
    })

    it('should be able to find a peer', (done) => {
      const peerKey = 'QmTp9VkYvnHyrqKQuFPiuZkiX9gPcqj6x5LJ1rmWuSySnL'
      const mockApi = nock('https://ipfs.io')
        .post('/api/v0/dht/findpeer')
        .query({
          arg: peerKey,
          timeout: '30000ms',
          'stream-channels': true
        })
        .reply(200, `{"Extra":"","ID":"some other id","Responses":null,"Type":0}\n{"Extra":"","ID":"","Responses":[{"Addrs":["/ip4/127.0.0.1/tcp/4001"],"ID":"${peerKey}"}],"Type":2}\n`, [
          'Content-Type', 'application/json',
          'X-Chunked-Output', '1'
        ])

      nodeA.peerRouting.findPeer(peerKey, (err, peerInfo) => {
        expect(err).to.not.exist()
        expect(peerInfo.id.toB58String()).to.equal(peerKey)
        expect(mockApi.isDone()).to.equal(true)
        done()
      })
    })

    it('should error when a peer cannot be found', (done) => {
      const peerKey = 'key of a peer not on the network'
      const mockApi = nock('https://ipfs.io')
        .post('/api/v0/dht/findpeer')
        .query({
          arg: peerKey,
          timeout: '30000ms',
          'stream-channels': true
        })
        .reply(200, `{"Extra":"","ID":"some other id","Responses":null,"Type":6}\n{"Extra":"","ID":"yet another id","Responses":null,"Type":0}\n{"Extra":"routing:not found","ID":"","Responses":null,"Type":3}\n`, [
          'Content-Type', 'application/json',
          'X-Chunked-Output', '1'
        ])

      nodeA.peerRouting.findPeer(peerKey, (err, peerInfo) => {
        expect(err).to.exist()
        expect(peerInfo).to.not.exist()
        expect(mockApi.isDone()).to.equal(true)
        done()
      })
    })

    it('should handle errors from the api', (done) => {
      const peerKey = 'key of a peer not on the network'
      const mockApi = nock('https://ipfs.io')
        .post('/api/v0/dht/findpeer')
        .query({
          arg: peerKey,
          timeout: '30000ms',
          'stream-channels': true
        })
        .reply(502)

      nodeA.peerRouting.findPeer(peerKey, (err, peerInfo) => {
        expect(err).to.exist()
        expect(peerInfo).to.not.exist()
        expect(mockApi.isDone()).to.equal(true)
        done()
      })
    })
  })

  describe('via the dht and a delegate', () => {
    let nodeA
    let delegate

    before((done) => {
      parallel([
        // Create the node using the delegate
        (cb) => {
          delegate = new DelegatedPeerRouter({
            host: 'ipfs.io',
            protocol: 'https',
            port: '443'
          })
          createNode('/ip4/0.0.0.0/tcp/0', {
            modules: {
              peerRouting: [ delegate ]
            }
          }, (err, node) => {
            expect(err).to.not.exist()
            nodeA = node
            nodeA.start(cb)
          })
        }
      ], done)
    })

    after((done) => nodeA.stop(done))

    describe('findPeer', () => {
      it('should only use the dht if it finds the peer', (done) => {
        const results = [true]
        const dhtStub = sinon.stub(nodeA._dht, 'findPeer').callsArgWith(2, null, results)
        const delegateStub = sinon.stub(delegate, 'findPeer').throws(() => {
          return new Error('the delegate should not have been called')
        })

        nodeA.peerRouting.findPeer('a peer id', (err, results) => {
          expect(err).to.not.exist()
          expect(results).to.equal(results)
          expect(dhtStub.calledOnce).to.equal(true)
          expect(delegateStub.notCalled).to.equal(true)
          delegateStub.restore()
          dhtStub.restore()
          done()
        })
      })

      it('should use the delegate if the dht fails to find the peer', (done) => {
        const results = [true]
        const dhtStub = sinon.stub(nodeA._dht, 'findPeer').callsArgWith(2, null, undefined)
        const delegateStub = sinon.stub(delegate, 'findPeer').callsArgWith(2, null, results)

        nodeA.peerRouting.findPeer('a peer id', (err, results) => {
          expect(err).to.not.exist()
          expect(results).to.deep.equal(results)
          expect(dhtStub.calledOnce).to.equal(true)
          expect(delegateStub.calledOnce).to.equal(true)
          delegateStub.restore()
          dhtStub.restore()
          done()
        })
      })
    })
  })

  describe('no routers', () => {
    let nodeA
    before((done) => {
      createNode('/ip4/0.0.0.0/tcp/0', {
        config: {
          dht: {
            enabled: false
          }
        }
      }, (err, node) => {
        expect(err).to.not.exist()
        nodeA = node
        done()
      })
    })

    it('.findPeer should return an error with no options', (done) => {
      nodeA.peerRouting.findPeer('a cid', (err) => {
        expect(err).to.exist()
        done()
      })
    })

    it('.findPeer should return an error with options', (done) => {
      nodeA.peerRouting.findPeer('a cid', { maxTimeout: 5000 }, (err) => {
        expect(err).to.exist()
        done()
      })
    })
  })
})
