/* eslint-disable max-classes-per-file */
const _ = require('lodash'),

    scopeLibraries = {
        JSON: require('liquid-json'),
        _: require('lodash3').noConflict(),
        CryptoJS: require('crypto-js'),
        atob: require('atob'),
        btoa: require('btoa'),
        tv4: require('tv4'),
        xml2Json: require('./xml2Json'),
        Backbone: require('backbone'),
        cheerio: require('cheerio')
    },

    LEGACY_GLOBS = [
        'tests', 'globals', 'environment', 'data', 'request', 'responseCookies', 'responseHeaders', 'responseTime',
        'responseCode', 'responseBody', 'iteration', 'postman',
        // scope libraries
        'JSON', '_', 'CryptoJS', 'atob', 'btoa', 'tv4', 'xml2Json', 'Backbone', 'cheerio'
    ],

    E = '',
    FUNCTION = 'function',
    TARGET_TEST = 'test',

    LEGACY_ASSERTION_ERROR_MESSAGE_PREFIX = 'expected ',
    LEGACY_ASSERTION_ERROR_MESSAGE_SUFFIX = ' to be truthy',

    /**
     * Different modes for a request body.
     *
     * @enum {String}
     */
    REQUEST_MODES = {
        RAW: 'raw',
        URLENCODED: 'urlencoded',
        FORMDATA: 'formdata',
        FILE: 'file'
    };

function getRequestBody (request) {
    var mode = _.get(request, 'body.mode'),
        body = _.get(request, 'body'),
        empty = body ? body.isEmpty() : true,
        content,
        computedBody;

    if (empty) {
        return;
    }

    content = body[mode];

    if (_.isFunction(content && content.all)) {
        content = content.all();
    }

    if (mode === REQUEST_MODES.RAW) {
        computedBody = {
            body: content
        };
    }
    else if (mode === REQUEST_MODES.URLENCODED) {
        computedBody = {
            form: _.reduce(content, function (accumulator, param) {
                if (param.disabled) { return accumulator; }

                // This is actually pretty simple,
                // If the variable already exists in the accumulator, we need to make the value an Array with
                // all the variable values inside it.
                if (accumulator[param.key]) {
                    _.isArray(accumulator[param.key]) ? accumulator[param.key].push(param.value) :
                        (accumulator[param.key] = [accumulator[param.key], param.value]);
                }
                else {
                    accumulator[param.key] = param.value;
                }

                return accumulator;
            }, {})
        };
    }
    else if (request.body.mode === REQUEST_MODES.FORMDATA) {
        computedBody = {
            formData: _.reduce(content, function (accumulator, param) {
                if (param.disabled) { return accumulator; }

                // This is actually pretty simple,
                // If the variable already exists in the accumulator, we need to make the value an Array with
                // all the variable values inside it.
                if (accumulator[param.key]) {
                    _.isArray(accumulator[param.key]) ? accumulator[param.key].push(param.value) :
                        (accumulator[param.key] = [accumulator[param.key], param.value]);
                }
                else {
                    accumulator[param.key] = param.value;
                }

                return accumulator;
            }, {})
        };
    }
    else if (request.body.mode === REQUEST_MODES.FILE) {
        computedBody = {
            body: _.get(request, 'body.file.content')
        };
    }

    return computedBody;
}

/**
 * Raises a single assertion event with an array of assertions from legacy `tests` object.
 *
 * @param  {Uniscope} scope -
 * @param  {Object} pmapi -
 * @param  {Function} onAssertion -
 */
function raiseAssertionEvent (scope, pmapi, onAssertion) {
    var tests = scope._imports && scope._imports.tests,
        assertionIndex = pmapi.test.index(),
        assertions;

    if (_.isEmpty(tests)) {
        return;
    }

    assertions = _.map(tests, function (value, key) {
        var assertionName = String(key),
            passed = Boolean(value),
            assertionError = null;

        // fake an assertion error for legacy tests
        if (!passed) {
            assertionError = new Error(LEGACY_ASSERTION_ERROR_MESSAGE_PREFIX +
                    String(value) + LEGACY_ASSERTION_ERROR_MESSAGE_SUFFIX);
            assertionError.name = 'AssertionError';
        }

        // @todo Move getAssertionObject function from pmapi-setup-runner.js to a common place and reuse it here too
        return {
            name: assertionName,
            skipped: false,
            passed: passed,
            error: assertionError,
            index: assertionIndex++
        };
    });

    onAssertion(assertions);
}

class PostmanLegacyInterface {
    /**
     * @param {Object} execution -
     * @param {Object} globalvars -
     */
    constructor (execution, globalvars) {
        this.__execution = execution;
        this.__environment = globalvars.environment;
        this.__globals = globalvars.globals;
    }

    setEnvironmentVariable (key, value) {
        if ((value === false || value) && typeof (value && value.toString) === FUNCTION) {
            value = value.toString();
        }
        this.__environment[key] = value;

        return this.__execution.environment.set(key, value);
    }

    getEnvironmentVariable (key) {
        return this.__execution.environment.get(key);
    }

    clearEnvironmentVariables () {
        for (var prop in this.__environment) {
            if (Object.hasOwnProperty.call(this.__environment, prop)) {
                delete this.__environment[prop];
            }
        }

        return this.__execution.environment.clear();
    }

    clearEnvironmentVariable (key) {
        key && (delete this.__environment[key]);

        return this.__execution.environment.unset(key);
    }

    setGlobalVariable (key, value) {
        if ((value === false || value) && typeof (value && value.toString) === FUNCTION) {
            value = value.toString();
        }
        this.__globals[key] = value;

        return this.__execution.globals.set(key, value);
    }

    getGlobalVariable (key) {
        return this.__execution.globals.get(key);
    }

    clearGlobalVariables () {
        for (var prop in this.__globals) {
            if (Object.hasOwnProperty.call(this.__globals, prop)) {
                delete this.__globals[prop];
            }
        }

        return this.__execution.globals.clear();
    }

    clearGlobalVariable (key) {
        key && (delete this.__globals[key]);

        return this.__execution.globals.unset(key);
    }

    setNextRequest (what) {
        this.__execution.return && (this.__execution.return.nextRequest = what);
    }
}

/**
 * @constructor
 * @extends {PostmanLegacyInterface}
 */
class PostmanLegacyTestInterface extends PostmanLegacyInterface {
    /**
     * @param {String} cookieName -
     * @returns {Object}
     */
    getResponseCookie (cookieName) {
        return this.__execution.cookies ? this.__execution.cookies.one(String(cookieName)) : undefined;
    }

    /**
     * @param {String} headerName -
     * @returns {String}
     */
    getResponseHeader (headerName) {
        var header = (this.__execution.response && this.__execution.response.headers) &&
            this.__execution.response.headers.one(headerName);

        return header ? header.value : undefined;
    }
}

module.exports = {
    /**
     *
     * @param  {Uniscope} scope -
     * @param  {Execution} execution -
     *
     * @note ensure that globalvars variables added here are added as part of the LEGACY_GLOBS array
     */
    setup (scope, execution) {
        /**
         * @name SandboxGlobals
         * @type {Object}
         */
        var globalvars = _.assign({}, scopeLibraries);

        // set global variables that are exposed in legacy interface
        // ---------------------------------------------------------

        // 1. set the tests object (irrespective of target)
        /**
         * Store your assertions in this object
         *
         * @memberOf SandboxGlobals
         * @type {Object}
         */
        globalvars.tests = {};

        // 2. set common environment, globals and data
        /**
         * All global variables at the initial stages when the script ran
         *
         * @memberOf SandboxGlobals
         * @type {Object}
         */
        globalvars.globals = execution.globals.syncVariablesTo();

        /**
         * All environment variables at the initial stages when script ran
         *
         * @memberOf SandboxGlobals
         * @type {Object}
         */
        globalvars.environment = execution.environment.syncVariablesTo();

        /**
         * The data object if it was passed during a collection run
         *
         * @memberOf SandboxGlobals
         * @type {Object}
         */
        globalvars.data = execution.data || (execution.data = {});

        // 3. set the request object in legacy structure
        /**
         * The request that will be sent (or has been sent)
         *
         * @memberOf SandboxGlobals
         * @type {Object}
         */
        globalvars.request = execution.request ? {
            id: execution.legacy ? execution.legacy._itemId : undefined,
            name: execution.legacy ? execution.legacy._itemName : undefined,
            description: execution.request.description ?
                execution.request.description.toString() : undefined,
            headers: execution.request.headers.toObject(true, false, true, true),
            method: execution.request.method,
            url: execution.request.url.toString(),
            data: (function (request) {
                var body = getRequestBody(request);

                return body ? (body.form || body.formData || body.body || {}) : {};
            }(execution.request))
        } : {};

        // 4. set the response related objects
        if (execution.target === TARGET_TEST) {
            /**
             * Stores the response cookies
             *
             * @memberOf SandboxGlobals
             * @type {Array.<Object>}
             */
            globalvars.responseCookies = execution.cookies || [];

            /**
             * Stores the response headers with the keys being case sensitive
             *
             * @memberOf SandboxGlobals
             * @type {Array.<Object>}
             */
            globalvars.responseHeaders = {};
            execution.response && execution.response.headers.each(function (header) {
                header && !header.disabled && (globalvars.responseHeaders[header.key] = header.value);
            });

            /**
             * @memberOf SandboxGlobals
             * @type {Number}
             */
            globalvars.responseTime = execution.response ? execution.response.responseTime : NaN;

            /**
             * @memberOf SandboxGlobals
             * @type {Number}
             */
            globalvars.responseCode = execution.response ? _.clone(execution.response.details()) : {
                code: NaN,
                name: E,
                details: E
            };

            /**
             * @memberOf SandboxGlobals
             * @type {String}
             */
            globalvars.responseBody = execution.response ? execution.response.text() : undefined;
        }

        // 5. add the iteration information
        globalvars.iteration = _.isObject(execution.cursor) ? execution.cursor.iteration : 0;

        // 6. create the postman interface object
        /**
         * @memberOf SandboxGlobals
         * @type {PostmanLegacyInterface}
         */
        globalvars.postman = new (execution.target === TARGET_TEST ?
            PostmanLegacyTestInterface : PostmanLegacyInterface)(execution, globalvars);

        // make a final pass to ensure that the global variables are present


        // all the globals are now added to scope
        scope.import(globalvars);
        globalvars = null; // dereference

        // add a flag to ensure that when teardown is called, it does not tear down a scope that was never setup
        scope.__postman_legacy_setup = true;
    },

    teardown (scope) {
        if (!scope.__postman_legacy_setup) {
            return;
        }

        for (var i = 0, ii = LEGACY_GLOBS.length; i < ii; i++) {
            scope.unset(LEGACY_GLOBS[i]);
        }

        scope.__postman_legacy_setup = false;
    },

    /**
     * This is the place where we should put all the tasks
     * that need to be executed after the completion of script execution
     *
     * @param  {Uniscope} scope -
     * @param  {Object} pmapi -
     * @param  {Function} onAssertion -
     */
    finish (scope, pmapi, onAssertion) {
        if (!scope.__postman_legacy_setup) {
            return;
        }

        raiseAssertionEvent(scope, pmapi, onAssertion);
    }
};
