/**
Copyright: 2024 Contrast Security, Inc
  Contact: support@contrastsecurity.com
  License: Commercial

  NOTICE: This Software and the patented inventions embodied within may only be
  used as part of Contrast Security’s commercial offerings. Even though it is
  made available through public repositories, use of this Software is subject to
  the applicable End User Licensing Agreement found at
  https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
  between Contrast Security and the End User. The Software may not be reverse
  engineered, modified, repackaged, sold, redistributed or otherwise used in a
  way not consistent with the End User License Agreement.
*/
'use strict';
const _ = require('lodash');
const { AsyncStorage, KEYS } = require('./core/async-storage');
const agentEmitter = require('./agent-emitter');
const logger = require('./core/logger')('contrast:coverage');
const RouteCoverage = require('./reporter/models/route-coverage');

class RouteCoverageInfo {
  constructor() {
    logger.debug('enabling coverage features');
    this.emitter = agentEmitter;
    this.routeInfo = new Map();
    this.discovered = new Set();
    this.registerListeners();
  }

  /**
   * Registers route-discovery and route-observation
   * events to format/emit other events for discovery
   * and observations
   */
  registerListeners() {
    this.emitter.on('route-discovery', (event) => {
      this.discover(event);
    });

    this.emitter.on(
      'route-observation',
      ({ id, url, verb, stack, discoveryMethod }) => {
        const info = { id, url, verb };
        const observed = this.observe({
          info,
          stack,
          sensor: this,
          discoveryMethod
        });

        if (observed) {
          // used to attach observation to a Finding
          AsyncStorage.set(KEYS.ROUTE, observed);
          this.clearObservations(id);
        }
      }
    );
  }

  /**
   * Add RouteCoverage to routeInfo map to be used to
   * lookup signature in route observation.  Keep track of
   * all the signatures discovered during this AppUpdate iteration
   * @param {Object} infos Route discovery data
   */
  discover(infos) {
    if (_.isEmpty(infos) || !_.isObject(infos)) {
      return;
    }

    for (const id of Object.keys(infos)) {
      const info = infos[id];

      this.routeInfo.set(id, new RouteCoverage(info));

      this.discovered.add(id);
    }
  }

  /**
   * Gets the signature from routeInfo, calls discoveryMethod
   * if it was not discovered and a discoveryMethod exists(dynamic route
   * registrations in Express/Kraken). Appends observations for Findings
   * and returns the serialized RouteCoverage to be used for observation
   * @param {Object} info   The route info being updated
   * @returns {Object} The up-to-date DTM
   */
  observe({ info, stack, sensor, discoveryMethod }) {
    if (_.isEmpty(info) || !_.isObject(info)) {
      return null;
    }

    const { id } = info;
    const discovered = this.routeInfo.has(id);

    // this is for Express/Kraken where routes
    // are defined after the initial startup
    // rediscover what is in the stack
    if (!discovered && discoveryMethod) {
      discoveryMethod(sensor, stack);
    }

    const route = this.routeInfo.get(id);
    if (!route) {
      logger.debug('Unable to observe routes not discovered %j', info);
      return null;
    }

    route.addObservation(info);

    return route.toSerializable();
  }

  /**
   * Removes the observations on a route
   * after the observations are reported
   */
  clearObservations(id) {
    const route = this.routeInfo.get(id);

    if (route) {
      route.observations = [];
    }
  }

  /**
   * Clear routes discovered after being reported
   */
  clearRoutes() {
    this.discovered.clear();
  }

  /**
   * Returns all route DTMs that have been reported since last reset.
   * We store routes internally by an ID that we generate.
   * @returns {RouteCoverageDTM[]}
   */
  getReport() {
    const report = new Map();

    for (const id of this.discovered) {
      const { signature } = this.routeInfo.get(id);
      const existing = report.get(signature);
      if (!existing) {
        report.set(signature, { signature });
      }
    }

    return Array.from(report.values());
  }
}

let coverage;
module.exports = function memoizedRouteCoverage() {
  if (!coverage) coverage = new RouteCoverageInfo();
  return coverage;
};
