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

/**
 * Assertion Constructor
 *
 * 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
 * @returns {unknown}
 * @private
 */
export function Assertion (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);
}

Object.defineProperty(Assertion, 'includeStack', {
  get: function() {
    console.warn('Assertion.includeStack is deprecated, use chai.config.includeStack instead.');
    return config.includeStack;
  },
  set: function(value) {
    console.warn('Assertion.includeStack is deprecated, use chai.config.includeStack instead.');
    config.includeStack = value;
  }
});

Object.defineProperty(Assertion, 'showDiff', {
  get: function() {
    console.warn('Assertion.showDiff is deprecated, use chai.config.showDiff instead.');
    return config.showDiff;
  },
  set: function(value) {
    console.warn('Assertion.showDiff is deprecated, use chai.config.showDiff instead.');
    config.showDiff = value;
  }
});

Assertion.addProperty = function (name, fn) {
  util.addProperty(this.prototype, name, fn);
};

Assertion.addMethod = function (name, fn) {
  util.addMethod(this.prototype, name, fn);
};

Assertion.addChainableMethod = function (name, fn, chainingBehavior) {
  util.addChainableMethod(this.prototype, name, fn, chainingBehavior);
};

Assertion.overwriteProperty = function (name, fn) {
  util.overwriteProperty(this.prototype, name, fn);
};

Assertion.overwriteMethod = function (name, fn) {
  util.overwriteMethod(this.prototype, name, fn);
};

Assertion.overwriteChainableMethod = function (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} expression to be tested
 * @param {string | Function} message or function that returns message to display if expression fails
 * @param {string | Function} negatedMessage 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
 * @private
 */

Assertion.prototype.assert = function (expr, msg, negateMsg, expected, _actual, showDiff) {
  var 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);
    var actual = util.getActual(this, arguments);
    var assertionErrorObjectProperties = {
        actual: actual
      , expected: expected
      , showDiff: showDiff
    };

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

    throw new AssertionError(
      msg,
      assertionErrorObjectProperties,
      (config.includeStack) ? this.assert : util.flag(this, 'ssfi'));
  }
};

/**
 * ### ._obj
 *
 * Quick reference to stored `actual` value for plugin developers.
 *
 * @private
 */
Object.defineProperty(Assertion.prototype, '_obj',
  { get: function () {
      return util.flag(this, 'object');
    }
  , set: function (val) {
      util.flag(this, 'object', val);
    }
});
