"use strict";

const tap = require("tap");
const createPool = require("../").createPool;
const utils = require("./utils");
const ResourceFactory = utils.ResourceFactory;

// tap.test('Pool expands only to max limit', function (t) {
//   const resourceFactory = new ResourceFactory()

//   const config = {
//     max: 1
//   }

//   const pool = createPool(resourceFactory, config)

//     // NOTES:
//     // - request a resource
//     // - once we have it, request another and check the pool is fool
//   pool.acquire(function (err, obj) {
//     t.error(err)
//     const poolIsFull = !pool.acquire(function (err, obj) {
//       t.error(err)
//       t.equal(1, resourceFactory.created)
//       pool.release(obj)
//       utils.stopPool(pool)
//       t.end()
//     })
//     t.ok(poolIsFull)
//     t.equal(1, resourceFactory.created)
//     pool.release(obj)
//   })
// })

// tap.test('Pool respects min limit', function (t) {
//   const resourceFactory = new ResourceFactory()

//   const config
//     min: 1,
//     max: 2
//   }

//   const pool = createPool(resourceFactory, config)

//     // FIXME: this logic only works because we know it takes ~1ms to create a resource
//     // we need better hooks into the pool probably to observe this...
//   setTimeout(function () {
//     t.equal(resourceFactory.created, 1)
//     utils.stopPool(pool)
//     t.end()
//   }, 10)
// })

tap.test("min and max limit defaults", function(t) {
  const resourceFactory = new ResourceFactory();

  const pool = createPool(resourceFactory);

  t.equal(1, pool.max);
  t.equal(0, pool.min);
  utils.stopPool(pool);
  t.end();
});

tap.test("malformed min and max limits are ignored", function(t) {
  const resourceFactory = new ResourceFactory();

  const config = {
    min: "asf",
    max: []
  };
  const pool = createPool(resourceFactory, config);

  t.equal(1, pool.max);
  t.equal(0, pool.min);
  utils.stopPool(pool);
  t.end();
});

tap.test("min greater than max sets to max", function(t) {
  const resourceFactory = new ResourceFactory();

  const config = {
    min: 5,
    max: 3
  };
  const pool = createPool(resourceFactory, config);

  t.equal(3, pool.max);
  t.equal(3, pool.min);
  utils.stopPool(pool);
  t.end();
});

tap.test("supports priority on borrow", function(t) {
  // NOTE: this test is pretty opaque about what it's really testing/expecting...
  let borrowTimeLow = 0;
  let borrowTimeHigh = 0;
  let borrowCount = 0;

  const resourceFactory = new ResourceFactory();

  const config = {
    max: 1,
    priorityRange: 2
  };

  const pool = createPool(resourceFactory, config);

  function lowPriorityOnFulfilled(obj) {
    const time = Date.now();
    if (time > borrowTimeLow) {
      borrowTimeLow = time;
    }
    borrowCount++;
    pool.release(obj);
  }

  function highPriorityOnFulfilled(obj) {
    const time = Date.now();
    if (time > borrowTimeHigh) {
      borrowTimeHigh = time;
    }
    borrowCount++;
    pool.release(obj);
  }

  const operations = [];

  for (let i = 0; i < 10; i++) {
    const op = pool.acquire(1).then(lowPriorityOnFulfilled);
    operations.push(op);
  }

  for (let i = 0; i < 10; i++) {
    const op = pool.acquire(0).then(highPriorityOnFulfilled);
    operations.push(op);
  }

  Promise.all(operations)
    .then(function() {
      t.equal(20, borrowCount);
      t.equal(true, borrowTimeLow >= borrowTimeHigh);
      utils.stopPool(pool);
      t.end();
    })
    .catch(t.threw);
});

// FIXME: bad test!
// pool.destroy makes no obligations to user about when it will destroy the resource
// we should test that "destroyed" objects are not acquired again instead
// tap.test('removes correct object on reap', function (t) {
//   const resourceFactory = new ResourceFactory()

//   const config
//     max: 2
//   }

//   const pool = createPool(resourceFactory, config)

//   const op1 = pool.acquire()
//   .then(function (client) {
//     return new Promise(function (resolve, reject) {
//       // should be removed second
//       setTimeout(function () {
//         pool.destroy(client)
//         resolve()
//       }, 5)
//     })
//   })

//   const op2 = pool.acquire()
//   .then(function (client) {
//     pool.destroy(client)
//   })

//   Promise.all([op1, op2]).then(function () {
//     t.equal(1, resourceFactory.bin[0].id)
//     t.equal(0, resourceFactory.bin[1].id)
//     utils.stopPool(pool)
//     t.end()
//   })
//   .catch(t.threw)
// })

tap.test("evictor removes instances on idletimeout", function(t) {
  const resourceFactory = new ResourceFactory();
  const config = {
    min: 2,
    max: 2,
    idleTimeoutMillis: 50,
    evictionRunIntervalMillis: 10
  };
  const pool = createPool(resourceFactory, config);

  setTimeout(function() {
    pool
      .acquire()
      .then(res => {
        t.ok(res.id > 1);
        return pool.release(res);
      })
      .then(() => {
        utils.stopPool(pool);
        t.end();
      });
  }, 120);
});

tap.test("tests drain", function(t) {
  const count = 5;
  let acquired = 0;

  const resourceFactory = new ResourceFactory();
  const config = {
    max: 2,
    idletimeoutMillis: 300000
  };
  const pool = createPool(resourceFactory, config);

  const operations = [];

  function onAcquire(client) {
    acquired += 1;
    t.equal(typeof client.id, "number");
    setTimeout(function() {
      pool.release(client);
    }, 250);
  }

  // request 5 resources that release after 250ms
  for (let i = 0; i < count; i++) {
    const op = pool.acquire().then(onAcquire);
    operations.push(op);
  }
  // FIXME: what does this assertion prove?
  t.notEqual(count, acquired);

  Promise.all(operations)
    .then(function() {
      return pool.drain();
    })
    .then(function() {
      t.equal(count, acquired);
      // short circuit the absurdly long timeouts above.
      pool.clear();
    })
    .then(function() {
      // subsequent calls to acquire should resolve an error.
      return pool.acquire().then(t.fail, function(e) {
        t.type(e, Error);
      });
    })
    .then(function() {
      t.end();
    });
});

tap.test("clear promise resolves with no value", function(t) {
  let resources = [];
  const factory = {
    create: function create() {
      return new Promise(function tryCreate(resolve, reject) {
        let resource = resources.shift();
        if (resource) {
          resolve(resource);
        } else {
          process.nextTick(tryCreate.bind(this, resolve, reject));
        }
      });
    },
    destroy: function() {
      return Promise.resolve();
    }
  };
  const pool = createPool(factory, { max: 3, min: 3 });
  Promise.all([pool.acquire(), pool.acquire(), pool.acquire()]).then(all => {
    all.forEach(resource => {
      process.nextTick(pool.release.bind(pool), resource);
    });
  });

  t.equal(pool.pending, 3, "all acquisitions pending");

  pool
    .drain()
    .then(() => pool.clear())
    .then(resolved => {
      t.equal(resolved, undefined, "clear promise resolves with no value");
      t.end();
    });

  process.nextTick(() => {
    resources.push("a");
    resources.push("b");
    resources.push("c");
  });
});

tap.test("handle creation errors", function(t) {
  let created = 0;
  const resourceFactory = {
    create: function() {
      created++;
      if (created < 5) {
        return Promise.reject(new Error("Error occurred."));
      } else {
        return Promise.resolve({ id: created });
      }
    },
    destroy: function(client) {}
  };
  const config = {
    max: 1
  };

  const pool = createPool(resourceFactory, config);

  // FIXME: this section no longer proves anything as factory
  // errors no longer bubble up through the acquire call
  // we need to make the Pool an Emitter

  // ensure that creation errors do not populate the pool.
  // for (const i = 0; i < 5; i++) {
  //   pool.acquire(function (err, client) {
  //     t.ok(err instanceof Error)
  //     t.ok(client === null)
  //   })
  // }

  let called = false;
  pool
    .acquire()
    .then(function(client) {
      t.equal(typeof client.id, "number");
      called = true;
      pool.release(client);
    })
    .then(function() {
      t.ok(called);
      t.equal(pool.pending, 0);
      utils.stopPool(pool);
      t.end();
    })
    .catch(t.threw);
});

tap.test("handle creation errors for delayed creates", function(t) {
  let attempts = 0;

  const resourceFactory = {
    create: function() {
      attempts++;
      if (attempts <= 5) {
        return Promise.reject(new Error("Error occurred."));
      } else {
        return Promise.resolve({ id: attempts });
      }
    },
    destroy: function(client) {
      return Promise.resolve();
    }
  };

  const config = {
    max: 1
  };

  const pool = createPool(resourceFactory, config);

  let errorCount = 0;
  pool.on("factoryCreateError", function(err) {
    t.ok(err instanceof Error);
    errorCount++;
  });

  let called = false;
  pool
    .acquire()
    .then(function(client) {
      t.equal(typeof client.id, "number");
      called = true;
      pool.release(client);
    })
    .then(function() {
      t.ok(called);
      t.equal(errorCount, 5);
      t.equal(pool.pending, 0);
      utils.stopPool(pool);
      t.end();
    })
    .catch(t.threw);
});

tap.test("getPoolSize", function(t) {
  let assertionCount = 0;
  const resourceFactory = new ResourceFactory();
  const config = {
    max: 2
  };

  const pool = createPool(resourceFactory, config);

  const borrowedResources = [];

  t.equal(pool.size, 0);
  assertionCount += 1;

  pool
    .acquire()
    .then(function(obj) {
      borrowedResources.push(obj);
      t.equal(pool.size, 1);
      assertionCount += 1;
    })
    .then(function() {
      return pool.acquire();
    })
    .then(function(obj) {
      borrowedResources.push(obj);
      t.equal(pool.size, 2);
      assertionCount += 1;
    })
    .then(function() {
      pool.release(borrowedResources.shift());
      pool.release(borrowedResources.shift());
    })
    .then(function() {
      return pool.acquire();
    })
    .then(function(obj) {
      // should still be 2
      t.equal(pool.size, 2);
      assertionCount += 1;
      pool.release(obj);
    })
    .then(function() {
      t.equal(assertionCount, 4);
      utils.stopPool(pool);
      t.end();
    })
    .catch(t.threw);
});

tap.test("availableObjectsCount", function(t) {
  let assertionCount = 0;
  const resourceFactory = new ResourceFactory();
  const config = {
    max: 2
  };

  const pool = createPool(resourceFactory, config);

  const borrowedResources = [];

  t.equal(pool.available, 0);
  assertionCount += 1;

  pool
    .acquire()
    .then(function(obj) {
      borrowedResources.push(obj);
      t.equal(pool.available, 0);
      assertionCount += 1;
    })
    .then(function() {
      return pool.acquire();
    })
    .then(function(obj) {
      borrowedResources.push(obj);
      t.equal(pool.available, 0);
      assertionCount += 1;
    })
    .then(function() {
      pool.release(borrowedResources.shift());
      t.equal(pool.available, 1);
      assertionCount += 1;

      pool.release(borrowedResources.shift());
      t.equal(pool.available, 2);
      assertionCount += 1;
    })
    .then(function() {
      return pool.acquire();
    })
    .then(function(obj) {
      t.equal(pool.available, 1);
      assertionCount += 1;
      pool.release(obj);

      t.equal(pool.available, 2);
      assertionCount += 1;
    })
    .then(function() {
      t.equal(assertionCount, 7);
      utils.stopPool(pool);
      t.end();
    })
    .catch(t.threw);
});

// FIXME: bad test!
// pool.destroy makes no obligations to user about when it will destroy the resource
// we should test that "destroyed" objects are not acquired again instead
// tap.test('removes from available objects on destroy', function (t) {
//   let destroyCalled = false
//   const factory = {
//     create: function () { return Promise.resolve({}) },
//     destroy: function (client) { destroyCalled = true; return Promise.resolve() }
//   }

//   const config
//     max: 2
//   }

//   const pool = createPool(factory, config)

//   pool.acquire().then(function (obj) {
//     pool.destroy(obj)
//   })
//   .then(function () {
//     t.equal(destroyCalled, true)
//     t.equal(pool.available, 0)
//     utils.stopPool(pool)
//     t.end()
//   })
//   .catch(t.threw)
// })

// FIXME: bad test!
// pool.destroy makes no obligations to user about when it will destroy the resource
// we should test that "destroyed" objects are not acquired again instead
// tap.test('removes from available objects on validation failure', function (t) {
//   const destroyCalled = false
//   const validateCalled = false
//   const count = 0
//   const factory = {
//     create: function () { return Promise.resolve({count: count++}) },
//     destroy: function (client) { destroyCalled = client.count },
//     validate: function (client) {
//       validateCalled = true
//       return Promise.resolve(client.count > 0)
//     }
//   }

//   const config
//     max: 2,
//     testOnBorrow: true
//   }

//   const pool = createPool(factory, config)

//   pool.acquire()
//   .then(function (obj) {
//     pool.release(obj)
//     t.equal(obj.count, 0)
//   })
//   .then(function () {
//     return pool.acquire()
//   })
//   .then(function (obj2) {
//     pool.release(obj2)
//     t.equal(obj2.count, 1)
//   })
//   .then(function () {
//     t.equal(validateCalled, true)
//     t.equal(destroyCalled, 0)
//     t.equal(pool.available, 1)
//     utils.stopPool(pool)
//     t.end()
//   })
//   .catch(t.threw)
// })

tap.test(
  "do schedule again if error occured when creating new Objects async",
  function(t) {
    // NOTE: we're simulating the first few resource attempts failing
    let resourceCreationAttempts = 0;

    const factory = {
      create: function() {
        resourceCreationAttempts++;
        if (resourceCreationAttempts < 2) {
          return Promise.reject(new Error("Create Error"));
        }
        return Promise.resolve({});
      },
      destroy: function(client) {}
    };

    const config = {
      max: 1
    };

    const pool = createPool(factory, config);
    // pool.acquire(function () {})
    pool
      .acquire()
      .then(function(obj) {
        t.equal(pool.available, 0);
        pool.release(obj);
        utils.stopPool(pool);
        t.end();
      })
      .catch(t.threw);
  }
);

tap.test("returns only valid object to the pool", function(t) {
  const pool = createPool(new ResourceFactory(), { max: 1 });

  pool
    .acquire()
    .then(function(obj) {
      t.equal(pool.available, 0);
      t.equal(pool.borrowed, 1);

      // Invalid release
      pool
        .release({})
        .catch(function(err) {
          t.match(err.message, /Resource not currently part of this pool/);
        })
        .then(function() {
          t.equal(pool.available, 0);
          t.equal(pool.borrowed, 1);

          // Valid release
          pool.release(obj).catch(t.error);
          t.equal(pool.available, 1);
          t.equal(pool.borrowed, 0);
          utils.stopPool(pool);
          t.end();
        });
    })
    .catch(t.threw);
});

tap.test("validate acquires object from the pool", function(t) {
  const pool = createPool(new ResourceFactory(), { max: 1 });

  pool
    .acquire()
    .then(function(obj) {
      t.equal(pool.available, 0);
      t.equal(pool.borrowed, 1);
      pool.release(obj);
      utils.stopPool(pool);
      t.end();
    })
    .catch(t.threw);
});

tap.test("release to pool should work", function(t) {
  const pool = createPool(new ResourceFactory(), { max: 1 });

  pool
    .acquire()
    .then(function(obj) {
      t.equal(pool.available, 0);
      t.equal(pool.borrowed, 1);
      t.equal(pool.pending, 1);
      pool.release(obj);
    })
    .catch(t.threw);

  pool
    .acquire()
    .then(function(obj) {
      t.equal(pool.available, 0);
      t.equal(pool.borrowed, 1);
      t.equal(pool.pending, 0);
      pool.release(obj);
      utils.stopPool(pool);
      t.end();
    })
    .catch(t.threw);
});

tap.test(
  "isBorrowedResource should return true for borrowed resource",
  function(t) {
    const pool = createPool(new ResourceFactory(), { max: 1 });

    pool
      .acquire()
      .then(function(obj) {
        t.equal(pool.isBorrowedResource(obj), true);
        pool.release(obj);
        utils.stopPool(pool);
        t.end();
      })
      .catch(t.threw);
  }
);

tap.test(
  "isBorrowedResource should return false for released resource",
  function(t) {
    const pool = createPool(new ResourceFactory(), { max: 1 });

    pool
      .acquire()
      .then(function(obj) {
        pool.release(obj);
        return obj;
      })
      .then(function(obj) {
        t.equal(pool.isBorrowedResource(obj), false);
        utils.stopPool(pool);
        t.end();
      })
      .catch(t.threw);
  }
);

tap.test("destroy should redispense", function(t) {
  const pool = createPool(new ResourceFactory(), { max: 1 });

  pool
    .acquire()
    .then(function(obj) {
      t.equal(pool.available, 0);
      t.equal(pool.borrowed, 1);
      t.equal(pool.pending, 1);
      pool.destroy(obj);
    })
    .catch(t.threw);

  pool
    .acquire()
    .then(function(obj) {
      t.equal(pool.available, 0);
      t.equal(pool.borrowed, 1);
      t.equal(pool.pending, 0);
      pool.release(obj);
      utils.stopPool(pool);
      t.end();
    })
    .catch(t.threw);
});

tap.test("evictor start with acquire when autostart is false", function(t) {
  const pool = createPool(new ResourceFactory(), {
    evictionRunIntervalMillis: 10000,
    autostart: false
  });

  t.equal(pool._scheduledEviction, null);

  pool
    .acquire()
    .then(function(obj) {
      t.notEqual(pool._scheduledEviction, null);
      pool.release(obj);
      utils.stopPool(pool);
      t.end();
    })
    .catch(t.threw);
});

tap.test("use method", function(t) {
  const pool = createPool(new ResourceFactory());
  const result = pool.use(function(resource) {
    t.equal(0, resource.id);
    return Promise.resolve();
  });
  result.then(function() {
    t.end();
  });
});

tap.test("use method should resolve after fn promise is resolved", function(t) {
  const pool = createPool(new ResourceFactory());
  let done_with_resource = false;
  const result = pool.use(function(resource) {
    return new Promise(function(resolve, reject) {
      setImmediate(function() {
        done_with_resource = true;
        resolve("value");
      });
    });
  });
  result.then(val => {
    t.equal(done_with_resource, true);
    t.equal(val, "value");
    t.end();
  });
});

tap.test("evictor should not run when softIdleTimeoutMillis is -1", function(
  t
) {
  const resourceFactory = new ResourceFactory();
  const pool = createPool(resourceFactory, {
    evictionRunIntervalMillis: 10
  });
  pool
    .acquire()
    .then(res => pool.release(res))
    .then(() => {
      return new Promise(res => setTimeout(res, 30));
    })
    .then(() => t.equal(resourceFactory.destroyed, 0))
    .then(() => {
      utils.stopPool(pool);
      t.end();
    });
});

tap.test("should respect when maxWaitingClients is set to 0 ", function(t) {
  let assertionCount = 0;
  const resourceFactory = new ResourceFactory();
  const config = {
    max: 2,
    maxWaitingClients: 0
  };

  const pool = createPool(resourceFactory, config);

  const borrowedResources = [];

  t.equal(pool.size, 0);
  assertionCount += 1;

  pool
    .acquire()
    .then(function(obj) {
      borrowedResources.push(obj);
      t.equal(pool.size, 1);
      assertionCount += 1;
    })
    .then(function() {
      return pool.acquire();
    })
    .then(function(obj) {
      borrowedResources.push(obj);
      t.equal(pool.size, 2);
      assertionCount += 1;
    })
    .then(function() {
      return pool.acquire();
    })
    .then(function(obj) {
      // should not go in here
      t.equal(1, 2);
    })
    .catch(error => {
      t.equal(error.message, "max waitingClients count exceeded");
      t.end();
    });
});

tap.test("should provide a way to wait until the pool is ready", function(t) {
  const resourceFactory = new ResourceFactory();
  const config = {
    min: 2,
    max: 4
  };

  const pool = createPool(resourceFactory, config);

  pool.ready().then(() => {
    t.ok(
      pool.available >= config.min,
      "expected available resources to be at least as the minimum"
    );
    t.end();
  });
});
