<commit_before>var createStandardErrorMessage = require('./createStandardErrorMessage');
var utils = require('./utils');
var magicpen = require('magicpen');
var extend = utils.extend;
var leven = require('leven');
var makePromise = require('./makePromise');
var addAdditionalPromiseMethods = require('./addAdditionalPromiseMethods');
var isPendingPromise = require('./isPendingPromise');
var oathbreaker = require('./oathbreaker');
var UnexpectedError = require('./UnexpectedError');
var notifyPendingPromise = require('./notifyPendingPromise');
var defaultDepth = require('./defaultDepth');
var createWrappedExpectProto = require('./createWrappedExpectProto');
var AssertionString = require('./AssertionString');
var throwIfNonUnexpectedError = require('./throwIfNonUnexpectedError');
var makeDiffResultBackwardsCompatible = require('./makeDiffResultBackwardsCompatible');

function isAssertionArg(arg) {
    return arg.type.is('assertion');
}

var anyType = {
    _unexpectedType: true,
    name: 'any',
    level: 0,
    identify: function () {
        return true;
    },
    equal: utils.objectIs,
    inspect: function (value, depth, output) {
        if (output && output.isMagicPen) {
            return output.text(value);
        } else {
            // Guard against node.js' require('util').inspect eagerly calling .inspect() on objects
            return 'type: ' + this.name;
        }
    },
    diff: function (actual, expected, output, diff, inspect) {
        return null;
    },
    typeEqualityCache: {},
    is: function (typeOrTypeName) {
        var typeName;
        if (typeof typeOrTypeName === 'string') {
            typeName = typeOrTypeName;
        } else {
            typeName = typeOrTypeName.name;
        }

        var cachedValue = this.typeEqualityCache[typeName];
        if (typeof cachedValue !== 'undefined') {
            return cachedValue;
        }

        var result = false;
        if (this.name === typeName) {
            result = true;
        } else if (this.baseType) {
            result = this.baseType.is(typeName);
        }
        this.typeEqualityCache[typeName] = result;
        return result;
    }
};


function Unexpected(options) {
    options = options || {};
    this.assertions = options.assertions || {};
    this.typeByName = options.typeByName || {any: anyType};
    this.types = options.types || [anyType];
    if (options.output) {
        this.output = options.output;
    } else {
        this.output = magicpen();
        this.output.inline = false;
        this.output.diff = false;
    }
    this._outputFormat = options.format || magicpen.defaultFormat;
    this.installedPlugins = options.installedPlugins || [];

    // Make bound versions of these two helpers up front to save a bit when creating wrapped expects:
    var that = this;
    this.getType = function (typeName) {
        return utils.findFirst(that.types, function (type) {
            return type.name === typeName;
        });
    };
    this.findTypeOf = function (obj) {
        return utils.findFirst(that.types || [], function (type) {
            return type.identify(obj);
        });
    };
    this.findCommonType = function (a, b) {
        var aAncestorIndex = {};
        var current = this.findTypeOf(a);
        while (current) {
            aAncestorIndex[current.name] = current;
            current = current.baseType;
        }
        current = this.findTypeOf(b);
        while (current) {
            if (aAncestorIndex[current.name]) {
                return current;
            }
            current = current.baseType;
        }
    };
    this._wrappedExpectProto = createWrappedExpectProto(this);
}

var OR = {};
function getOrGroups(expectations) {
    var orGroups = [[]];
    expectations.forEach(function (expectation) {
        if (expectation === OR) {
            orGroups.push([]);
        } else {
            orGroups[orGroups.length - 1].push(expectation);
        }
    });
    return orGroups;
}

function evaluateGroup(expect, subject, orGroup) {
    return orGroup.map(function (expectation) {
        var args = Array.prototype.slice.call(expectation);
        args.unshift(subject);
        return {
            expectation: args,
            promise: makePromise(function () {
                return expect.apply(expect, args);
            })
        };
    });
}

function writeGroupEvaluationsToOutput(expect, output, groupEvaluations) {
    var hasOrClauses = groupEvaluations.length > 1;
    var hasAndClauses = groupEvaluations.some(function (groupEvaluation) {
        return groupEvaluation.length > 1;
    });
    groupEvaluations.forEach(function (groupEvaluation, i) {
        if (i > 0) {
            if (hasAndClauses) {
                output.nl();
            } else {
                output.sp();
            }
            output.jsComment('or').nl();
        }

        var groupFailed = false;
        groupEvaluation.forEach(function (evaluation, j) {
            if (j > 0) {
                output.jsComment(' and').nl();
            }
            var isRejected = evaluation.promise.isRejected();
            if (isRejected && !groupFailed) {
                groupFailed = true;
                var err = evaluation.promise.reason();

                if (hasAndClauses || hasOrClauses) {
                    output.error('⨯ ');
                }

                output.block(function (output) {
                    output.append(err.getErrorMessage(output));
                });
            } else {
                if (isRejected) {
                    output.error('⨯ ');
                } else {
                    output.success('✓ ');
                }

                var expectation = evaluation.expectation;
                output.block(function (output) {
                    var subject = expectation[0];
                    var subjectOutput = function (output) {
                        output.appendInspected(subject);
                    };
                    var args = expectation.slice(2);
                    var argsOutput = args.map(function (arg) {
                        return function (output) {
                            output.appendInspected(arg);
                        };
                    });
                    var testDescription = expectation[1];
                    createStandardErrorMessage(output, subjectOutput, testDescription, argsOutput, {
                        subject: subject
                    });
                });
            }
        });
    });
}

function createExpectIt(expect, expectations) {
    var orGroups = getOrGroups(expectations);

    function expectIt(subject) {
        var groupEvaluations = [];
        var promises = [];
        orGroups.forEach(function (orGroup) {
            var evaluations = evaluateGroup(expect, subject, orGroup);
            evaluations.forEach(function (evaluation) {
                promises.push(evaluation.promise);
            });
            groupEvaluations.push(evaluations);
        });

        return oathbreaker(expect.promise.settle(promises).then(function () {
            groupEvaluations.forEach(function (groupEvaluation) {
                groupEvaluation.forEach(function (evaluation) {
                    if (evaluation.promise.isRejected() && evaluation.promise.reason().errorMode === 'bubbleThrough') {
                        throw evaluation.promise.reason();
                    }
                });
            });

            if (!groupEvaluations.some(function (groupEvaluation) {
                return groupEvaluation.every(function (evaluation) {
                    return evaluation.promise.isFulfilled();
                });
            })) {
                expect.fail(function (output) {
                    writeGroupEvaluationsToOutput(expect, output, groupEvaluations);
                });
            }
        }));
    }
    expectIt._expectIt = true;
    expectIt._expectations = expectations;
    expectIt._OR = OR;
    expectIt.and = function () {
        var copiedExpectations = expectations.slice();
        copiedExpectations.push(arguments);
        return createExpectIt(expect, copiedExpectations);
    };
    expectIt.or = function () {
        var copiedExpectations = expectations.slice();
        copiedExpectations.push(OR, arguments);
        return createExpectIt(expect, copiedExpectations);
    };
    return expectIt;
}

Unexpected.prototype.it = function () { // ...
    return createExpectIt(this.expect, [arguments]);
};

Unexpected.prototype.equal = function (actual, expected, depth, seen) {
    var that = this;

    depth = typeof depth === 'number' ? depth : 100;
    if (depth <= 0) {
        // detect recursive loops in the structure
        seen = seen || [];
        if (seen.indexOf(actual) !== -1) {
            throw new Error('Cannot compare circular structures');
        }
        seen.push(actual);
    }

    return this.findCommonType(actual, expected).equal(actual, expected, function (a, b) {
        return that.equal(a, b, depth - 1, seen);
    });
};

Unexpected.prototype.inspect = function (obj, depth, outputOrFormat) {
    var seen = [];
    var that = this;
    var printOutput = function (obj, currentDepth, output) {
        var objType = that.findTypeOf(obj);
        if (currentDepth <= 0 && objType.is('object') && !objType.is('expect.it')) {
            return output.text('...');
        }

        seen = seen || [];
        if (seen.indexOf(obj) !== -1) {
            return output.text('[Circular]');
        }

        return objType.inspect(obj, currentDepth, output, function (v, childDepth) {
            output = output.clone();
            seen.push(obj);
            if (typeof childDepth === 'undefined') {
                childDepth = currentDepth - 1;
            }
            output = printOutput(v, childDepth, output) || output;
            seen.pop();
            return output;
        });
    };

    var output = typeof outputOrFormat === 'string' ? this.createOutput(outputOrFormat) : outputOrFormat;
    output = output || this.createOutput();
    return printOutput(obj, typeof depth === 'number' ? depth : defaultDepth, output) || output;
};

Unexpected.prototype.expandTypeAlternations = function (assertion) {
    var that = this;
    function createPermutations(args, i) {
        if (i === args.length) {
            return [];
        }

        var result = [];
        args[i].forEach(function (arg) {
            var tails = createPermutations(args, i + 1);
            if (tails.length) {
                tails.forEach(function (tail) {
                    result.push([arg].concat(tail));
                });
            } else if (arg.type.is('assertion')) {
                result.push([
                    { type: arg.type, minimum: 1, maximum: 1 },
                    { type: that.typeByName.any, minimum: 0, maximum: Infinity }
                ]);
                result.push([
                    { type: that.typeByName['expect.it'], minimum: 1, maximum: 1 }
                ]);
                if (arg.minimum === 0) { // <assertion?>
                    result.push([]);
                }
            } else {
                result.push([arg]);
            }
        });
        return result;
    }
    var result = [];
    assertion.subject.forEach(function (subjectRequirement) {
        if (assertion.args.length) {
            createPermutations(assertion.args, 0).forEach(function (args) {
                result.push(extend({}, assertion, {
                    subject: subjectRequirement,
                    args: args
                }));
            });
        } else {
            result.push(extend({}, assertion, {
                subject: subjectRequirement,
                args: []
            }));
        }
    });
    return result;
};

Unexpected.prototype.parseAssertion = function (assertionString) {
    var that = this;
    var tokens = [];
    var nextIndex = 0;

    function lookupType(typeName) {
        var result = that.typeByName[typeName];
        if (!result) {
            throw new Error('Unknown type: ' + typeName + ' in ' + assertionString);
        }
        return result;
    }

    function parseType(assertionString) {
        return assertionString.split('|').map(function (type) {
            var matchNameAndOperator = type.match(/^([a-z_](?:|[a-z0-9_.-]*[_a-z0-9]))([+*?]|)$/i);
            if (!matchNameAndOperator) {
                throw new SyntaxError('Cannot parse type declaration:' + type);
            }
            var operator = matchNameAndOperator[2];
            return {
                minimum: !operator || operator === '+' ? 1 : 0,
                maximum: operator === '*' || operator === '+' ? Infinity : 1,
                type: lookupType(matchNameAndOperator[1])
            };
        });
    }

    function hasVarargs(types) {
        return types.some(function (type) {
            return type.minimum !== 1 || type.maximum !== 1;
        });
    }
    assertionString.replace(/\s*<((?:[a-z_](?:|[a-z0-9_.-]*[_a-z0-9])[?*+]?)(?:\|(?:[a-z_](?:|[a-z0-9_.-]*[_a-z0-9])[?*+]?))*)>|\s*([^<]+)/ig, function ($0, $1, $2, index) {
        if (index !== nextIndex) {
            throw new SyntaxError('Cannot parse token at index ' + nextIndex + ' in ' + assertionString);
        }
        if ($1) {
            tokens.push(parseType($1));
        } else {
            tokens.push($2.trim());
        }
        nextIndex += $0.length;
    });

    var assertion;
    if (tokens.length === 1 && typeof tokens[0] === 'string') {
        assertion = {
            subject: parseType('any'),
            assertion: tokens[0],
            args: [parseType('any*')]
        };
    } else {
        assertion = {
            subject: tokens[0],
            assertion: tokens[1],
            args: tokens.slice(2)
        };
    }

    if (!Array.isArray(assertion.subject)) {
        throw new SyntaxError('Missing subject type in ' + assertionString);
    }
    if (typeof assertion.assertion !== 'string')  {
        throw new SyntaxError('Missing assertion in ' + assertionString);
    }
    if (hasVarargs(assertion.subject)) {
        throw new SyntaxError('The subject type cannot have varargs: ' + assertionString);
    }
    if (assertion.args.some(function (arg) { return typeof arg === 'string'; })) {
        throw new SyntaxError('Only one assertion string is supported (see #225)');
    }

    if (assertion.args.slice(0, -1).some(hasVarargs)) {
        throw new SyntaxError('Only the last argument type can have varargs: ' + assertionString);
    }
    if ([assertion.subject].concat(assertion.args.slice(0, -1)).some(function (argRequirements) {
        return argRequirements.some(function (argRequirement) {
            return argRequirement.type.is('assertion');
        });
    })) {
        throw new SyntaxError('Only the last argument type can be <assertion>: ' + assertionString);
    }

    var lastArgRequirements = assertion.args[assertion.args.length - 1] || [];
    var assertionRequirements = lastArgRequirements.filter(function (argRequirement) {
        return argRequirement.type.is('assertion');
    });

    if (assertionRequirements.length > 0 && lastArgRequirements.length > 1) {
        throw new SyntaxError('<assertion> cannot be alternated with other types: ' + assertionString);
    }

    if (assertionRequirements.some(function (argRequirement) {
        return argRequirement.maximum !== 1;
    })) {
        throw new SyntaxError('<assertion+> and <assertion*> are not allowed: ' + assertionString);
    }

    return this.expandTypeAlternations(assertion);
};

var placeholderSplitRegexp = /(\{(?:\d+)\})/g;
var placeholderRegexp = /\{(\d+)\}/;
Unexpected.prototype.fail = function (arg) {
    if (arg instanceof UnexpectedError) {
        arg._hasSerializedErrorMessage = false;
        throw arg;
    }

    if (utils.isError(arg)) {
        throw arg;
    }

    var error = new UnexpectedError(this.expect);
    if (typeof arg === 'function') {
        error.errorMode = 'bubble';
        error.output = arg;
    } else if (arg && typeof arg === 'object') {
        if (typeof arg.message !== 'undefined') {
            error.errorMode = 'bubble';
        }
        error.output = function (output) {
            if (typeof arg.message !== 'undefined') {
                if (arg.message.isMagicPen) {
                    output.append(arg.message);
                } else if (typeof arg.message === 'function') {
                    arg.message.call(output, output);
                } else {
                    output.text(String(arg.message));
                }
            } else {
                output.error('Explicit failure');
            }
        };
        Object.keys(arg).forEach(function (key) {
            var value = arg[key];
            if (key === 'diff') {
                error.createDiff = value;
            } else if (key !== 'message') {
                error[key] = value;
            }
        });
    } else {
        var placeholderArgs;
        if (arguments.length > 0) {
            placeholderArgs = new Array(arguments.length - 1);
            for (var i = 1 ; i < arguments.length ; i += 1) {
                placeholderArgs[i - 1] = arguments[i];
            }
        }
        error.errorMode = 'bubble';
        error.output = function (output) {
            var message = arg ? String(arg) : 'Explicit failure';
            var tokens = message.split(placeholderSplitRegexp);
            tokens.forEach(function (token) {
                var match = placeholderRegexp.exec(token);
                if (match) {
                    var index = match[1];
                    if (index in placeholderArgs) {
                        var placeholderArg = placeholderArgs[index];
                        if (placeholderArg && placeholderArg.isMagicPen) {
                            output.append(placeholderArg);
                        } else {
                            output.appendInspected(placeholderArg);
                        }
                    } else {
                        output.text(match[0]);
                    }

                } else {
                    output.error(token);
                }
            });
        };
    }

    throw error;
};

function compareSpecificities(a, b) {
    for (var i = 0; i < Math.min(a.length, b.length); i += 1) {
        var c = b[i] - a[i];
        if (c !== 0) {
            return c;
        }
    }
    return b.length - a.length;
}

function calculateAssertionSpecificity(assertion) {
    return [assertion.subject.type.level].concat(assertion.args.map(function (argRequirement) {
        var bonus = argRequirement.minimum === 1 && argRequirement.maximum === 1 ?
            0.5 : 0;
        return bonus + argRequirement.type.level;
    }));
}

// expect.addAssertion(pattern, handler)
// expect.addAssertion([pattern, ...], handler)
Unexpected.prototype.addAssertion = function (patternOrPatterns, handler) {
    if (arguments.length > 2 || typeof handler !== 'function' || (typeof patternOrPatterns !== 'string' && !Array.isArray(patternOrPatterns))) {
        var errorMessage = "Syntax: expect.addAssertion(<string|array[string]>, function (expect, subject, ...) { ... });";
        if ((typeof handler === 'string' || Array.isArray(handler)) && typeof arguments[2] === 'function') {
            errorMessage +=
                '\nAs of Unexpected 10, the syntax for adding assertions that apply only to specific\n' +
                'types has changed. See http://unexpected.js.org/api/addAssertion/';
        }
        throw new Error(errorMessage);
    }
    var patterns = (Array.isArray(patternOrPatterns) ? patternOrPatterns : [patternOrPatterns]);
    patterns.forEach(function (pattern) {
        if (typeof pattern !== 'string' || pattern === '') {
            throw new Error('Assertion patterns must be a non-empty string');
        } else {
            if (pattern !== pattern.trim()) {
                throw new Error("Assertion patterns can't start or end with whitespace");
            }
        }
    });

    var that = this;
    var assertions = this.assertions;

    var defaultValueByFlag = {};
    var assertionHandlers = [];
    patterns.forEach(function (pattern) {
        var assertionDeclarations = that.parseAssertion(pattern);
        assertionDeclarations.forEach(function (assertionDeclaration) {
            ensureValidUseOfParenthesesOrBrackets(assertionDeclaration.assertion);
            var expandedAssertions = expandAssertion(assertionDeclaration.assertion);
            expandedAssertions.forEach(function (expandedAssertion) {
                Object.keys(expandedAssertion.flags).forEach(function (flag) {
                    defaultValueByFlag[flag] = false;
                });

                assertionHandlers.push({
                    handler: handler,
                    alternations: expandedAssertion.alternations,
                    flags: expandedAssertion.flags,
                    subject: assertionDeclaration.subject,
                    args: assertionDeclaration.args,
                    testDescriptionString: expandedAssertion.text,
                    declaration: pattern
                });
            });
        });
    });

    assertionHandlers.forEach(function (handler) {
        // Make sure that all flags are defined.
        handler.flags = extend({}, defaultValueByFlag, handler.flags);

        var assertionHandlers = assertions[handler.testDescriptionString];
        handler.specificity = calculateAssertionSpecificity(handler);
        if (!assertionHandlers) {
            assertions[handler.testDescriptionString] = [handler];
        } else {
            var i = 0;
            while (i < assertionHandlers.length && compareSpecificities(handler.specificity, assertionHandlers[i].specificity) > 0) {
                i += 1;
            }
            assertionHandlers.splice(i, 0, handler);
        }
    });

    return this.expect; // for chaining
};

Unexpected.prototype.addType = function (type) {
    var that = this;
    var baseType;
    if (typeof type.name !== 'string' || !/^[a-z_](?:|[a-z0-9_.-]*[_a-z0-9])$/i.test(type.name)) {
        throw new Error('A type must be given a non-empty name and must match ^[a-z_](?:|[a-z0-9_.-]*[_a-z0-9])$');
    }

    if (typeof type.identify !== 'function' && type.identify !== false) {
        throw new Error('Type ' + type.name + ' must specify an identify function or be declared abstract by setting identify to false');
    }

    if (this.getType(type.name)) {
        throw new Error('The type with the name ' + type.name + ' already exists');
    }

    if (type.base) {
        baseType = utils.findFirst(this.types, function (t) {
            return t.name === type.base;
        });

        if (!baseType) {
            throw new Error('Unknown base type: ' + type.base);
        }
    } else {
        baseType = anyType;
    }

    var extendedBaseType = Object.create(baseType);
    extendedBaseType.inspect = function (value, depth, output) {
        if (!output || !output.isMagicPen) {
            throw new Error('You need to pass the output to baseType.inspect() as the third parameter');
        }

        return baseType.inspect(value, depth, output.clone(), function (value, depth) {
            return output.clone().appendInspected(value, depth);
        });
    };

    extendedBaseType.diff = function (actual, expected, output) {
        if (!output || !output.isMagicPen) {
            throw new Error('You need to pass the output to baseType.diff() as the third parameter');
        }

        return makeDiffResultBackwardsCompatible(baseType.diff(actual, expected,
                      output.clone(),
                      function (actual, expected) {
                          return that.diff(actual, expected, output.clone());
                      },
                      function (value, depth) {
                          return output.clone().appendInspected(value, depth);
                      },
                      that.equal.bind(that)));
    };

    extendedBaseType.equal = function (actual, expected) {
        return baseType.equal(actual, expected, that.equal.bind(that));
    };

    var extendedType = extend({}, baseType, type, { baseType: extendedBaseType });
    var originalInspect = extendedType.inspect;

    extendedType.inspect = function (obj, depth, output, inspect) {
        if (arguments.length < 2) {
            return 'type: ' + type.name;
        } else {
            return originalInspect.call(this, obj, depth, output, inspect);
        }
    };
    if (extendedType.identify === false) {
        this.types.push(extendedType);
    } else {
        this.types.unshift(extendedType);
    }

    extendedType.level = baseType.level + 1;
    extendedType.typeEqualityCache = {};
    this.typeByName[extendedType.name] = extendedType;

    return this.expect;
};

Unexpected.prototype.addStyle = function () { // ...
    return this.output.addStyle.apply(this.output, arguments);
};

Unexpected.prototype.installTheme = function () { // ...
    return this.output.installTheme.apply(this.output, arguments);
};

Unexpected.prototype.use = function (plugin) {
    var existingPlugin = utils.findFirst(this.installedPlugins, function (installedPlugin) {
        if (installedPlugin === plugin) {
            return true;
        } else if (typeof plugin === 'function' && typeof installedPlugin === 'function') {
            var pluginName = utils.getFunctionName(plugin);
            return pluginName !== '' && pluginName === utils.getFunctionName(installedPlugin);
        } else {
            return installedPlugin.name === plugin.name;
        }
    });

    if (existingPlugin) {
        if (existingPlugin === plugin || (typeof plugin.version !== 'undefined' && plugin.version === existingPlugin.version)) {
            // No-op
            return this.expect;
        } else {
            throw new Error("Another instance of the plugin '" + plugin.name + "' " +
                            "is already installed" +
                            (typeof existingPlugin.version !== 'undefined' ?
                                ' (version ' + existingPlugin.version +
                                (typeof plugin.version !== 'undefined' ?
                                    ', trying to install ' + plugin.version : '') +
                                ')' : '') +
                            ". Please check your node_modules folder for unmet peerDependencies.");
        }
    }

    if ((typeof plugin !== 'function' && (typeof plugin !== 'object' || typeof plugin.installInto !== 'function')) ||
        (typeof plugin.name !== 'undefined' && typeof plugin.name !== 'string') ||
        (typeof plugin.dependencies !== 'undefined' && !Array.isArray(plugin.dependencies))) {
        throw new Error('Plugins must be functions or adhere to the following interface\n' +
                        '{\n' +
                        '  name: <an optional plugin name>,\n' +
                        '  version: <an optional semver version string>,\n' +
                        '  dependencies: <an optional list of dependencies>,\n' +
                        '  installInto: <a function that will update the given expect instance>\n' +
                        '}');
    }

    if (plugin.name === 'unexpected-promise') {
        throw new Error('The unexpected-promise plugin was pulled into Unexpected as of 8.5.0. This means that the plugin is no longer supported.');
    }

    if (plugin.dependencies) {
        var installedPlugins = this.installedPlugins;
        var unfulfilledDependencies = plugin.dependencies.filter(function (dependency) {
            return !installedPlugins.some(function (plugin) {
                return plugin.name === dependency;
            });
        });

        if (unfulfilledDependencies.length === 1) {
            throw new Error(plugin.name + ' requires plugin ' + unfulfilledDependencies[0]);
        } else if (unfulfilledDependencies.length > 1) {
            throw new Error(plugin.name + ' requires plugins ' +<commit_msg>Make sure that expect.use can handle function.name not being present on IE<commit_after>var createStandardErrorMessage = require('./createStandardErrorMessage');
var utils = require('./utils');
var magicpen = require('magicpen');
var extend = utils.extend;
var leven = require('leven');
var makePromise = require('./makePromise');
var addAdditionalPromiseMethods = require('./addAdditionalPromiseMethods');
var isPendingPromise = require('./isPendingPromise');
var oathbreaker = require('./oathbreaker');
var UnexpectedError = require('./UnexpectedError');
var notifyPendingPromise = require('./notifyPendingPromise');
var defaultDepth = require('./defaultDepth');
var createWrappedExpectProto = require('./createWrappedExpectProto');
var AssertionString = require('./AssertionString');
var throwIfNonUnexpectedError = require('./throwIfNonUnexpectedError');
var makeDiffResultBackwardsCompatible = require('./makeDiffResultBackwardsCompatible');

function isAssertionArg(arg) {
    return arg.type.is('assertion');
}

var anyType = {
    _unexpectedType: true,
    name: 'any',
    level: 0,
    identify: function () {
        return true;
    },
    equal: utils.objectIs,
    inspect: function (value, depth, output) {
        if (output && output.isMagicPen) {
            return output.text(value);
        } else {
            // Guard against node.js' require('util').inspect eagerly calling .inspect() on objects
            return 'type: ' + this.name;
        }
    },
    diff: function (actual, expected, output, diff, inspect) {
        return null;
    },
    typeEqualityCache: {},
    is: function (typeOrTypeName) {
        var typeName;
        if (typeof typeOrTypeName === 'string') {
            typeName = typeOrTypeName;
        } else {
            typeName = typeOrTypeName.name;
        }

        var cachedValue = this.typeEqualityCache[typeName];
        if (typeof cachedValue !== 'undefined') {
            return cachedValue;
        }

        var result = false;
        if (this.name === typeName) {
            result = true;
        } else if (this.baseType) {
            result = this.baseType.is(typeName);
        }
        this.typeEqualityCache[typeName] = result;
        return result;
    }
};


function Unexpected(options) {
    options = options || {};
    this.assertions = options.assertions || {};
    this.typeByName = options.typeByName || {any: anyType};
    this.types = options.types || [anyType];
    if (options.output) {
        this.output = options.output;
    } else {
        this.output = magicpen();
        this.output.inline = false;
        this.output.diff = false;
    }
    this._outputFormat = options.format || magicpen.defaultFormat;
    this.installedPlugins = options.installedPlugins || [];

    // Make bound versions of these two helpers up front to save a bit when creating wrapped expects:
    var that = this;
    this.getType = function (typeName) {
        return utils.findFirst(that.types, function (type) {
            return type.name === typeName;
        });
    };
    this.findTypeOf = function (obj) {
        return utils.findFirst(that.types || [], function (type) {
            return type.identify(obj);
        });
    };
    this.findCommonType = function (a, b) {
        var aAncestorIndex = {};
        var current = this.findTypeOf(a);
        while (current) {
            aAncestorIndex[current.name] = current;
            current = current.baseType;
        }
        current = this.findTypeOf(b);
        while (current) {
            if (aAncestorIndex[current.name]) {
                return current;
            }
            current = current.baseType;
        }
    };
    this._wrappedExpectProto = createWrappedExpectProto(this);
}

var OR = {};
function getOrGroups(expectations) {
    var orGroups = [[]];
    expectations.forEach(function (expectation) {
        if (expectation === OR) {
            orGroups.push([]);
        } else {
            orGroups[orGroups.length - 1].push(expectation);
        }
    });
    return orGroups;
}

function evaluateGroup(expect, subject, orGroup) {
    return orGroup.map(function (expectation) {
        var args = Array.prototype.slice.call(expectation);
        args.unshift(subject);
        return {
            expectation: args,
            promise: makePromise(function () {
                return expect.apply(expect, args);
            })
        };
    });
}

function writeGroupEvaluationsToOutput(expect, output, groupEvaluations) {
    var hasOrClauses = groupEvaluations.length > 1;
    var hasAndClauses = groupEvaluations.some(function (groupEvaluation) {
        return groupEvaluation.length > 1;
    });
    groupEvaluations.forEach(function (groupEvaluation, i) {
        if (i > 0) {
            if (hasAndClauses) {
                output.nl();
            } else {
                output.sp();
            }
            output.jsComment('or').nl();
        }

        var groupFailed = false;
        groupEvaluation.forEach(function (evaluation, j) {
            if (j > 0) {
                output.jsComment(' and').nl();
            }
            var isRejected = evaluation.promise.isRejected();
            if (isRejected && !groupFailed) {
                groupFailed = true;
                var err = evaluation.promise.reason();

                if (hasAndClauses || hasOrClauses) {
                    output.error('⨯ ');
                }

                output.block(function (output) {
                    output.append(err.getErrorMessage(output));
                });
            } else {
                if (isRejected) {
                    output.error('⨯ ');
                } else {
                    output.success('✓ ');
                }

                var expectation = evaluation.expectation;
                output.block(function (output) {
                    var subject = expectation[0];
                    var subjectOutput = function (output) {
                        output.appendInspected(subject);
                    };
                    var args = expectation.slice(2);
                    var argsOutput = args.map(function (arg) {
                        return function (output) {
                            output.appendInspected(arg);
                        };
                    });
                    var testDescription = expectation[1];
                    createStandardErrorMessage(output, subjectOutput, testDescription, argsOutput, {
                        subject: subject
                    });
                });
            }
        });
    });
}

function createExpectIt(expect, expectations) {
    var orGroups = getOrGroups(expectations);

    function expectIt(subject) {
        var groupEvaluations = [];
        var promises = [];
        orGroups.forEach(function (orGroup) {
            var evaluations = evaluateGroup(expect, subject, orGroup);
            evaluations.forEach(function (evaluation) {
                promises.push(evaluation.promise);
            });
            groupEvaluations.push(evaluations);
        });

        return oathbreaker(expect.promise.settle(promises).then(function () {
            groupEvaluations.forEach(function (groupEvaluation) {
                groupEvaluation.forEach(function (evaluation) {
                    if (evaluation.promise.isRejected() && evaluation.promise.reason().errorMode === 'bubbleThrough') {
                        throw evaluation.promise.reason();
                    }
                });
            });

            if (!groupEvaluations.some(function (groupEvaluation) {
                return groupEvaluation.every(function (evaluation) {
                    return evaluation.promise.isFulfilled();
                });
            })) {
                expect.fail(function (output) {
                    writeGroupEvaluationsToOutput(expect, output, groupEvaluations);
                });
            }
        }));
    }
    expectIt._expectIt = true;
    expectIt._expectations = expectations;
    expectIt._OR = OR;
    expectIt.and = function () {
        var copiedExpectations = expectations.slice();
        copiedExpectations.push(arguments);
        return createExpectIt(expect, copiedExpectations);
    };
    expectIt.or = function () {
        var copiedExpectations = expectations.slice();
        copiedExpectations.push(OR, arguments);
        return createExpectIt(expect, copiedExpectations);
    };
    return expectIt;
}

Unexpected.prototype.it = function () { // ...
    return createExpectIt(this.expect, [arguments]);
};

Unexpected.prototype.equal = function (actual, expected, depth, seen) {
    var that = this;

    depth = typeof depth === 'number' ? depth : 100;
    if (depth <= 0) {
        // detect recursive loops in the structure
        seen = seen || [];
        if (seen.indexOf(actual) !== -1) {
            throw new Error('Cannot compare circular structures');
        }
        seen.push(actual);
    }

    return this.findCommonType(actual, expected).equal(actual, expected, function (a, b) {
        return that.equal(a, b, depth - 1, seen);
    });
};

Unexpected.prototype.inspect = function (obj, depth, outputOrFormat) {
    var seen = [];
    var that = this;
    var printOutput = function (obj, currentDepth, output) {
        var objType = that.findTypeOf(obj);
        if (currentDepth <= 0 && objType.is('object') && !objType.is('expect.it')) {
            return output.text('...');
        }

        seen = seen || [];
        if (seen.indexOf(obj) !== -1) {
            return output.text('[Circular]');
        }

        return objType.inspect(obj, currentDepth, output, function (v, childDepth) {
            output = output.clone();
            seen.push(obj);
            if (typeof childDepth === 'undefined') {
                childDepth = currentDepth - 1;
            }
            output = printOutput(v, childDepth, output) || output;
            seen.pop();
            return output;
        });
    };

    var output = typeof outputOrFormat === 'string' ? this.createOutput(outputOrFormat) : outputOrFormat;
    output = output || this.createOutput();
    return printOutput(obj, typeof depth === 'number' ? depth : defaultDepth, output) || output;
};

Unexpected.prototype.expandTypeAlternations = function (assertion) {
    var that = this;
    function createPermutations(args, i) {
        if (i === args.length) {
            return [];
        }

        var result = [];
        args[i].forEach(function (arg) {
            var tails = createPermutations(args, i + 1);
            if (tails.length) {
                tails.forEach(function (tail) {
                    result.push([arg].concat(tail));
                });
            } else if (arg.type.is('assertion')) {
                result.push([
                    { type: arg.type, minimum: 1, maximum: 1 },
                    { type: that.typeByName.any, minimum: 0, maximum: Infinity }
                ]);
                result.push([
                    { type: that.typeByName['expect.it'], minimum: 1, maximum: 1 }
                ]);
                if (arg.minimum === 0) { // <assertion?>
                    result.push([]);
                }
            } else {
                result.push([arg]);
            }
        });
        return result;
    }
    var result = [];
    assertion.subject.forEach(function (subjectRequirement) {
        if (assertion.args.length) {
            createPermutations(assertion.args, 0).forEach(function (args) {
                result.push(extend({}, assertion, {
                    subject: subjectRequirement,
                    args: args
                }));
            });
        } else {
            result.push(extend({}, assertion, {
                subject: subjectRequirement,
                args: []
            }));
        }
    });
    return result;
};

Unexpected.prototype.parseAssertion = function (assertionString) {
    var that = this;
    var tokens = [];
    var nextIndex = 0;

    function lookupType(typeName) {
        var result = that.typeByName[typeName];
        if (!result) {
            throw new Error('Unknown type: ' + typeName + ' in ' + assertionString);
        }
        return result;
    }

    function parseType(assertionString) {
        return assertionString.split('|').map(function (type) {
            var matchNameAndOperator = type.match(/^([a-z_](?:|[a-z0-9_.-]*[_a-z0-9]))([+*?]|)$/i);
            if (!matchNameAndOperator) {
                throw new SyntaxError('Cannot parse type declaration:' + type);
            }
            var operator = matchNameAndOperator[2];
            return {
                minimum: !operator || operator === '+' ? 1 : 0,
                maximum: operator === '*' || operator === '+' ? Infinity : 1,
                type: lookupType(matchNameAndOperator[1])
            };
        });
    }

    function hasVarargs(types) {
        return types.some(function (type) {
            return type.minimum !== 1 || type.maximum !== 1;
        });
    }
    assertionString.replace(/\s*<((?:[a-z_](?:|[a-z0-9_.-]*[_a-z0-9])[?*+]?)(?:\|(?:[a-z_](?:|[a-z0-9_.-]*[_a-z0-9])[?*+]?))*)>|\s*([^<]+)/ig, function ($0, $1, $2, index) {
        if (index !== nextIndex) {
            throw new SyntaxError('Cannot parse token at index ' + nextIndex + ' in ' + assertionString);
        }
        if ($1) {
            tokens.push(parseType($1));
        } else {
            tokens.push($2.trim());
        }
        nextIndex += $0.length;
    });

    var assertion;
    if (tokens.length === 1 && typeof tokens[0] === 'string') {
        assertion = {
            subject: parseType('any'),
            assertion: tokens[0],
            args: [parseType('any*')]
        };
    } else {
        assertion = {
            subject: tokens[0],
            assertion: tokens[1],
            args: tokens.slice(2)
        };
    }

    if (!Array.isArray(assertion.subject)) {
        throw new SyntaxError('Missing subject type in ' + assertionString);
    }
    if (typeof assertion.assertion !== 'string')  {
        throw new SyntaxError('Missing assertion in ' + assertionString);
    }
    if (hasVarargs(assertion.subject)) {
        throw new SyntaxError('The subject type cannot have varargs: ' + assertionString);
    }
    if (assertion.args.some(function (arg) { return typeof arg === 'string'; })) {
        throw new SyntaxError('Only one assertion string is supported (see #225)');
    }

    if (assertion.args.slice(0, -1).some(hasVarargs)) {
        throw new SyntaxError('Only the last argument type can have varargs: ' + assertionString);
    }
    if ([assertion.subject].concat(assertion.args.slice(0, -1)).some(function (argRequirements) {
        return argRequirements.some(function (argRequirement) {
            return argRequirement.type.is('assertion');
        });
    })) {
        throw new SyntaxError('Only the last argument type can be <assertion>: ' + assertionString);
    }

    var lastArgRequirements = assertion.args[assertion.args.length - 1] || [];
    var assertionRequirements = lastArgRequirements.filter(function (argRequirement) {
        return argRequirement.type.is('assertion');
    });

    if (assertionRequirements.length > 0 && lastArgRequirements.length > 1) {
        throw new SyntaxError('<assertion> cannot be alternated with other types: ' + assertionString);
    }

    if (assertionRequirements.some(function (argRequirement) {
        return argRequirement.maximum !== 1;
    })) {
        throw new SyntaxError('<assertion+> and <assertion*> are not allowed: ' + assertionString);
    }

    return this.expandTypeAlternations(assertion);
};

var placeholderSplitRegexp = /(\{(?:\d+)\})/g;
var placeholderRegexp = /\{(\d+)\}/;
Unexpected.prototype.fail = function (arg) {
    if (arg instanceof UnexpectedError) {
        arg._hasSerializedErrorMessage = false;
        throw arg;
    }

    if (utils.isError(arg)) {
        throw arg;
    }

    var error = new UnexpectedError(this.expect);
    if (typeof arg === 'function') {
        error.errorMode = 'bubble';
        error.output = arg;
    } else if (arg && typeof arg === 'object') {
        if (typeof arg.message !== 'undefined') {
            error.errorMode = 'bubble';
        }
        error.output = function (output) {
            if (typeof arg.message !== 'undefined') {
                if (arg.message.isMagicPen) {
                    output.append(arg.message);
                } else if (typeof arg.message === 'function') {
                    arg.message.call(output, output);
                } else {
                    output.text(String(arg.message));
                }
            } else {
                output.error('Explicit failure');
            }
        };
        Object.keys(arg).forEach(function (key) {
            var value = arg[key];
            if (key === 'diff') {
                error.createDiff = value;
            } else if (key !== 'message') {
                error[key] = value;
            }
        });
    } else {
        var placeholderArgs;
        if (arguments.length > 0) {
            placeholderArgs = new Array(arguments.length - 1);
            for (var i = 1 ; i < arguments.length ; i += 1) {
                placeholderArgs[i - 1] = arguments[i];
            }
        }
        error.errorMode = 'bubble';
        error.output = function (output) {
            var message = arg ? String(arg) : 'Explicit failure';
            var tokens = message.split(placeholderSplitRegexp);
            tokens.forEach(function (token) {
                var match = placeholderRegexp.exec(token);
                if (match) {
                    var index = match[1];
                    if (index in placeholderArgs) {
                        var placeholderArg = placeholderArgs[index];
                        if (placeholderArg && placeholderArg.isMagicPen) {
                            output.append(placeholderArg);
                        } else {
                            output.appendInspected(placeholderArg);
                        }
                    } else {
                        output.text(match[0]);
                    }

                } else {
                    output.error(token);
                }
            });
        };
    }

    throw error;
};

function compareSpecificities(a, b) {
    for (var i = 0; i < Math.min(a.length, b.length); i += 1) {
        var c = b[i] - a[i];
        if (c !== 0) {
            return c;
        }
    }
    return b.length - a.length;
}

function calculateAssertionSpecificity(assertion) {
    return [assertion.subject.type.level].concat(assertion.args.map(function (argRequirement) {
        var bonus = argRequirement.minimum === 1 && argRequirement.maximum === 1 ?
            0.5 : 0;
        return bonus + argRequirement.type.level;
    }));
}

// expect.addAssertion(pattern, handler)
// expect.addAssertion([pattern, ...], handler)
Unexpected.prototype.addAssertion = function (patternOrPatterns, handler) {
    if (arguments.length > 2 || typeof handler !== 'function' || (typeof patternOrPatterns !== 'string' && !Array.isArray(patternOrPatterns))) {
        var errorMessage = "Syntax: expect.addAssertion(<string|array[string]>, function (expect, subject, ...) { ... });";
        if ((typeof handler === 'string' || Array.isArray(handler)) && typeof arguments[2] === 'function') {
            errorMessage +=
                '\nAs of Unexpected 10, the syntax for adding assertions that apply only to specific\n' +
                'types has changed. See http://unexpected.js.org/api/addAssertion/';
        }
        throw new Error(errorMessage);
    }
    var patterns = (Array.isArray(patternOrPatterns) ? patternOrPatterns : [patternOrPatterns]);
    patterns.forEach(function (pattern) {
        if (typeof pattern !== 'string' || pattern === '') {
            throw new Error('Assertion patterns must be a non-empty string');
        } else {
            if (pattern !== pattern.trim()) {
                throw new Error("Assertion patterns can't start or end with whitespace");
            }
        }
    });

    var that = this;
    var assertions = this.assertions;

    var defaultValueByFlag = {};
    var assertionHandlers = [];
    patterns.forEach(function (pattern) {
        var assertionDeclarations = that.parseAssertion(pattern);
        assertionDeclarations.forEach(function (assertionDeclaration) {
            ensureValidUseOfParenthesesOrBrackets(assertionDeclaration.assertion);
            var expandedAssertions = expandAssertion(assertionDeclaration.assertion);
            expandedAssertions.forEach(function (expandedAssertion) {
                Object.keys(expandedAssertion.flags).forEach(function (flag) {
                    defaultValueByFlag[flag] = false;
                });

                assertionHandlers.push({
                    handler: handler,
                    alternations: expandedAssertion.alternations,
                    flags: expandedAssertion.flags,
                    subject: assertionDeclaration.subject,
                    args: assertionDeclaration.args,
                    testDescriptionString: expandedAssertion.text,
                    declaration: pattern
                });
            });
        });
    });

    assertionHandlers.forEach(function (handler) {
        // Make sure that all flags are defined.
        handler.flags = extend({}, defaultValueByFlag, handler.flags);

        var assertionHandlers = assertions[handler.testDescriptionString];
        handler.specificity = calculateAssertionSpecificity(handler);
        if (!assertionHandlers) {
            assertions[handler.testDescriptionString] = [handler];
        } else {
            var i = 0;
            while (i < assertionHandlers.length && compareSpecificities(handler.specificity, assertionHandlers[i].specificity) > 0) {
                i += 1;
            }
            assertionHandlers.splice(i, 0, handler);
        }
    });

    return this.expect; // for chaining
};

Unexpected.prototype.addType = function (type) {
    var that = this;
    var baseType;
    if (typeof type.name !== 'string' || !/^[a-z_](?:|[a-z0-9_.-]*[_a-z0-9])$/i.test(type.name)) {
        throw new Error('A type must be given a non-empty name and must match ^[a-z_](?:|[a-z0-9_.-]*[_a-z0-9])$');
    }

    if (typeof type.identify !== 'function' && type.identify !== false) {
        throw new Error('Type ' + type.name + ' must specify an identify function or be declared abstract by setting identify to false');
    }

    if (this.getType(type.name)) {
        throw new Error('The type with the name ' + type.name + ' already exists');
    }

    if (type.base) {
        baseType = utils.findFirst(this.types, function (t) {
            return t.name === type.base;
        });

        if (!baseType) {
            throw new Error('Unknown base type: ' + type.base);
        }
    } else {
        baseType = anyType;
    }

    var extendedBaseType = Object.create(baseType);
    extendedBaseType.inspect = function (value, depth, output) {
        if (!output || !output.isMagicPen) {
            throw new Error('You need to pass the output to baseType.inspect() as the third parameter');
        }

        return baseType.inspect(value, depth, output.clone(), function (value, depth) {
            return output.clone().appendInspected(value, depth);
        });
    };

    extendedBaseType.diff = function (actual, expected, output) {
        if (!output || !output.isMagicPen) {
            throw new Error('You need to pass the output to baseType.diff() as the third parameter');
        }

        return makeDiffResultBackwardsCompatible(baseType.diff(actual, expected,
                      output.clone(),
                      function (actual, expected) {
                          return that.diff(actual, expected, output.clone());
                      },
                      function (value, depth) {
                          return output.clone().appendInspected(value, depth);
                      },
                      that.equal.bind(that)));
    };

    extendedBaseType.equal = function (actual, expected) {
        return baseType.equal(actual, expected, that.equal.bind(that));
    };

    var extendedType = extend({}, baseType, type, { baseType: extendedBaseType });
    var originalInspect = extendedType.inspect;

    extendedType.inspect = function (obj, depth, output, inspect) {
        if (arguments.length < 2) {
            return 'type: ' + type.name;
        } else {
            return originalInspect.call(this, obj, depth, output, inspect);
        }
    };
    if (extendedType.identify === false) {
        this.types.push(extendedType);
    } else {
        this.types.unshift(extendedType);
    }

    extendedType.level = baseType.level + 1;
    extendedType.typeEqualityCache = {};
    this.typeByName[extendedType.name] = extendedType;

    return this.expect;
};

Unexpected.prototype.addStyle = function () { // ...
    return this.output.addStyle.apply(this.output, arguments);
};

Unexpected.prototype.installTheme = function () { // ...
    return this.output.installTheme.apply(this.output, arguments);
};

function getPluginName(plugin) {
    if (typeof plugin === 'function') {
        return utils.getFunctionName(plugin);
    } else {
        return plugin.name;
    }
}

Unexpected.prototype.use = function (plugin) {
    if ((typeof plugin !== 'function' && (typeof plugin !== 'object' || typeof plugin.installInto !== 'function')) ||
        (typeof plugin.name !== 'undefined' && typeof plugin.name !== 'string') ||
        (typeof plugin.dependencies !== 'undefined' && !Array.isArray(plugin.dependencies))) {
        throw new Error(
            'Plugins must be functions or adhere to the following interface\n' +
            '{\n' +
            '  name: <an optional plugin name>,\n' +
            '  version: <an optional semver version string>,\n' +
            '  dependencies: <an optional list of dependencies>,\n' +
            '  installInto: <a function that will update the given expect instance>\n' +
            '}'
        );
    }


    var pluginName = getPluginName(plugin);

    var existingPlugin = utils.findFirst(this.installedPlugins, function (installedPlugin) {
        if (installedPlugin === plugin) {
            return true;
        } else {
            return pluginName && pluginName === getPluginName(installedPlugin);
        }
    });

    if (existingPlugin) {
        if (existingPlugin === plugin || (typeof plugin.version !== 'undefined' && plugin.version === existingPlugin.version)) {
            // No-op
            return this.expect;
        } else {
            throw new Error("Another instance of the plugin '" + pluginName + "' " +
                            "is already installed" +
                            (typeof existingPlugin.version !== 'undefined' ?
                                ' (version ' + existingPlugin.version +
                                (typeof plugin.version !== 'undefined' ?
                                    ', trying to install ' + plugin.version : '') +
                                ')' : '') +
                            ". Please check your node_modules folder for unmet peerDependencies.");
        }
    }

    if (pluginName === 'unexpected-promise') {
        throw new Error('The unexpected-promise plugin was pulled into Unexpected as of 8.5.0. This means that the plugin is no longer supported.');
    }

    if (plugin.dependencies) {
        var installedPlugins = this.installedPlugins;
        var unfulfilledDependencies = plugin.dependencies.filter(function (dependency) {
            return !installedPlugins.some(function (plugin) {
                return getPluginName(plugin) === dependency;
            });
        });

        if (unfulfilledDependencies.length === 1) {
            throw new Error(pluginName + ' requires plugin ' + unfulfilledDependencies[0]);
        } else if (unfulfilledDependencies.length > 1) {
            throw new Error(pluginName + ' requires plugins ' +
{'fraction_non_alphanumeric': 0.12641904356463743, 'fraction_numerical': 0.0038668937136370086, 'mean_word_length': 1.9398237471971633, 'pattern_counts': {'":': 0, '<': 39, '<?xml version=': 0, '>': 45, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7345314', 'n_tokens_mistral': 15236, 'n_tokens_neox': 14621, 'n_words': 3354}