text: 'use strict';
  
  const assert = require('assert').strict;
  const common = require('../common');
  const padManager = require('../../../node/db/PadManager');
  const plugins = require('../../../static/js/pluginfw/plugin_defs');
  const readOnlyManager = require('../../../node/db/ReadOnlyManager');
  
  describe(__filename, function () {
    let agent;
    let pad;
    let padId;
    let roPadId;
    let rev;
    let socket;
    let roSocket;
    const backups = {};
  
    before(async function () {
      agent = await common.init();
    });
  
    beforeEach(async function () {
      backups.hooks = {handleMessageSecurity: plugins.hooks.handleMessageSecurity};
      plugins.hooks.handleMessageSecurity = [];
      padId = common.randomString();
      assert(!await padManager.doesPadExist(padId));
      pad = await padManager.getPad(padId, 'dummy text\n');
      await pad.setText('\n'); // Make sure the pad is created.
      assert.equal(pad.text(), '\n');
      let res = await agent.get(`/p/${padId}`).expect(200);
      socket = await common.connect(res);
      const {type, data: clientVars} = await common.handshake(socket, padId);
      assert.equal(type, 'CLIENT_VARS');
      rev = clientVars.collab_client_vars.rev;
  
      roPadId = await readOnlyManager.getReadOnlyId(padId);
      res = await agent.get(`/p/${roPadId}`).expect(200);
      roSocket = await common.connect(res);
      await common.handshake(roSocket, roPadId, `t.${common.randomString(8)}`);
    });
  
    afterEach(async function () {
      Object.assign(plugins.hooks, backups.hooks);
      if (socket != null) socket.close();
      socket = null;
      if (roSocket != null) roSocket.close();
      roSocket = null;
      if (pad != null) await pad.remove();
      pad = null;
    });
  
    describe('CHANGESET_REQ', function () {
      it('users are unable to read changesets from other pads', async function () {
        const otherPadId = `${padId}other`;
        assert(!await padManager.doesPadExist(otherPadId));
        const otherPad = await padManager.getPad(otherPadId, 'other text\n');
        try {
          await otherPad.setText('other text\n');
          const resP = common.waitForSocketEvent(roSocket, 'message');
          await common.sendMessage(roSocket, {
            component: 'pad',
            padId: otherPadId, // The server should ignore this.
            type: 'CHANGESET_REQ',
            data: {
              granularity: 1,
              start: 0,
              requestID: 'requestId',
            },
          });
          const res = await resP;
          assert.equal(res.type, 'CHANGESET_REQ');
          assert.equal(res.data.requestID, 'requestId');
          // Should match padId's text, not otherPadId's text.
          assert.match(res.data.forwardsChangesets[0], /^[^$]*\$dummy text\n/);
        } finally {
          await otherPad.remove();
        }
      });
    });
  
    describe('USER_CHANGES', function () {
      const sendUserChanges =
          async (socket, cs) => await common.sendUserChanges(socket, {baseRev: rev, changeset: cs});
      const assertAccepted = async (socket, wantRev) => {
        await common.waitForAcceptCommit(socket, wantRev);
        rev = wantRev;
      };
      const assertRejected = async (socket) => {
        const msg = await common.waitForSocketEvent(socket, 'message');
        assert.deepEqual(msg, {disconnect: 'badChangeset'});
      };
  
      it('changes are applied', async function () {
        await Promise.all([
          assertAccepted(socket, rev + 1),
          sendUserChanges(socket, 'Z:1>5+5$hello'),
        ]);
        assert.equal(pad.text(), 'hello\n');
      });
  
      it('bad changeset is rejected', async function () {
        await Promise.all([
          assertRejected(socket),
          sendUserChanges(socket, 'this is not a valid changeset'),
        ]);
      });
  
      it('retransmission is accepted, has no effect', async function () {
        const cs = 'Z:1>5+5$hello';
        await Promise.all([
          assertAccepted(socket, rev + 1),
          sendUserChanges(socket, cs),
        ]);
        --rev;
        await Promise.all([
          assertAccepted(socket, rev + 1),
          sendUserChanges(socket, cs),
        ]);
        assert.equal(pad.text(), 'hello\n');
      });
  
      it('identity changeset is accepted, has no effect', async function () {
        await Promise.all([
          assertAccepted(socket, rev + 1),
          sendUserChanges(socket, 'Z:1>5+5$hello'),
        ]);
        await Promise.all([
          assertAccepted(socket, rev),
          sendUserChanges(socket, 'Z:6>0$'),
        ]);
        assert.equal(pad.text(), 'hello\n');
      });
  
      it('non-identity changeset with no net change is accepted, has no effect', async function () {
        await Promise.all([
          assertAccepted(socket, rev + 1),
          sendUserChanges(socket, 'Z:1>5+5$hello'),
        ]);
        await Promise.all([
          assertAccepted(socket, rev),
          sendUserChanges(socket, 'Z:6>0-5+5$hello'),
        ]);
        assert.equal(pad.text(), 'hello\n');
      });
  
      it('handleMessageSecurity can grant one-time write access', async function () {
        const cs = 'Z:1>5+5$hello';
        // First try to send a change and verify that it was dropped.
        await sendUserChanges(roSocket, cs);
        // sendUserChanges() waits for message ack, so if the message was accepted then head should
        // have already incremented by the time we get here.
        assert.equal(pad.head, rev); // Not incremented.
  
        // Now allow the change.
        plugins.hooks.handleMessageSecurity.push({hook_fn: () => 'permitOnce'});
        await Promise.all([
          assertAccepted(roSocket, rev + 1),
          sendUserChanges(roSocket, cs),
        ]);
        assert.equal(pad.text(), 'hello\n');
  
        // The next change should be dropped.
        plugins.hooks.handleMessageSecurity = [];
        await sendUserChanges(roSocket, 'Z:6>6=5+6$ world');
        assert.equal(pad.head, rev); // Not incremented.
        assert.equal(pad.text(), 'hello\n');
      });
    });
  });
  