// Copyright IBM Corp. 2015,2016. All Rights Reserved.
// Node module: loopback-connector-mysql
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

'use strict';
if (typeof Promise === 'undefined') {
  global.Promise = require('bluebird');
}
var Transaction = require('loopback-datasource-juggler').Transaction;
require('./init.js');
require('should');

var db, Post, Review;

describe('transactions with promise', function() {
  before(function(done) {
    db = global.getDataSource({collation: 'utf8_general_ci',
      createDatabase: true});
    db.once('connected', function() {
      Post = db.define('PostTX', {
        title: {type: String, length: 255, index: true},
        content: {type: String},
      }, {mysql: {engine: 'INNODB'}});
      Review = db.define('ReviewTX', {
        author: String,
        content: {type: String},
      }, {mysql: {engine: 'INNODB'}});
      Post.hasMany(Review, {as: 'reviews', foreignKey: 'postId'});
      db.automigrate(['PostTX', 'ReviewTX'], done);
    });
  });

  after(function(done) {
    // disconnect from this db to avoid too many connection error
    // due to multiple instance of connection pool
    db.disconnect();
    done();
  });

  var currentTx;
  var hooks = [];
  // Return an async function to start a transaction and create a post
  function createPostInTx(post, timeout) {
    return function(done) {
      // Transaction.begin(db.connector, Transaction.READ_COMMITTED,
      var promise = Post.beginTransaction({
        isolationLevel: Transaction.READ_COMMITTED,
        timeout: timeout,
      });
      promise.then(function(tx) {
        (typeof tx.id).should.be.eql('string');
        currentTx = tx;
        hooks = [];
        tx.observe('before commit', function(context, next) {
          hooks.push('before commit');
          next();
        });
        tx.observe('after commit', function(context, next) {
          hooks.push('after commit');
          next();
        });
        tx.observe('before rollback', function(context, next) {
          hooks.push('before rollback');
          next();
        });
        tx.observe('after rollback', function(context, next) {
          hooks.push('after rollback');
          next();
        });
      }).then(function() {
        Post.create(post, {transaction: currentTx}).then(
          function(p) {
            p.reviews.create({
              author: 'John',
              content: 'Review for ' + p.title,
            }, {transaction: currentTx}).then(
              function(c) {
                done(null, c);
              });
          });
      }).catch(done);
    };
  }

  // Return an async function to find matching posts and assert number of
  // records to equal to the count
  function expectToFindPosts(where, count, inTx) {
    return function(done) {
      var options = {};
      if (inTx) {
        options.transaction = currentTx;
      }
      Post.find({where: where}, options).then(
        function(posts) {
          posts.length.should.be.eql(count);
          if (count) {
            // Find related reviews
            // Please note the empty {} is required, otherwise, the options
            // will be treated as a filter
            posts[0].reviews({}, options).then(function(reviews) {
              reviews.length.should.be.eql(count);
              done();
            });
          } else {
            done();
          }
        }).catch(done);
    };
  }

  describe('commit', function() {
    var post = {title: 't1', content: 'c1'};
    before(createPostInTx(post));

    it('should not see the uncommitted insert', expectToFindPosts(post, 0));

    it('should see the uncommitted insert from the same transaction',
      expectToFindPosts(post, 1, true));

    it('should commit a transaction', function(done) {
      currentTx.commit().then(function() {
        hooks.should.be.eql(['before commit', 'after commit']);
        done();
      }).catch(done);
    });

    it('should see the committed insert', expectToFindPosts(post, 1));

    it('should report error if the transaction is not active', function(done) {
      currentTx.commit().catch(function(err) {
        (err).should.be.instanceof(Error);
        done();
      });
    });
  });

  describe('rollback', function() {
    var post = {title: 't2', content: 'c2'};
    before(createPostInTx(post));

    it('should not see the uncommitted insert', expectToFindPosts(post, 0));

    it('should see the uncommitted insert from the same transaction',
      expectToFindPosts(post, 1, true));

    it('should rollback a transaction', function(done) {
      currentTx.rollback().then(function() {
        hooks.should.be.eql(['before rollback', 'after rollback']);
        done();
      }).catch(done);
    });

    it('should not see the rolledback insert', expectToFindPosts(post, 0));

    it('should report error if the transaction is not active', function(done) {
      currentTx.rollback().catch(function(err) {
        (err).should.be.instanceof(Error);
        done();
      });
    });
  });

  describe('timeout', function() {
    var post = {title: 't3', content: 'c3'};
    before(createPostInTx(post, 500));

    it('should invoke the timeout hook', function(done) {
      currentTx.observe('timeout', function(context, next) {
        next();
        // It will only proceed upon timeout
        done();
      });
    });

    it('should rollback the transaction if timeout', function(done) {
      Post.find({where: {title: 't3'}}, {transaction: currentTx},
        function(err, posts) {
          if (err) return done(err);
          posts.length.should.be.eql(0);
          done();
        });
    });
  });
});