const { EnvironmentContext, JestEnvironment } = require("@jest/environment");
const { LegacyFakeTimers, ModernFakeTimers } = require("@jest/fake-timers");
const { Config, Global } = require("@jest/types");
const { ModuleMocker } = require("jest-mock");
const JestUtil = require("jest-util");
const { parseHTML } = require("linkedom");
const VM = require("vm");
const { Script } = VM;

/**
 * Happy DOM Jest Environment.
 */
module.exports = class LinkedomEnv {
  /**
   * Constructor.
   *
   * @param config Jest config.
   * @param options Options.
   */
  constructor(config, options) {
    const { window } = parseHTML(`
      <!doctype html>
      <html lang="en"> <head /> <body /> </html>
    `);
    this.global = window;
    this.moduleMocker = new ModuleMocker(this.global);

    VM.createContext(this.global);

    // Functions are not an instanceof the "Function" class in the VM context, so therefore we set it to the used "Function" class.
    VM.runInContext("window.Function = (() => {}).constructor;", this.global);

    // Node's error-message stack size is limited to 10, but it's pretty useful to see more than that when a test fails.
    this.global.Error.stackTraceLimit = 100;

    JestUtil.installCommonGlobals(this.global, config.globals);

    // Removes window.fetch() as it should not be used in a test environment.
    delete this.global.fetch;
    delete this.global.window.fetch;

    if (options.console) {
      this.global.console = options.console;
      this.global.window.console = options.console;
    }

    this.fakeTimers = new LegacyFakeTimers({
      config,
      global: this.global,
      moduleMocker: this.moduleMocker,
      timerConfig: {
        idToRef: (id) => id,
        refToId: (ref) => ref,
      },
    });

    this.fakeTimersModern = new ModernFakeTimers({
      config,
      global: this.global,
    });
  }

  /**
   * Setup.
   *
   * @return Promise.
   */
  async setup() {}

  /**
   * Teardown.
   *
   * @return Promise.
   */
  async teardown() {
    this.fakeTimers.dispose();
    this.fakeTimersModern.dispose();
    // this.global.happyDOM.cancelAsync();

    this.global = null;
    this.moduleMocker = null;
    this.fakeTimers = null;
    this.fakeTimersModern = null;
  }

  /**
   * Runs a script.
   *
   * @param script Script.
   * @returns Result.
   */
  runScript(script) {
    return script.runInContext(this.global);
  }

  /**
   * Returns the VM context.
   *
   * @return Context.
   */
  getVmContext() {
    return this.global;
  }
};