'use strict'

module.exports = function abstractTests (opts) {
  const builder = opts.builder
  const test = opts.test

  test('support on and emit', function (t) {
    t.plan(4)

    const e = builder()
    const expected = {
      topic: 'hello world',
      payload: { my: 'message' }
    }

    e.on('hello world', function (message, cb) {
      t.equal(e.current, 1, 'number of current messages')
      t.deepEqual(message, expected)
      t.equal(this, e)
      cb()
    }, function () {
      e.emit(expected, function () {
        e.close(function () {
          t.pass('closed')
        })
      })
    })
  })

  test('support multiple subscribers', function (t) {
    t.plan(3)

    const e = builder()
    const expected = {
      topic: 'hello world',
      payload: { my: 'message' }
    }

    e.on('hello world', function (message, cb) {
      t.ok(message, 'message received')
      cb()
    }, function () {
      e.on('hello world', function (message, cb) {
        t.ok(message, 'message received')
        cb()
      }, function () {
        e.emit(expected, function () {
          e.close(function () {
            t.pass('closed')
          })
        })
      })
    })
  })

  test('support multiple subscribers and unsubscribers', function (t) {
    t.plan(2)

    const e = builder()
    const expected = {
      topic: 'hello world',
      payload: { my: 'message' }
    }

    function first (message, cb) {
      t.fail('first listener should not receive any events')
      cb()
    }

    function second (message, cb) {
      t.ok(message, 'second listener must receive the message')
      cb()
      e.close(function () {
        t.pass('closed')
      })
    }

    e.on('hello world', first, function () {
      e.on('hello world', second, function () {
        e.removeListener('hello world', first, function () {
          e.emit(expected)
        })
      })
    })
  })

  test('removeListener', function (t) {
    t.plan(1)

    const e = builder()
    const expected = {
      topic: 'hello world',
      payload: { my: 'message' }
    }
    let toRemoveCalled = false

    function toRemove (message, cb) {
      toRemoveCalled = true
      cb()
    }

    e.on('hello world', function (message, cb) {
      cb()
    }, function () {
      e.on('hello world', toRemove, function () {
        e.removeListener('hello world', toRemove, function () {
          e.emit(expected, function () {
            e.close(function () {
              t.notOk(toRemoveCalled, 'the toRemove function must not be called')
            })
          })
        })
      })
    })
  })

  test('without a callback on emit and on', function (t) {
    t.plan(1)

    const e = builder()
    const expected = {
      topic: 'hello world',
      payload: { my: 'message' }
    }

    e.on('hello world', function (message, cb) {
      cb()
      e.close(function () {
        t.pass('closed')
      })
    })

    setTimeout(function () {
      e.emit(expected)
    }, 100)
  })

  test('without any listeners', function (t) {
    t.plan(2)

    const e = builder()
    const expected = {
      topic: 'hello world',
      payload: { my: 'message' }
    }

    e.emit(expected)
    t.equal(e.current, 0, 'reset the current messages trackers')
    e.close(function () {
      t.pass('closed')
    })
  })

  test('support one level wildcard', function (t) {
    t.plan(2)

    const e = builder()
    const expected = {
      topic: 'hello/world',
      payload: { my: 'message' }
    }

    e.on('hello/+', function (message, cb) {
      t.equal(message.topic, 'hello/world')
      cb()
    }, function () {
      // this will not be catched
      e.emit({ topic: 'hello/my/world' })

      // this will be catched
      e.emit(expected, function () {
        e.close(function () {
          t.pass('closed')
        })
      })
    })
  })

  test('support one level wildcard - not match empty words', function (t) {
    t.plan(2)

    const e = builder({ matchEmptyLevels: false })
    const expected = {
      topic: 'hello/dummy/world',
      payload: { my: 'message' }
    }

    e.on('hello/+/world', function (message, cb) {
      t.equal(message.topic, 'hello/dummy/world')
      cb()
    }, function () {
      // this will not be catched
      e.emit({ topic: 'hello//world' })

      // this will be catched
      e.emit(expected, function () {
        e.close(function () {
          t.pass('closed')
        })
      })
    })
  })

  test('support one level wildcard - match empty words', function (t) {
    t.plan(3)

    const e = builder({ matchEmptyLevels: true })

    e.on('hello/+/world', function (message, cb) {
      const topic = message.topic
      if (topic === 'hello//world' || topic === 'hello/dummy/world') {
        t.pass('received ' + topic)
      }
      cb()
    }, function () {
      // this will be catched
      e.emit({ topic: 'hello//world' })
      // this will be catched
      e.emit({ topic: 'hello/dummy/world' }, function () {
        e.close(function () {
          t.pass('closed')
        })
      })
    })
  })

  test('support one level wildcard - match empty words', function (t) {
    t.plan(2)

    const e = builder({ matchEmptyLevels: true })

    e.on('hello/+', function (message, cb) {
      t.equal(message.topic, 'hello/')
      cb()
    }, function () {
      // this will be catched
      e.emit({ topic: 'hello/' }, function () {
        e.close(function () {
          t.pass('closed')
        })
      })
    })
  })

  test('support one level wildcard - not match empty words', function (t) {
    t.plan(1)

    const e = builder({ matchEmptyLevels: false })

    e.on('hello/+', function (message, cb) {
      t.fail('should not catch')
      cb()
    }, function () {
      // this will not be catched
      e.emit({ topic: 'hello/' }, function () {
        e.close(function () {
          t.pass('closed')
        })
      })
    })
  })

  test('support changing one level wildcard', function (t) {
    t.plan(2)

    const e = builder({ wildcardOne: '~' })
    const expected = {
      topic: 'hello/world',
      payload: { my: 'message' }
    }

    e.on('hello/~', function (message, cb) {
      t.equal(message.topic, 'hello/world')
      cb()
    }, function () {
      e.emit(expected, function () {
        e.close(function () {
          t.pass('closed')
        })
      })
    })
  })

  test('support deep wildcard', function (t) {
    t.plan(2)

    const e = builder()
    const expected = {
      topic: 'hello/my/world',
      payload: { my: 'message' }
    }

    e.on('hello/#', function (message, cb) {
      t.equal(message.topic, 'hello/my/world')
      cb()
    }, function () {
      e.emit(expected, function () {
        e.close(function () {
          t.pass('closed')
        })
      })
    })
  })

  test('support deep wildcard without separator', function (t) {
    t.plan(2)

    const e = builder()
    const expected = {
      topic: 'hello',
      payload: { my: 'message' }
    }

    e.on('#', function (message, cb) {
      t.equal(message.topic, expected.topic)
      cb()
    }, function () {
      e.emit(expected, function () {
        e.close(function () {
          t.pass('closed')
        })
      })
    })
  })

  test('support deep wildcard - match empty words', function (t) {
    t.plan(2)

    const e = builder({ matchEmptyLevels: true })
    const expected = {
      topic: 'hello',
      payload: { my: 'message' }
    }

    const wrong = {
      topic: 'hellooo',
      payload: { my: 'message' }
    }

    e.on('hello/#', function (message, cb) {
      t.equal(message.topic, expected.topic)
      cb()
    }, function () {
      e.emit(wrong) // this should not be received
      e.emit(expected, function () {
        e.close(function () {
          t.pass('closed')
        })
      })
    })
  })

  test('support changing deep wildcard', function (t) {
    t.plan(2)

    const e = builder({ wildcardSome: '*' })
    const expected = {
      topic: 'hello/my/world',
      payload: { my: 'message' }
    }

    e.on('hello/*', function (message, cb) {
      t.equal(message.topic, 'hello/my/world')
      cb()
    }, function () {
      e.emit(expected, function () {
        e.close(function () {
          t.pass('closed')
        })
      })
    })
  })

  test('support changing the level separator', function (t) {
    t.plan(2)

    const e = builder({ separator: '~' })
    const expected = {
      topic: 'hello~world',
      payload: { my: 'message' }
    }

    e.on('hello~+', function (message, cb) {
      t.equal(message.topic, 'hello~world')
      cb()
    }, function () {
      e.emit(expected, function () {
        e.close(function () {
          t.pass('closed')
        })
      })
    })
  })

  test('close support', function (t) {
    const e = builder()
    let check = false

    t.notOk(e.closed, 'must have a false closed property')

    e.close(function () {
      t.ok(check, 'must delay the close callback')
      t.ok(e.closed, 'must have a true closed property')
      t.end()
    })

    check = true
  })

  test('emit after close errors', function (t) {
    const e = builder()

    e.close(function () {
      e.emit({ topic: 'hello' }, function (err) {
        t.ok(err, 'must return an error')
        t.end()
      })
    })
  })

  test('support multiple subscribers with wildcards', function (t) {
    const e = builder()
    const expected = {
      topic: 'hello/world',
      payload: { my: 'message' }
    }
    let firstCalled = false
    let secondCalled = false

    e.on('hello/#', function (message, cb) {
      t.notOk(firstCalled, 'first subscriber must only be called once')
      firstCalled = true
      cb()
    })

    e.on('hello/+', function (message, cb) {
      t.notOk(secondCalled, 'second subscriber must only be called once')
      secondCalled = true
      cb()
    }, function () {
      e.emit(expected, function () {
        e.close(function () {
          t.end()
        })
      })
    })
  })

  test('support multiple subscribers with wildcards (deep)', function (t) {
    const e = builder()
    const expected = {
      topic: 'hello/my/world',
      payload: { my: 'message' }
    }
    let firstCalled = false
    let secondCalled = false

    e.on('hello/#', function (message, cb) {
      t.notOk(firstCalled, 'first subscriber must only be called once')
      firstCalled = true
      cb()
    })

    e.on('hello/+/world', function (message, cb) {
      t.notOk(secondCalled, 'second subscriber must only be called once')
      secondCalled = true
      cb()
    }, function () {
      e.emit(expected, function () {
        e.close(function () {
          t.end()
        })
      })
    })
  })

  test('emit & receive buffers', function (t) {
    const e = builder()
    const msg = Buffer.from('hello')
    const expected = {
      topic: 'hello',
      payload: msg
    }

    e.on('hello', function (message, cb) {
      t.deepEqual(msg, message.payload)
      cb()
    }, function () {
      e.emit(expected, function () {
        e.close(function () {
          t.end()
        })
      })
    })
  })

  test('packets are emitted in order', function (t) {
    const e = builder()
    const total = 10000
    const topic = 'test'

    let received = 0

    e.on(topic, function (msg, cb) {
      let fail = false
      if (received !== msg.payload) {
        t.fail(`leak detected. Count: ${received} - Payload: ${msg.payload}`)
        fail = true
      }

      received++

      if (fail || received === total) {
        e.close(function () {
          t.end()
        })
      }
      cb()
    }, function () {
      for (let payload = 0; payload < total; payload++) {
        e.emit({ topic, payload })
      }
    })
  })

  test('calling emit without cb when closed doesn\'t throw error', function (t) {
    const e = builder()
    const msg = Buffer.from('hello')
    const expected = {
      topic: 'hello',
      payload: msg
    }

    e.close(function () {
      try {
        e.emit(expected)
      } catch (error) {
        t.error('throws error')
      }
      t.end()
    })
  })
}
