/**
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';

/**
 * execute conditionals (not, anyOf, oneOf, allOf, if/then/else) in the
 * same order as ajv.
 *
 * @param {object} schema to use for validations
 * @returns {boolean} validation result
 */
function executeConditionals(schema) {
  let valid = true;
  for (const key of conditionals) {
    // is this conditional present? if so validate its argument (value)
    if (key in schema) {
      const conditional = conditionalDispatch.get(key);

      // if the conditional has a validator to check its schema, do it.
      if (conditional.validator) {
        if (!conditional.validator(schema[key])) {
          this.logger.trace(`conditional schema ${key} not valid`);
          continue;
        }
      }
      valid = conditional.method.call(this, schema[key]) && valid;
      if (!valid && !this.allErrors) {
        return valid;
      }
    }
  }

  if ('if' in schema) {
    valid = handleIfThenElse.call(this, schema.if, schema.then, schema.else) && valid;
  }

  return valid;
}

/**
 * check whether the schema has properties other than conditionals
 *
 * @param {object} schema the schema to inspect
 * @returns {boolean} true if there are properties other than conditionals
 */
function notOnlyConditionals(schema) {
  const props = new Set([...Object.getOwnPropertyNames(schema)]);
  for (const conditionalName of conditionals) {
    props.delete(conditionalName);
  }
  props.delete('if');
  props.delete('then');
  props.delete('else');
  props.delete('$id');
  return props.size !== 0;
}

/**
 * handle not. it only defines what isn't matched.
 */
function handleNot(schema) {
  this.not = !this.not;
  const valid = this.evaluate(schema);
  this.not = !this.not;

  return !valid;
}

// the spec doesn't define whether all schema will be evaluated
// or not. the only safe way is to stop after the first.
// that requires the same logic as "if", i.e., an actual
// evaluation of the schema, not just matching the type.
function handleAnyOf(schema) {
  let valid = false;
  for (const leg of schema) {
    if (this.evaluate(leg)) {
      valid = true;
      break;
    }
  }
  return valid;
}

//
// all legs must pass validation. ajv executes all legs of the schema
// so do the same even though there is no need to.
//
function handleAllOf(schema) {
  let valid = true;
  for (const leg of schema) {
    valid = this.evaluate(leg) && valid;
  }
  return valid;
}

//
// only one leg can pass validation
//
function handleOneOf(schema) {
  let valid = 0;
  for (const leg of schema) {
    if (this.evaluate(leg)) {
      valid += 1;
    }
  }
  return valid === 1;
}

/**
 * @param {schema} ifSchema determines whether then or else leg is executed
 * @param {schema|undefined} thenSchema determine validation if ifSchema validates
 * @param {schema|undefined} elseSchema determine validation if ifSchema fails validation
 * @return {boolean} valid
 */
function handleIfThenElse(ifSchema, thenSchema, elseSchema) {
  if (this.evaluate(ifSchema)) {
    if (thenSchema === undefined) {
      return true;
    }
    return this.evaluate(thenSchema);
  } else {
    if (elseSchema === undefined) {
      return true;
    }
    return this.evaluate(elseSchema);
  }
}

/**
 * @param {any} item to be checked
 * @returns {boolean} true if item is a non-empty array
 */
function nonEmptyArray(item) {
  return Array.isArray(item) && item.length;
}

// not, anyOf, oneOf, and allOf are in the same order as ajv evaluates them.
// if/then/else is handled as a single function in the conditional handling
// function and is special-cased in notOnlyConditionals().
const conditionalDispatch = new Map([
  ['not', { method: handleNot }],
  ['anyOf', { method: handleAnyOf, validator: nonEmptyArray }],
  ['oneOf', { method: handleOneOf, validator: nonEmptyArray }],
  ['allOf', { method: handleAllOf, validator: nonEmptyArray }]
]);

const conditionals = [...conditionalDispatch.keys()];

module.exports = {
  executeConditionals,
  notOnlyConditionals
};
