'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

var fromString = require('uint8arrays/from-string');
var toString = require('uint8arrays/to-string');
var nanoid = require('nanoid');
var pushable = require('it-pushable');
var all = require('it-all');
var utils = require('./utils.js');
var chai_js = require('aegir/utils/chai.js');
var mocha = require('../utils/mocha.js');
var delay = require('delay');
var env_js = require('ipfs-utils/src/env.js');
var ipfsOptionsWebsocketsFilterAll = require('../utils/ipfs-options-websockets-filter-all.js');
var first = require('it-first');
var sinon = require('sinon');

function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }

var pushable__default = /*#__PURE__*/_interopDefaultLegacy(pushable);
var all__default = /*#__PURE__*/_interopDefaultLegacy(all);
var delay__default = /*#__PURE__*/_interopDefaultLegacy(delay);
var first__default = /*#__PURE__*/_interopDefaultLegacy(first);
var sinon__default = /*#__PURE__*/_interopDefaultLegacy(sinon);

function testSubscribe(factory, options) {
  const ipfsOptions = ipfsOptionsWebsocketsFilterAll.ipfsOptionsWebsocketsFilterAll();
  const describe = mocha.getDescribe(options);
  const it = mocha.getIt(options);
  describe('.pubsub.subscribe', function () {
    this.timeout(80 * 1000);
    let ipfs1;
    let ipfs2;
    let topic;
    let subscribedTopics = [];
    let ipfs1Id;
    let ipfs2Id;
    before(async () => {
      ipfs1 = (await factory.spawn({ ipfsOptions })).api;
      ipfs2 = (await factory.spawn({
        type: env_js.isWebWorker ? 'js' : undefined,
        ipfsOptions
      })).api;
      ipfs1Id = await ipfs1.id();
      ipfs2Id = await ipfs2.id();
    });
    beforeEach(() => {
      topic = utils.getTopic();
      subscribedTopics = [topic];
    });
    afterEach(async () => {
      const nodes = [
        ipfs1,
        ipfs2
      ];
      for (let i = 0; i < subscribedTopics.length; i++) {
        const topic = subscribedTopics[i];
        await Promise.all(nodes.map(ipfs => ipfs.pubsub.unsubscribe(topic)));
      }
      subscribedTopics = [];
      await delay__default["default"](100);
    });
    after(() => factory.clean());
    describe('single node', () => {
      it('should subscribe to one topic', async () => {
        const msgStream = pushable__default["default"]();
        await ipfs1.pubsub.subscribe(topic, msg => {
          msgStream.push(msg);
          msgStream.end();
        });
        await ipfs1.pubsub.publish(topic, fromString.fromString('hi'));
        const msg = await first__default["default"](msgStream);
        chai_js.expect(toString.toString(msg.data)).to.equal('hi');
        chai_js.expect(msg).to.have.property('seqno');
        chai_js.expect(msg.seqno).to.be.an.instanceof(Uint8Array);
        chai_js.expect(msg.topicIDs[0]).to.eq(topic);
        chai_js.expect(msg).to.have.property('from', ipfs1Id.id);
      });
      it('should subscribe to one topic with options', async () => {
        const msgStream = pushable__default["default"]();
        await ipfs1.pubsub.subscribe(topic, msg => {
          msgStream.push(msg);
          msgStream.end();
        }, {});
        await ipfs1.pubsub.publish(topic, fromString.fromString('hi'));
        for await (const msg of msgStream) {
          chai_js.expect(toString.toString(msg.data)).to.equal('hi');
          chai_js.expect(msg).to.have.property('seqno');
          chai_js.expect(msg.seqno).to.be.an.instanceof(Uint8Array);
          chai_js.expect(msg.topicIDs[0]).to.eq(topic);
          chai_js.expect(msg).to.have.property('from', ipfs1Id.id);
        }
      });
      it('should subscribe to topic multiple times with different handlers', async () => {
        const msgStream1 = pushable__default["default"]();
        const msgStream2 = pushable__default["default"]();
        const handler1 = msg => {
          msgStream1.push(msg);
          msgStream1.end();
        };
        const handler2 = msg => {
          msgStream2.push(msg);
          msgStream2.end();
        };
        await Promise.all([
          ipfs1.pubsub.subscribe(topic, handler1),
          ipfs1.pubsub.subscribe(topic, handler2)
        ]);
        await ipfs1.pubsub.publish(topic, fromString.fromString('hello'));
        const [handler1Msg] = await all__default["default"](msgStream1);
        chai_js.expect(toString.toString(handler1Msg.data)).to.eql('hello');
        const [handler2Msg] = await all__default["default"](msgStream2);
        chai_js.expect(toString.toString(handler2Msg.data)).to.eql('hello');
        await ipfs1.pubsub.unsubscribe(topic, handler1);
        await delay__default["default"](100);
        chai_js.expect(await ipfs1.pubsub.ls()).to.eql([topic]);
        await ipfs1.pubsub.unsubscribe(topic, handler2);
        await delay__default["default"](100);
        chai_js.expect(await ipfs1.pubsub.ls()).to.eql([]);
      });
      it('should allow discover option to be passed', async () => {
        const msgStream = pushable__default["default"]();
        await ipfs1.pubsub.subscribe(topic, msg => {
          msgStream.push(msg);
          msgStream.end();
        }, { discover: true });
        await ipfs1.pubsub.publish(topic, fromString.fromString('hi'));
        for await (const msg of msgStream) {
          chai_js.expect(toString.toString(msg.data)).to.eql('hi');
        }
      });
    });
    describe('multiple connected nodes', () => {
      before(() => {
        if (ipfs1.pubsub.setMaxListeners) {
          ipfs1.pubsub.setMaxListeners(100);
        }
        if (ipfs2.pubsub.setMaxListeners) {
          ipfs2.pubsub.setMaxListeners(100);
        }
        const ipfs2Addr = ipfs2Id.addresses.find(ma => ma.nodeAddress().address === '127.0.0.1');
        if (!ipfs2Addr) {
          throw new Error('No address found');
        }
        return ipfs1.swarm.connect(ipfs2Addr);
      });
      it('should receive messages from a different node with floodsub', async function () {
        if (!env_js.isNode) {
          return this.skip();
        }
        const expectedString = 'should receive messages from a different node with floodsub';
        const topic = `floodsub-${ nanoid.nanoid() }`;
        const ipfs1 = (await factory.spawn({ ipfsOptions: { config: { Pubsub: { Router: 'floodsub' } } } })).api;
        const ipfs1Id = await ipfs1.id();
        const ipfs2 = (await factory.spawn({
          type: env_js.isWebWorker ? 'go' : undefined,
          ipfsOptions: { config: { Pubsub: { Router: 'floodsub' } } }
        })).api;
        const ipfs2Id = await ipfs2.id();
        await ipfs1.swarm.connect(ipfs2Id.addresses[0]);
        const msgStream1 = pushable__default["default"]();
        const msgStream2 = pushable__default["default"]();
        const sub1 = msg => {
          msgStream1.push(msg);
          msgStream1.end();
        };
        const sub2 = msg => {
          msgStream2.push(msg);
          msgStream2.end();
        };
        const abort1 = new AbortController();
        const abort2 = new AbortController();
        await Promise.all([
          ipfs1.pubsub.subscribe(topic, sub1, { signal: abort1.signal }),
          ipfs2.pubsub.subscribe(topic, sub2, { signal: abort2.signal })
        ]);
        await utils.waitForPeers(ipfs2, topic, [ipfs1Id.id], 30000);
        await ipfs2.pubsub.publish(topic, fromString.fromString(expectedString));
        const [sub1Msg] = await all__default["default"](msgStream1);
        chai_js.expect(toString.toString(sub1Msg.data)).to.be.eql(expectedString);
        chai_js.expect(sub1Msg.from).to.eql(ipfs2Id.id);
        const [sub2Msg] = await all__default["default"](msgStream2);
        chai_js.expect(toString.toString(sub2Msg.data)).to.be.eql(expectedString);
        chai_js.expect(sub2Msg.from).to.eql(ipfs2Id.id);
        abort1.abort();
        abort2.abort();
      });
      it('should receive messages from a different node', async () => {
        const expectedString = 'hello from the other side';
        const msgStream1 = pushable__default["default"]();
        const msgStream2 = pushable__default["default"]();
        const sub1 = msg => {
          msgStream1.push(msg);
          msgStream1.end();
        };
        const sub2 = msg => {
          msgStream2.push(msg);
          msgStream2.end();
        };
        await Promise.all([
          ipfs1.pubsub.subscribe(topic, sub1),
          ipfs2.pubsub.subscribe(topic, sub2)
        ]);
        await utils.waitForPeers(ipfs2, topic, [ipfs1Id.id], 30000);
        await delay__default["default"](5000);
        await ipfs2.pubsub.publish(topic, fromString.fromString(expectedString));
        const [sub1Msg] = await all__default["default"](msgStream1);
        chai_js.expect(toString.toString(sub1Msg.data)).to.be.eql(expectedString);
        chai_js.expect(sub1Msg.from).to.eql(ipfs2Id.id);
        const [sub2Msg] = await all__default["default"](msgStream2);
        chai_js.expect(toString.toString(sub2Msg.data)).to.be.eql(expectedString);
        chai_js.expect(sub2Msg.from).to.eql(ipfs2Id.id);
      });
      it('should round trip a non-utf8 binary buffer', async () => {
        const expectedHex = 'a36161636179656162830103056164a16466666666f4';
        const buffer = fromString.fromString(expectedHex, 'base16');
        const msgStream1 = pushable__default["default"]();
        const msgStream2 = pushable__default["default"]();
        const sub1 = msg => {
          msgStream1.push(msg);
          msgStream1.end();
        };
        const sub2 = msg => {
          msgStream2.push(msg);
          msgStream2.end();
        };
        await Promise.all([
          ipfs1.pubsub.subscribe(topic, sub1),
          ipfs2.pubsub.subscribe(topic, sub2)
        ]);
        await utils.waitForPeers(ipfs2, topic, [ipfs1Id.id], 30000);
        await delay__default["default"](5000);
        await ipfs2.pubsub.publish(topic, buffer);
        const [sub1Msg] = await all__default["default"](msgStream1);
        chai_js.expect(toString.toString(sub1Msg.data, 'base16')).to.be.eql(expectedHex);
        chai_js.expect(sub1Msg.from).to.eql(ipfs2Id.id);
        const [sub2Msg] = await all__default["default"](msgStream2);
        chai_js.expect(toString.toString(sub2Msg.data, 'base16')).to.be.eql(expectedHex);
        chai_js.expect(sub2Msg.from).to.eql(ipfs2Id.id);
      });
      it('should receive multiple messages', async () => {
        const outbox = [
          'hello',
          'world',
          'this',
          'is',
          'pubsub'
        ];
        const msgStream1 = pushable__default["default"]();
        const msgStream2 = pushable__default["default"]();
        let sub1Called = 0;
        const sub1 = msg => {
          msgStream1.push(msg);
          sub1Called++;
          if (sub1Called === outbox.length)
            msgStream1.end();
        };
        let sub2Called = 0;
        const sub2 = msg => {
          msgStream2.push(msg);
          sub2Called++;
          if (sub2Called === outbox.length)
            msgStream2.end();
        };
        await Promise.all([
          ipfs1.pubsub.subscribe(topic, sub1),
          ipfs2.pubsub.subscribe(topic, sub2)
        ]);
        await utils.waitForPeers(ipfs2, topic, [ipfs1Id.id], 30000);
        await delay__default["default"](5000);
        for (let i = 0; i < outbox.length; i++) {
          await ipfs2.pubsub.publish(topic, fromString.fromString(outbox[i]));
        }
        const sub1Msgs = await all__default["default"](msgStream1);
        sub1Msgs.forEach(msg => chai_js.expect(msg.from).to.eql(ipfs2Id.id));
        const inbox1 = sub1Msgs.map(msg => toString.toString(msg.data));
        chai_js.expect(inbox1.sort()).to.eql(outbox.sort());
        const sub2Msgs = await all__default["default"](msgStream2);
        sub2Msgs.forEach(msg => chai_js.expect(msg.from).to.eql(ipfs2Id.id));
        const inbox2 = sub2Msgs.map(msg => toString.toString(msg.data));
        chai_js.expect(inbox2.sort()).to.eql(outbox.sort());
      });
      it('should send/receive 100 messages', async function () {
        this.timeout(2 * 60 * 1000);
        const msgBase = 'msg - ';
        const count = 100;
        const msgStream = pushable__default["default"]();
        let subCalled = 0;
        const sub = msg => {
          msgStream.push(msg);
          subCalled++;
          if (subCalled === count)
            msgStream.end();
        };
        await Promise.all([
          ipfs1.pubsub.subscribe(topic, sub),
          ipfs2.pubsub.subscribe(topic, () => {
          })
        ]);
        await utils.waitForPeers(ipfs1, topic, [ipfs2Id.id], 30000);
        await delay__default["default"](5000);
        const startTime = new Date().getTime();
        for (let i = 0; i < count; i++) {
          const msgData = fromString.fromString(msgBase + i);
          await ipfs2.pubsub.publish(topic, msgData);
        }
        const msgs = await all__default["default"](msgStream);
        const duration = new Date().getTime() - startTime;
        const opsPerSec = Math.floor(count / (duration / 1000));
        console.log(`Send/Receive 100 messages took: ${ duration } ms, ${ opsPerSec } ops / s`);
        msgs.forEach(msg => {
          chai_js.expect(msg.from).to.eql(ipfs2Id.id);
          chai_js.expect(toString.toString(msg.data).startsWith(msgBase)).to.be.true();
        });
      });
      it('should receive messages from a different node on lots of topics', async () => {
        this.timeout(5 * 60 * 1000);
        const numTopics = 20;
        const topics = [];
        const expectedStrings = [];
        const msgStreams = [];
        for (let i = 0; i < numTopics; i++) {
          const topic = `pubsub-topic-${ Math.random() }`;
          topics.push(topic);
          const msgStream1 = pushable__default["default"]();
          const msgStream2 = pushable__default["default"]();
          msgStreams.push({
            msgStream1,
            msgStream2
          });
          const sub1 = msg => {
            msgStream1.push(msg);
            msgStream1.end();
          };
          const sub2 = msg => {
            msgStream2.push(msg);
            msgStream2.end();
          };
          await Promise.all([
            ipfs1.pubsub.subscribe(topic, sub1),
            ipfs2.pubsub.subscribe(topic, sub2)
          ]);
          await utils.waitForPeers(ipfs2, topic, [ipfs1Id.id], 30000);
        }
        await delay__default["default"](5000);
        for (let i = 0; i < numTopics; i++) {
          const expectedString = `hello pubsub ${ Math.random() }`;
          expectedStrings.push(expectedString);
          await ipfs2.pubsub.publish(topics[i], fromString.fromString(expectedString));
        }
        for (let i = 0; i < numTopics; i++) {
          const [sub1Msg] = await all__default["default"](msgStreams[i].msgStream1);
          chai_js.expect(toString.toString(sub1Msg.data)).to.equal(expectedStrings[i]);
          chai_js.expect(sub1Msg.from).to.eql(ipfs2Id.id);
          const [sub2Msg] = await all__default["default"](msgStreams[i].msgStream2);
          chai_js.expect(toString.toString(sub2Msg.data)).to.equal(expectedStrings[i]);
          chai_js.expect(sub2Msg.from).to.eql(ipfs2Id.id);
        }
      });
      it('should unsubscribe multiple handlers', async () => {
        this.timeout(2 * 60 * 1000);
        const topic = `topic-${ Math.random() }`;
        const handler1 = sinon__default["default"].stub();
        const handler2 = sinon__default["default"].stub();
        await Promise.all([
          ipfs1.pubsub.subscribe(topic, sinon__default["default"].stub()),
          ipfs2.pubsub.subscribe(topic, handler1),
          ipfs2.pubsub.subscribe(topic, handler2)
        ]);
        await utils.waitForPeers(ipfs1, topic, [ipfs2Id.id], 30000);
        chai_js.expect(handler1).to.have.property('callCount', 0);
        chai_js.expect(handler2).to.have.property('callCount', 0);
        await ipfs1.pubsub.publish(topic, fromString.fromString('hello world 1'));
        await delay__default["default"](1000);
        chai_js.expect(handler1).to.have.property('callCount', 1);
        chai_js.expect(handler2).to.have.property('callCount', 1);
        await ipfs2.pubsub.unsubscribe(topic);
        await ipfs1.pubsub.publish(topic, fromString.fromString('hello world 2'));
        await delay__default["default"](1000);
        chai_js.expect(handler1).to.have.property('callCount', 1);
        chai_js.expect(handler2).to.have.property('callCount', 1);
      });
      it('should unsubscribe individual handlers', async () => {
        this.timeout(2 * 60 * 1000);
        const topic = `topic-${ Math.random() }`;
        const handler1 = sinon__default["default"].stub();
        const handler2 = sinon__default["default"].stub();
        await Promise.all([
          ipfs1.pubsub.subscribe(topic, sinon__default["default"].stub()),
          ipfs2.pubsub.subscribe(topic, handler1),
          ipfs2.pubsub.subscribe(topic, handler2)
        ]);
        await utils.waitForPeers(ipfs1, topic, [ipfs2Id.id], 30000);
        chai_js.expect(handler1).to.have.property('callCount', 0);
        chai_js.expect(handler2).to.have.property('callCount', 0);
        await ipfs1.pubsub.publish(topic, fromString.fromString('hello world 1'));
        await delay__default["default"](1000);
        chai_js.expect(handler1).to.have.property('callCount', 1);
        chai_js.expect(handler2).to.have.property('callCount', 1);
        await ipfs2.pubsub.unsubscribe(topic, handler1);
        await ipfs1.pubsub.publish(topic, fromString.fromString('hello world 2'));
        await delay__default["default"](1000);
        chai_js.expect(handler1).to.have.property('callCount', 1);
        chai_js.expect(handler2).to.have.property('callCount', 2);
      });
    });
  });
}

exports.testSubscribe = testSubscribe;
