/**
 * "Buffered" reporter used internally by a worker process when running in parallel mode.
 * @module nodejs/reporters/parallel-buffered
 * @public
 */

'use strict';

/**
 * @typedef {import('../../types.d.ts').BufferedEvent} BufferedEvent
 * @typedef {import('../../runner.js')} Runner
 */

/**
 * Module dependencies.
 */

const {
  EVENT_SUITE_BEGIN,
  EVENT_SUITE_END,
  EVENT_TEST_FAIL,
  EVENT_TEST_PASS,
  EVENT_TEST_PENDING,
  EVENT_TEST_BEGIN,
  EVENT_TEST_END,
  EVENT_TEST_RETRY,
  EVENT_DELAY_BEGIN,
  EVENT_DELAY_END,
  EVENT_HOOK_BEGIN,
  EVENT_HOOK_END,
  EVENT_RUN_END
} = require('../../runner').constants;
const {SerializableEvent, SerializableWorkerResult} = require('../serializer');
const debug = require('debug')('mocha:reporters:buffered');
const Base = require('../../reporters/base');

/**
 * List of events to listen to; these will be buffered and sent
 * when `Mocha#run` is complete (via {@link ParallelBuffered#done}).
 */
const EVENT_NAMES = [
  EVENT_SUITE_BEGIN,
  EVENT_SUITE_END,
  EVENT_TEST_BEGIN,
  EVENT_TEST_PENDING,
  EVENT_TEST_FAIL,
  EVENT_TEST_PASS,
  EVENT_TEST_RETRY,
  EVENT_TEST_END,
  EVENT_HOOK_BEGIN,
  EVENT_HOOK_END
];

/**
 * Like {@link EVENT_NAMES}, except we expect these events to only be emitted
 * by the `Runner` once.
 */
const ONCE_EVENT_NAMES = [EVENT_DELAY_BEGIN, EVENT_DELAY_END];

/**
 * The `ParallelBuffered` reporter is used by each worker process in "parallel"
 * mode, by default.  Instead of reporting to `STDOUT`, etc., it retains a
 * list of events it receives and hands these off to the callback passed into
 * {@link Mocha#run}. That callback will then return the data to the main
 * process.
 * @public
 */
class ParallelBuffered extends Base {
  /**
   * Calls {@link ParallelBuffered#createListeners}
   * @param {Runner} runner
   */
  constructor(runner, opts) {
    super(runner, opts);

    /**
     * Retained list of events emitted from the {@link Runner} instance.
     * @type {BufferedEvent[]}
     * @public
     */
    this.events = [];

    /**
     * Map of `Runner` event names to listeners (for later teardown)
     * @public
     * @type {Map<string,EventListener>}
     */
    this.listeners = new Map();

    this.createListeners(runner);
  }

  /**
   * Returns a new listener which saves event data in memory to
   * {@link ParallelBuffered#events}. Listeners are indexed by `eventName` and stored
   * in {@link ParallelBuffered#listeners}. This is a defensive measure, so that we
   * don't a) leak memory or b) remove _other_ listeners that may not be
   * associated with this reporter.
   *
   * Subclasses could override this behavior.
   *
   * @public
   * @param {string} eventName - Name of event to create listener for
   * @returns {EventListener}
   */
  createListener(eventName) {
    const listener = (runnable, err) => {
      this.events.push(SerializableEvent.create(eventName, runnable, err));
    };
    return this.listeners.set(eventName, listener).get(eventName);
  }

  /**
   * Creates event listeners (using {@link ParallelBuffered#createListener}) for each
   * reporter-relevant event emitted by a {@link Runner}. This array is drained when
   * {@link ParallelBuffered#done} is called by {@link Runner#run}.
   *
   * Subclasses could override this behavior.
   * @public
   * @param {Runner} runner - Runner instance
   * @returns {ParallelBuffered}
   * @chainable
   */
  createListeners(runner) {
    EVENT_NAMES.forEach(evt => {
      runner.on(evt, this.createListener(evt));
    });
    ONCE_EVENT_NAMES.forEach(evt => {
      runner.once(evt, this.createListener(evt));
    });

    runner.once(EVENT_RUN_END, () => {
      debug('received EVENT_RUN_END');
      this.listeners.forEach((listener, evt) => {
        runner.removeListener(evt, listener);
        this.listeners.delete(evt);
      });
    });

    return this;
  }

  /**
   * Calls the {@link Mocha#run} callback (`callback`) with the test failure
   * count and the array of {@link BufferedEvent} objects. Resets the array.
   *
   * This is called directly by `Runner#run` and should not be called by any other consumer.
   *
   * Subclasses could override this.
   *
   * @param {number} failures - Number of failed tests
   * @param {Function} callback - The callback passed to {@link Mocha#run}.
   * @public
   */
  done(failures, callback) {
    callback(SerializableWorkerResult.create(this.events, failures));
    this.events = []; // defensive
  }
}

module.exports = ParallelBuffered;
