"use strict";

const serializeJavascript = require("serialize-javascript");
const rewiremock = require("rewiremock/node");
const { SerializableWorkerResult } = require("../../lib/nodejs/serializer.js");
const sinon = require("sinon");

const WORKER_PATH = require.resolve("../../lib/nodejs/worker.cjs");

describe("worker", function () {
  let worker;
  let stubs;

  beforeEach(function () {
    stubs = {
      workerpool: {
        isMainThread: false,
        worker: sinon.stub(),
      },
    };
    sinon.spy(process, "removeAllListeners");
  });

  describe("when run as main process", function () {
    it("should throw", function () {
      expect(() => {
        rewiremock.proxy(WORKER_PATH, {
          workerpool: {
            isMainThread: true,
            worker: stubs.workerpool.worker,
          },
        });
      }, "to throw");
    });
  });

  describe("when run as worker process", function () {
    let mocha;

    beforeEach(function () {
      mocha = {
        addFile: sinon.stub().returnsThis(),
        loadFilesAsync: sinon.stub().resolves(),
        run: sinon.stub().callsArgAsync(0),
        unloadFiles: sinon.stub().returnsThis(),
      };
      stubs.Mocha = Object.assign(sinon.stub().returns(mocha), {
        bdd: sinon.stub(),
        interfaces: {},
      });

      stubs.serializer = {
        serialize: sinon.stub(),
      };

      stubs.runHelpers = {
        handleRequires: sinon.stub().resolves({}),
        validateLegacyPlugin: sinon.stub(),
      };

      stubs.plugin = {
        aggregateRootHooks: sinon.stub().resolves(),
      };

      worker = rewiremock.proxy(WORKER_PATH, {
        workerpool: stubs.workerpool,
        "../../lib/mocha.cjs": stubs.Mocha,
        "../../lib/nodejs/serializer.js": stubs.serializer,
        "../../lib/cli/run-helpers.cjs": stubs.runHelpers,
        "../../lib/plugin-loader.js": stubs.plugin,
      });
    });

    it("should register itself with workerpool", function () {
      expect(stubs.workerpool.worker, "to have a call satisfying", [
        { run: worker.run },
      ]);
    });

    describe("function", function () {
      describe("run()", function () {
        describe("when called without arguments", function () {
          it("should reject", async function () {
            return expect(worker.run, "to be rejected with error satisfying", {
              code: "ERR_MOCHA_INVALID_ARG_TYPE",
            });
          });
        });

        describe("when passed a non-string `options` value", function () {
          it("should reject", async function () {
            return expect(
              () => worker.run("foo.js", 42),
              "to be rejected with error satisfying",
              {
                code: "ERR_MOCHA_INVALID_ARG_TYPE",
              },
            );
          });
        });

        describe("when passed an invalid string `options` value", function () {
          it("should reject", async function () {
            return expect(
              () => worker.run("foo.js", "tomfoolery"),
              "to be rejected with error satisfying",
              {
                code: "ERR_MOCHA_INVALID_ARG_VALUE",
              },
            );
          });
        });

        describe('when called with empty "filepath" argument', function () {
          it("should reject", async function () {
            return expect(
              () => worker.run(""),
              "to be rejected with error satisfying",
              {
                code: "ERR_MOCHA_INVALID_ARG_TYPE",
              },
            );
          });
        });

        describe('when the file at "filepath" argument is unloadable', function () {
          it("should reject", async function () {
            mocha.loadFilesAsync.rejects();
            return expect(
              () => worker.run("some-non-existent-file.js"),
              "to be rejected",
            );
          });
        });

        describe('when the file at "filepath" is loadable', function () {
          let result;
          beforeEach(function () {
            result = SerializableWorkerResult.create();

            mocha.loadFilesAsync.resolves();
            mocha.run.yields(result);
          });

          it('should handle "--require"', async function () {
            await worker.run(
              "some-file.js",
              serializeJavascript({ require: "foo" }),
            );
            expect(
              stubs.runHelpers.handleRequires,
              "to have a call satisfying",
              [
                "foo",
                { ignoredPlugins: ["mochaGlobalSetup", "mochaGlobalTeardown"] },
              ],
            ).and("was called once");
          });

          it('should handle "--ui"', async function () {
            const argv = { foo: "bar" };
            await worker.run("some-file.js", serializeJavascript(argv));

            expect(
              stubs.runHelpers.validateLegacyPlugin,
              "to have a call satisfying",
              [argv, "ui", stubs.Mocha.interfaces],
            ).and("was called once");
          });

          it("should call Mocha#run", async function () {
            await worker.run("some-file.js");
            expect(mocha.run, "was called once");
          });

          it("should remove all uncaughtException listeners", async function () {
            await worker.run("some-file.js");
            expect(process.removeAllListeners, "to have a call satisfying", [
              "uncaughtException",
            ]);
          });

          it("should remove all unhandledRejection listeners", async function () {
            await worker.run("some-file.js");
            expect(process.removeAllListeners, "to have a call satisfying", [
              "unhandledRejection",
            ]);
          });

          describe("when serialization succeeds", function () {
            beforeEach(function () {
              stubs.serializer.serialize.returnsArg(0);
            });

            it("should resolve with a SerializedWorkerResult", async function () {
              return expect(
                worker.run("some-file.js"),
                "to be fulfilled with",
                result,
              );
            });
          });

          describe("when serialization fails", function () {
            beforeEach(function () {
              stubs.serializer.serialize.throws();
            });

            it("should reject", async function () {
              return expect(worker.run("some-file.js"), "to be rejected");
            });
          });

          describe("when run twice", function () {
            it("should initialize only once", async function () {
              await worker.run("some-file.js");
              await worker.run("some-other-file.js");

              expect(stubs.runHelpers, "to satisfy", {
                handleRequires: expect.it("was called once"),
                validateLegacyPlugin: expect.it("was called once"),
              });
            });
          });
        });
      });
    });
  });

  afterEach(function () {
    sinon.restore();
    // this is needed due to `require.cache` getting dumped in watch mode
    process.removeAllListeners("beforeExit");
  });
});
