'use strict';

/* eslint-env jest */

const compose = require('../../lib/compose');
const assert = require('assert');

function wait(ms){
  return new Promise(resolve => setTimeout(resolve, ms || 1));
}

function isPromise(x){
  return x && typeof x.then === 'function';
}

describe('Koa Compose', () => {
  it('should work', async() => {
    const arr = [];
    const stack = [];

    stack.push(async(context, next) => {
      arr.push(1);
      await wait(1);
      await next();
      await wait(1);
      arr.push(6);
    });

    stack.push(async(context, next) => {
      arr.push(2);
      await wait(1);
      await next();
      await wait(1);
      arr.push(5);
    });

    stack.push(async(context, next) => {
      arr.push(3);
      await wait(1);
      await next();
      await wait(1);
      arr.push(4);
    });

    await compose(stack)({});
    assert.deepEqual(arr, [1, 2, 3, 4, 5, 6]);
  });

  it('should be able to be called twice', () => {
    let stack = [];

    stack.push(async(context, next) => {
      context.arr.push(1);
      await wait(1);
      await next();
      await wait(1);
      context.arr.push(6);
    });

    stack.push(async(context, next) => {
      context.arr.push(2);
      await wait(1);
      await next();
      await wait(1);
      context.arr.push(5);
    });

    stack.push(async(context, next) => {
      context.arr.push(3);
      await wait(1);
      await next();
      await wait(1);
      context.arr.push(4);
    });

    const fn = compose(stack);
    const ctx1 = { arr: [] };
    const ctx2 = { arr: [] };
    const out = [1, 2, 3, 4, 5, 6];

    return fn(ctx1).then(() => {
      assert.deepEqual(out, ctx1.arr);
      return fn(ctx2);
    }).then(() => {
      assert.deepEqual(out, ctx2.arr);
    });
  });

  it('should only accept an array', () => {
    let err;
    try {
      compose();
      throw new Error('should not be called');
    } catch (e) {
      err = e;
    }
    return assert(err instanceof TypeError);
  });

  it('should create next functions that return a Promise', () => {
    const stack = [];
    const arr = [];
    for (let i = 0; i < 5; i++) {
      stack.push((context, next) => {
        arr.push(next());
      });
    }

    compose(stack)({});

    for (let next of arr) {
      assert(isPromise(next), 'one of the functions next is not a Promise');
    }
  });

  it('should work with 0 middleware', () => {
    return compose([])({});
  });

  it('should only accept middleware as functions', () => {
    let err;
    try {
      compose([{}]);
      throw new Error('should not be called');
    } catch (e) {
      err = e;
    }
    return assert(err instanceof TypeError);
  });

  it('should work when yielding at the end of the stack', async() => {
    let stack = [];
    let called = false;

    stack.push(async(ctx, next) => {
      await next();
      called = true;
    });

    await compose(stack)({});
    assert(called);
  });

  it('should reject on errors in middleware', () => {
    let stack = [];

    stack.push(() => { throw new Error(); });

    return compose(stack)({})
      .then(() => {
        throw new Error('promise was not rejected');
      })
      .catch(e => {
        assert(e instanceof Error);
      });
  });

  it('should work when yielding at the end of the stack with yield*', () => {
    let stack = [];

    stack.push(async(ctx, next) => {
      await next;
    });

    return compose(stack)({});
  });

  it('should keep the context', () => {
    const ctx = {};

    const stack = [];

    stack.push(async(ctx2, next) => {
      await next();
      assert.strictEqual(ctx2, ctx);
    });

    stack.push(async(ctx2, next) => {
      await next();
      assert.strictEqual(ctx2, ctx);
    });

    stack.push(async(ctx2, next) => {
      await next();
      assert.strictEqual(ctx2, ctx);
    });

    return compose(stack)(ctx);
  });

  it('should catch downstream errors', async() => {
    const arr = [];
    const stack = [];

    stack.push(async(ctx, next) => {
      arr.push(1);
      try {
        arr.push(6);
        await next();
        arr.push(7);
      } catch (err) {
        arr.push(2);
      }
      arr.push(3);
    });

    stack.push(async(ctx, next) => {
      arr.push(4);
      throw new Error();
    });

    await compose(stack)({});
    assert.deepEqual(arr, [1, 6, 4, 2, 3]);
  });

  it('should compose w/ next', () => {
    let called = false;

    return compose([])({}, async() => {
      called = true;
    }).then(() => {
      assert(called);
    });
  });

  it('should handle errors in wrapped non-async functions', () => {
    const stack = [];

    stack.push(() => {
      throw new Error();
    });

    return compose(stack)({}).then(() => {
      throw new Error('promise was not rejected');
    }).catch(e => {
      assert(e instanceof Error);
    });
  });

  // https://github.com/koajs/compose/pull/27#issuecomment-143109739
  it('should compose w/ other compositions', () => {
    let called = [];

    return compose([
      compose([
        (ctx, next) => {
          called.push(1);
          return next();
        },
        (ctx, next) => {
          called.push(2);
          return next();
        }
      ]),
      (ctx, next) => {
        called.push(3);
        return next();
      }
    ])({}).then(() => assert.deepEqual(called, [1, 2, 3]));
  });

  it('should throw if next() is called multiple times', () => {
    return compose([
      async(ctx, next) => {
        await next();
        await next();
      }
    ])({}).then(() => {
      throw new Error('boom');
    }, err => {
      assert(/multiple times/.test(err.message));
    });
  });

  it('should return a valid middleware', () => {
    let val = 0;
    return compose([
      compose([
        (ctx, next) => {
          val++;
          return next();
        },
        (ctx, next) => {
          val++;
          return next();
        }
      ]),
      (ctx, next) => {
        val++;
        return next();
      }
    ])({}).then(() => {
      assert.strictEqual(val, 3);
    });
  });

  it('should return last return value', () => {
    const stack = [];

    stack.push(async(context, next) => {
      let val = await next();
      assert.strictEqual(val, 2);
      return 1;
    });

    stack.push(async(context, next) => {
      const val = await next();
      assert.strictEqual(val, 0);
      return 2;
    });

    const next = () => 0;
    return compose(stack)({}, next).then(val => {
      assert.strictEqual(val, 1);
    });
  });

  it('should not affect the original middleware array', () => {
    const middleware = [];
    const fn1 = (ctx, next) => {
      return next();
    };
    middleware.push(fn1);

    for (const fn of middleware) {
      assert.equal(fn, fn1);
    }

    compose(middleware);

    for (const fn of middleware) {
      assert.equal(fn, fn1);
    }
  });

  it('should not get stuck on the passed in next', () => {
    const middleware = [(ctx, next) => {
      ctx.middleware++;
      return next();
    }];
    const ctx = {
      middleware: 0,
      next: 0
    };

    return compose(middleware)(ctx, (ctx, next) => {
      ctx.next++;
      return next();
    }).then(() => {
      assert.strictEqual(ctx.middleware, 1);
      assert.strictEqual(ctx.next, 1);
    });
  });
});