'use strict'

const { test } = require('tap')
const http = require('http')
const ws = require('websocket-stream')
const mqtt = require('mqtt')
const { setup, connect, delay } = require('./helper')
const aedes = require('../')

;[{ ver: 3, id: 'MQIsdp' }, { ver: 4, id: 'MQTT' }].forEach(function (ele) {
  test('connect and connack (minimal)', function (t) {
    t.plan(2)

    const s = setup()
    t.teardown(s.broker.close.bind(s.broker))

    s.inStream.write({
      cmd: 'connect',
      protocolId: ele.id,
      protocolVersion: ele.ver,
      clean: true,
      clientId: 'my-client',
      keepalive: 0
    })

    s.outStream.on('data', function (packet) {
      t.same(packet, {
        cmd: 'connack',
        returnCode: 0,
        length: 2,
        qos: 0,
        retain: false,
        dup: false,
        topic: null,
        payload: null,
        sessionPresent: false
      }, 'successful connack')
      t.equal(s.client.version, ele.ver)
    })
  })
})

// [MQTT-3.1.2-2]
test('reject client requested for unacceptable protocol version', function (t) {
  t.plan(4)

  const broker = aedes()
  t.teardown(broker.close.bind(broker))

  const s = setup(broker)

  s.inStream.write({
    cmd: 'connect',
    protocolId: 'MQIsdp',
    protocolVersion: 5,
    clean: true,
    clientId: 'my-client',
    keepalive: 0
  })
  s.outStream.on('data', function (packet) {
    t.equal(packet.cmd, 'connack')
    t.equal(packet.returnCode, 1, 'unacceptable protocol version')
    t.equal(broker.connectedClients, 0)
  })
  broker.on('clientError', function (client, err) {
    t.fail('should not raise clientError error')
  })
  broker.on('connectionError', function (client, err) {
    t.equal(err.message, 'unacceptable protocol version')
  })
})

// [MQTT-3.1.2-1], Guarded in mqtt-packet
test('reject client requested for unsupported protocol version', function (t) {
  t.plan(3)

  const broker = aedes()
  t.teardown(broker.close.bind(broker))

  const s = setup(broker)

  s.inStream.write({
    cmd: 'connect',
    protocolId: 'MQTT',
    protocolVersion: 2,
    clean: true,
    clientId: 'my-client',
    keepalive: 0
  })
  s.outStream.on('data', function (packet) {
    t.fail('no data sent')
  })
  broker.on('connectionError', function (client, err) {
    t.equal(client.version, null)
    t.equal(err.message, 'Invalid protocol version')
    t.equal(broker.connectedClients, 0)
  })
})

// Guarded in mqtt-packet
test('reject clients with no clientId running on MQTT 3.1.0', function (t) {
  t.plan(3)

  const broker = aedes()
  t.teardown(broker.close.bind(broker))

  const s = setup(broker)

  s.inStream.write({
    cmd: 'connect',
    protocolId: 'MQIsdp',
    protocolVersion: 3,
    clean: true,
    keepalive: 0
  })
  s.outStream.on('data', function (packet) {
    t.fail('no data sent')
  })
  broker.on('connectionError', function (client, err) {
    t.equal(client.version, null)
    t.equal(err.message, 'clientId must be supplied before 3.1.1')
    t.equal(broker.connectedClients, 0)
  })
})

// [MQTT-3.1.3-7], Guarded in mqtt-packet
test('reject clients without clientid and clean=false on MQTT 3.1.1', function (t) {
  t.plan(2)

  const broker = aedes()
  t.teardown(broker.close.bind(broker))

  const s = setup(broker)

  s.inStream.write({
    cmd: 'connect',
    protocolId: 'MQTT',
    protocolVersion: 4,
    clean: false,
    clientId: '',
    keepalive: 0
  })
  s.outStream.on('data', function (packet) {
    t.fail('no data sent')
  })
  broker.on('connectionError', function (client, err) {
    t.equal(err.message, 'clientId must be given if cleanSession set to 0')
    t.equal(broker.connectedClients, 0)
  })
})

test('clients without clientid and clean=true on MQTT 3.1.1 will get a generated clientId', function (t) {
  t.plan(5)

  const broker = aedes()
  t.teardown(broker.close.bind(broker))

  const s = setup(broker)

  s.inStream.write({
    cmd: 'connect',
    protocolId: 'MQTT',
    protocolVersion: 4,
    clean: true,
    keepalive: 0
  })
  s.outStream.on('data', function (packet) {
    t.equal(packet.cmd, 'connack')
    t.equal(packet.returnCode, 0)
    t.equal(broker.connectedClients, 1)
    t.equal(s.client.version, 4)
  })
  broker.on('connectionError', function (client, err) {
    t.error(err, 'no error')
  })
  broker.on('client', function (client) {
    t.ok(client.id.startsWith('aedes_'))
  })
})

test('client connect error while fetching subscriptions', function (t) {
  t.plan(2)

  const broker = aedes()
  t.teardown(broker.close.bind(broker))

  const s = setup(broker)

  broker.persistence.subscriptionsByClient = function (c, cb) {
    cb(new Error('error'), [], c)
  }

  s.inStream.write({
    cmd: 'connect',
    protocolId: 'MQTT',
    protocolVersion: 4,
    clean: false,
    clientId: 'my-client',
    keepalive: 0
  })

  broker.on('clientError', function (client, err) {
    t.equal(client.version, 4)
    t.pass('throws error')
  })
})

test('client connect clear outgoing', function (t) {
  t.plan(1)

  const clientId = 'abcde'
  const brokerId = 'pippo'

  const broker = aedes({ id: brokerId })
  t.teardown(broker.close.bind(broker))

  const subs = [{ clientId }]
  const packet = {
    cmd: 'publish',
    topic: 'hello',
    payload: Buffer.from('world'),
    qos: 1,
    brokerId,
    brokerCounter: 2,
    retain: true,
    messageId: 42,
    dup: false
  }

  broker.persistence.outgoingEnqueueCombi(subs, packet, function () {
    const s = setup(broker)

    s.inStream.write({
      cmd: 'connect',
      protocolId: 'MQTT',
      protocolVersion: 4,
      clean: true,
      clientId,
      keepalive: 0
    })

    broker.on('clientReady', function (client) {
      broker.persistence.outgoingUpdate(client, packet, function (err) {
        t.equal('no such packet', err.message, 'packet not found')
      })
    })
  })
})

test('clients with zero-byte clientid and clean=true on MQTT 3.1.1 will get a generated clientId', function (t) {
  t.plan(5)

  const broker = aedes()
  t.teardown(broker.close.bind(broker))

  const s = setup(broker)

  s.inStream.write({
    cmd: 'connect',
    protocolId: 'MQTT',
    protocolVersion: 4,
    clean: true,
    clientId: '',
    keepalive: 0
  })
  s.outStream.on('data', function (packet) {
    t.equal(packet.cmd, 'connack')
    t.equal(packet.returnCode, 0)
    t.equal(broker.connectedClients, 1)
    t.equal(s.client.version, 4)
  })
  broker.on('connectionError', function (client, err) {
    t.error(err, 'no error')
  })
  broker.on('client', function (client) {
    t.ok(client.id.startsWith('aedes_'))
  })
})

// [MQTT-3.1.3-7]
test('reject clients with > 23 clientId length in MQTT 3.1.0', function (t) {
  t.plan(7)

  const broker = aedes()
  t.teardown(broker.close.bind(broker))

  const s = setup(broker)

  const conn = s.client.conn
  const end = conn.end

  conn.end = function () {
    t.fail('should not call `conn.end()`')
    end()
  }

  function drain () {
    t.pass('should empty connection request queue')
  }

  conn._writableState.getBuffer = () => [{ callback: drain }, { callback: drain }]

  s.inStream.write({
    cmd: 'connect',
    protocolId: 'MQIsdp',
    protocolVersion: 3,
    clean: true,
    clientId: 'abcdefghijklmnopqrstuvwxyz',
    keepalive: 0
  })
  s.outStream.on('data', function (packet) {
    t.equal(packet.cmd, 'connack')
    t.equal(packet.returnCode, 2, 'identifier rejected')
    t.equal(broker.connectedClients, 0)
    t.equal(s.client.version, null)
  })
  broker.on('connectionError', function (client, err) {
    t.equal(err.message, 'identifier rejected')
  })
})

test('connect clients with > 23 clientId length using aedes maxClientsIdLength option in MQTT 3.1.0', function (t) {
  t.plan(4)

  const broker = aedes({ maxClientsIdLength: 26 })
  t.teardown(broker.close.bind(broker))

  const s = setup(broker)

  s.inStream.write({
    cmd: 'connect',
    protocolId: 'MQTT',
    protocolVersion: 3,
    clean: true,
    clientId: 'abcdefghijklmnopqrstuvwxyz',
    keepalive: 0
  })
  s.outStream.on('data', function (packet) {
    t.equal(packet.cmd, 'connack')
    t.equal(packet.returnCode, 0)
    t.equal(broker.connectedClients, 1)
    t.equal(s.client.version, 3)
  })
  broker.on('connectionError', function (client, err) {
    t.error(err, 'no error')
  })
})

test('connect with > 23 clientId length in MQTT 3.1.1', function (t) {
  t.plan(4)

  const broker = aedes()
  t.teardown(broker.close.bind(broker))

  const s = setup(broker)

  s.inStream.write({
    cmd: 'connect',
    protocolId: 'MQTT',
    protocolVersion: 4,
    clean: true,
    clientId: 'abcdefghijklmnopqrstuvwxyz',
    keepalive: 0
  })
  s.outStream.on('data', function (packet) {
    t.equal(packet.cmd, 'connack')
    t.equal(packet.returnCode, 0)
    t.equal(broker.connectedClients, 1)
    t.equal(s.client.version, 4)
  })
  broker.on('connectionError', function (client, err) {
    t.error(err, 'no error')
  })
})

// [MQTT-3.1.0-1]
test('the first Packet MUST be a CONNECT Packet', function (t) {
  t.plan(2)

  const broker = aedes()
  t.teardown(broker.close.bind(broker))

  const packet = {
    cmd: 'publish',
    topic: 'hello',
    payload: Buffer.from('world'),
    qos: 0,
    retain: false
  }
  const s = setup(broker)
  s.inStream.write(packet)

  broker.on('connectionError', function (client, err) {
    t.equal(err.message, 'Invalid protocol')
  })
  setImmediate(() => {
    t.ok(s.conn.destroyed, 'close connection if first packet is not a CONNECT')
    s.conn.destroy()
  })
})

// [MQTT-3.1.0-2]
test('second CONNECT Packet sent from a Client as a protocol violation and disconnect the Client', function (t) {
  t.plan(4)

  const broker = aedes()
  t.teardown(broker.close.bind(broker))

  const packet = {
    cmd: 'connect',
    protocolId: 'MQTT',
    protocolVersion: 4,
    clean: true,
    clientId: 'my-client',
    keepalive: 0
  }
  broker.on('clientError', function (client, err) {
    t.equal(err.message, 'Invalid protocol')
  })
  const s = connect(setup(broker), { clientId: 'abcde' })
  s.broker.on('clientReady', function () {
    t.ok(broker.clients.abcde.connected)
    // destory client when there is a 2nd cmd:connect, even the clientId is dfferent
    s.inStream.write(packet)
    setImmediate(() => {
      t.equal(broker.clients.abcde, undefined, 'client instance is removed')
      t.ok(s.conn.destroyed, 'close connection if packet is a CONNECT after network is established')
    })
  })
})

test('connect handler calls done when preConnect throws error', function (t) {
  t.plan(1)

  const broker = aedes({
    preConnect: function (client, packet, done) {
      done(Error('error in preconnect'))
    }
  })

  t.teardown(broker.close.bind(broker))

  const s = setup(broker)

  const handleConnect = require('../lib/handlers/connect')

  handleConnect(s.client, {}, function done (err) {
    t.equal(err.message, 'error in preconnect', 'calls done with error')
  })
})

test('handler calls done when disconnect or unknown packet cmd is received', function (t) {
  t.plan(2)

  const broker = aedes()

  t.teardown(broker.close.bind(broker))

  const s = setup(broker)

  const handle = require('../lib/handlers/index')

  handle(s.client, { cmd: 'disconnect' }, function done () {
    t.pass('calls done when disconnect cmd is received')
  })

  handle(s.client, { cmd: 'fsfadgragae' }, function done () {
    t.pass('calls done when unknown cmd is received')
  })
})

test('reject second CONNECT Packet sent while first CONNECT still in preConnect stage', function (t) {
  t.plan(2)

  const packet1 = {
    cmd: 'connect',
    protocolId: 'MQTT',
    protocolVersion: 4,
    clean: true,
    clientId: 'my-client-1',
    keepalive: 0
  }
  const packet2 = {
    cmd: 'connect',
    protocolId: 'MQTT',
    protocolVersion: 4,
    clean: true,
    clientId: 'my-client-2',
    keepalive: 0
  }

  let i = 0
  const broker = aedes({
    preConnect: function (client, packet, done) {
      const ms = i++ === 0 ? 2000 : 500
      setTimeout(function () {
        done(null, true)
      }, ms)
    }
  })
  t.teardown(broker.close.bind(broker))

  const s = setup(broker)

  broker.on('connectionError', function (client, err) {
    t.equal(err.info.clientId, 'my-client-2')
    t.equal(err.message, 'Invalid protocol')
  })

  const msg = async (s, ms, msg) => {
    await delay(ms)
    s.inStream.write(msg)
  }

  ;(async () => {
    await Promise.all([msg(s, 100, packet1), msg(s, 200, packet2)])
  })().catch(
    (error) => {
      t.fail(error)
    }
  )
})

// [MQTT-3.1.2-1], Guarded in mqtt-packet
test('reject clients with wrong protocol name', function (t) {
  t.plan(2)

  const broker = aedes()
  t.teardown(broker.close.bind(broker))

  const s = setup(broker)

  s.inStream.write({
    cmd: 'connect',
    protocolId: 'MQTT_hello',
    protocolVersion: 3,
    clean: true,
    clientId: 'my-client',
    keepalive: 0
  })
  s.outStream.on('data', function (packet) {
    t.fail('no data sent')
  })
  broker.on('connectionError', function (client, err) {
    t.equal(err.message, 'Invalid protocolId')
    t.equal(broker.connectedClients, 0)
  })
})

test('After first CONNECT Packet, others are queued until \'connect\' event', function (t) {
  t.plan(2)

  const queueLimit = 50
  const broker = aedes({ queueLimit })
  t.teardown(broker.close.bind(broker))

  const publishP = {
    cmd: 'publish',
    topic: 'hello',
    payload: Buffer.from('world'),
    qos: 0,
    retain: false
  }

  const connectP = {
    cmd: 'connect',
    protocolId: 'MQTT',
    protocolVersion: 4,
    clean: true,
    clientId: 'abcde',
    keepalive: 0
  }

  const s = setup(broker)
  s.inStream.write(connectP)

  process.once('warning', e => t.fail('Memory leak detected'))

  for (let i = 0; i < queueLimit; i++) {
    s.inStream.write(publishP)
  }

  broker.on('client', function (client) {
    t.equal(client._parser._queue.length, queueLimit, 'Packets have been queued')

    client.once('connected', () => {
      t.equal(client._parser._queue, null, 'Queue is empty')
      s.conn.destroy()
    })
  })
})

test('Test queue limit', function (t) {
  t.plan(1)

  const queueLimit = 50
  const broker = aedes({ queueLimit })
  t.teardown(broker.close.bind(broker))

  const publishP = {
    cmd: 'publish',
    topic: 'hello',
    payload: Buffer.from('world'),
    qos: 0,
    retain: false
  }

  const connectP = {
    cmd: 'connect',
    protocolId: 'MQTT',
    protocolVersion: 4,
    clean: true,
    clientId: 'abcde',
    keepalive: 0
  }

  const s = setup(broker)
  s.inStream.write(connectP)

  process.once('warning', e => t.fail('Memory leak detected'))

  for (let i = 0; i < queueLimit + 1; i++) {
    s.inStream.write(publishP)
  }

  broker.on('connectionError', function (conn, err) {
    t.equal(err.message, 'Client queue limit reached', 'Queue error is thrown')
    s.conn.destroy()
  })
})

;[['fail with no error msg', 3, null, false], ['succeed with no error msg', 9, null, true], ['fail with error msg', 6, new Error('connection banned'), false], ['succeed with error msg', 6, new Error('connection banned'), true]].forEach(function (ele, idx) {
  const title = ele[0]
  const plan = ele[1]
  const err = ele[2]
  const ok = ele[3]
  test('preConnect handler - ' + title, function (t) {
    t.plan(plan)

    const broker = aedes({
      preConnect: function (client, packet, done) {
        t.ok(client.connecting)
        t.notOk(client.connected)
        t.equal(client.version, null)
        return done(err, ok)
      }
    })
    t.teardown(broker.close.bind(broker))

    const s = setup(broker)

    s.inStream.write({
      cmd: 'connect',
      protocolId: 'MQTT',
      protocolVersion: 4,
      clean: true,
      clientId: 'my-client-' + idx,
      keepalive: 0
    })
    broker.on('client', function (client) {
      if (ok && !err) {
        t.ok(client.connecting)
        t.notOk(client.connected)
        t.pass('register client ok')
      } else {
        t.fail('no reach here')
      }
    })
    broker.on('clientReady', function (client) {
      t.notOk(client.connecting)
      t.ok(client.connected)
      t.pass('connect ok')
    })
    broker.on('clientError', function (client, err) {
      t.fail('no client error')
    })
    broker.on('connectionError', function (client, err) {
      if (err) {
        t.notOk(client.connecting)
        t.notOk(client.connected)
        t.equal(err.message, 'connection banned')
      } else {
        t.fail('no connection error')
      }
    })
  })
})

// websocket-stream based connections
test('websocket clients have access to the request object', function (t) {
  t.plan(3)

  const port = 4883
  const broker = aedes()
  broker.on('client', function (client) {
    if (client.req) {
      t.pass('client request object present')
      if (client.req.headers) {
        t.equal('sample', client.req.headers['x-test-protocol'])
      }
    } else {
      t.fail('no request object present')
    }
  })

  const server = http.createServer()
  ws.createServer({
    server
  }, broker.handle)

  server.listen(port, function (err) {
    t.error(err, 'no error')
  })

  const client = mqtt.connect(`ws://localhost:${port}`, {
    wsOptions: {
      headers: {
        'X-Test-Protocol': 'sample'
      }
    }
  })

  t.teardown(() => {
    client.end(true)
    broker.close()
    server.close()
  })
})
