'use strict'
/* eslint-env mocha */

const chai = require('chai')
chai.use(require('dirty-chai'))
const { expect } = chai
const nock = require('nock')
const sinon = require('sinon')

const pDefer = require('p-defer')
const mergeOptions = require('merge-options')

const DelegatedPeerRouter = require('libp2p-delegated-peer-routing')

const peerUtils = require('../utils/creators/peer')
const { baseOptions, routingOptions } = require('./utils')

describe('peer-routing', () => {
  describe('no routers', () => {
    let node

    before(async () => {
      [node] = await peerUtils.createPeer({
        config: baseOptions
      })
    })

    it('.findPeer should return an error', async () => {
      await expect(node.peerRouting.findPeer('a cid'))
        .to.eventually.be.rejected()
        .and.to.have.property('code', 'NO_ROUTERS_AVAILABLE')
    })
  })

  describe('via dht router', () => {
    const number = 5
    let nodes

    before(async () => {
      nodes = await peerUtils.createPeer({
        number,
        config: routingOptions
      })

      // Ring dial
      await Promise.all(
        nodes.map((peer, i) => peer.dial(nodes[(i + 1) % number].peerId))
      )
    })

    after(() => {
      sinon.restore()
    })

    after(() => Promise.all(nodes.map((n) => n.stop())))

    it('should use the nodes dht', () => {
      const deferred = pDefer()

      sinon.stub(nodes[0]._dht, 'findPeer').callsFake(() => {
        deferred.resolve()
        return nodes[1].peerId
      })

      nodes[0].peerRouting.findPeer()
      return deferred.promise
    })
  })

  describe('via delegate router', () => {
    let node
    let delegate

    beforeEach(async () => {
      delegate = new DelegatedPeerRouter({
        host: '0.0.0.0',
        protocol: 'http',
        port: 60197
      })

      ;[node] = await peerUtils.createPeer({
        config: mergeOptions(baseOptions, {
          modules: {
            peerRouting: [delegate]
          },
          config: {
            dht: {
              enabled: false
            }
          }
        })
      })
    })

    afterEach(() => {
      nock.cleanAll()
      sinon.restore()
    })

    afterEach(() => node.stop())

    it('should use the delegate router to find peers', async () => {
      const deferred = pDefer()

      sinon.stub(delegate, 'findPeer').callsFake(() => {
        deferred.resolve()
        return 'fake peer-id'
      })

      await node.peerRouting.findPeer()
      return deferred.promise
    })

    it('should be able to find a peer', async () => {
      const peerKey = 'QmTp9VkYvnHyrqKQuFPiuZkiX9gPcqj6x5LJ1rmWuSySnL'
      const mockApi = nock('http://0.0.0.0:60197')
        .post('/api/v0/dht/findpeer')
        .query(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'
        ])

      const peer = await node.peerRouting.findPeer(peerKey)

      expect(peer.id).to.equal(peerKey)
      expect(mockApi.isDone()).to.equal(true)
    })

    it('should error when a peer cannot be found', async () => {
      const peerKey = 'key of a peer not on the network'
      const mockApi = nock('http://0.0.0.0:60197')
        .post('/api/v0/dht/findpeer')
        .query(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'
        ])

      await expect(node.peerRouting.findPeer(peerKey))
        .to.eventually.be.rejected()

      expect(mockApi.isDone()).to.equal(true)
    })

    it('should handle errors from the api', async () => {
      const peerKey = 'key of a peer not on the network'
      const mockApi = nock('http://0.0.0.0:60197')
        .post('/api/v0/dht/findpeer')
        .query(true)
        .reply(502)

      await expect(node.peerRouting.findPeer(peerKey))
        .to.eventually.be.rejected()

      expect(mockApi.isDone()).to.equal(true)
    })
  })

  describe('via dht and delegate routers', () => {
    let node
    let delegate

    beforeEach(async () => {
      delegate = new DelegatedPeerRouter({
        host: '0.0.0.0',
        protocol: 'http',
        port: 60197
      })

      ;[node] = await peerUtils.createPeer({
        config: mergeOptions(routingOptions, {
          modules: {
            peerRouting: [delegate]
          }
        })
      })
    })

    afterEach(() => {
      sinon.restore()
    })

    afterEach(() => node.stop())

    it('should only use the dht if it finds the peer', async () => {
      const dhtDeferred = pDefer()

      sinon.stub(node._dht, 'findPeer').callsFake(() => {
        dhtDeferred.resolve()
        return { id: node.peerId }
      })
      sinon.stub(delegate, 'findPeer').callsFake(() => {
        throw new Error('the delegate should not have been called')
      })

      await node.peerRouting.findPeer('a peer id')
      await dhtDeferred.promise
    })

    it('should use the delegate if the dht fails to find the peer', async () => {
      const results = [true]

      sinon.stub(node._dht, 'findPeer').callsFake(() => {})
      sinon.stub(delegate, 'findPeer').callsFake(() => {
        return results
      })

      const peer = await node.peerRouting.findPeer('a peer id')
      expect(peer).to.eql(results)
    })
  })
})