import getDisplayName from 'react-display-name';
import {ShallowWrapper} from 'enzyme';
import expect from 'expect';

/**
 * Checks if the given value is an enzyme wrapper.
 * @param  {Any} actual - The value to check.
 * @return {Boolean} - Whether it's enzyme.
 */
const isEnzymeWrapper = (actual) => actual instanceof ShallowWrapper;

/**
 * Throws an error if the given argument isn't an enzyme wrapper.
 * @param  {Any} actual - Preferably an enzyme wrapper.
 * @return {undefined}
 * @throws {Error} - If the type isn't enzyme.
 */
const assertIsEnzymeWrapper = (actual) => expect.assert(
  isEnzymeWrapper(actual),
  `${actual} is not an enzyme wrapper.`
);

const asserted = expect();

const original = {
  toBeAn: asserted.toBeAn,
  toBeA: asserted.toBeA,
};

/**
 * Assert a component has a property.
 * @param  {String} prop - An expected property.
 * @param  {Any} [value] - An expected value.
 * @return {this} - The current expect assertion.
 */
export function toHaveProp (prop, value) {
  assertIsEnzymeWrapper(this.actual);

  const actual = this.actual.props();
  const displayName = this.actual.name();

  expect.assert(
    actual.hasOwnProperty(prop),
    `Expected ${displayName} to have prop "${prop}".`
  );

  if (value !== undefined) {
    expect.assert(
      actual[prop] === value,
      `Expected ${displayName} property "${prop}" to be "${value}".`
    );
  }

  return this;
}

/**
 * Verifies a component was given certain props.
 * @param  {Object} props - Expected props.
 * @return {this} - The current assertion.
 */
export function toHaveProps (props) {

  // Props should be an object.
  expect.assert(
    props instanceof Object,
    `Method "toHaveProps()" expected a props object, was given "${props}".`
  );

  // Make sure we're dealing with an enzyme wrapper.
  assertIsEnzymeWrapper(this.actual);

  // Check each expected prop.
  Object.keys(props).forEach((key) => {
    const value = props[key];

    // Assert!
    this.toHaveProp(key, value);
  });

  // For chaining.
  return this;
}

/**
 * Assert the type of an enzyme wrapper.
 * @param  {String|Function} type - The type you expect your element to be.
 * @return {this} - The expectation.
 */
export function toBeA (type) {
  const isWrapper = isEnzymeWrapper(this.actual);

  // Defer everything non-enzyme related to the original method.
  if (isWrapper === false) {
    return original.toBeA.apply(this, arguments);
  }

  // User-friendly component description.
  const displayName = getDisplayName(type);
  const element = this.actual;
  const { article = 'a' } = this;

  // Check the type.
  expect.assert(
    element.is(type),
    `Expected ${element.name()} to be ${article} ${displayName}`
  );

  return this;
}

/**
 * Same as `.toBeA(type)`, but with different wording.
 * @param  {String|Function} type - The type you expect your element to be.
 * @return {this} - The expectation context.
 */
export function toBeAn (type) {
  const isWrapper = isEnzymeWrapper(this.actual);

  // Disregard non-enzyme things.
  if (!isWrapper) {
    return original.toBeAn.apply(this, arguments);
  }

  // Set the correct article form.
  this.article = 'an';
  this.toBeA(type);

  return this;
}

@@ -762,16 +762,687 @@
 eA,%0A%7D;%0A%0A
+/**%0A * Only attempts an enzyme assertion if the given value is an enzyme wrapper.%0A * @param  %7BFunction%7D defaultAssertion - An expect assertion to overload.%0A * @param  %7BFunction%7D enzymeAssertion - An assertion handler for enzyme types.%0A * @return %7Bthis%7D - The expectation context.%0A */%0Aconst handleEnzymeActual = (defaultAssertion, enzymeAssertion) =%3E %7B%0A  return function assertion () %7B%0A%0A    // Is the value an enzyme wrapper?%0A    if (isEnzymeWrapper(this.actual)) %7B%0A%0A      // Use the enzyme assertion.%0A      enzymeAssertion.apply(this, arguments);%0A%0A      return this;%0A    %7D%0A%0A    // Otherwise, use the built-in.%0A    return defaultAssertion.apply(this, arguments);%0A  %7D;%0A%7D;%0A%0A
 /**%0A * A
@@ -2876,230 +2876,74 @@
 ort 
-function toBeA (type) %7B%0A  const isWrapper = isEnzymeWrapper(this.actual);%0A%0A  // Defer everything non-enzyme related to the original method.%0A  if (isWrapper === false) %7B%0A    return original.toBeA.apply(this, arguments);%0A  %7D
+const toBeA = handleEnzymeActual(original.toBeA, function (type) %7B
 %0A%0A  
@@ -3231,32 +3231,34 @@
   return this;%0A%7D
+);
 %0A%0A/**%0A * Same as
@@ -3438,192 +3438,76 @@
 ort 
-function toBeAn (type) %7B%0A  const isWrapper = isEnzymeWrapper(this.actual);%0A%0A  // Disregard non-enzyme things.%0A  if (!isWrapper) %7B%0A    return original.toBeAn.apply(this, arguments);%0A  %7D
+const toBeAn = handleEnzymeActual(original.toBeAn, function (type) %7B
 %0A%0A  
@@ -3561,16 +3561,30 @@
  = 'an';
+%0A%0A  // Assert!
 %0A  this.
@@ -3596,26 +3596,28 @@
 (type);%0A%0A  return this;%0A
-
 %7D
+);
 %0A

Refactor expect method overloading