var _ = require('lodash'),
    uuid = require('uuid'),
    Response = require('postman-collection').Response,
    visualizer = require('../../visualizer'),

    /**
     * List of request properties which can be mutated via pre-request
     *
     * @private
     * @const
     * @type {String[]}
     */
    ALLOWED_REQUEST_MUTATIONS = ['url', 'method', 'headers', 'body'],

    extractVisualizerData,
    getResponseJSON;

/**
 * Returns visualizer data from the latest execution result.
 *
 * @param {Array} prereqExecutions - pre-script executions results
 * @param {Array} testExecutions - test-script executions results
 * @returns {Object|undefined} - visualizer data
 */
extractVisualizerData = function (prereqExecutions, testExecutions) {
    var visualizerData,
        i;

    if (_.isArray(testExecutions)) {
        // loop through the test executions in reverse order to return data from latest execution
        for (i = testExecutions.length - 1; i >= 0; i--) {
            visualizerData = _.get(testExecutions[i], 'result.return.visualizer');

            if (visualizerData) {
                return visualizerData;
            }
        }
    }

    if (_.isArray(prereqExecutions)) {
        // extract visualizer data from pre-request script results if it is not found earlier
        for (i = prereqExecutions.length - 1; i >= 0; i--) {
            visualizerData = _.get(prereqExecutions[i], 'result.return.visualizer');

            if (visualizerData) {
                return visualizerData;
            }
        }
    }
};

/**
 * Convert response into a JSON serializable object.
 * The stream property is converted to base64 string for performance reasons.
 *
 * @param {Object} response - SDK Response instance
 * @returns {Object}
 */
getResponseJSON = function (response) {
    if (!Response.isResponse(response)) {
        return;
    }

    return {
        id: response.id,
        code: response.code,
        status: response.status,
        header: response.headers && response.headers.toJSON(),
        stream: response.stream && {
            type: 'Base64',
            data: response.stream.toString('base64')
        },
        responseTime: response.responseTime
    };
};

/**
 * Add options
 * stopOnError:Boolean
 *
 * @type {Object}
 */
module.exports = {
    init: function (done) {
        // @todo - code item global timeout and delay here
        done();
    },

    triggers: ['beforeItem', 'item', 'beforePrerequest', 'prerequest', 'beforeTest', 'test'],

    process: {
        /**
         * @param {Function=} callback -
         * @param {Object} payload -
         * @param {Function} next -
         * @todo  validate payload
         */
        item (callback, payload, next) {
            // adjust for polymorphic instructions
            if (!next && _.isFunction(payload) && !_.isFunction(callback)) {
                next = payload;
                payload = callback;
                callback = null;
            }

            var item = payload.item,
                originalRequest = item.request.clone(),
                coords = payload.coords,
                data = _.isObject(payload.data) ? payload.data : {},
                environment = _.isObject(payload.environment) ? payload.environment : {},
                globals = _.isObject(payload.globals) ? payload.globals : {},
                collectionVariables = _.isObject(payload.collectionVariables) ? payload.collectionVariables : {},
                _variables = _.isObject(payload._variables) ? payload._variables : {},
                stopOnError = _.has(payload, 'stopOnError') ? payload.stopOnError : this.options.stopOnError,

                // @todo: this is mostly coded in event extension and we are
                // still not sure whether that is the right place for it to be.
                abortOnFailure = this.options.abortOnFailure,
                stopOnFailure = this.options.stopOnFailure,
                delay = _.get(this.options, 'delay.item'),

                ctxTemplate;

            // validate minimum parameters required for the command to work
            if (!(item && coords)) {
                return next(new Error('runtime: item execution is missing required parameters'));
            }

            // store a common uuid in the coords
            coords.ref = uuid.v4();

            // here we code to queue prerequest script, then make a request and then execute test script
            this.triggers.beforeItem(null, coords, item);

            this.queueDelay(function () {
                // create the context object for scripts to run
                ctxTemplate = {
                    collectionVariables: collectionVariables,
                    _variables: _variables,
                    globals: globals,
                    environment: environment,
                    data: data,
                    request: item.request
                };

                // @todo make it less nested by coding Instruction.thenQueue
                this.queue('event', {
                    name: 'prerequest',
                    item: item,
                    coords: coords,
                    context: ctxTemplate,
                    trackContext: ['globals', 'environment', 'collectionVariables'],
                    stopOnScriptError: stopOnError,
                    stopOnFailure: stopOnFailure
                }).done(function (prereqExecutions, prereqExecutionError) {
                    // if stop on error is marked and script executions had an error,
                    // do not proceed with more commands, instead we bail out
                    if ((stopOnError || stopOnFailure) && prereqExecutionError) {
                        this.triggers.item(null, coords, item); // @todo - should this trigger receive error?

                        return callback && callback.call(this, prereqExecutionError, {
                            prerequest: prereqExecutions
                        });
                    }

                    // update allowed request mutation properties with the mutated context
                    // @note from this point forward, make sure this mutated
                    // request instance is used for upcoming commands.
                    ALLOWED_REQUEST_MUTATIONS.forEach(function (property) {
                        if (_.has(ctxTemplate, ['request', property])) {
                            item.request[property] = ctxTemplate.request[property];
                        }

                        // update property's parent reference
                        if (item.request[property] && typeof item.request[property].setParent === 'function') {
                            item.request[property].setParent(item.request);
                        }
                    });

                    this.queue('request', {
                        item: item,
                        globals: ctxTemplate.globals,
                        environment: ctxTemplate.environment,
                        collectionVariables: ctxTemplate.collectionVariables,
                        _variables: ctxTemplate._variables,
                        data: ctxTemplate.data,
                        coords: coords,
                        source: 'collection'
                    }).done(function (result, requestError) {
                        !result && (result = {});

                        var request = result.request,
                            response = result.response,
                            cookies = result.cookies;

                        if ((stopOnError || stopOnFailure) && requestError) {
                            this.triggers.item(null, coords, item); // @todo - should this trigger receive error?

                            return callback && callback.call(this, requestError, {
                                request
                            });
                        }

                        // also the test object requires the updated request object (since auth helpers may modify it)
                        request && (ctxTemplate.request = request);

                        // @note convert response instance to plain object.
                        // we want to avoid calling Response.toJSON() which triggers toJSON on Response.stream buffer.
                        // Because that increases the size of stringified object by 3 times.
                        // Also, that increases the total number of tokens (buffer.data) whereas Buffer.toString
                        // generates a single string that is easier to stringify and sent over the UVM bridge.
                        response && (ctxTemplate.response = getResponseJSON(response));

                        // set cookies for this transaction
                        cookies && (ctxTemplate.cookies = cookies);

                        // the context template also has a test object to store assertions
                        ctxTemplate.tests = {}; // @todo remove

                        this.queue('event', {
                            name: 'test',
                            item: item,
                            coords: coords,
                            context: ctxTemplate,
                            trackContext: ['tests', 'globals', 'environment', 'collectionVariables'],
                            stopOnScriptError: stopOnError,
                            abortOnFailure: abortOnFailure,
                            stopOnFailure: stopOnFailure
                        }).done(function (testExecutions, testExecutionError) {
                            var visualizerData = extractVisualizerData(prereqExecutions, testExecutions),
                                visualizerResult;

                            if (visualizerData) {
                                visualizer.processTemplate(visualizerData.template,
                                    visualizerData.data,
                                    visualizerData.options,
                                    function (err, processedTemplate) {
                                        visualizerResult = {
                                            // bubble up the errors while processing template through visualizer result
                                            error: err,

                                            // add processed template and data to visualizer result
                                            processedTemplate: processedTemplate,
                                            data: visualizerData.data
                                        };

                                        // trigger an event saying that item has been processed
                                        this.triggers.item(null, coords, item, visualizerResult);
                                    }.bind(this));
                            }
                            else {
                                // trigger an event saying that item has been processed
                                // @todo - should this trigger receive error?
                                this.triggers.item(null, coords, item, null);
                            }

                            // reset mutated request with original request instance
                            // @note request mutations are not persisted across iterations
                            item.request = originalRequest;

                            callback && callback.call(this, ((stopOnError || stopOnFailure) && testExecutionError) ?
                                testExecutionError : null, {
                                prerequest: prereqExecutions,
                                request: request,
                                response: response,
                                test: testExecutions
                            });
                        });
                    });
                });
            }.bind(this), {
                time: delay,
                source: 'item',
                cursor: coords
            }, next);
        }
    }
};
