text: var Backend = require('../../lib/backend');
  var expect = require('chai').expect;
  var MemoryDb = require('../../lib/db/memory');
  var MemoryMilestoneDb = require('../../lib/milestone-db/memory');
  var sinon = require('sinon');
  var async = require('async');
  var json0v2 = require('ot-json0-v2').type;
  var types = require('../../lib/types');
  
  describe('SnapshotVersionRequest', function() {
    var backend;
  
    beforeEach(function() {
      backend = new Backend();
    });
  
    afterEach(function(done) {
      backend.close(done);
    });
  
    describe('a document with some simple versions', function() {
      var v0 = {
        id: 'don-quixote',
        v: 0,
        type: null,
        data: undefined,
        m: null
      };
  
      var v1 = {
        id: 'don-quixote',
        v: 1,
        type: 'http://sharejs.org/types/JSONv0',
        data: {
          title: 'Don Quixote'
        },
        m: null
      };
  
      var v2 = {
        id: 'don-quixote',
        v: 2,
        type: 'http://sharejs.org/types/JSONv0',
        data: {
          title: 'Don Quixote',
          author: 'Miguel de Cervante'
        },
        m: null
      };
  
      var v3 = {
        id: 'don-quixote',
        v: 3,
        type: 'http://sharejs.org/types/JSONv0',
        data: {
          title: 'Don Quixote',
          author: 'Miguel de Cervantes'
        },
        m: null
      };
  
      beforeEach(function(done) {
        var doc = backend.connect().get('books', 'don-quixote');
        doc.create({title: 'Don Quixote'}, function(error) {
          if (error) return done(error);
          doc.submitOp({p: ['author'], oi: 'Miguel de Cervante'}, function(error) {
            if (error) return done(error);
            doc.submitOp({p: ['author'], od: 'Miguel de Cervante', oi: 'Miguel de Cervantes'}, done);
          });
        });
      });
  
      it('fetches v1', function(done) {
        backend.connect().fetchSnapshot('books', 'don-quixote', 1, function(error, snapshot) {
          if (error) return done(error);
          expect(snapshot).to.eql(v1);
          done();
        });
      });
  
      it('fetches v2', function(done) {
        backend.connect().fetchSnapshot('books', 'don-quixote', 2, function(error, snapshot) {
          if (error) return done(error);
          expect(snapshot).to.eql(v2);
          done();
        });
      });
  
      it('fetches v3', function(done) {
        backend.connect().fetchSnapshot('books', 'don-quixote', 3, function(error, snapshot) {
          if (error) return done(error);
          expect(snapshot).to.eql(v3);
          done();
        });
      });
  
      it('returns an empty snapshot if the version is 0', function(done) {
        backend.connect().fetchSnapshot('books', 'don-quixote', 0, function(error, snapshot) {
          if (error) return done(error);
          expect(snapshot).to.eql(v0);
          done();
        });
      });
  
      it('throws if the version is undefined', function() {
        var fetch = function() {
          backend.connect().fetchSnapshot('books', 'don-quixote', undefined, function() {});
        };
  
        expect(fetch).to.throw(Error);
      });
  
      it('fetches the latest version when the optional version is not provided', function(done) {
        backend.connect().fetchSnapshot('books', 'don-quixote', function(error, snapshot) {
          if (error) return done(error);
          expect(snapshot).to.eql(v3);
          done();
        });
      });
  
      it('throws without a callback', function() {
        var fetch = function() {
          backend.connect().fetchSnapshot('books', 'don-quixote');
        };
  
        expect(fetch).to.throw(Error);
      });
  
      it('throws if the version is -1', function() {
        var fetch = function() {
          backend.connect().fetchSnapshot('books', 'don-quixote', -1, function() {});
        };
  
        expect(fetch).to.throw(Error);
      });
  
      it('errors if the version is a string', function() {
        var fetch = function() {
          backend.connect().fetchSnapshot('books', 'don-quixote', 'foo', function() { });
        };
  
        expect(fetch).to.throw(Error);
      });
  
      it('errors if asking for a version that does not exist', function(done) {
        backend.connect().fetchSnapshot('books', 'don-quixote', 4, function(error, snapshot) {
          expect(error.code).to.equal('ERR_OP_VERSION_NEWER_THAN_CURRENT_SNAPSHOT');
          expect(snapshot).to.equal(undefined);
          done();
        });
      });
  
      it('returns an empty snapshot if trying to fetch a non-existent document', function(done) {
        backend.connect().fetchSnapshot('books', 'does-not-exist', 0, function(error, snapshot) {
          if (error) return done(error);
          expect(snapshot).to.eql({
            id: 'does-not-exist',
            v: 0,
            type: null,
            data: undefined,
            m: null
          });
          done();
        });
      });
  
      it('starts pending, and finishes not pending', function(done) {
        var connection = backend.connect();
  
        connection.fetchSnapshot('books', 'don-quixote', null, function(error) {
          if (error) return done(error);
          expect(connection.hasPending()).to.equal(false);
          done();
        });
  
        expect(connection.hasPending()).to.equal(true);
      });
  
      it('deletes the request from the connection', function(done) {
        var connection = backend.connect();
  
        connection.fetchSnapshot('books', 'don-quixote', function(error) {
          if (error) return done(error);
          expect(connection._snapshotRequests).to.eql({});
          done();
        });
  
        expect(connection._snapshotRequests).to.not.eql({});
      });
  
      it('emits a ready event when done', function(done) {
        var connection = backend.connect();
  
        connection.fetchSnapshot('books', 'don-quixote', function(error) {
          if (error) return done(error);
        });
  
        var snapshotRequest = connection._snapshotRequests[1];
        snapshotRequest.on('ready', done);
      });
  
      it('fires the connection.whenNothingPending', function(done) {
        var connection = backend.connect();
        var snapshotFetched = false;
  
        connection.fetchSnapshot('books', 'don-quixote', function(error) {
          if (error) return done(error);
          snapshotFetched = true;
        });
  
        connection.whenNothingPending(function() {
          expect(snapshotFetched).to.equal(true);
          done();
        });
      });
  
      it('can drop its connection and reconnect, and the callback is just called once', function(done) {
        var connection = backend.connect();
  
        // Here we hook into middleware to make sure that we get the following flow:
        // - Connection established
        // - Connection attempts to fetch a snapshot
        // - Snapshot is about to be returned
        // - Connection is dropped before the snapshot is returned
        // - Connection is re-established
        // - Connection re-requests the snapshot
        // - This time the fetch operation is allowed to complete (because of the connectionInterrupted flag)
        // - The done callback is called just once (if it's called twice, then mocha will complain)
        var connectionInterrupted = false;
        backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function(request, callback) {
          if (!connectionInterrupted) {
            connection.close();
            backend.connect(connection);
            connectionInterrupted = true;
          }
  
          callback();
        });
  
        connection.fetchSnapshot('books', 'don-quixote', done);
      });
  
      it('cannot send the same request twice over a connection', function(done) {
        var connection = backend.connect();
  
        // Here we hook into the middleware to make sure that we get the following flow:
        // - Attempt to fetch a snapshot
        // - The snapshot request is temporarily stored on the Connection
        // - Snapshot is about to be returned (ie the request was already successfully sent)
        // - We attempt to resend the request again
        // - The done callback is call just once, because the second request does not get sent
        //   (if the done callback is called twice, then mocha will complain)
        var hasResent = false;
        backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function(request, callback) {
          if (!hasResent) {
            connection._snapshotRequests[1]._onConnectionStateChanged();
            hasResent = true;
          }
  
          callback();
        });
  
        connection.fetchSnapshot('books', 'don-quixote', done);
      });
  
      describe('readSnapshots middleware', function() {
        it('triggers the middleware', function(done) {
          backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots,
            function(request) {
              expect(request.snapshots[0]).to.eql(v3);
              expect(request.snapshotType).to.equal(backend.SNAPSHOT_TYPES.byVersion);
              done();
            }
          );
  
          backend.connect().fetchSnapshot('books', 'don-quixote', 3, function() { });
        });
  
        it('can have its snapshot manipulated in the middleware', function(done) {
          backend.middleware[backend.MIDDLEWARE_ACTIONS.readSnapshots] = [
            function(request, callback) {
              request.snapshots[0].data.title = 'Alice in Wonderland';
              callback();
            }
          ];
  
          backend.connect().fetchSnapshot('books', 'don-quixote', function(error, snapshot) {
            if (error) return done(error);
            expect(snapshot.data.title).to.equal('Alice in Wonderland');
            done();
          });
        });
  
        it('respects errors thrown in the middleware', function(done) {
          backend.middleware[backend.MIDDLEWARE_ACTIONS.readSnapshots] = [
            function(request, callback) {
              callback({message: 'foo'});
            }
          ];
  
          backend.connect().fetchSnapshot('books', 'don-quixote', 0, function(error) {
            expect(error.message).to.equal('foo');
            done();
          });
        });
      });
  
      describe('with a registered projection', function() {
        beforeEach(function() {
          backend.addProjection('bookTitles', 'books', {title: true});
        });
  
        it('applies the projection to a snapshot', function(done) {
          backend.connect().fetchSnapshot('bookTitles', 'don-quixote', 2, function(error, snapshot) {
            if (error) return done(error);
  
            expect(snapshot.data.title).to.equal('Don Quixote');
            expect(snapshot.data.author).to.equal(undefined);
            done();
          });
        });
      });
    });
  
    describe('a document that is currently deleted', function() {
      beforeEach(function(done) {
        var doc = backend.connect().get('books', 'catch-22');
        doc.create({title: 'Catch 22'}, function(error) {
          if (error) return done(error);
          doc.del(function(error) {
            done(error);
          });
        });
      });
  
      it('returns a null type', function(done) {
        backend.connect().fetchSnapshot('books', 'catch-22', null, function(error, snapshot) {
          expect(snapshot).to.eql({
            id: 'catch-22',
            v: 2,
            type: null,
            data: undefined,
            m: null
          });
  
          done();
        });
      });
  
      it('fetches v1', function(done) {
        backend.connect().fetchSnapshot('books', 'catch-22', 1, function(error, snapshot) {
          if (error) return done(error);
  
          expect(snapshot).to.eql({
            id: 'catch-22',
            v: 1,
            type: 'http://sharejs.org/types/JSONv0',
            data: {
              title: 'Catch 22'
            },
            m: null
          });
  
          done();
        });
      });
    });
  
    describe('a document that was deleted and then created again', function() {
      beforeEach(function(done) {
        var doc = backend.connect().get('books', 'hitchhikers-guide');
        doc.create({title: 'Hitchhiker\'s Guide to the Galaxy'}, function(error) {
          if (error) return done(error);
          doc.del(function(error) {
            if (error) return done(error);
            doc.create({title: 'The Restaurant at the End of the Universe'}, function(error) {
              done(error);
            });
          });
        });
      });
  
      it('fetches the latest version of the document', function(done) {
        backend.connect().fetchSnapshot('books', 'hitchhikers-guide', null, function(error, snapshot) {
          if (error) return done(error);
  
          expect(snapshot).to.eql({
            id: 'hitchhikers-guide',
            v: 3,
            type: 'http://sharejs.org/types/JSONv0',
            data: {
              title: 'The Restaurant at the End of the Universe'
            },
            m: null
          });
  
          done();
        });
      });
    });
  
    describe('milestone snapshots enabled for every other version', function() {
      var milestoneDb;
      var db;
      var backendWithMilestones;
  
      beforeEach(function() {
        var options = {interval: 2};
        db = new MemoryDb();
        milestoneDb = new MemoryMilestoneDb(options);
        backendWithMilestones = new Backend({
          db: db,
          milestoneDb: milestoneDb
        });
      });
  
      afterEach(function(done) {
        backendWithMilestones.close(done);
      });
  
      it('fetches a snapshot using the milestone', function(done) {
        var doc = backendWithMilestones.connect().get('books', 'mocking-bird');
  
        async.waterfall([
          doc.create.bind(doc, {title: 'To Kill a Mocking Bird'}),
          doc.submitOp.bind(doc, {p: ['author'], oi: 'Harper Lea'}),
          doc.submitOp.bind(doc, {p: ['author'], od: 'Harper Lea', oi: 'Harper Lee'}),
          function(next) {
            sinon.spy(milestoneDb, 'getMilestoneSnapshot');
            sinon.spy(db, 'getOps');
            backendWithMilestones.connect().fetchSnapshot('books', 'mocking-bird', 3, next);
          },
          function(snapshot, next) {
            expect(milestoneDb.getMilestoneSnapshot.calledOnce).to.equal(true);
            expect(db.getOps.calledWith('books', 'mocking-bird', 2, 3)).to.equal(true);
            expect(snapshot.v).to.equal(3);
            expect(snapshot.data).to.eql({title: 'To Kill a Mocking Bird', author: 'Harper Lee'});
            next();
          }
        ], done);
      });
    });
  
    describe('invalid json0 path', function() {
      beforeEach(function(done) {
        var doc = backend.connect().get('series', 'his-dark-materials');
        doc.create([{title: 'Golden Compass'}], function(error) {
          if (error) return done(error);
          doc.submitOp({p: ['0', 'title'], od: 'Golden Compass', oi: 'Northern Lights'}, function(error) {
            if (error) return done(error);
            doc.submitOp({p: ['1'], li: {title: 'Subtle Knife'}}, done);
          });
        });
      });
  
      describe('json0v1', function() {
        it('fetches v2 with json0v1', function(done) {
          backend.connect().fetchSnapshot('series', 'his-dark-materials', 2, function(error, snapshot) {
            if (error) return done(error);
            expect(snapshot.data).to.eql([{title: 'Northern Lights'}]);
            done();
          });
        });
      });
  
      describe('json0v2', function() {
        var defaultType;
  
        beforeEach(function() {
          defaultType = types.defaultType;
          types.defaultType = json0v2;
          types.register(json0v2);
        });
  
        afterEach(function() {
          types.defaultType = defaultType;
          types.register(defaultType);
        });
  
        it('fetches v2 with json0v2', function(done) {
          backend.connect().fetchSnapshot('series', 'his-dark-materials', 2, function(error, snapshot) {
            if (error) return done(error);
            expect(snapshot.data).to.eql([{title: 'Northern Lights'}]);
            done();
          });
        });
      });
    });
  });
  