/*!
 * chai
 * http://chaijs.com
 * Copyright(c) 2011-2014 Jake Luer <jake@alogicalparadox.com>
 * MIT Licensed
 */

import {config} from './config.js';
import {AssertionError} from 'assertion-error';
import * as util from './utils/index.js';

export class Assertion {
  /** @type {{}} */
  __flags = {};

  /**
   * Creates object for chaining.
   * `Assertion` objects contain metadata in the form of flags. Three flags can
   * be assigned during instantiation by passing arguments to this constructor:
   *
   * - `object`: This flag contains the target of the assertion. For example, in
   * the assertion `expect(numKittens).to.equal(7);`, the `object` flag will
   * contain `numKittens` so that the `equal` assertion can reference it when
   * needed.
   *
   * - `message`: This flag contains an optional custom error message to be
   * prepended to the error message that's generated by the assertion when it
   * fails.
   *
   * - `ssfi`: This flag stands for "start stack function indicator". It
   * contains a function reference that serves as the starting point for
   * removing frames from the stack trace of the error that's created by the
   * assertion when it fails. The goal is to provide a cleaner stack trace to
   * end users by removing Chai's internal functions. Note that it only works
   * in environments that support `Error.captureStackTrace`, and only when
   * `Chai.config.includeStack` hasn't been set to `false`.
   *
   * - `lockSsfi`: This flag controls whether or not the given `ssfi` flag
   * should retain its current value, even as assertions are chained off of
   * this object. This is usually set to `true` when creating a new assertion
   * from within another assertion. It's also temporarily set to `true` before
   * an overwritten assertion gets called by the overwriting assertion.
   *
   * - `eql`: This flag contains the deepEqual function to be used by the assertion.
   *
   * @param {unknown} obj target of the assertion
   * @param {string} [msg] (optional) custom error message
   * @param {Function} [ssfi] (optional) starting point for removing stack frames
   * @param {boolean} [lockSsfi] (optional) whether or not the ssfi flag is locked
   */
  constructor(obj, msg, ssfi, lockSsfi) {
    util.flag(this, 'ssfi', ssfi || Assertion);
    util.flag(this, 'lockSsfi', lockSsfi);
    util.flag(this, 'object', obj);
    util.flag(this, 'message', msg);
    util.flag(this, 'eql', config.deepEqual || util.eql);

    return util.proxify(this);
  }

  /** @returns {boolean} */
  static get includeStack() {
    console.warn(
      'Assertion.includeStack is deprecated, use chai.config.includeStack instead.'
    );
    return config.includeStack;
  }

  /** @param {boolean} value */
  static set includeStack(value) {
    console.warn(
      'Assertion.includeStack is deprecated, use chai.config.includeStack instead.'
    );
    config.includeStack = value;
  }

  /** @returns {boolean} */
  static get showDiff() {
    console.warn(
      'Assertion.showDiff is deprecated, use chai.config.showDiff instead.'
    );
    return config.showDiff;
  }

  /** @param {boolean} value */
  static set showDiff(value) {
    console.warn(
      'Assertion.showDiff is deprecated, use chai.config.showDiff instead.'
    );
    config.showDiff = value;
  }

  /**
   * @param {string} name
   * @param {Function} fn
   */
  static addProperty(name, fn) {
    util.addProperty(this.prototype, name, fn);
  }

  /**
   * @param {string} name
   * @param {Function} fn
   */
  static addMethod(name, fn) {
    util.addMethod(this.prototype, name, fn);
  }

  /**
   * @param {string} name
   * @param {Function} fn
   * @param {Function} chainingBehavior
   */
  static addChainableMethod(name, fn, chainingBehavior) {
    util.addChainableMethod(this.prototype, name, fn, chainingBehavior);
  }

  /**
   * @param {string} name
   * @param {Function} fn
   */
  static overwriteProperty(name, fn) {
    util.overwriteProperty(this.prototype, name, fn);
  }

  /**
   * @param {string} name
   * @param {Function} fn
   */
  static overwriteMethod(name, fn) {
    util.overwriteMethod(this.prototype, name, fn);
  }

  /**
   * @param {string} name
   * @param {Function} fn
   * @param {Function} chainingBehavior
   */
  static overwriteChainableMethod(name, fn, chainingBehavior) {
    util.overwriteChainableMethod(this.prototype, name, fn, chainingBehavior);
  }

  /**
   * ### .assert(expression, message, negateMessage, expected, actual, showDiff)
   *
   * Executes an expression and check expectations. Throws AssertionError for reporting if test doesn't pass.
   *
   * @name assert
   * @param {unknown} _expr to be tested
   * @param {string | Function} msg or function that returns message to display if expression fails
   * @param {string | Function} _negateMsg or function that returns negatedMessage to display if negated expression fails
   * @param {unknown} expected value (remember to check for negation)
   * @param {unknown} _actual (optional) will default to `this.obj`
   * @param {boolean} showDiff (optional) when set to `true`, assert will display a diff in addition to the message if expression fails
   * @returns {void}
   */
  assert(_expr, msg, _negateMsg, expected, _actual, showDiff) {
    const ok = util.test(this, arguments);
    if (false !== showDiff) showDiff = true;
    if (undefined === expected && undefined === _actual) showDiff = false;
    if (true !== config.showDiff) showDiff = false;

    if (!ok) {
      msg = util.getMessage(this, arguments);
      const actual = util.getActual(this, arguments);
      /** @type {Record<PropertyKey, unknown>} */
      const assertionErrorObjectProperties = {
        actual: actual,
        expected: expected,
        showDiff: showDiff
      };

      const operator = util.getOperator(this, arguments);
      if (operator) {
        assertionErrorObjectProperties.operator = operator;
      }

      throw new AssertionError(
        msg,
        assertionErrorObjectProperties,
        // @ts-expect-error Not sure what to do about these types yet
        config.includeStack ? this.assert : util.flag(this, 'ssfi')
      );
    }
  }

  /**
   * Quick reference to stored `actual` value for plugin developers.
   *
   * @returns {unknown}
   */
  get _obj() {
    return util.flag(this, 'object');
  }

  /**
   * Quick reference to stored `actual` value for plugin developers.
   *
   * @param {unknown} val
   */
  set _obj(val) {
    util.flag(this, 'object', val);
  }
}
