'use strict';
const expect = require('chai').expect;
const mock = require('mongodb-mock-server');
const genClusterTime = require('../common').genClusterTime;
const sessionCleanupHandler = require('../common').sessionCleanupHandler;

const core = require('../../../../lib/core');
const Server = core.Server;
const Long = core.BSON.Long;
const ObjectId = core.BSON.ObjectId;
const Timestamp = core.BSON.Timestamp;
const ClientSession = core.Sessions.ClientSession;
const ServerSessionPool = core.Sessions.ServerSessionPool;

const test = {};
describe('Sessions (Single)', function() {
  afterEach(() => mock.cleanup());
  beforeEach(() => {
    return mock.createServer().then(mockServer => {
      test.server = mockServer;
    });
  });

  it('should recognize and set `clusterTime` on the topology', {
    metadata: { requires: { topology: 'single' } },
    test: function(done) {
      const clusterTime = genClusterTime(Date.now());
      test.server.setMessageHandler(request => {
        request.reply(
          Object.assign({}, mock.DEFAULT_ISMASTER, {
            $clusterTime: clusterTime
          })
        );
      });

      const client = new Server(test.server.address());
      client.on('error', done);
      client.once('connect', () => {
        expect(client.clusterTime).to.eql(clusterTime);
        client.destroy();
        done();
      });

      client.connect();
    }
  });

  it('should track the highest `$clusterTime` seen', {
    metadata: { requires: { topology: 'single' } },
    test: function(done) {
      const clusterTime = genClusterTime(Date.now()),
        futureClusterTime = genClusterTime(Date.now() + 10 * 60 * 1000);

      test.server.setMessageHandler(request => {
        const doc = request.document;
        if (doc.ismaster) {
          request.reply(
            Object.assign({}, mock.DEFAULT_ISMASTER, {
              $clusterTime: clusterTime
            })
          );
        } else if (doc.insert) {
          request.reply({
            ok: 1,
            n: [],
            lastOp: new Date(),
            $clusterTime: futureClusterTime
          });
        }
      });

      const client = new Server(test.server.address());
      client.on('error', done);
      client.once('connect', () => {
        expect(client.clusterTime).to.exist;
        expect(client.clusterTime).to.eql(clusterTime);

        client.insert('test.test', [{ created: new Date() }], function(err) {
          expect(err).to.not.exist;
          expect(client.clusterTime).to.exist;
          expect(client.clusterTime).to.not.eql(clusterTime);
          expect(client.clusterTime).to.eql(futureClusterTime);

          client.destroy();
          done();
        });
      });

      client.connect();
    }
  });

  it('should track the highest `$clusterTime` seen, and store it on a session if available', {
    metadata: { requires: { topology: 'single' } },
    test: function(_done) {
      const clusterTime = genClusterTime(Date.now()),
        futureClusterTime = genClusterTime(Date.now() + 10 * 60 * 1000);

      test.server.setMessageHandler(request => {
        const doc = request.document;
        if (doc.ismaster) {
          request.reply(
            Object.assign({}, mock.DEFAULT_ISMASTER, {
              $clusterTime: clusterTime
            })
          );
        } else if (doc.insert) {
          request.reply({
            ok: 1,
            n: [],
            lastOp: new Date(),
            $clusterTime: futureClusterTime
          });
        }
      });

      const client = new Server(test.server.address());
      const sessionPool = new ServerSessionPool(client);
      const session = new ClientSession(client, sessionPool);
      const done = sessionCleanupHandler(session, sessionPool, _done);

      client.on('error', done);
      client.once('connect', () => {
        expect(client.clusterTime).to.exist;
        expect(client.clusterTime).to.eql(clusterTime);

        client.insert('test.test', [{ created: new Date() }], { session: session }, function(err) {
          expect(err).to.not.exist;
          expect(client.clusterTime).to.exist;
          expect(client.clusterTime).to.not.eql(clusterTime);
          expect(client.clusterTime).to.eql(futureClusterTime);
          expect(session.clusterTime).to.eql(futureClusterTime);

          client.destroy();
          done();
        });
      });

      client.connect();
    }
  });

  it('should send `clusterTime` on outgoing messages', {
    metadata: { requires: { topology: 'single' } },
    test: function(done) {
      const clusterTime = genClusterTime(Date.now());
      let sentIsMaster = false,
        command = null;

      test.server.setMessageHandler(request => {
        if (sentIsMaster) {
          command = request.document;
          request.reply({ ok: 1 });
          return;
        }

        sentIsMaster = true;
        request.reply(
          Object.assign({}, mock.DEFAULT_ISMASTER, {
            maxWireVersion: 6,
            $clusterTime: clusterTime
          })
        );
      });

      const client = new Server(test.server.address());
      client.on('error', done);
      client.once('connect', () => {
        client.command('admin.$cmd', { ping: 1 }, err => {
          expect(err).to.not.exist;
          expect(command.$clusterTime).to.eql(clusterTime);

          client.destroy();
          done();
        });
      });

      client.connect();
    }
  });

  it('should send the highest `clusterTime` between topology and session if it exists', {
    metadata: { requires: { topology: 'single' } },
    test: function(_done) {
      const clusterTime = genClusterTime(Date.now()),
        futureClusterTime = genClusterTime(Date.now() + 10 * 60 * 1000);

      let sentIsMaster = false,
        command = null;
      test.server.setMessageHandler(request => {
        if (sentIsMaster) {
          command = request.document;
          request.reply({ ok: 1 });
          return;
        }

        sentIsMaster = true;
        request.reply(
          Object.assign({}, mock.DEFAULT_ISMASTER, {
            maxWireVersion: 6,
            $clusterTime: clusterTime
          })
        );
      });

      const client = new Server(test.server.address());
      const sessionPool = new ServerSessionPool(client);
      const session = new ClientSession(client, sessionPool, {
        initialClusterTime: futureClusterTime
      });

      const done = sessionCleanupHandler(session, sessionPool, _done);

      client.on('error', done);
      client.once('connect', () => {
        client.command('admin.$cmd', { ping: 1 }, { session: session }, err => {
          expect(err).to.not.exist;
          expect(command.$clusterTime).to.eql(futureClusterTime);
          client.destroy();
          done();
        });
      });

      client.connect();
    }
  });

  it('should return server sessions to the pool on `endSession`', {
    metadata: { requires: { topology: 'single' } },
    test: function(_done) {
      let sentIsMaster = false;
      test.server.setMessageHandler(request => {
        if (sentIsMaster) {
          request.reply({ ok: 1 });
          return;
        }

        sentIsMaster = true;
        request.reply(
          Object.assign({}, mock.DEFAULT_ISMASTER, {
            maxWireVersion: 6,
            logicalSessionTimeoutMinutes: 10
          })
        );
      });

      const client = new Server(test.server.address());
      const sessionPool = new ServerSessionPool(client);
      const session = new ClientSession(client, sessionPool);
      const clientServerSession = session.serverSession;
      const done = sessionCleanupHandler(session, sessionPool, _done);

      client.on('error', done);
      client.once('connect', () => {
        client.command('admin.$cmd', { ping: 1 }, { session: session }, err => {
          expect(err).to.not.exist;

          session.endSession(err => {
            expect(err).to.not.exist;
            expect(session.hasEnded).to.be.true;
            expect(sessionPool.sessions).to.have.length(1);
            expect(sessionPool.sessions[0]).to.eql(clientServerSession);

            client.destroy();
            done();
          });
        });
      });

      client.connect();
    }
  });

  it('should default `logicalSessionTimeoutMinutes` to `null`', {
    metadata: { requires: { topology: 'single' } },
    test: function() {
      const single = new Server();
      expect(single.logicalSessionTimeoutMinutes).to.equal(null);
    }
  });

  it('should track `logicalSessionTimeoutMinutes`', {
    metadata: { requires: { topology: 'single' } },
    test: function(done) {
      test.server.setMessageHandler(request => {
        request.reply(
          Object.assign({}, mock.DEFAULT_ISMASTER, {
            logicalSessionTimeoutMinutes: 10
          })
        );
      });

      var client = new Server(test.server.address());
      client.on('error', done);
      client.once('connect', () => {
        expect(client.logicalSessionTimeoutMinutes).to.equal(10);
        client.destroy();
        done();
      });

      client.connect();
    }
  });

  it(
    'should add `lsid` to commands sent to the server, and update the session `lastUse` when a session is provided',
    {
      metadata: { requires: { topology: 'single' } },
      test: function(_done) {
        const client = new Server(test.server.address());
        const sessionPool = new ServerSessionPool(client);
        const session = new ClientSession(client, sessionPool);
        const initialLastUse = session.serverSession.lastUse;
        const done = sessionCleanupHandler(session, sessionPool, _done);

        let sentIsMaster = false,
          command = null;
        test.server.setMessageHandler(request => {
          if (sentIsMaster) {
            command = request.document;
            request.reply({ ok: 1 });
            return;
          }

          sentIsMaster = true;
          request.reply(
            Object.assign({}, mock.DEFAULT_ISMASTER, {
              maxWireVersion: 6,
              logicalSessionTimeoutMinutes: 10
            })
          );
        });

        client.on('error', done);
        client.once('connect', () => {
          // we want to run this a little bit later just in case it runs immediately
          setTimeout(() => {
            client.command('admin.$cmd', { ping: 1 }, { session: session }, err => {
              expect(err).to.not.exist;
              expect(command.lsid).to.eql(session.id);
              expect(session.serverSession.lastUse).to.not.eql(initialLastUse);

              client.destroy();
              done();
            });
          }, 250);
        });

        client.connect();
      }
    }
  );

  it('should use the same session for all getMore issued by a cursor', {
    metadata: { requires: { topology: 'single' } },
    test: function(_done) {
      const client = new Server(test.server.address());
      const sessionPool = new ServerSessionPool(client);
      const session = new ClientSession(client, sessionPool);
      const done = sessionCleanupHandler(session, sessionPool, _done);

      let commands = [];
      test.server.setMessageHandler(request => {
        const doc = request.document;
        if (doc.ismaster) {
          request.reply(
            Object.assign({}, mock.DEFAULT_ISMASTER, {
              maxWireVersion: 6
            })
          );
        } else if (doc.find) {
          commands.push(doc);
          request.reply({
            cursor: {
              id: Long.fromNumber(1),
              ns: 'test.t',
              firstBatch: []
            },
            ok: 1
          });
        } else if (doc.getMore) {
          commands.push(doc);
          request.reply({
            cursor: {
              id: Long.ZERO,
              ns: 'test.t',
              nextBatch: [{ _id: new ObjectId(), a: 1 }]
            },
            ok: 1
          });
        }
      });

      client.on('error', done);
      client.once('connect', () => {
        const cursor = client.cursor(
          'test.test',
          {
            find: 'test',
            query: {},
            batchSize: 2
          },
          {
            session: session
          }
        );

        // Execute next
        cursor._next(function(err) {
          expect(err).to.not.exist;
          expect(commands[0].lsid).to.eql(session.id);

          cursor._next(function(err) {
            expect(err).to.not.exist;
            expect(commands[1].lsid).to.eql(session.id);

            client.destroy();
            done();
          });
        });
      });

      client.connect();
    }
  });

  it('should use the same session for any killCursor issued by a cursor', {
    metadata: { requires: { topology: 'single' } },
    test: function(_done) {
      const client = new Server(test.server.address());
      const sessionPool = new ServerSessionPool(client);
      const session = new ClientSession(client, sessionPool);
      const done = sessionCleanupHandler(session, sessionPool, _done);

      let commands = [];
      test.server.setMessageHandler(request => {
        const doc = request.document;
        if (doc.ismaster) {
          request.reply(
            Object.assign({}, mock.DEFAULT_ISMASTER, {
              maxWireVersion: 6
            })
          );
        } else if (doc.find) {
          commands.push(doc);
          request.reply({
            cursor: {
              id: Long.fromNumber(1),
              ns: 'test.t',
              firstBatch: []
            },
            ok: 1
          });
        } else if (doc.getMore) {
          commands.push(doc);
          request.reply({
            cursor: {
              id: Long.fromNumber(1),
              ns: 'test.t',
              nextBatch: [{ _id: new ObjectId(), a: 1 }]
            },
            ok: 1
          });
        } else if (doc.killCursors) {
          commands.push(doc);
          request.reply({ ok: 1 });
        }
      });

      client.on('error', done);
      client.once('connect', () => {
        const cursor = client.cursor(
          'test.test',
          {
            find: 'test',
            query: {},
            batchSize: 2
          },
          {
            session: session
          }
        );

        // Execute next
        cursor._next(function(err) {
          expect(err).to.not.exist;

          cursor.kill(err => {
            expect(err).to.not.exist;
            commands.forEach(command => expect(command.lsid).to.eql(session.id));

            client.destroy();
            done();
          });
        });
      });

      client.connect();
    }
  });

  it('should not hang on endSession when topology is closed', {
    metadata: { requires: { topology: 'single' } },
    test: function(_done) {
      const client = new Server(test.server.address());
      const sessionPool = new ServerSessionPool(client);
      const session = new ClientSession(client, sessionPool);
      const done = sessionCleanupHandler(session, sessionPool, _done);

      test.server.setMessageHandler(request => {
        const doc = request.document;
        if (doc.ismaster) {
          request.reply(
            Object.assign({}, mock.DEFAULT_ISMASTER, {
              maxWireVersion: 6
            })
          );
        }
      });

      client.on('error', done);
      client.once('connect', () => {
        client.destroy({ force: true });
        session.endSession(() => done());
      });

      client.connect();
    }
  });

  it('should not allow use of an expired session', {
    metadata: { requires: { topology: 'single' } },
    test: function(_done) {
      const client = new Server(test.server.address());
      const sessionPool = new ServerSessionPool(client);
      const session = new ClientSession(client, sessionPool);
      const done = sessionCleanupHandler(session, sessionPool, _done);

      test.server.setMessageHandler(request => {
        const doc = request.document;
        if (doc.ismaster) {
          request.reply(
            Object.assign({}, mock.DEFAULT_ISMASTER, {
              maxWireVersion: 6
            })
          );
        } else if (doc.endSessions) {
          request.reply({ ok: 1 });
        }
      });

      client.on('error', done);
      client.once('connect', () => {
        session.endSession(() => {
          client.command('admin.$cmd', { ping: 1 }, { session: session }, err => {
            expect(err).to.exist;

            client.destroy();
            done();
          });
        });
      });

      client.connect();
    }
  });

  it.skip('should not allow use of session object across clients', {
    metadata: { requires: { topology: 'single' } },
    test: function(_done) {
      const client = new Server(test.server.address());
      const client2 = new Server(test.server.address());
      const sessionPool = new ServerSessionPool(client);
      const session = new ClientSession(client, sessionPool);
      const done = sessionCleanupHandler(session, sessionPool, _done);

      test.server.setMessageHandler(request => {
        const doc = request.document;
        if (doc.ismaster) {
          request.reply(
            Object.assign({}, mock.DEFAULT_ISMASTER, {
              maxWireVersion: 6
            })
          );
        } else if (doc.ping) {
          request.reply({ ok: 1 });
        } else if (doc.endSessions) {
          request.reply({ ok: 1 });
        }
      });

      client2.on('error', done);
      client2.once('connect', () => {
        client2.command('admin.$cmd', { ping: 1 }, { session: session }, err => {
          expect(err).to.exist;
          done();
        });
      });

      client2.connect();
    }
  });

  it('should track the highest `operationTime` seen, if causal consistency is enabled', {
    metadata: { requires: { topology: 'single' } },
    test: function(_done) {
      const client = new Server(test.server.address()),
        sessionPool = new ServerSessionPool(client),
        session = new ClientSession(client, sessionPool, { causalConsistency: true }),
        insertOperationTime1 = Timestamp.fromNumber(Date.now()),
        insertOperationTime2 = Timestamp.fromNumber(Date.now() + 10 * 60 * 1000);
      const done = sessionCleanupHandler(session, sessionPool, _done);

      let insertCount = 0;
      test.server.setMessageHandler(request => {
        const doc = request.document;
        if (doc.ismaster) {
          request.reply(mock.DEFAULT_ISMASTER_36);
        } else if (doc.insert) {
          request.reply({
            ok: 1,
            operationTime: insertCount === 0 ? insertOperationTime1 : insertOperationTime2
          });

          insertCount++;
        }
      });

      client.on('error', done);
      client.once('connect', () => {
        client.insert('db.test', [{ a: 42 }], { session: session }, err => {
          expect(err).to.not.exist;
          expect(session.operationTime).to.exist;
          expect(session.operationTime).to.eql(insertOperationTime1);

          client.insert('db.test', [{ b: 52 }], { session: session }, err => {
            expect(err).to.not.exist;
            expect(session.operationTime).to.exist;
            expect(session.operationTime).to.eql(insertOperationTime2);

            client.destroy();
            done();
          });
        });
      });

      client.connect();
    }
  });

  it('should emit an `ended` signal when the session is ended', {
    metadata: { requires: { topology: 'single' } },
    test: function(_done) {
      const client = new Server(test.server.address());
      const sessionPool = new ServerSessionPool(client);
      const session = new ClientSession(client, sessionPool);
      const done = sessionCleanupHandler(session, sessionPool, _done);

      test.server.setMessageHandler(request => {
        const doc = request.document;
        if (doc.ismaster) {
          request.reply(
            Object.assign({}, mock.DEFAULT_ISMASTER, {
              maxWireVersion: 6
            })
          );
        } else if (doc.endSessions) {
          request.reply({ ok: 1 });
        }
      });

      client.on('error', done);
      client.once('connect', () => {
        let endedSignalReceived = false;
        session.once('ended', () => (endedSignalReceived = true));
        session.endSession(() => {
          expect(endedSignalReceived).to.be.true;

          client.destroy();
          done();
        });
      });

      client.connect();
    }
  });
});
