/**
 * QUnit-TAP - A TAP Output Producer Plugin for QUnit
 *
 * https://github.com/twada/qunit-tap
 * version: 1.4.2
 *
 * Copyright (c) 2010-2014 Takuto Wada
 * Dual licensed under the MIT and GPLv2 licenses.
 *   https://raw.github.com/twada/qunit-tap/master/MIT-LICENSE.txt
 *   https://raw.github.com/twada/qunit-tap/master/GPL-LICENSE.txt
 *
 * A part of extend function is:
 *   Copyright 2012 jQuery Foundation and other contributors
 *   Released under the MIT license.
 *   http://jquery.org/license
 *
 * A part of stripTags function is:
 *   Copyright (c) 2005-2010 Sam Stephenson
 *   Released under the MIT license.
 *   http://prototypejs.org
 */
(function (root, factory) {
    'use strict';

    // using returnExports UMD pattern
    if (typeof define === 'function' && define.amd) {
        define(factory);
    } else if (typeof exports === 'object') {
        module.exports = factory();
    } else {
        root.qunitTap = factory();
    }
}(this, function () {
    'use strict';

    var qunitTapVersion = '1.4.2',
        slice = Array.prototype.slice;

    // borrowed from qunit.js
    function extend (a, b) {
        var prop;
        for (prop in b) {
            if (b.hasOwnProperty(prop)) {
                if (typeof b[prop] === 'undefined') {
                    delete a[prop];
                } else {
                    a[prop] = b[prop];
                }
            }
        }
        return a;
    }

    function indexOf (ary, element) {
        var i;
        for (i = 0; i < ary.length; i += 1) {
            if (ary[i] === element) {
                return i;
            }
        }
        return -1;
    }

    function removeElement (ary, element) {
        var index = indexOf(ary, element);
        if (index !== -1) {
            return ary.splice(index, 1);
        } else {
            return [];
        }
    }

    function isPlanRequired (conf) {
        return (typeof conf !== 'undefined' && typeof conf.requireExpects !== 'undefined' && conf.requireExpects);
    }

    function isPassed (details) {
        return !!(details.result);
    }

    function isFailed (details) {
        return !(isPassed(details));
    }

    function isAssertOkFailed (details) {
        return isFailed(details) && typeof details.expected === 'undefined' && typeof details.actual === 'undefined';
    }

    // borrowed from prototype.js
    // not required since QUnit.log receives raw data (details). see jquery/qunit@c2cde34
    function stripTags (str) {
        if (!str) {
            return str;
        }
        return str.replace(/<\w+(\s+("[^"]*"|'[^']*'|[^>])+)?>|<\/\w+>/gi, '');
    }

    function escapeLineEndings (str) {
        return str.replace(/(\r?\n)/g, '$&# ');
    }

    function ltrim (str) {
        return str.replace(/^\s+/, '');
    }

    function noop (obj) {
        return obj;
    }

    function render (desc, fieldName, fieldValue, formatter) {
        desc.push(fieldName + ': ' + formatter(fieldValue));
    }

    function renderIf (shouldRender, desc, fieldName, fieldValue, formatter) {
        if (!shouldRender || typeof fieldValue === 'undefined') {
            return;
        }
        render(desc, fieldName, fieldValue, formatter);
    }

    function formatTestLine (testLine, rest) {
        if (!rest) {
            return testLine;
        }
        return testLine + ' - ' + escapeLineEndings(rest);
    }

    var extractDetailsFrom = (function () {
        var detailsExtractor;

        function setupExtractor (logArguments) {
            switch (logArguments.length) {
            case 1:  // details
                detailsExtractor = function (args) { return args[0]; };
                break;
            case 2:  // result, message(with tags)
                detailsExtractor = function (args) { return {result: args[0], message: stripTags(args[1])}; };
                break;
            case 3:  // result, message, details
                detailsExtractor = function (args) { return args[2]; };
                break;
            default:
                throw new Error('QUnit-TAP does not support QUnit#log arguments like this.');
            }
        }

        return function (logArguments) {
            if (detailsExtractor) {
                return detailsExtractor(logArguments);
            }
            setupExtractor(logArguments);
            return detailsExtractor(logArguments);
        };
    })();

    var createCallbackAppenderFor = function (qu) {
        // detect QUnit's multipleCallbacks feature. see jquery/qunit@34f6bc1
        var isMultipleLoggingCallbacksSupported =
                (typeof qu.config !== 'undefined' &&
                 typeof qu.config.log !== 'undefined' &&
                 typeof qu.config.done !== 'undefined' &&
                 typeof qu.config.testDone !== 'undefined' &&
                 typeof qu.config.moduleStart !== 'undefined' &&
                 typeof qu.config.testStart !== 'undefined');
        return function (subject, observer, event) {
            var originalLoggingCallback = subject[event],
                callback;
            if (isMultipleLoggingCallbacksSupported) {
                callback = function () {
                    // make listener methods (moduleStart,testStart,log, ...) overridable.
                    observer[event].apply(observer, slice.apply(arguments));
                };
                originalLoggingCallback(callback);
            } else if (typeof originalLoggingCallback === 'function') {
                // do not overwrite old-style logging callbacks
                callback = function () {
                    var args = slice.apply(arguments);
                    originalLoggingCallback.apply(subject, args);
                    observer[event].apply(observer, args);
                };
                subject[event] = callback;
            }
            return callback;
        };
    };


    /**
     * QUnit-TAP - A TAP Output Producer Plugin for QUnit
     * @param qunitObject QUnit object reference.
     * @param printLikeFunction print-like function for TAP output (assumes line-separator is added by this function for each call).
     * @param options configuration options to customize default behavior.
     * @return object to provide QUnit-TAP API and customization subject.
     */
    function qunitTap(qunitObject, printLikeFunction, options) {
        if (!qunitObject) {
            throw new Error('should pass QUnit object reference. Please check QUnit\'s "require" path if you are using Node.js (or any CommonJS env).');
        } else if (typeof printLikeFunction !== 'function') {
            throw new Error('should pass print-like function');
        }

        var qu = qunitObject,
            tap = {},
            jsDumpExists = (typeof qu.jsDump !== 'undefined' && typeof qu.jsDump.parse === 'function'),
            explain = (jsDumpExists ? function explain (obj) { return qu.jsDump.parse(obj); } : noop),
            deprecateOption = function deprecateOption (optionName, fallback) {
                // option deprecation and fallback function
                if (!options || typeof options !== 'object') {
                    return;
                }
                if (typeof options[optionName] === 'undefined') {
                    return;
                }
                printLikeFunction('# WARNING: Option "' + optionName + '" is deprecated and will be removed in future version.');
                fallback(options[optionName]);
            },
            targetEvents = [
                'moduleStart',
                'testStart',
                'log',
                'testDone',
                'done'
            ],
            registeredCallbacks = {};


        tap.config = extend(
            {
                initialCount: 1,
                showModuleNameOnFailure: true,
                showTestNameOnFailure: true,
                showExpectationOnFailure: true,
                showSourceOnFailure: true
            },
            options
        );
        deprecateOption('noPlan', function (flag) {
            printLikeFunction('# Now QUnit-TAP works as with "noPlan: true" by default. If you want to delare plan explicitly, please use "QUnit.config.requireExpects" option instead.');
            tap.config.noPlan = flag;
        });
        deprecateOption('count', function (count) {
            tap.config.initialCount = (count + 1);
        });
        deprecateOption('showDetailsOnFailure', function (flag) {
            tap.config.showModuleNameOnFailure = flag;
            tap.config.showTestNameOnFailure = flag;
            tap.config.showExpectationOnFailure = flag;
            tap.config.showSourceOnFailure = flag;
        });
        tap.VERSION = qunitTapVersion;
        tap.puts = printLikeFunction;
        tap.count = tap.config.initialCount - 1;
        tap.expectedCount = tap.config.initialCount - 1;

        function isEnabled (configName) {
            return tap.config[configName];
        }

        function formatDetails (details) {
            if (isPassed(details)) {
                return details.message;
            }
            var desc = [];
            if (details.message) {
                desc.push(details.message);
            }
            if (isEnabled('showExpectationOnFailure') && !(isAssertOkFailed(details))) {
                render(desc, 'expected', details.expected, explain);
                render(desc, 'got', details.actual, explain);
            }
            renderIf(isEnabled('showTestNameOnFailure'), desc, 'test', details.name, noop);
            renderIf(isEnabled('showModuleNameOnFailure'), desc, 'module', details.module, noop);
            renderIf(isEnabled('showSourceOnFailure'), desc, 'source', details.source, ltrim);
            return desc.join(', ');
        }

        function printPlanLine (toCount) {
            tap.puts(tap.config.initialCount + '..' + toCount);
        }

        function unsubscribeEvent (eventName) {
            var listeners;
            if (indexOf(targetEvents, eventName) === -1) {
                return;
            }
            listeners = qu.config[eventName];
            if (typeof listeners === 'undefined') {
                return;
            }
            removeElement(listeners, registeredCallbacks[eventName]);
        }

        function unsubscribeEvents (eventNames) {
            var i;
            for (i = 0; i < eventNames.length; i += 1) {
                unsubscribeEvent(eventNames[i]);
            }
        }

        tap.explain = explain;

        tap.note = function note (obj) {
            tap.puts(escapeLineEndings('# ' + obj));
        };

        tap.diag = function diag (obj) {
            tap.note(obj);
            return false;
        };

        tap.moduleStart = function moduleStart (arg) {
            var name = (typeof arg === 'string') ? arg : arg.name;
            tap.note('module: ' + name);
        };

        tap.testStart = function testStart (arg) {
            var name = (typeof arg === 'string') ? arg : arg.name;
            tap.note('test: ' + name);
        };

        tap.log = function log () {
            var details = extractDetailsFrom(arguments),
                testLine = '';
            tap.count += 1;
            if (isFailed(details)) {
                testLine += 'not ';
            }
            testLine += ('ok ' + tap.count);
            tap.puts(formatTestLine(testLine, formatDetails(details)));
        };

        tap.testDone = function testDone () {
            if (isPlanRequired(qu.config)) {
                tap.expectedCount += qu.config.current.expected;
            }
        };

        tap.done = function done () {
            if (typeof tap.config.noPlan !== 'undefined' && !(tap.config.noPlan)) {
                // Do nothing until removal of 'noPlan' option.
            } else if (isPlanRequired(qu.config)) {
                printPlanLine(tap.expectedCount);
            } else {
                printPlanLine(tap.count);
            }
        };

        tap.unsubscribe = function unsubscribe () {
            if (typeof qu.config === 'undefined') {
                return;
            }
            if (arguments.length === 0) {
                unsubscribeEvents(targetEvents);
            } else {
                unsubscribeEvents(slice.apply(arguments));
            }
        };

        (function () {
            var appendCallback = createCallbackAppenderFor(qu),
                eventName, i, callback;
            for (i = 0; i < targetEvents.length; i += 1) {
                eventName = targetEvents[i];
                callback = appendCallback(qu, tap, eventName);
                registeredCallbacks[eventName] = callback;
            }
        })();

        return tap;
    }

    qunitTap.qunitTap = function () {
        throw new Error('[BC BREAK] Since 1.4.0, QUnit-TAP exports single qunitTap function as module.exports. Therefore, require("qunit-tap") returns qunitTap function itself. Please fix your code if you are using Node.js (or any CommonJS env).');
    };

    // using substack pattern (export single function)
    return qunitTap;
}));
