index.js

/* eslint valid-jsdoc: 2 */
'use strict'

function isAPromise (promise) {
  return promise !== null &&
    (typeof promise === 'object' || typeof promise === 'function') &&
    typeof promise.then === 'function'
}

function deepEquals (a, b) {
  if (a === b) return true
  if (a === null || b === null) return false

  const type = typeof a
  const otherType = typeof b

  // TODO SUPPORT SYMBOLS
  if (type !== otherType || type !== 'object' || a.constructor !== b.constructor) {
    return false
  }

  if (Array.isArray(a)) {
    if (a.length === b.length) return a.every((el, i) => deepEquals(el, b[i]))
    return false
  }

  const names = Object.getOwnPropertyNames(a)
  if (names.length !== Object.getOwnPropertyNames(b).length) return false
  return names.every((n) => Object.prototype.hasOwnProperty.call(b, n) && deepEquals(a[n], b[n]))
}

const messageField = /{([\s\S]+?)}/g
function processMessage (message, values) {
  return message.replace(messageField, (match, field) => {
    if (field in values) return values[field]
    return match
  })
}

function requireTestFunction (fn, message = 'Test function required') {
  if (typeof fn !== 'function') throw new Error(message)
}

/**
 * Assertions will throw this error when a test fails
 */
class AssertionError extends Error {
  /**
   * @param {string} message - Error message
   * @param {*} [actual] - Actual value
   * @param {*} [expected] - Expected value
   */
  constructor (message, actual, expected) {
    super(message)
    this.name = 'Assertion Error'
    this.actual = actual
    this.expected = expected
  }
}

/**
 * @typedef {function(Assertion)} assertionCallback
 */

/**
 * It contains all the assertion methods.
 */
class Assertion {
  /**
   * Do no use it directly. Use the [module function]{@link module:xassert}
   * @param {*} actual - actual value, promise or function
   * @param {string} [name] - name of the field that could be used in the error messages
   * @param {Assertion} [parent] - parent assertion
   */
  constructor (actual, name, parent) {
    this.actual = actual
    this.name = name
    this.parent = parent
  }

  /**
   * @example
   * console.log(assert('orange').isAString().getActual()) // prints 'orange'
   * @returns {*} current value
   */
  getActual () {
    return this.actual
  }

  /**
   * @private
   * @deprecated
   */
  get ref () {
    return this.actual
  }

  /**
   * @deprecated {@link Assertion#getActual}
   * @returns {*} actual value
   */
  getRef () {
    return this.actual
  }

  /**
   * @example
   * console.log(assert('orange').isAString().getName()) // prints 'actual value'
   * console.log(assert('orange', 'fruit').isAString().getName()) // prints 'fruit'
   * @returns {string} current name
   */
  getName () {
    if (this.name) return this.name
    if (isAPromise(this.actual)) return 'promise'
    if (typeof this.actual === 'function') return 'function'
    return 'actual value'
  }

  /**
   * @returns {string} full name including parent names
   */
  getFullName () {
    return this.parent ? this.parent.getFullName() + ' ' + this.getName() : this.getName()
  }

  /**
   * It could be use for meaningful chains
   * @example
   * assert('a').isAString().andIt.hasLengthOf(1)
   * @member {this}
   */
  get andIt () {
    return this
  }

  /**
   * @param {string} name - name of the field
   * @returns {Assertion} new ValueAssertion with the same value and a new name
   */
  named (name) {
    return new Assertion(this.actual, name)
  }

  /**
   * @private
   */

  /**
   * It fires an AssertionError
   * @private
   * @param {string} message - error message
   * @param {*} [expected] - expected value
   * @returns {void}
   */
  fire (message, expected) {
    throw new AssertionError(
      processMessage(message, { name: this.getFullName() }),
      this.actual, expected)
  }

  /**
   * Asserts that the actual value is strictly equal to expected value
   *
   * @example
   * assert(value).isEqualTo('Banana')
   * @param {*} expected - expected value
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is not strictly equal to expected value
   * @return {this} chainable method
   */
  isEqualTo (expected, message = '{name} is different than expected value') {
    if (expected !== this.actual) this.fire(message, expected)
    return this
  }

  /**
   * Asserts that the actual value is strictly equal to any of expected values
   *
   * @example
   * assert(value).isEqualToAnyOf(['Banana', 'Apple'])
   * @param {*} expected - expected value
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is not strictly equal to any of expected values
   * @return {this} chainable method
   */
  isEqualToAnyOf (expected, message = '{name} is different than any expected value') {
    if (expected.every(arg => arg !== this.actual)) this.fire(message)
    return this
  }

  /**
   * Asserts that the actual value is not strictly equal to expected value
   *
   * @example
   * assert(value).isNotEqualTo('Banana')
   * @param {*} expected - expected value
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is strictly equal to expected value
   * @return {this} chainable method
   */
  isNotEqualTo (expected, message = '{name} is equal to expected value') {
    if (expected === this.actual) this.fire(message)
    return this
  }

  /**
   * Asserts that the actual value is not strictly equal to any of expected values
   *
   * @example
   * assert(value).isNotEqualToAnyOf(['Banana', 'Apple'])
   * @param {*} expected - expected value
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is strictly equal to any of expected values
   * @return {this} chainable method
   */
  isNotEqualToAnyOf (expected, message = '{name} is equal to some expected value') {
    if (expected.some(arg => arg === this.actual)) this.fire(message)
    return this
  }

  /**
   * Asserts that the actual value is deeply equal to expected value
   *
   * @example
   * assert({ c: 3 }).isDeeplyEqualTo({ c: 3 }) // Passes
   * assert({ c: 3 }).isDeeplyEqualTo('3') // Fail
   * @param {*} expected - expected value
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is not deeply equal to expected value
   * @return {this} chainable method
   */
  isDeeplyEqualTo (expected, message = '{name} is not deeply equal to expected value') {
    if (!deepEquals(this.actual, expected)) this.fire(message, expected)
    return this
  }

  /**
   * Alias of {@link module:xassert.Assertion#isDeeplyEqualTo}
   * @example
   * assert({ c: 3 }).is({ c: 3 }) // Passes
   * @param {*} expected - expected value
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is not deeply equal to expected value
   * @return {this} chainable method
   */
  is (expected, message = '{name} is not expected value') {
    return this.isDeeplyEqualTo(expected, message)
  }

  /**
   * Asserts that the actual value is not deeply equal to expected value
   *
   * @example
   * assert({ c: 3 }).isNotDeeplyEqualTo({ c: 3 }) // Fail
   * assert({ c: 3 }).isNotDeeplyEqualTo('3') // Passes
   * @param {*} expected - expected value
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is deeply equal to expected values
   * @return {this} chainable method
   */
  isNotDeeplyEqualTo (expected, message = 'actual is deeply equal to expected') {
    if (deepEquals(this.actual, expected)) this.fire(message, expected)
    return this
  }

  /**
   * Asserts that the actual value is deeply equal to any of expected values
   *
   * @example
   * assert({ c: 3 }).isDeeplyEqualToAnyOf([{ a: 3 } ,{ c: 3 }]) // Passes
   * assert({ c: 3 }).isDeeplyEqualToAnyOf([{ a: 3 } ,{ c: 4 }])') // Fail
   * @param {*} expected - expected value
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is not deeply equal to any of expected values
   * @return {this} chainable method
   */
  isDeeplyEqualToAnyOf (expected, message = '{name} is different than any of the expected values') {
    if (expected.every(arg => !deepEquals(this.actual, arg))) {
      this.fire(message)
    }
    return this
  }

  /**
   * Asserts that the actual value is not deeply equal to any of expected values
   *
   * @example
   * assert({ c: 3 }).isNotDeeplyEqualToAnyOf([{ a: 3 } ,{ c: 3 }]) // Fail
   * assert({ c: 3 }).isNotDeeplyEqualToAnyOf([{ a: 3 } ,{ c: 4 }])') // Passes
   * @param {*} expected - expected value
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is deeply equal to any of expected values
   * @return {this} chainable method
   */
  isNotDeeplyEqualToAnyOf (expected, message = '{name} is equal one of non expected values') {
    if (expected.some(arg => deepEquals(this.actual, arg))) {
      this.fire(message)
    }
    return this
  }

  // ######### IS & IS NOT ############

  /**
   * Asserts that the actual value is null
   *
   * @example
   * assert(null).isNull() // Passes
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is not null
   * @return {this} chainable method
   */
  isNull (message = '{name} is not null') {
    if (this.actual !== null) this.fire(message, null)
    return this
  }

  /**
   * Asserts that the actual value is strictly true
   *
   * @example
   * assert(true).isTrue() // Passes
   * assert('apple').isTrue() // Fail
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is not strictly true
   * @return {this} chainable method
   */
  isTrue (message = '{name} is not strictly true') {
    if (this.actual !== true) this.fire(message, true)
    return this
  }

  /**
   * Asserts that the actual value is strictly false
   *
   * @example
   * assert(false).isFalse() // Passes
   * assert('apple').isFalse() // Fail
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is not strictly false
   * @return {this} chainable method
   */
  isFalse (message = '{name} is not strictly false') {
    if (this.actual !== false) this.fire(message, false)
    return this
  }

  /**
   * Asserts that the actual value is truthy
   *
   * @example
   * assert(true).isTruthy() // Passes
   * assert('apple').isTruthy() // Passes
   * assert(null).isTruthy() // Fail
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is not truthy
   * @return {this} chainable method
   */
  isTruthy (message = '{name} is not truthy') {
    if (!this.actual) this.fire(message)
    return this
  }

  /**
   * Asserts that the actual value is falsy
   *
   * @example
   * assert(false).isFalsy() // Passes
   * assert(undefined).isFalsy() // Passes
   * assert('').isFalsy() // Passes
   * assert('apple').isFalsy() // Passes
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is not falsy
   * @return {this} chainable method
   */
  isFalsy (message = '{name} is not falsy') {
    if (this.actual) this.fire(message)
    return this
  }

  /**
   * Asserts that the actual value is not null
   *
   * @example
   * assert('a').isNotNull() // Passes
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is not null
   * @return {this} chainable method
   */
  isNotNull (message = '{name} is null') {
    if (this.actual === null) this.fire(message)
    return this
  }

  /**
   * Asserts that the actual value is undefined
   *
   * @example
   * assert(undefined).isUndefined() // Passes
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is not null
   * @return {this} chainable method
   */
  isUndefined (message = '{name} is not undefined') {
    if (typeof this.actual !== 'undefined') this.fire(message, undefined)
    return this
  }

  /**
   * Asserts that the actual value is not undefined
   *
   * @example
   * assert(undefined).isNotUndefined() // Fail
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is undefined
   * @return {this} chainable method
   */
  isNotUndefined (message = '{name} is undefined') {
    if (typeof this.actual === 'undefined') this.fire(message)
    return this
  }

  /**
   * Asserts that the actual value is NaN
   *
   * @example
   * assert('j').isNaN() // Passes
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is not NaN
   * @return {this} chainable method
   */
  isNaN (message = '{name} is not NaN') {
    if (!isNaN(this.actual)) this.fire(message, NaN)
    return this
  }

  /**
   * Asserts that the actual value is not NaN
   *
   * @example
   * assert('j').isNotNaN() // Fail
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is  NaN
   * @return {this} chainable method
   */
  isNotNaN (message = '{name} is NaN') {
    if (isNaN(this.actual)) this.fire(message)
    return this
  }

  /**
   * Asserts that the actual value is a promise
   *
   * @example
   * assert(Promise.resolve(3)).isAPromise() // Passes
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is not a promise
   * @return {this} chainable method
   */
  isAPromise (message = '{name} is not a promise') {
    if (!isAPromise(this.actual)) this.fire(message)
    return this
  }

  /**
   * Asserts that the actual value is not promise
   *
   * @example
   * assert(Promise.resolve(3)).isNotAPromise() // Fail
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is a promise
   * @return {this} chainable method
   */
  isNotAPromise (message = '{name} is a promise') {
    if (isAPromise(this.actual)) this.fire(message)
    return this
  }

  /**
   * Asserts that the actual value is a number
   *
   * @example
   * assert(4.3).isANumber() // Passes
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is not a number
   * @return {this} chainable method
   */
  isANumber (message = '{name} is not a number') {
    if (typeof this.actual !== 'number') this.fire(message)
    return this
  }

  /**
   * Asserts that the actual value is not a number
   *
   * @example
   * assert(4.3).isNotANumber() // Fail
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is a number
   * @return {this} chainable method
   */
  isNotANumber (message = '{name} is a number') {
    if (typeof this.actual === 'number') this.fire(message)
    return this
  }

  /**
   * Asserts that the actual value is a string
   *
   * @example
   * assert('banana').isAString() // Passes
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is not a string
   * @return {this} chainable method
   */
  isAString (message = '{name} is not a string') {
    if (typeof this.actual !== 'string') this.fire(message)
    return this
  }

  /**
   * Asserts that the actual value is not a string
   *
   * @example
   * assert('banana').isNotAString() // Fail
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is a string
   * @return {this} chainable method
   */
  isNotAString (message = '{name} is a string') {
    if (typeof this.actual === 'string') this.fire(message)
    return this
  }

  /**
   * Asserts that the actual value is an array
   *
   * @example
   * assert([2, 3]).isAnArray() // Passes
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is not an array
   * @return {this} chainable method
   */
  isAnArray (message = '{name} is not an array') {
    if (!Array.isArray(this.actual)) this.fire(message, this.actual)
    return this
  }

  /**
   * Asserts that the actual value is not an array
   *
   * @example
   * assert(33).isNotAnArray() // Passes
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is an array
   * @return {this} chainable method
   */
  isNotAnArray (message = '{name} is an array') {
    if (Array.isArray(this.actual)) this.fire(message, this.actual)
    return this
  }

  /**
   * Asserts that every value of the array pass the test
   * @example
   * assert([3, 6]).every(it => it.isAbove(2)) // Passes
   * @param {assertionCallback} test - test for each element
   * @throws {AssertionError}
   * when any value fails the test
   * @return {this} chainable method
   */
  every (test) {
    requireTestFunction(test)
    this.actual.forEach((it, i) => test(new Assertion(it, 'at index ' + i, this)))
    return this
  }

  /**
   * Asserts that some value of the array pass the test
   * @example
   * assert([3, 6]).some(it => it.isAbove(5)) // Passes
   * @param {assertionCallback} test - test for each element
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when no value passes the test
   * @return {this} chainable method
   */
  some (test, message = '{name} does not contain any item that passes any test') {
    requireTestFunction(test)
    const result = this.actual.some(it => {
      try { test(new Assertion(it)) } catch (error) {
        if (error instanceof AssertionError) return false
        throw error
      }
      return true
    })
    if (!result) this.fire(message)
    return this
  }

  /**
   * Asserts that the actual value includes the expected value using deep equality
   * @example
   * assert([{ a: 4 }, { a: 6 }]).includes({ a: 6 }) // Passes
   * @param {*} expected - expected value
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value does not include the expected value
   * @return {this} chainable method
   */
  includes (expected, message = '{name} does not include the given item') {
    if (!this.actual.some(it => deepEquals(it, expected))) this.fire(message)
    return this
  }

  /**
   * Asserts that the actual value only includes the expected values using deep equality.
   * It could include duplicates.
   * @example
   * assert([1, 2]).includes([2, 1, 3]) // Passes
   * assert([1, 1]).includes([2, 1, 3]) // Passes
   * assert([1, 7]).includes([2, 1, 3]) // Fails
   * @param {Array} expected - expected value
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value does not only include the expected values
   * @return {this} chainable method
   */
  includesOnly (expected, message = '{name} does not include only the given items') {
    if (!this.actual.some(a => expected.some(b => deepEquals(a, b)))) this.fire(message)
    return this
  }

  /**
   * Asserts that the actual value at a given index pass a tests
   * @example
   * assert([3, 6]).every(it => it.isAbove(2)) // Passes
   * @param {number} index - index to be tested
   * @param {assertionCallback} test - test
   * @throws {AssertionError}
   * when any value at a given index fails the test
   * @return {this} chainable method
   */
  item (index, test) {
    test(new Assertion(this.actual[index], 'at index ' + index, this))
    return this
  }

  /**
   * Asserts that the actual value has the given property and run some test on it
   * @example
   * assert({ a: 3 }).includesProperty('a', it => it.isAbove(2)) // Passes
   * @param {string} name - name of the property
   * @param {assertionCallback} test - test for the property
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value does not have
   * the given property or the tests fails
   * @return {this} chainable method
   */
  includesProperty (name, test, message = '{name} does not contain the property {property}') {
    if (!(this.actual && name in this.actual)) this.fire(processMessage(message, { property: name }))
    if (typeof test === 'function') test(new Assertion(this.actual[name], name + ' property', this))
    return this
  }

  /**
   * Asserts that the actual value does not have the given property
   * @example
   * assert({ a: 3 }).doesNotIncludeProperty('b') // Passes
   * @param {string} name - name of the property
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value has the given property
   * @return {this} chainable method
   */
  doesNotIncludeProperty (name, message = '{name} contains the property {property}') {
    if (this.actual && name in this.actual) this.fire(processMessage(message, { property: name }))
    return this
  }

  /**
   * Asserts that the actual value has the own given property and run some test on it
   * @example
   * assert({ a: 3 }).includesOwnProperty('a', it => it.isAbove(2)) // Passes
   * @param {string} name - name of the property
   * @param {assertionCallback} test - test for the property
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value does not have
   * the own given property or the tests fails
   * @return {this} chainable method
   */
  includesOwnProperty (name, test, message = '{name} does not include the own property {property}') {
    if (!(this.actual instanceof Object && Object.prototype.hasOwnProperty.call(this.actual, name))) {
      this.fire(processMessage(message, { property: name }))
    }
    if (typeof test === 'function') test(new Assertion(this.actual[name], name + ' own property', this))
    return this
  }

  /**
   * Asserts that the actual value does not have the own given property
   * @example
   * assert({ a: 3 }).doesNotIncludeOwnProperty('b') // Passes
   * @param {string} name - name of the property
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value has the own given property
   * @return {this} chainable method
   */
  doesNotIncludeOwnProperty (name, message = '{name} contains the own property {property}') {
    if (this.actual instanceof Object && Object.prototype.hasOwnProperty.call(this.actual, name)) {
      this.fire(processMessage(message, { property: name }))
    }
    return this
  }

  /**
   * Asserts that the property length exists and optionally pass some test against it
   * @example
   * assert('').hasLength() // Passes
   * assert(1).hasLength() // Fails
   * assert([2]).hasLength() // Passes
   * assert([2, 5]).hasLength(it => it.isAbove(1)) // Passes
   * @param {assertionCallback} [test] - test for the property
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the property does not exists or the test fails
   * @return {this} chainable method
   */
  hasLength (test, message = '{name} does not have length property') {
    const ref = this.actual
    // Empty strings are falsy
    if (ref === null || ref === undefined || typeof ref.length !== 'number') this.fire(message)
    // 'length' in 'a string' throws an error
    if (typeof test === 'function') test(new Assertion(ref.length, 'length property', this))
    return this
  }

  /**
   * Asserts that the property length exists and it is equal to the giving number
   * @example
   * assert('').hasLengthOf(0) // Passes
   * assert(1).hasLengthOf(1) // Fails
   * assert([2, 3]).hasLengthOf(2) // Passes
   * @param {number} expected - expected length
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the property does not exists or the test fails
   * @return {this} chainable method
   */
  hasLengthOf (expected, message) {
    this.hasLength(it => it.isEqualTo(expected, message))
    return this
  }

  /**
   * Asserts that the actual value is above the given number
   * @example
   * assert(4).isAbove(3) // Passes
   * assert(4).isAbove(4) // Fails
   * @param {number} number - given number
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is not above the given number
   * @return {this} chainable method
   */
  isAbove (number, message = '{name} is not above expected value') {
    if (this.actual <= number) this.fire(message, number)
    return this
  }

  /**
   * Asserts that the actual value is at least the given number
   * @example
   * assert(4).isAtLeast(4) // Passes
   * assert(4).isAtLeast(5) // Fails
   * @param {number} number - given number
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is not at least the given number
   * @return {this} chainable method
   */
  isAtLeast (number, message = '{name} is not at least as expected value') {
    if (this.actual < number) this.fire(message, number)
    return this
  }

  /**
   * Asserts that the actual value is below the given number
   * @example
   * assert(4).isBelow(5) // Passes
   * assert(4).isBelow(4) // Fails
   * @param {number} number - given number
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is not bellow the given number
   * @return {this} chainable method
   */
  isBelow (number, message = '{name} is not below expected value') {
    if (this.actual >= number) this.fire(message, number)
    return this
  }

  /**
   * Asserts that the actual value is at most the given number
   * @example
   * assert(4).isAtMost(4) // Passes
   * assert(4).isAtMost(3) // Fails
   * @param {number} number - given number
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is not at most the given number
   * @return {this} chainable method
   */
  isAtMost (number, message = '{name} is not at most as expected value') {
    if (this.actual > number) this.fire(message, number)
    return this
  }

  /**
   * Asserts that the actual value is a instance of a given class
   * @example
   * assert(new Cat()).isInstanceOf(Animal) // Passes
   * assert(null).isInstanceOf(Object) // Passes
   * assert(aCar).isInstanceOf(Plane) // Fail
   * @param {number} expected - expected class
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is not a instance of a given class
   * @return {this} chainable method
   */
  isInstanceOf (expected, message = '{name} is not instance of class ' + expected.name) {
    if (!(this.actual instanceof expected)) this.fire(message, expected)
    return this
  }

  /**
   * Asserts that the actual value is frozen
   * @example
   * assert(Object.freeze({ a: 1 })).isFrozen() // Passes
   * assert({ a: 1 }).isFrozen() // Fail
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is not frozen
   * @return {this} chainable method
   */
  isFrozen (message = '{name} is not frozen') {
    if (!Object.isFrozen(this.actual)) this.fire(message)
    return this
  }

  /**
   * Asserts that the actual value is not frozen
   * @example
   * assert(Object.freeze({ a: 1 })).isNotFrozen() // Fail
   * assert({ a: 1 }).isNotFrozen() // Passes
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value is frozen
   * @return {this} chainable method
   */
  isNotFrozen (message = '{name} is frozen') {
    if (Object.isFrozen(this.actual)) this.fire(message)
    return this
  }

  /**
   * Asserts that the promise is fulfilled and the test passes
   * @example
   * assert(Promise.resolve(3)).isFulfilled() // Passes
   * assert(Promise.resolve(3)).isFulfilled(it => it.isEqualTo(3)) // Passes
   * assert(Promise.resolve(3)).isFulfilled(it => it.isEqualTo(5)) // Fail
   * assert(Promise.reject(new Error())).isFulfilled() // Fail
   * @param {assertionCallback} [test] - test for the resolved value
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the promise is rejected and the test fails
   * @return {Promise<*>} resolved promise with the value
   */
  isFulfilled (test, message = '{name} has been rejected') {
    return this.actual.then(
      value => {
        if (typeof test === 'function') test(new Assertion(value, 'resolved actual value', this))
        return value
      },
      ex => this.fire(message)
    )
  }

  /**
   * Asserts that the promise is rejected and the test passes
   * @example
   * assert(Promise.resolve(3)).isRejected() // Fail
   * assert(Promise.reject(new Error)).isRejected(it => it.isInstanceOf(Error)) // Passes
   * @param {assertionCallback} [test] - test for the resolved value
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the promise is fulfilled and the test fails
   * @return {Promise<*>} resolved promise with the error
   */
  isRejected (test, message = '{name} has been fulfilled') {
    return this.actual.then(
      value => this.fire(message),
      error => {
        if (typeof test === 'function') test(new Assertion(error, 'error'))
        return error
      }
    )
  }

  /**
   * Asserts that the provided function throws an exception and optionally tests the error
   * @example
   * assert(() => throw new Error()).throws() // Passes
   * assert(() => throw new Error()).throws(it => it.isInstanceOf(Error)) // Passes
   * assert(() => 3).throws() // Fails
   * @param {assertionCallback} [test] - test error
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the provided function does not throw an exception or the test fails
   * @return {this} chainable method
   */
  throws (test, message = '{name} did not throw') {
    try {
      this.actual()
    } catch (error) {
      if (test) {
        requireTestFunction(test)
        test(new Assertion(error, 'error', this))
      }
      return this
    }
    this.fire(message)
  }

  /**
   * Asserts that the provided function throws the given exception
   * @example
   * assert(() => throw new NotFoundError()).throwsA(NotFoundError) // Passes
   * assert(() => throw new ServerError()).throwsA(NotFoundError) // Fails
   * @param {function} [classRef] - class reference
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the provided function does not throw the given exception
   * @return {this} chainable method
   */
  throwsA (classRef, message = '{name} is not a {class}') {
    return this.throws(it => it.isInstanceOf(
      classRef,
      processMessage(message, { class: classRef.name })
    ))
  }

  /**
   * Asserts that the provided function throws the given exception.
   * Alias of {@link module:xassert.Assertion#throwsA}
   * @example
   * assert(() => throw new Error()).throwsAn(Error) // Passes
   * assert(() => throw new Error()).throwsAn(InvalidFormat) // Fails
   * @param {function} [classRef] - class reference
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the provided function does not throw the given exception
   * @return {this} chainable method
   */
  throwsAn (classRef, message = '{name} in not an {class}') {
    return this.throwsA(classRef, message)
  }

  /**
   * Asserts that the actual value matches the given regular expression.
   * @example
   * assert(() => throw new Error()).throwsAn(Error) // Passes
   * assert(() => throw new Error()).throwsAn(InvalidFormat) // Fails
   * @param {RegExp} [re] - regular expression
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value does not match the given regular expression
   * @return {this} chainable method
   */
  matches (re, message = '{name} does not match the given regular expression: {regexp}') {
    if (!re.test(this.actual)) this.fire(processMessage(message, { regexp: re }))
    return this
  }

  /**
   * Asserts that the actual value contains the given string.
   * @example
   * assert('abcd').contains('bc') // Passes
   * assert('abcd').contains('ac') // Fails
   * @param {string} str - given string
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value does not contain the given string
   * @return {this} chainable method
   */
  contains (str, message = '{name} does not contain the given string') {
    if (this.actual.indexOf(str) === -1) this.fire(message)
    return this
  }

  /**
   * Asserts that the actual value starts with the given string.
   * @example
   * assert('abcd').startsWith('ab') // Passes
   * assert('abcd').startsWith('bc') // Fails
   * @param {string} str - given string
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value does not start with the given string
   * @return {this} chainable method
   */
  startsWith (str, message = '{name} does not start with the given string') {
    if (!this.actual.startsWith(str)) this.fire(message)
    return this
  }

  /**
   * Asserts that the actual value ends with the given string.
   * @example
   * assert('abcd').endsWith('cd') // Passes
   * assert('abcd').endsWith('bc') // Fails
   * @param {string} str - given string
   * @param {string} [message] - error message
   * @throws {AssertionError}
   * when the actual value does not end with the given string
   * @return {this} chainable method
   */
  endsWith (str, message = '{name} does not end with the given string') {
    if (!this.actual.endsWith(str)) this.fire(message)
    return this
  }

  satisfies (test, message = '{name} does not satisfy the given test') {
    requireTestFunction(test, 'satisfies requires a test function')
    if (!test(this.actual)) this.fire(message)
    return this
  }
}

/**
 * Extensible assertions.
 * @module xassert
 */

/**
 * Creates and returns a value assertion
 * @alias module:xassert
 * @param {*} ref - actual value, promise or function
 * @param {string} [name] - alias for the actual value
 * @returns {Assertion} new assertion instance
 * @example
 * const assert = require('xassert')
 * assert(4).isANumber()
 */
function assert (ref, name) {
  return new Assertion(ref, name)
}

/**
 * @param {string} message - message for the {@link AssertionError} constructor
 * @returns {void}
 * @example
 * const assert = require('xassert')
 * assert.fail('Ops!') // This line will throw an AssertionError
 */
assert.fail = function fail (message) {
  throw new AssertionError(message)
}

/**
 * Support function to easily create tests. If the editor supports
 * JSDOC comments it will assist you.
 * @param {assertionCallback} test - test
 * @returns {Assertion} return the same test
 * @example
 * const isABanana = assert.fn(it => it.isEqualTo('BANANA'))
 * // same as "const isABanana = it => it.isEqualTo('BANANA')"
 * const object = { a:'BANANA', b:'APPLE' }
 * isABanana(assert('BANANA'))
 * assert(object)
 *   .includesProperty('a', isABanana) // Passes
 *   .includesProperty('b', isABanana) // Fails
 */
assert.fn = function callback (test) {
  requireTestFunction(test)
  return test
}

assert.Assertion = Assertion
assert.AssertionError = AssertionError

module.exports = assert