"use strict";

const rewiremock = require("rewiremock/node");
const sinon = require("sinon");

describe("class BufferedWorkerPool", function () {
  let BufferedWorkerPool;
  let pool;
  let stats;
  let serializeJavascript;
  let serializer;
  let result;

  beforeEach(function () {
    stats = {
      totalWorkers: 10,
      busyWorkers: 8,
      idleWorkers: 2,
      pendingTasks: 3,
    };
    result = { failures: 0, events: [] };
    pool = {
      terminate: sinon.stub().resolves(),
      exec: sinon.stub().resolves(result),
      stats: sinon.stub().returns(stats),
    };
    serializer = {
      deserialize: sinon.stub(),
    };

    serializeJavascript = sinon.spy(require("serialize-javascript"));
    BufferedWorkerPool = rewiremock.proxy(
      require.resolve("../../lib/nodejs/buffered-worker-pool"),
      {
        workerpool: {
          pool: sinon.stub().returns(pool),
          cpus: 8,
        },
        "../../lib/nodejs/serializer": serializer,
        "serialize-javascript": serializeJavascript,
      },
    ).BufferedWorkerPool;

    // reset cache
    BufferedWorkerPool.resetOptionsCache();
  });

  afterEach(function () {
    sinon.restore();
  });

  describe("static method", function () {
    describe("create()", function () {
      it("should return a BufferedWorkerPool instance", function () {
        expect(
          BufferedWorkerPool.create({ foo: "bar" }),
          "to be a",
          BufferedWorkerPool,
        );
      });

      describe("when passed no arguments", function () {
        it("should not throw", function () {
          expect(BufferedWorkerPool.create, "not to throw");
        });
      });
    });

    describe("serializeOptions()", function () {
      describe("when passed no arguments", function () {
        it("should not throw", function () {
          expect(BufferedWorkerPool.serializeOptions, "not to throw");
        });
      });

      it("should return a serialized string", function () {
        expect(
          BufferedWorkerPool.serializeOptions({ foo: "bar" }),
          "to be a",
          "string",
        );
      });

      describe("when called multiple times with the same object", function () {
        it("should not perform serialization twice", function () {
          const obj = { foo: "bar" };
          BufferedWorkerPool.serializeOptions(obj);
          BufferedWorkerPool.serializeOptions(obj);
          expect(serializeJavascript, "was called once");
        });

        it("should return the same value", function () {
          const obj = { foo: "bar" };
          expect(
            BufferedWorkerPool.serializeOptions(obj),
            "to be",
            BufferedWorkerPool.serializeOptions(obj),
          );
        });
      });
    });
  });

  describe("constructor", function () {
    it("should apply defaults", function () {
      expect(new BufferedWorkerPool(), "to satisfy", {
        options: {
          workerType: "process",
          forkOpts: { execArgv: process.execArgv },
          maxWorkers: expect.it("to be greater than or equal to", 1),
        },
      });
    });
  });

  describe("instance method", function () {
    let workerPool;

    beforeEach(function () {
      workerPool = BufferedWorkerPool.create();
    });

    describe("stats()", function () {
      it("should return the object returned by `workerpool.Pool#stats`", function () {
        expect(workerPool.stats(), "to be", stats);
      });
    });

    describe("run()", function () {
      describe("when passed no arguments", function () {
        it("should reject", async function () {
          return expect(workerPool.run(), "to be rejected with", {
            code: "ERR_MOCHA_INVALID_ARG_TYPE",
          });
        });
      });

      describe("when passed a non-string filepath", function () {
        it("should reject", async function () {
          return expect(workerPool.run(123), "to be rejected with", {
            code: "ERR_MOCHA_INVALID_ARG_TYPE",
          });
        });
      });

      it("should serialize the options object", async function () {
        await workerPool.run("file.js", { foo: "bar" });

        expect(pool.exec, "to have a call satisfying", [
          "run",
          ["file.js", '{"foo":"bar"}'],
        ]).and("was called once");
      });

      it("should deserialize the result", async function () {
        await workerPool.run("file.js", { foo: "bar" });
        expect(serializer.deserialize, "to have a call satisfying", [
          result,
        ]).and("was called once");
      });
    });

    describe("terminate()", function () {
      describe("when called with `force`", function () {
        beforeEach(async function () {
          await workerPool.terminate(true);
        });

        it('should delegate to the underlying pool w/ "force" behavior', async function () {
          expect(pool.terminate, "to have a call satisfying", [true]).and(
            "was called once",
          );
        });
      });

      describe("when called without `force`", function () {
        beforeEach(async function () {
          await workerPool.terminate();
        });

        it('should delegate to the underlying pool w/o "force" behavior', async function () {
          expect(pool.terminate, "to have a call satisfying", [false]).and(
            "was called once",
          );
        });
      });
    });
  });
});
