'use strict';

var assert = require('assert');
var fs = require('fs');
var test = require('testit');
var Promise = require('promise/lib/es6-extensions.js');
var throat = require('../')(Promise);

var sentA = {}, sentB = {}, sentC = {}
function job() {
  var resolve, reject;
  var promise = new Promise(function (_resolve, _reject) {
    resolve = _resolve;
    reject = _reject;
  });
  function executeJob() {
    if (executeJob.isRun) throw new Error('Job was run multiple times');
    executeJob.isRun = true;
    executeJob.args = Array.prototype.slice.call(arguments);
    return promise;
  }
  executeJob.fail = function (err) {
    reject(err);
  };
  executeJob.complete = function (val) {
    resolve(val);
  };
  executeJob.isRun = false
  return executeJob;
}

function Processed(val) {
  this.val = val
}
function worker(max) {
  var concurrent = 0
  function execute() {
    concurrent++
    if (concurrent > max) throw new Error('Extra processes were run in parallel.')
    var res = new Processed(Array.prototype.slice.call(arguments))
    return new Promise(function (resolve) {
      setTimeout(function () {
        concurrent--
        resolve(res)
      }, 100)
    })
  }
  return execute
}

test('throat(n)', function () {
  test('throat(1) acts as a lock', function () {
    var lock = throat(1)
    var a = job(), b = job(), c = job();
    var resA = lock(a, 123)
    var resB = lock(b, 456)
    var resC = lock(c, 789)
    assert(a.isRun)
    assert(!b.isRun)
    assert(!c.isRun)
    a.complete(sentA)
    return resA.then(function (resA) {
      assert(resA === sentA)
      assert(a.isRun)
      assert(b.isRun)
      assert(!c.isRun)
      b.fail(sentB)
      return resB
        .then(function () {
          throw new Error('b should have been rejected')
        }, function (errB) {
          assert(errB === sentB)
        })
    })
    .then(function () {
      assert(a.isRun)
      assert(b.isRun)
      assert(c.isRun)
      assert.deepEqual(a.args, [123]);
      assert.deepEqual(b.args, [456]);
      assert.deepEqual(c.args, [789]);
      c.complete(sentC)
      return resC
    })
    .then(function (resC) {
      assert(resC === sentC)
    })
  })
  test('throat(2) lets two processes acquire the same lock', function () {
    var lock = throat(2)
    var a = job(), b = job(), c = job();
    var resA = lock(a)
    var resB = lock(b)
    var resC = lock(c)
    assert(a.isRun)
    assert(b.isRun)
    assert(!c.isRun)
    a.complete(sentA)
    return resA.then(function (resA) {
      assert(resA === sentA)
      assert(a.isRun)
      assert(b.isRun)
      assert(c.isRun)
      b.fail(sentB)
      return resB
        .then(function () {
          throw new Error('b should have been rejected')
        }, function (errB) {
          assert(errB === sentB)
        })
    })
    .then(function () {
      assert(a.isRun)
      assert(b.isRun)
      assert(c.isRun)
      c.complete(sentC)
      return resC
    })
    .then(function (resC) {
      assert(resC === sentC)
    })
  })
  test('throat(3) lets three processes acquire the same lock', function () {
    var lock = throat(3)
    var a = job(), b = job(), c = job();
    var resA = lock(a)
    var resB = lock(b)
    var resC = lock(c)
    assert(a.isRun)
    assert(b.isRun)
    assert(c.isRun)
    a.complete(sentA)
    return resA.then(function (resA) {
      assert(resA === sentA)
      assert(a.isRun)
      assert(b.isRun)
      assert(c.isRun)
      b.fail(sentB)
      return resB
        .then(function () {
          throw new Error('b should have been rejected')
        }, function (errB) {
          assert(errB === sentB)
        })
    })
    .then(function () {
      assert(a.isRun)
      assert(b.isRun)
      assert(c.isRun)
      c.complete(sentC)
      return resC
    })
    .then(function (resC) {
      assert(resC === sentC)
    })
  })
})


test('throat(n, fn)', function () {
  test('throat(1, fn) acts as a sequential worker', function () {
    return Promise.all([sentA, sentB, sentC].map(throat(1, worker(1))))
      .then(function (res) {
        assert(res[0] instanceof Processed && res[0].val.length > 1 && res[0].val[0] === sentA)
        assert(res[1] instanceof Processed && res[1].val.length > 1 && res[1].val[0] === sentB)
        assert(res[2] instanceof Processed && res[2].val.length > 1 && res[2].val[0] === sentC)
      })
  })
  test('throat(2, fn) works on two inputs in parallel', function () {
    return Promise.all([sentA, sentB, sentC].map(throat(2, worker(2))))
      .then(function (res) {
        assert(res[0] instanceof Processed && res[0].val.length > 1 && res[0].val[0] === sentA)
        assert(res[1] instanceof Processed && res[1].val.length > 1 && res[1].val[0] === sentB)
        assert(res[2] instanceof Processed && res[2].val.length > 1 && res[2].val[0] === sentC)
      })
  })
  test('throat(3, fn) works on three inputs in parallel', function () {
    return Promise.all([sentA, sentB, sentC].map(throat(3, worker(3))))
      .then(function (res) {
        assert(res[0] instanceof Processed && res[0].val.length > 1 && res[0].val[0] === sentA)
        assert(res[1] instanceof Processed && res[1].val.length > 1 && res[1].val[0] === sentB)
        assert(res[2] instanceof Processed && res[2].val.length > 1 && res[2].val[0] === sentC)
      })
  })
})

test('throat(fn, n)', function () {
  test('throat(fn, 1) acts as a sequential worker', function () {
    return Promise.all([sentA, sentB, sentC].map(throat(worker(1), 1)))
      .then(function (res) {
        assert(res[0] instanceof Processed && res[0].val.length > 1 && res[0].val[0] === sentA)
        assert(res[1] instanceof Processed && res[1].val.length > 1 && res[1].val[0] === sentB)
        assert(res[2] instanceof Processed && res[2].val.length > 1 && res[2].val[0] === sentC)
      })
  })
  test('throat(fn, 2) works on two inputs in parallel', function () {
    return Promise.all([sentA, sentB, sentC].map(throat(worker(2), 2)))
      .then(function (res) {
        assert(res[0] instanceof Processed && res[0].val.length > 1 && res[0].val[0] === sentA)
        assert(res[1] instanceof Processed && res[1].val.length > 1 && res[1].val[0] === sentB)
        assert(res[2] instanceof Processed && res[2].val.length > 1 && res[2].val[0] === sentC)
      })
  })
  test('throat(fn, 3) works on three inputs in parallel', function () {
    return Promise.all([sentA, sentB, sentC].map(throat(worker(3), 3)))
      .then(function (res) {
        assert(res[0] instanceof Processed && res[0].val.length > 1 && res[0].val[0] === sentA)
        assert(res[1] instanceof Processed && res[1].val.length > 1 && res[1].val[0] === sentB)
        assert(res[2] instanceof Processed && res[2].val.length > 1 && res[2].val[0] === sentC)
      })
  })
})

test('with native promises', function () {
  var throat = require('../')
  throat.Promise = Promise
  return Promise.all([sentA, sentB, sentC].map(throat(1, worker(1))))
    .then(function (res) {
      assert(res[0] instanceof Processed && res[0].val.length > 1 && res[0].val[0] === sentA)
      assert(res[1] instanceof Processed && res[1].val.length > 1 && res[1].val[0] === sentB)
      assert(res[2] instanceof Processed && res[2].val.length > 1 && res[2].val[0] === sentC)
    })
})

test('without native promises', function () {
  var throat = require('../')
  throat.Promise = null
  try {
    throat(1)
  } catch (ex) {
    assert(/provide a Promise polyfill/.test(ex.message));
    return;
  }
  throw new Error('Expected a failure');
})

test('type errors', function () {
  test('size as a string', function () {
    try {
      throat('foo')
    } catch (ex) {
      assert(/Expected throat size to be a number/.test(ex.message));
      return;
    }
    throw new Error('Expected a failure')
  })
  test('fn as a string', function () {
    try {
      throat(2, 'foo')
    } catch (ex) {
      assert(/Expected throat fn to be a function/.test(ex.message));
      return;
    }
    throw new Error('Expected a failure')
  })
  test('late fn as a string', function () {
    try {
      throat(2)('foo')
    } catch (ex) {
      assert(/Expected throat fn to be a function/.test(ex.message));
      return;
    }
    throw new Error('Expected a failure')
  })
})
