// Copyright IBM Corp. 2017. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

'use strict';
/* global getSchema:false */
const DataSource = require('..').DataSource;
const EventEmitter = require('events');
const Connector = require('loopback-connector').Connector;
const Transaction = require('loopback-connector').Transaction;
const should = require('./init.js');

describe('Transactions on memory connector', function() {
  let db, tx;

  before(() => {
    db = getSchema();
    db.define('Model');
  });

  it('returns an EventEmitter object', done => {
    tx = db.transaction();
    tx.should.be.instanceOf(EventEmitter);
    done();
  });

  it('exposes and caches slave models', done => {
    testModelCaching(tx.models, db.models);
    done();
  });

  it('changes count when committing', done => {
    db.models.Model.count((err, count) => {
      should.not.exist(err);
      should.exist(count);
      count.should.equal(0);
      tx.models.Model.create(Array(1), () => {
        // Only called after tx.commit()!
      });
      tx.commit(err => {
        should.not.exist(err);
        db.models.Model.count((err, count) => {
          should.not.exist(err);
          should.exist(count);
          count.should.equal(1);
          done();
        });
      });
    });
  });
});

describe('Transactions on test connector without execute()', () => {
  let db, tx;

  before(() => {
    db = createDataSource();
  });

  beforeEach(resetState);

  it('resolves to an EventEmitter', done => {
    const promise = db.transaction();
    promise.should.be.Promise();
    promise.then(transaction => {
      should.exist(transaction);
      transaction.should.be.instanceof(EventEmitter);
      tx = transaction;
      done();
    }, done);
  });

  it('exposes and caches slave models', done => {
    testModelCaching(tx.models, db.models);
    done();
  });

  it('does not allow nesting of transactions', done => {
    (() => tx.transaction()).should.throw('Nesting transactions is not supported');
    done();
  });

  it('calls commit() on the connector', done => {
    db.transaction().then(tx => {
      tx.commit(err => {
        callCount.should.deepEqual({commit: 1, rollback: 0, create: 0});
        done(err);
      });
    }, done);
  });

  it('calls rollback() on the connector', done => {
    db.transaction().then(tx => {
      tx.rollback(err => {
        callCount.should.deepEqual({commit: 0, rollback: 1, create: 0});
        done(err);
      });
    }, done);
  });
});

describe('Transactions on test connector with execute()', () => {
  let db;

  before(() => {
    db = createDataSource();
  });

  beforeEach(resetState);

  it('passes models and calls commit() automatically', done => {
    db.transaction(models => {
      testModelCaching(models, db.models);
      return models.Model.create({});
    }, err => {
      callCount.should.deepEqual({commit: 1, rollback: 0, create: 1});
      transactionPassed.should.be.true();
      done(err);
    });
  });

  it('calls rollback() automatically when throwing an error', done => {
    let error;
    db.transaction(models => {
      error = new Error('exception');
      throw error;
    }, err => {
      error.should.equal(err);
      callCount.should.deepEqual({commit: 0, rollback: 1, create: 0});
      done();
    });
  });

  it('reports execution timeouts', done => {
    let timedOut = false;
    db.transaction(models => {
      setTimeout(() => {
        models.Model.create({}, function(err) {
          if (!timedOut) {
            done(new Error('Timeout was ineffective'));
          } else {
            should.exist(err);
            err.message.should.startWith('The transaction is not active:');
            done();
          }
        });
      }, 50);
    }, {
      timeout: 25,
    }, err => {
      timedOut = true;
      should.exist(err);
      err.code.should.equal('TRANSACTION_TIMEOUT');
      err.message.should.equal('Transaction is rolled back due to timeout');
      callCount.should.deepEqual({commit: 0, rollback: 1, create: 0});
    });
  });
});

function createDataSource() {
  let db = new DataSource({
    initialize: (dataSource, cb) => {
      dataSource.connector = new TestConnector();
      cb();
    },
  });
  db.define('Model');
  return db;
}

function testModelCaching(txModels, dbModels) {
  should.exist(txModels);
  // Test models caching mechanism:
  // Model property should be a accessor with a getter first:
  const accessor = Object.getOwnPropertyDescriptor(txModels, 'Model');
  should.exist(accessor);
  should.exist(accessor.get);
  accessor.get.should.be.Function();
  const Model = txModels.Model;
  should.exist(Model);
  // After accessing it once, it should be a normal cached property:
  const desc = Object.getOwnPropertyDescriptor(txModels, 'Model');
  should.exist(desc.value);
  Model.should.equal(txModels.Model);
  Model.prototype.should.be.instanceof(dbModels.Model);
}

let callCount;
let transactionPassed;

function resetState() {
  callCount = {commit: 0, rollback: 0, create: 0};
  transactionPassed = false;
}

class TestConnector extends Connector {
  constructor() {
    super('test');
  }

  beginTransaction(isolationLevel, cb) {
    this.currentTransaction = new Transaction(this, this);
    process.nextTick(() => cb(null, this.currentTransaction));
  }

  commit(tx, cb) {
    callCount.commit++;
    cb();
  }

  rollback(tx, cb) {
    callCount.rollback++;
    cb();
  }

  create(model, data, options, cb) {
    callCount.create++;
    const transaction = options.transaction;
    const current = this.currentTransaction;
    transactionPassed = transaction &&
      (current === transaction || current === transaction.connection);
    cb();
  }
}