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

const chai = require('chai')
chai.use(require('dirty-chai'))
const expect = chai.expect
const parallel = require('async/parallel')
const series = require('async/series')
const signalling = require('libp2p-webrtc-star/src/sig-server')
const rendezvous = require('libp2p-websocket-star-rendezvous')
const TCP = require('libp2p-tcp')
const WS = require('libp2p-websockets')
const WSStar = require('libp2p-websocket-star')
const WRTCStar = require('libp2p-webrtc-star')
const wrtc = require('wrtc')

const createNode = require('./utils/create-node.js')
const tryEcho = require('./utils/try-echo')
const echo = require('./utils/echo')

describe('transports', () => {
  describe('TCP only', () => {
    let nodeA
    let nodeB

    before((done) => {
      parallel([
        (cb) => createNode('/ip4/0.0.0.0/tcp/0', (err, node) => {
          expect(err).to.not.exist()
          nodeA = node
          node.handle('/echo/1.0.0', echo)
          node.start(cb)
        }),
        (cb) => createNode('/ip4/0.0.0.0/tcp/0', (err, node) => {
          expect(err).to.not.exist()
          nodeB = node
          node.handle('/echo/1.0.0', echo)
          node.start(cb)
        })
      ], done)
    })

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

    it('nodeA.dial nodeB using PeerInfo without proto (warmup)', (done) => {
      nodeA.dial(nodeB.peerInfo, (err) => {
        expect(err).to.not.exist()

        // Some time for Identify to finish
        setTimeout(check, 500)

        function check () {
          parallel([
            (cb) => {
              const peers = nodeA.peerBook.getAll()
              expect(err).to.not.exist()
              expect(Object.keys(peers)).to.have.length(1)
              cb()
            },
            (cb) => {
              const peers = nodeB.peerBook.getAll()
              expect(err).to.not.exist()
              expect(Object.keys(peers)).to.have.length(1)
              cb()
            }
          ], done)
        }
      })
    })

    it('nodeA.dial nodeB using PeerInfo', (done) => {
      nodeA.dialProtocol(nodeB.peerInfo, '/echo/1.0.0', (err, conn) => {
        expect(err).to.not.exist()

        tryEcho(conn, done)
      })
    })

    it('nodeA.hangUp nodeB using PeerInfo (first)', (done) => {
      nodeA.hangUp(nodeB.peerInfo, (err) => {
        expect(err).to.not.exist()
        setTimeout(check, 500)

        function check () {
          parallel([
            (cb) => {
              const peers = nodeA.peerBook.getAll()
              expect(Object.keys(peers)).to.have.length(1)
              expect(nodeA._switch.connection.getAll()).to.have.length(0)
              cb()
            },
            (cb) => {
              const peers = nodeB.peerBook.getAll()
              expect(Object.keys(peers)).to.have.length(1)
              expect(nodeB._switch.connection.getAll()).to.have.length(0)
              cb()
            }
          ], done)
        }
      })
    })

    it('nodeA.dialProtocol nodeB using multiaddr', (done) => {
      nodeA.dialProtocol(nodeB.peerInfo.multiaddrs.toArray()[0], '/echo/1.0.0', (err, conn) => {
        // Some time for Identify to finish
        setTimeout(check, 500)

        function check () {
          expect(err).to.not.exist()
          series([
            (cb) => {
              const peers = nodeA.peerBook.getAll()
              expect(Object.keys(peers)).to.have.length(1)
              expect(nodeA._switch.connection.getAll()).to.have.length(1)
              cb()
            },
            (cb) => {
              const peers = nodeB.peerBook.getAll()
              expect(Object.keys(peers)).to.have.length(1)
              expect(nodeA._switch.connection.getAll()).to.have.length(1)
              cb()
            }
          ], () => tryEcho(conn, done))
        }
      })
    })

    it('nodeA.hangUp nodeB using multiaddr (second)', (done) => {
      nodeA.hangUp(nodeB.peerInfo.multiaddrs.toArray()[0], (err) => {
        expect(err).to.not.exist()
        setTimeout(check, 500)

        function check () {
          parallel([
            (cb) => {
              const peers = nodeA.peerBook.getAll()
              expect(Object.keys(peers)).to.have.length(1)
              expect(nodeA._switch.connection.getAll()).to.have.length(0)
              cb()
            },
            (cb) => {
              const peers = nodeB.peerBook.getAll()
              expect(Object.keys(peers)).to.have.length(1)
              expect(nodeB._switch.connection.getAll()).to.have.length(0)
              cb()
            }
          ], done)
        }
      })
    })

    it('nodeA.dialProtocol nodeB using PeerId', (done) => {
      nodeA.dialProtocol(nodeB.peerInfo.id, '/echo/1.0.0', (err, conn) => {
        // Some time for Identify to finish
        setTimeout(check, 500)

        function check () {
          expect(err).to.not.exist()
          series([
            (cb) => {
              const peers = nodeA.peerBook.getAll()
              expect(Object.keys(peers)).to.have.length(1)
              expect(nodeA._switch.connection.getAll()).to.have.length(1)
              cb()
            },
            (cb) => {
              const peers = nodeB.peerBook.getAll()
              expect(Object.keys(peers)).to.have.length(1)
              expect(nodeA._switch.connection.getAll()).to.have.length(1)
              cb()
            }
          ], () => tryEcho(conn, done))
        }
      })
    })

    it('nodeA.hangUp nodeB using PeerId (third)', (done) => {
      nodeA.hangUp(nodeB.peerInfo.id, (err) => {
        expect(err).to.not.exist()
        setTimeout(check, 500)

        function check () {
          parallel([
            (cb) => {
              const peers = nodeA.peerBook.getAll()
              expect(Object.keys(peers)).to.have.length(1)
              expect(nodeA._switch.connection.getAll()).to.have.length(0)
              cb()
            },
            (cb) => {
              const peers = nodeB.peerBook.getAll()
              expect(Object.keys(peers)).to.have.length(1)
              expect(nodeB._switch.connection.getAll()).to.have.length(0)
              cb()
            }
          ], done)
        }
      })
    })

    it('.dialFSM check conn and close', (done) => {
      nodeA.dialFSM(nodeB.peerInfo, (err, connFSM) => {
        expect(err).to.not.exist()

        connFSM.once('muxed', () => {
          expect(
            nodeA._switch.connection.getAllById(nodeB.peerInfo.id.toB58String())
          ).to.have.length(1)

          connFSM.once('error', done)
          connFSM.once('close', () => {
            // ensure the connection is closed
            expect(
              nodeA._switch.connection.getAllById(nodeB.peerInfo.id.toB58String())
            ).to.have.length(0)
            done()
          })

          connFSM.close()
        })
      })
    })

    it('.dialFSM with a protocol, do an echo and close', (done) => {
      nodeA.dialFSM(nodeB.peerInfo, '/echo/1.0.0', (err, connFSM) => {
        expect(err).to.not.exist()
        connFSM.once('connection', (conn) => {
          expect(
            nodeA._switch.connection.getAllById(nodeB.peerInfo.id.toB58String())
          ).to.have.length(1)
          tryEcho(conn, () => {
            connFSM.close()
          })
        })
        connFSM.once('error', done)
        connFSM.once('close', () => {
          // ensure the connection is closed
          expect(
            nodeA._switch.connection.getAllById(nodeB.peerInfo.id.toB58String())
          ).to.have.length(0)
          done()
        })
      })
    })
  })

  describe('TCP + WebSockets', () => {
    let nodeTCP
    let nodeTCPnWS
    let nodeWS

    before((done) => {
      parallel([
        (cb) => createNode([
          '/ip4/0.0.0.0/tcp/0'
        ], (err, node) => {
          expect(err).to.not.exist()
          nodeTCP = node
          node.handle('/echo/1.0.0', echo)
          node.start(cb)
        }),
        (cb) => createNode([
          '/ip4/0.0.0.0/tcp/0',
          '/ip4/127.0.0.1/tcp/25011/ws'
        ], (err, node) => {
          expect(err).to.not.exist()
          nodeTCPnWS = node
          node.handle('/echo/1.0.0', echo)
          node.start(cb)
        }),
        (cb) => createNode([
          '/ip4/127.0.0.1/tcp/25022/ws'
        ], (err, node) => {
          expect(err).to.not.exist()
          nodeWS = node
          node.handle('/echo/1.0.0', echo)
          node.start(cb)
        })
      ], done)
    })

    after((done) => {
      parallel([
        (cb) => nodeTCP.stop(cb),
        (cb) => nodeTCPnWS.stop(cb),
        (cb) => nodeWS.stop(cb)
      ], done)
    })

    it('nodeTCP.dial nodeTCPnWS using PeerInfo', (done) => {
      nodeTCP.dial(nodeTCPnWS.peerInfo, (err) => {
        expect(err).to.not.exist()

        // Some time for Identify to finish
        setTimeout(check, 500)

        function check () {
          parallel([
            (cb) => {
              const peers = nodeTCP.peerBook.getAll()
              expect(Object.keys(peers)).to.have.length(1)
              expect(nodeTCP._switch.connection.getAll()).to.have.length(1)
              cb()
            },
            (cb) => {
              const peers = nodeTCPnWS.peerBook.getAll()
              expect(Object.keys(peers)).to.have.length(1)
              expect(nodeTCPnWS._switch.connection.getAll()).to.have.length(1)
              cb()
            }
          ], done)
        }
      })
    })

    it('nodeTCP.hangUp nodeTCPnWS using PeerInfo', (done) => {
      nodeTCP.hangUp(nodeTCPnWS.peerInfo, (err) => {
        expect(err).to.not.exist()
        setTimeout(check, 500)

        function check () {
          parallel([
            (cb) => {
              const peers = nodeTCP.peerBook.getAll()
              expect(Object.keys(peers)).to.have.length(1)
              expect(nodeTCP._switch.connection.getAll()).to.have.length(0)
              cb()
            },
            (cb) => {
              const peers = nodeTCPnWS.peerBook.getAll()
              expect(Object.keys(peers)).to.have.length(1)
              expect(nodeTCPnWS._switch.connection.getAll()).to.have.length(0)
              cb()
            }
          ], done)
        }
      })
    })

    it('nodeTCPnWS.dial nodeWS using PeerInfo', (done) => {
      nodeTCPnWS.dial(nodeWS.peerInfo, (err) => {
        expect(err).to.not.exist()

        // Some time for Identify to finish
        setTimeout(check, 500)

        function check () {
          parallel([
            (cb) => {
              const peers = nodeTCPnWS.peerBook.getAll()
              expect(Object.keys(peers)).to.have.length(2)
              expect(nodeTCPnWS._switch.connection.getAll()).to.have.length(1)
              cb()
            },
            (cb) => {
              const peers = nodeWS.peerBook.getAll()
              expect(Object.keys(peers)).to.have.length(1)
              expect(nodeWS._switch.connection.getAll()).to.have.length(1)
              cb()
            }
          ], done)
        }
      })
    })

    it('nodeTCPnWS.hangUp nodeWS using PeerInfo', (done) => {
      nodeTCPnWS.hangUp(nodeWS.peerInfo, (err) => {
        expect(err).to.not.exist()
        setTimeout(check, 500)

        function check () {
          parallel([
            (cb) => {
              const peers = nodeTCPnWS.peerBook.getAll()
              expect(Object.keys(peers)).to.have.length(2)
              expect(nodeTCPnWS._switch.connection.getAll()).to.have.length(0)

              cb()
            },
            (cb) => {
              const peers = nodeWS.peerBook.getAll()
              expect(Object.keys(peers)).to.have.length(1)
              expect(nodeWS._switch.connection.getAll()).to.have.length(0)
              cb()
            }
          ], done)
        }
      })
    })

    // Until https://github.com/libp2p/js-libp2p/issues/46 is resolved
    // Everynode will be able to dial in WebSockets
    it.skip('nodeTCP.dial nodeWS using PeerInfo is unsuccesful', (done) => {
      nodeTCP.dial(nodeWS.peerInfo, (err) => {
        expect(err).to.exist()
        done()
      })
    })
  })

  describe('TCP + WebSockets + WebRTCStar', () => {
    let nodeAll
    let nodeTCP
    let nodeWS
    let nodeWebRTCStar

    let ss

    before(function (done) {
      this.timeout(5 * 1000)

      parallel([
        (cb) => signalling.start({ port: 24642 }, (err, server) => {
          expect(err).to.not.exist()
          ss = server
          cb()
        }),
        (cb) => {
          const wstar = new WRTCStar({ wrtc: wrtc })

          createNode([
            '/ip4/0.0.0.0/tcp/0',
            '/ip4/127.0.0.1/tcp/25011/ws',
            '/ip4/127.0.0.1/tcp/24642/ws/p2p-webrtc-star'
          ], {
            modules: {
              transport: [
                TCP,
                WS,
                wstar
              ],
              peerDiscovery: [wstar.discovery]
            },
            config: {
              peerDiscovery: {
                [wstar.discovery.tag]: {
                  enabled: true
                }
              }
            }
          }, (err, node) => {
            expect(err).to.not.exist()
            nodeAll = node
            node.handle('/echo/1.0.0', echo)
            node.start(cb)
          })
        },
        (cb) => createNode([
          '/ip4/0.0.0.0/tcp/0'
        ], (err, node) => {
          expect(err).to.not.exist()
          nodeTCP = node
          node.handle('/echo/1.0.0', echo)
          node.start(cb)
        }),
        (cb) => createNode([
          '/ip4/127.0.0.1/tcp/25022/ws'
        ], (err, node) => {
          expect(err).to.not.exist()
          nodeWS = node
          node.handle('/echo/1.0.0', echo)
          node.start(cb)
        }),

        (cb) => {
          const wstar = new WRTCStar({ wrtc: wrtc })

          createNode([
            '/ip4/127.0.0.1/tcp/24642/ws/p2p-webrtc-star'
          ], {
            modules: {
              transport: [wstar],
              peerDiscovery: [wstar.discovery]
            },
            config: {
              peerDiscovery: {
                [wstar.discovery.tag]: {
                  enabled: true
                }
              }
            }
          }, (err, node) => {
            expect(err).to.not.exist()
            nodeWebRTCStar = node
            node.handle('/echo/1.0.0', echo)
            node.start(cb)
          })
        }
      ], done)
    })

    after(function (done) {
      this.timeout(10 * 1000)

      parallel([
        (cb) => nodeAll.stop(cb),
        (cb) => nodeTCP.stop(cb),
        (cb) => nodeWS.stop(cb),
        (cb) => nodeWebRTCStar.stop(cb),
        (cb) => ss.stop(cb)
      ], done)
    })

    function check (otherNode, muxed, peers, callback) {
      let i = 1;
      [nodeAll, otherNode].forEach((node) => {
        expect(Object.keys(node.peerBook.getAll())).to.have.length(i-- ? peers : 1)
        expect(node._switch.connection.getAll()).to.have.length(muxed)
      })
      callback()
    }

    it('nodeAll.dial nodeTCP using PeerInfo', (done) => {
      nodeAll.dial(nodeTCP.peerInfo, (err) => {
        expect(err).to.not.exist()
        // Some time for Identify to finish
        setTimeout(() => check(nodeTCP, 1, 1, done), 500)
      })
    })

    it('nodeAll.hangUp nodeTCP using PeerInfo', (done) => {
      nodeAll.hangUp(nodeTCP.peerInfo, (err) => {
        expect(err).to.not.exist()
        // Some time for Identify to finish
        setTimeout(() => check(nodeTCP, 0, 1, done), 500)
      })
    })

    it('nodeAll.dial nodeWS using PeerInfo', (done) => {
      nodeAll.dial(nodeWS.peerInfo, (err) => {
        expect(err).to.not.exist()
        // Some time for Identify to finish
        setTimeout(() => check(nodeWS, 1, 2, done), 500)
      })
    })

    it('nodeAll.hangUp nodeWS using PeerInfo', (done) => {
      nodeAll.hangUp(nodeWS.peerInfo, (err) => {
        expect(err).to.not.exist()
        // Some time for Identify to finish
        setTimeout(() => check(nodeWS, 0, 2, done), 500)
      })
    })

    it('nodeAll.dial nodeWebRTCStar using PeerInfo', function (done) {
      this.timeout(40 * 1000)

      nodeAll.dial(nodeWebRTCStar.peerInfo, (err) => {
        expect(err).to.not.exist()
        // Some time for Identify to finish
        setTimeout(() => check(nodeWebRTCStar, 1, 3, done), 500)
      })
    })

    it('nodeAll.hangUp nodeWebRTCStar using PeerInfo', (done) => {
      nodeAll.hangUp(nodeWebRTCStar.peerInfo, (err) => {
        expect(err).to.not.exist()
        setTimeout(() => check(nodeWebRTCStar, 0, 3, done), 500)
      })
    })
  })

  describe('TCP + WebSockets + WebSocketStar', () => {
    let nodeAll
    let nodeTCP
    let nodeWS
    let nodeWebSocketStar

    let ss

    before((done) => {
      parallel([
        (cb) => {
          rendezvous.start({ port: 24642 }, (err, server) => {
            expect(err).to.not.exist()
            ss = server
            cb()
          })
        },
        (cb) => {
          const wstar = new WSStar()

          createNode([
            '/ip4/0.0.0.0/tcp/0',
            '/ip4/127.0.0.1/tcp/25011/ws',
            '/ip4/127.0.0.1/tcp/24642/ws/p2p-websocket-star'
          ], {
            modules: {
              transport: [
                TCP,
                WS,
                wstar
              ],
              peerDiscovery: [wstar.discovery]
            },
            config: {
              peerDiscovery: {
                [wstar.discovery.tag]: {
                  enabled: true
                }
              }
            }
          }, (err, node) => {
            expect(err).to.not.exist()
            nodeAll = node
            wstar.lazySetId(node.peerInfo.id)
            node.handle('/echo/1.0.0', echo)
            node.start(cb)
          })
        },
        (cb) => createNode([
          '/ip4/0.0.0.0/tcp/0'
        ], (err, node) => {
          expect(err).to.not.exist()
          nodeTCP = node
          node.handle('/echo/1.0.0', echo)
          node.start(cb)
        }),
        (cb) => createNode([
          '/ip4/127.0.0.1/tcp/25022/ws'
        ], (err, node) => {
          expect(err).to.not.exist()
          nodeWS = node
          node.handle('/echo/1.0.0', echo)
          node.start(cb)
        }),

        (cb) => {
          const wstar = new WSStar({})

          createNode([
            '/ip4/127.0.0.1/tcp/24642/ws/p2p-websocket-star'
          ], {
            modules: {
              transport: [wstar],
              peerDiscovery: [wstar.discovery]
            },
            config: {
              peerDiscovery: {
                [wstar.discovery.tag]: {
                  enabled: true
                }
              }
            }
          }, (err, node) => {
            expect(err).to.not.exist()
            nodeWebSocketStar = node
            wstar.lazySetId(node.peerInfo.id)
            node.handle('/echo/1.0.0', echo)
            node.start(cb)
          })
        }
      ], done)
    })

    after((done) => {
      parallel([
        (cb) => nodeAll.stop(cb),
        (cb) => nodeTCP.stop(cb),
        (cb) => nodeWS.stop(cb),
        (cb) => nodeWebSocketStar.stop(cb),
        (cb) => ss.stop(cb)
      ], done)
    })

    function check (otherNode, muxed, peers, done) {
      let i = 1;
      [nodeAll, otherNode].forEach((node) => {
        expect(Object.keys(node.peerBook.getAll())).to.have.length(i-- ? peers : 1)
        expect(node._switch.connection.getAll()).to.have.length(muxed)
      })
      done()
    }

    it('nodeAll.dial nodeTCP using PeerInfo', (done) => {
      nodeAll.dial(nodeTCP.peerInfo, (err) => {
        expect(err).to.not.exist()
        // Some time for Identify to finish
        setTimeout(() => check(nodeTCP, 1, 1, done), 500)
      })
    })

    it('nodeAll.hangUp nodeTCP using PeerInfo', (done) => {
      nodeAll.hangUp(nodeTCP.peerInfo, (err) => {
        expect(err).to.not.exist()
        // Some time for Identify to finish
        setTimeout(() => check(nodeTCP, 0, 1, done), 500)
      })
    })

    it('nodeAll.dial nodeWS using PeerInfo', (done) => {
      nodeAll.dial(nodeWS.peerInfo, (err) => {
        expect(err).to.not.exist()
        // Some time for Identify to finish
        setTimeout(() => check(nodeWS, 1, 2, done), 500)
      })
    })

    it('nodeAll.hangUp nodeWS using PeerInfo', (done) => {
      nodeAll.hangUp(nodeWS.peerInfo, (err) => {
        expect(err).to.not.exist()
        // Some time for Identify to finish
        setTimeout(() => check(nodeWS, 0, 2, done), 500)
      })
    })

    it('nodeAll.dial nodeWebSocketStar using PeerInfo', (done) => {
      nodeAll.dial(nodeWebSocketStar.peerInfo, (err) => {
        expect(err).to.not.exist()
        // Some time for Identify to finish
        setTimeout(() => check(nodeWebSocketStar, 1, 3, done), 500)
      })
    })

    it('nodeAll.hangUp nodeWebSocketStar using PeerInfo', (done) => {
      nodeAll.hangUp(nodeWebSocketStar.peerInfo, (err) => {
        expect(err).to.not.exist()
        // Some time for Identify to finish
        setTimeout(() => check(nodeWebSocketStar, 0, 3, done), 500)
      })
    })
  })
})