'use strict'

const { test } = require('tap')
const { through } = require('../lib/utils')
const Faketimers = require('@sinonjs/fake-timers')
const { setup, connect, subscribe, noError } = require('./helper')
const aedes = require('../')

// [MQTT-3.3.1-9]
test('live retain packets', function (t) {
  t.plan(5)
  const expected = {
    cmd: 'publish',
    topic: 'hello',
    payload: Buffer.from('world'),
    retain: false,
    dup: false,
    length: 12,
    qos: 0
  }

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

  subscribe(t, s, 'hello', 0, function () {
    s.outStream.on('data', function (packet) {
      t.same(packet, expected)
    })

    s.broker.publish({
      cmd: 'publish',
      topic: 'hello',
      payload: Buffer.from('world'),
      retain: true,
      dup: false,
      length: 12,
      qos: 0
    }, function () {
      t.pass('publish finished')
    })
  })
})

test('retain messages', function (t) {
  t.plan(4)

  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: true
  }

  broker.subscribe('hello', function (packet, cb) {
    cb()

    // defer this or it will receive the message which
    // is being published
    setImmediate(function () {
      subscribe(t, subscriber, 'hello', 0, function () {
        subscriber.outStream.once('data', function (packet) {
          t.same(packet, expected, 'packet must match')
        })
      })
    })
  })

  publisher.inStream.write(expected)
})

test('retain messages propagates through broker subscriptions', function (t) {
  t.plan(1)

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

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

  const subscriberFunc = function (packet, cb) {
    packet = Object.assign({}, packet)
    delete packet.brokerId
    delete packet.brokerCounter
    cb()
    setImmediate(function () {
      t.same(packet, expected, 'packet must not have been modified')
    })
  }

  broker.subscribe('hello', subscriberFunc, function () {
    broker.publish(expected)
  })
})

test('avoid wrong deduping of retain messages', function (t) {
  t.plan(7)

  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: true
  }

  broker.subscribe('hello', function (packet, cb) {
    cb()
    // subscribe and publish another topic
    subscribe(t, subscriber, 'hello2', 0, function () {
      cb()

      publisher.inStream.write({
        cmd: 'publish',
        topic: 'hello2',
        payload: Buffer.from('world'),
        qos: 0,
        dup: false
      })

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

  publisher.inStream.write(expected)
})

test('reconnected subscriber will not receive retained messages when QoS 0 and clean', function (t) {
  t.plan(4)

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

  const publisher = connect(setup(broker), { clean: true })
  let subscriber = connect(setup(broker), { clean: true })
  const expected = {
    cmd: 'publish',
    topic: 'hello',
    payload: Buffer.from('world'),
    qos: 0,
    retain: false,
    dup: false,
    length: 12
  }
  subscribe(t, subscriber, 'hello', 0, function () {
    publisher.inStream.write({
      cmd: 'publish',
      topic: 'hello',
      payload: 'world',
      qos: 0,
      retain: false
    })
    subscriber.outStream.once('data', function (packet) {
      t.same(packet, expected, 'packet must match')
      subscriber.inStream.end()
      publisher.inStream.write({
        cmd: 'publish',
        topic: 'hello',
        payload: 'foo',
        qos: 0,
        retain: true
      })
      subscriber = connect(setup(broker), { clean: true })
      subscriber.outStream.on('data', function (packet) {
        t.fail('should not received retain message')
      })
    })
  })
})

test('subscriber will not receive retained messages when QoS is 128', function (t) {
  t.plan(3)

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

  const pubPacket = {
    cmd: 'publish',
    topic: 'hello',
    payload: 'world',
    qos: 1,
    retain: true,
    messageId: 42
  }

  broker.authorizeSubscribe = function (client, sub, callback) {
    if (sub.topic === pubPacket.topic) {
      callback(null, null)
    } else {
      callback(null, sub)
    }
  }

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

  publisher.inStream.write(pubPacket)

  publisher.outStream.on('data', function (packet) {
    const subscriber = connect(setup(broker), { clean: true })
    subscribe(t, subscriber, pubPacket.topic, 128, function () {
      subscriber.outStream.on('data', function (packet) {
        t.fail('should not received retain message')
      })
    })
  })
})

// [MQTT-3.3.1-6]
test('new QoS 0 subscribers receive QoS 0 retained messages when clean', function (t) {
  t.plan(9)

  const clock = Faketimers.createClock()
  const broker = aedes()
  t.teardown(function () {
    clock.reset()
    broker.close()
  })

  const publisher = connect(setup(broker), { clean: true })
  const expected = {
    cmd: 'publish',
    topic: 'hello/world',
    payload: Buffer.from('big big world'),
    qos: 0,
    retain: true,
    dup: false,
    length: 26
  }
  publisher.inStream.write({
    cmd: 'publish',
    topic: 'hello/world',
    payload: 'big big world',
    qos: 0,
    retain: true
  })
  const subscriber1 = connect(setup(broker), { clean: true })
  subscribe(t, subscriber1, 'hello/world', 0, function () {
    subscriber1.outStream.on('data', function (packet) {
      t.same(packet, expected, 'packet must match')
      clock.tick(100)
    })
  })
  const subscriber2 = connect(setup(broker), { clean: true })
  subscribe(t, subscriber2, 'hello/+', 0, function () {
    subscriber2.outStream.on('data', function (packet) {
      t.same(packet, expected, 'packet must match')
      clock.tick(100)
    })
  })

  clock.setTimeout(() => {
    t.equal(broker.counter, 9)
  }, 200)
})

// [MQTT-3.3.1-5]
test('new QoS 0 subscribers receive downgraded QoS 1 retained messages when clean', function (t) {
  t.plan(6)

  const broker = aedes()

  const publisher = connect(setup(broker), { clean: true })
  const expected = {
    cmd: 'publish',
    topic: 'hello',
    payload: Buffer.from('world'),
    qos: 0,
    retain: true,
    dup: false,
    length: 12
  }
  publisher.inStream.write({
    cmd: 'publish',
    topic: 'hello',
    payload: 'world',
    qos: 1,
    retain: true,
    messageId: 42
  })
  publisher.outStream.on('data', function (packet) {
    const subscriber = connect(setup(broker), { clean: true })
    subscribe(t, subscriber, 'hello', 0, function () {
      subscriber.outStream.on('data', function (packet) {
        t.not(packet.messageId, 42, 'messageId should not be the same')
        delete packet.messageId
        t.same(packet, expected, 'packet must match')
        broker.close()
      })
    })
  })
  broker.on('closed', function () {
    t.equal(broker.counter, 8)
  })
})

// [MQTT-3.3.1-10]
test('clean retained messages', function (t) {
  t.plan(3)

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

  const publisher = connect(setup(broker), { clean: true })
  publisher.inStream.write({
    cmd: 'publish',
    topic: 'hello',
    payload: 'world',
    qos: 0,
    retain: true
  })
  publisher.inStream.write({
    cmd: 'publish',
    topic: 'hello',
    payload: '',
    qos: 0,
    retain: true
  })
  const subscriber = connect(setup(broker), { clean: true })
  subscribe(t, subscriber, 'hello', 0, function () {
    subscriber.outStream.once('data', function (packet) {
      t.fail('should not received retain message')
    })
  })
})

// [MQTT-3.3.1-11]
test('broker not store zero-byte retained messages', function (t) {
  t.plan(0)

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

  const s = connect(setup(broker))

  s.inStream.write({
    cmd: 'publish',
    topic: 'hello',
    payload: '',
    retain: true
  })
  s.broker.on('publish', function (packet, client) {
    if (packet.topic.startsWith('$SYS/')) {
      return
    }
    const stream = s.broker.persistence.createRetainedStream(packet.topic)
    stream.pipe(through(function sendRetained (packet, enc, cb) {
      t.fail('not store zero-byte retained messages')
    }))
  })
})

test('fail to clean retained messages without retain flag', function (t) {
  t.plan(4)

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

  const publisher = connect(setup(broker), { clean: true })
  const expected = {
    cmd: 'publish',
    topic: 'hello',
    payload: Buffer.from('world'),
    qos: 0,
    retain: true,
    dup: false,
    length: 12
  }
  publisher.inStream.write({
    cmd: 'publish',
    topic: 'hello',
    payload: 'world',
    qos: 0,
    retain: true
  })
  publisher.inStream.write({
    cmd: 'publish',
    topic: 'hello',
    payload: '',
    qos: 0,
    retain: false
  })
  const subscriber = connect(setup(broker), { clean: true })
  subscribe(t, subscriber, 'hello', 0, function () {
    subscriber.outStream.on('data', function (packet) {
      t.same(packet, expected, 'packet must match')
    })
  })
})

test('only get the last retained messages in same topic', function (t) {
  t.plan(4)

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

  const publisher = connect(setup(broker), { clean: true })
  const expected = {
    cmd: 'publish',
    topic: 'hello',
    payload: Buffer.from('foo'),
    qos: 0,
    retain: true,
    dup: false,
    length: 10
  }
  publisher.inStream.write({
    cmd: 'publish',
    topic: 'hello',
    payload: 'world',
    qos: 0,
    retain: true
  })
  publisher.inStream.write({
    cmd: 'publish',
    topic: 'hello',
    payload: 'foo',
    qos: 0,
    retain: true
  })
  const subscriber = connect(setup(broker), { clean: true })
  subscribe(t, subscriber, 'hello', 0, function () {
    subscriber.outStream.on('data', function (packet) {
      t.same(packet, expected, 'packet must match')
    })
  })
})

test('deliver QoS 1 retained messages to new subscriptions', function (t) {
  t.plan(4)

  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: true
  }

  publisher.inStream.write({
    cmd: 'publish',
    topic: 'hello',
    payload: 'world',
    qos: 1,
    messageId: 42,
    retain: true
  })

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

test('deliver QoS 1 retained messages to established subscriptions', function (t) {
  t.plan(4)

  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')
    })
    publisher.inStream.write({
      cmd: 'publish',
      topic: 'hello',
      payload: 'world',
      qos: 1,
      messageId: 42,
      retain: true
    })
  })
})

test('deliver QoS 0 retained message with QoS 1 subscription', function (t) {
  t.plan(4)

  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: true
  }

  broker.mq.on('hello', function (msg, cb) {
    cb()

    // defer this or it will receive the message which
    // is being published
    setImmediate(function () {
      subscribe(t, subscriber, 'hello', 1, function () {
        subscriber.outStream.once('data', function (packet) {
          t.same(packet, expected, 'packet must match')
        })
      })
    })
  })

  publisher.inStream.write({
    cmd: 'publish',
    topic: 'hello',
    payload: Buffer.from('world'),
    qos: 0,
    messageId: 42,
    retain: true
  })
})

test('disconnect and retain messages with QoS 1 [clean=false]', function (t) {
  t.plan(7)

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

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

  subscribe(t, subscriber, 'hello', 1, function () {
    subscriber.inStream.write({
      cmd: 'disconnect'
    })

    subscriber.outStream.on('data', function (packet) {
      console.log('original', packet)
    })

    const publisher = noError(connect(setup(broker)), t)

    publisher.inStream.write({
      cmd: 'publish',
      topic: 'hello',
      payload: 'world',
      qos: 1,
      messageId: 42,
      retain: true
    })

    publisher.outStream.once('data', function (packet) {
      t.equal(packet.cmd, 'puback')

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

      subscriber.outStream.once('data', function (packet) {
        // receive any queued messages (no matter they are retained messages) at the disconnected time
        t.not(packet.messageId, 42, 'messageId must differ')
        delete packet.messageId
        packet.length = 14
        t.same(packet, expected, 'packet must match')

        // there should be no messages come from restored subscriptions
        subscriber.outStream.once('data', function (packet) {
          t.fail('should not receive any more messages')
        })
      })
    })
  })
})

test('disconnect and two retain messages with QoS 1 [clean=false]', function (t) {
  t.plan(15)

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

  let subscriber = noError(connect(setup(broker), { clean: false, clientId: 'abcde' }), t)
  const expected = {
    cmd: 'publish',
    topic: 'hello',
    qos: 1,
    dup: false,
    length: 14,
    retain: true
  }

  subscribe(t, subscriber, 'hello', 1, function () {
    subscriber.inStream.write({
      cmd: 'disconnect'
    })

    subscriber.outStream.on('data', function (packet) {
      console.log('original', packet)
    })

    const publisher = noError(connect(setup(broker)), t)

    publisher.inStream.write({
      cmd: 'publish',
      topic: 'hello',
      payload: 'world',
      qos: 1,
      messageId: 41,
      retain: true
    })

    publisher.outStream.once('data', function (packet) {
      t.equal(packet.cmd, 'puback')

      publisher.inStream.write({
        cmd: 'publish',
        topic: 'hello',
        payload: 'world2',
        qos: 1,
        messageId: 42,
        retain: true
      })

      publisher.outStream.once('data', function (packet) {
        t.equal(packet.cmd, 'puback')

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

        subscriber.outStream.once('data', function (packet) {
          // receive any queued messages (included retained messages) at the disconnected time
          t.not(packet.messageId, 41, 'messageId must differ')
          delete packet.messageId
          packet.length = 14
          expected.payload = Buffer.from('world')
          t.same(packet, expected, 'packet must match')

          // receive any queued messages (included retained messages) at the disconnected time
          subscriber.outStream.once('data', function (packet) {
            t.not(packet.messageId, 42, 'messageId must differ')
            delete packet.messageId
            packet.length = 14
            expected.payload = Buffer.from('world2')
            t.same(packet, expected, 'packet must match')

            // should get the last retained message when we do a subscribe
            subscribe(t, subscriber, 'hello', 1, function () {
              subscriber.outStream.on('data', function (packet) {
                t.not(packet.messageId, 42, 'messageId must differ')
                delete packet.messageId
                packet.length = 14
                expected.payload = Buffer.from('world2')
                t.same(packet, expected, 'packet must match')
              })
            })
          })
        })
      })
    })
  })
})
