'use strict'

const { test } = require('tap')
const concat = require('concat-stream')
const { setup, connect, subscribe } = require('./helper')
const aedes = require('../')

function publish (t, s, packet, done) {
  const msgId = packet.messageId

  s.inStream.write(packet)

  s.outStream.once('data', function (packet) {
    t.same(packet, {
      cmd: 'pubrec',
      messageId: msgId,
      length: 2,
      dup: false,
      retain: false,
      qos: 0
    }, 'pubrec must match')

    s.inStream.write({
      cmd: 'pubrel',
      messageId: msgId
    })

    s.outStream.once('data', function (packet) {
      t.same(packet, {
        cmd: 'pubcomp',
        messageId: msgId,
        length: 2,
        dup: false,
        retain: false,
        qos: 0
      }, 'pubcomp must match')

      if (done) {
        done()
      }
    })
  })
}

function receive (t, subscriber, expected, done) {
  subscriber.outStream.once('data', function (packet) {
    t.not(packet.messageId, expected.messageId, 'messageId must differ')

    const msgId = packet.messageId
    delete packet.messageId
    delete expected.messageId
    t.same(packet, expected, 'packet must match')

    subscriber.inStream.write({
      cmd: 'pubrec',
      messageId: msgId
    })

    subscriber.outStream.once('data', function (packet) {
      subscriber.inStream.write({
        cmd: 'pubcomp',
        messageId: msgId
      })
      t.same(packet, {
        cmd: 'pubrel',
        messageId: msgId,
        length: 2,
        qos: 1,
        retain: false,
        dup: false
      }, 'pubrel must match')

      if (done) {
        done()
      }
    })
  })
}

test('publish QoS 2', function (t) {
  t.plan(2)

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

  const packet = {
    cmd: 'publish',
    topic: 'hello',
    payload: 'world',
    qos: 2,
    messageId: 42
  }
  publish(t, s, packet)
})

test('subscribe QoS 2', function (t) {
  t.plan(8)

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

  const publisher = connect(setup(broker))
  const subscriber = connect(setup(broker))
  const toPublish = {
    cmd: 'publish',
    topic: 'hello',
    payload: Buffer.from('world'),
    qos: 2,
    messageId: 42,
    dup: false,
    length: 14,
    retain: false
  }

  subscribe(t, subscriber, 'hello', 2, function () {
    publish(t, publisher, toPublish)

    receive(t, subscriber, toPublish)
  })
})

test('publish QoS 2 throws error on write', function (t) {
  t.plan(1)

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

  s.broker.on('client', function (client) {
    client.connected = false
    client.connecting = false

    s.inStream.write({
      cmd: 'publish',
      topic: 'hello',
      payload: 'world',
      qos: 2,
      messageId: 42
    })
  })

  s.broker.on('clientError', function (client, err) {
    t.equal(err.message, 'connection closed', 'throws error')
  })
})

test('pubrec handler calls done when outgoingUpdate fails (clean=false)', function (t) {
  t.plan(1)

  const s = connect(setup(), { clean: false })
  t.teardown(s.broker.close.bind(s.broker))

  const handle = require('../lib/handlers/pubrec.js')

  s.broker.persistence.outgoingUpdate = function (client, pubrel, done) {
    done(Error('throws error'))
  }

  handle(s.client, { messageId: 42 }, function done () {
    t.pass('calls done on error')
  })
})

test('client.publish with clean=true subscribption QoS 2', function (t) {
  t.plan(8)

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

  const toPublish = {
    cmd: 'publish',
    topic: 'hello',
    payload: Buffer.from('world'),
    qos: 2,
    messageId: 42,
    dup: false,
    length: 14,
    retain: false
  }
  let brokerClient = null

  broker.on('client', function (client) {
    brokerClient = client

    brokerClient.on('error', function (err) {
      t.error(err)
    })
  })

  const subscriber = connect(setup(broker), { clean: true })

  subscribe(t, subscriber, 'hello', 2, function () {
    t.pass('subscribed')
    receive(t, subscriber, toPublish)
    brokerClient.publish(toPublish, function (err) {
      t.error(err)
    })
  })
})

test('call published method with client with QoS 2', function (t) {
  t.plan(9)

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

  const publisher = connect(setup(broker))
  const subscriber = connect(setup(broker))
  const toPublish = {
    cmd: 'publish',
    topic: 'hello',
    payload: Buffer.from('world'),
    qos: 2,
    messageId: 42,
    dup: false,
    length: 14,
    retain: false
  }

  broker.published = function (packet, client, cb) {
    // Client is null for all server publishes
    if (packet.topic.split('/')[0] !== '$SYS') {
      t.ok(client, 'client must be passed to published method')
      cb()
    }
  }

  subscribe(t, subscriber, 'hello', 2, function () {
    publish(t, publisher, toPublish)

    receive(t, subscriber, toPublish)
  })
})

;[true, false].forEach(function (cleanSession) {
  test(`authorized forward publish packets in QoS 2 [clean=${cleanSession}]`, function (t) {
    t.plan(9)

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

    const opts = { clean: cleanSession }
    const publisher = connect(setup(broker), { clientId: 'my-client-xyz-8' })
    const subscriber = connect(setup(broker), { ...opts, clientId: 'abcde' })
    const forwarded = {
      cmd: 'publish',
      topic: 'hello',
      payload: Buffer.from('world'),
      qos: 2,
      retain: false,
      dup: false,
      messageId: undefined,
      clientId: 'my-client-xyz-8'
    }
    const expected = {
      cmd: 'publish',
      topic: 'hello',
      payload: Buffer.from('world'),
      qos: 2,
      retain: false,
      length: 14,
      dup: false
    }
    broker.authorizeForward = function (client, packet) {
      forwarded.brokerId = broker.id
      forwarded.brokerCounter = broker.counter
      delete packet.nl
      t.same(packet, forwarded, 'forwarded packet must match')
      return packet
    }

    subscribe(t, subscriber, 'hello', 2, function () {
      subscriber.outStream.once('data', function (packet) {
        t.not(packet.messageId, 42)
        delete packet.messageId
        t.same(packet, expected, 'packet must match')
      })

      publish(t, publisher, {
        cmd: 'publish',
        topic: 'hello',
        payload: Buffer.from('world'),
        qos: 2,
        retain: false,
        messageId: 42,
        dup: false
      }, function () {
        const stream = broker.persistence.outgoingStream({ id: 'abcde' })
        stream.pipe(concat(function (list) {
          if (cleanSession) {
            t.equal(list.length, 0, 'should have empty item in queue')
          } else {
            t.equal(list.length, 1, 'should have one item in queue')
          }
        }))
      })
    })
  })
})

;[true, false].forEach(function (cleanSession) {
  test(`unauthorized forward publish packets in QoS 2 [clean=${cleanSession}]`, function (t) {
    t.plan(6)

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

    const opts = { clean: cleanSession }

    const publisher = connect(setup(broker))
    const subscriber = connect(setup(broker), { ...opts, clientId: 'abcde' })

    broker.authorizeForward = function (client, packet) {

    }

    subscribe(t, subscriber, 'hello', 2, function () {
      subscriber.outStream.once('data', function (packet) {
        t.fail('should not receive any packets')
      })

      publish(t, publisher, {
        cmd: 'publish',
        topic: 'hello',
        payload: Buffer.from('world'),
        qos: 2,
        retain: false,
        messageId: 42,
        dup: false
      }, function () {
        const stream = broker.persistence.outgoingStream({ id: 'abcde' })
        stream.pipe(concat(function (list) {
          t.equal(list.length, 0, 'should empty in queue')
        }))
      })
    })
  })
})

test('subscribe QoS 0, but publish QoS 2', function (t) {
  t.plan(6)

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

  const publisher = connect(setup(broker))
  const subscriber = connect(setup(broker))
  const expected = {
    cmd: 'publish',
    topic: 'hello',
    payload: Buffer.from('world'),
    qos: 0,
    dup: false,
    length: 12,
    retain: false
  }

  subscribe(t, subscriber, 'hello', 0, function () {
    subscriber.outStream.once('data', function (packet) {
      t.same(packet, expected, 'packet must match')
    })

    publish(t, publisher, {
      cmd: 'publish',
      topic: 'hello',
      payload: Buffer.from('world'),
      qos: 2,
      retain: false,
      messageId: 42,
      dup: false
    })
  })
})

test('subscribe QoS 1, but publish QoS 2', function (t) {
  t.plan(6)

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

  const publisher = connect(setup(broker))
  const subscriber = connect(setup(broker))
  const expected = {
    cmd: 'publish',
    topic: 'hello',
    payload: Buffer.from('world'),
    qos: 1,
    dup: false,
    length: 14,
    retain: false
  }

  subscribe(t, subscriber, 'hello', 1, function () {
    subscriber.outStream.once('data', function (packet) {
      delete packet.messageId
      t.same(packet, expected, 'packet must match')
    })

    publish(t, publisher, {
      cmd: 'publish',
      topic: 'hello',
      payload: Buffer.from('world'),
      qos: 2,
      retain: false,
      messageId: 42,
      dup: false
    })
  })
})

test('restore QoS 2 subscriptions not clean', function (t) {
  t.plan(9)

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

  let subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' })
  const expected = {
    cmd: 'publish',
    topic: 'hello',
    payload: Buffer.from('world'),
    qos: 2,
    dup: false,
    length: 14,
    messageId: 42,
    retain: false
  }

  subscribe(t, subscriber, 'hello', 2, function () {
    subscriber.inStream.end()

    const publisher = connect(setup(broker))

    subscriber = connect(setup(broker), { clean: false, clientId: 'abcde' }, function (connect) {
      t.equal(connect.sessionPresent, true, 'session present is set to true')
      publish(t, publisher, expected)
    })

    receive(t, subscriber, expected)
  })
})

test('resend publish on non-clean reconnect QoS 2', function (t) {
  t.plan(8)

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

  const opts = { clean: false, clientId: 'abcde' }
  let subscriber = connect(setup(broker), opts)
  const expected = {
    cmd: 'publish',
    topic: 'hello',
    payload: Buffer.from('world'),
    qos: 2,
    dup: false,
    length: 14,
    messageId: 42,
    retain: false
  }

  subscribe(t, subscriber, 'hello', 2, function () {
    subscriber.inStream.end()

    const publisher = connect(setup(broker))

    publish(t, publisher, expected, function () {
      subscriber = connect(setup(broker), opts)

      receive(t, subscriber, expected)
    })
  })
})

test('resend pubrel on non-clean reconnect QoS 2', function (t) {
  t.plan(9)

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

  const opts = { clean: false, clientId: 'abcde' }
  let subscriber = connect(setup(broker), opts)
  const expected = {
    cmd: 'publish',
    topic: 'hello',
    payload: Buffer.from('world'),
    qos: 2,
    dup: false,
    length: 14,
    messageId: 42,
    retain: false
  }

  subscribe(t, subscriber, 'hello', 2, function () {
    subscriber.inStream.end()

    const publisher = connect(setup(broker))

    publish(t, publisher, expected, function () {
      subscriber = connect(setup(broker), opts)

      subscriber.outStream.once('data', function (packet) {
        t.not(packet.messageId, expected.messageId, 'messageId must differ')

        const msgId = packet.messageId
        delete packet.messageId
        delete expected.messageId
        t.same(packet, expected, 'packet must match')

        subscriber.inStream.write({
          cmd: 'pubrec',
          messageId: msgId
        })

        subscriber.outStream.once('data', function (packet) {
          t.same(packet, {
            cmd: 'pubrel',
            messageId: msgId,
            length: 2,
            qos: 1,
            retain: false,
            dup: false
          }, 'pubrel must match')

          subscriber.inStream.end()

          subscriber = connect(setup(broker), opts)

          subscriber.outStream.once('data', function (packet) {
            t.same(packet, {
              cmd: 'pubrel',
              messageId: msgId,
              length: 2,
              qos: 1,
              retain: false,
              dup: false
            }, 'pubrel must match')

            subscriber.inStream.write({
              cmd: 'pubcomp',
              messageId: msgId
            })
          })
        })
      })
    })
  })
})

test('publish after disconnection', function (t) {
  t.plan(10)

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

  const publisher = connect(setup(broker))
  const subscriber = connect(setup(broker))
  const toPublish = {
    cmd: 'publish',
    topic: 'hello',
    payload: Buffer.from('world'),
    qos: 2,
    messageId: 42,
    dup: false,
    length: 14,
    retain: false
  }
  const toPublish2 = {
    cmd: 'publish',
    topic: 'hello',
    payload: Buffer.from('worl2'),
    qos: 2,
    messageId: 43,
    dup: false,
    length: 14,
    retain: false
  }

  subscribe(t, subscriber, 'hello', 2, function () {
    publish(t, publisher, toPublish)

    receive(t, subscriber, toPublish, function () {
      publish(t, publisher, toPublish2)
    })
  })
})

test('multiple publish and store one', function (t) {
  t.plan(2)

  const broker = aedes()

  const sid = {
    id: 'abcde'
  }
  const s = connect(setup(broker), { clientId: sid.id })
  const toPublish = {
    cmd: 'publish',
    topic: 'hello',
    payload: Buffer.from('world'),
    qos: 2,
    retain: false,
    dup: false,
    messageId: 42
  }

  let count = 5
  while (count--) {
    s.inStream.write(toPublish)
  }
  let recvcnt = 0
  s.outStream.on('data', function (packet) {
    if (++recvcnt < 5) return
    broker.close(function () {
      broker.persistence.incomingGetPacket(sid, toPublish, function (err, origPacket) {
        delete origPacket.brokerId
        delete origPacket.brokerCounter
        t.same(origPacket, toPublish, 'packet must match')
        t.error(err)
      })
    })
  })
})

test('packet is written to stream after being stored', function (t) {
  const s = connect(setup())

  const broker = s.broker

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

  let packetStored = false

  const fn = broker.persistence.incomingStorePacket.bind(broker.persistence)

  s.broker.persistence.incomingStorePacket = function (client, packet, done) {
    packetStored = true
    t.pass('packet stored')
    fn(client, packet, done)
  }

  const packet = {
    cmd: 'publish',
    topic: 'hello',
    payload: 'world',
    qos: 2,
    messageId: 42
  }

  publish(t, s, packet)

  s.outStream.once('data', function (packet) {
    t.equal(packet.cmd, 'pubrec', 'pubrec received')
    t.equal(packetStored, true, 'after packet store')
    t.end()
  })
})

test('not send pubrec when persistence fails to store packet', function (t) {
  t.plan(2)

  const s = connect(setup())
  const broker = s.broker

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

  s.broker.persistence.incomingStorePacket = function (client, packet, done) {
    t.pass('packet stored')
    done(new Error('store error'))
  }
  s.broker.on('clientError', function (client, err) {
    t.equal(err.message, 'store error')
  })

  const packet = {
    cmd: 'publish',
    topic: 'hello',
    payload: 'world',
    qos: 2,
    messageId: 42
  }

  s.inStream.write(packet)
  s.outStream.once('data', function (packet) {
    t.fail('should not have pubrec')
  })
})
