odoo.define('web.MockServer', function (require) {
"use strict";

var Class = require('web.Class');
var Domain = require('web.Domain');
var pyUtils = require('web.py_utils');

var MockServer = Class.extend({
    /**
     * @constructor
     * @param {Object} data
     * @param {Object} options
     * @param {Object[]} [options.actions=[]]
     * @param {Object} [options.archs={}] dict of archs with keys being strings like
     *    'model,id,viewType'
     * @param {boolean} [options.debug=false] logs RPCs if set to true
     * @param {string} [options.currentDate] formatted string, default to
     *   current day
     */
    init: function (data, options) {
        options = options || {};
        this.data = data;
        for (var modelName in this.data) {
            var model = this.data[modelName];
            if (!('id' in model.fields)) {
                model.fields.id = {string: "ID", type: "integer"};
            }
            if (!('display_name' in model.fields)) {
                model.fields.display_name = {string: "Display Name", type: "char"};
            }
            if (!('__last_update' in model.fields)) {
                model.fields.__last_update = {string: "Last Modified on", type: "datetime"};
            }
            if (!('name' in model.fields)) {
                model.fields.name = {string: "Name", type: "char", default: "name"};
            }
            model.records = model.records || [];

            for (var i = 0; i < model.records.length; i++) {
                var record = model.records[i];
                this._applyDefaults(model, record);
            }
        }

        this.debug = options.debug;

        this.currentDate = options.currentDate || moment().format("YYYY-MM-DD");

        this.actions = options.actions || [];
        this.archs = options.archs || {};
    },

    //--------------------------------------------------------------------------
    // Public
    //--------------------------------------------------------------------------

    /**
     * Mocks a fields_get RPC for a given model.
     *
     * @param {string} model
     * @returns {Object}
     */
    fieldsGet: function (model) {
        return this.data[model].fields;
    },
    /**
     * helper: read a string describing an arch, and returns a simulated
     * 'field_view_get' call to the server. Calls processViews() of data_manager
     * to mimick the real behavior of a call to loadViews().
     *
     * @param {Object} params
     * @param {string|Object} params.arch a string OR a parsed xml document
     * @param {Number} [params.view_id] the id of the arch's view
     * @param {string} params.model a model name (that should be in this.data)
     * @param {Object} params.toolbar the actions possible in the toolbar
     * @param {Object} [params.viewOptions] the view options set in the test (optional)
     * @returns {Object} an object with 2 keys: arch and fields
     */
    fieldsViewGet: function (params) {
        var model = params.model;
        var toolbar = params.toolbar;
        var viewId = params.view_id;
        var viewOptions = params.viewOptions || {};
        if (!(model in this.data)) {
            throw new Error('Model ' + model + ' was not defined in mock server data');
        }
        var fields = $.extend(true, {}, this.data[model].fields);
        var fvg = this._fieldsViewGet(params.arch, model, fields, viewOptions.context);
        if (toolbar) {
            fvg.toolbar = toolbar;
        }
        if (viewId) {
            fvg.view_id = viewId;
        }
        return fvg;
    },
    /**
     * Simulate a complete RPC call. This is the main method for this class.
     *
     * This method also log incoming and outgoing data, and stringify/parse data
     * to simulate a barrier between the server and the client. It also simulate
     * server errors.
     *
     * @param {string} route
     * @param {Object} args
     * @returns {Deferred<any>}
     *          Resolved with the result of the RPC, stringified then parsed.
     *          If the RPC should fail, the deferred will be rejected with the
     *          error object, stringified then parsed.
     */
    performRpc: function (route, args) {
        var debug = this.debug;
        args = JSON.parse(JSON.stringify(args));
        if (debug) {
            console.log('%c[rpc] request ' + route, 'color: blue; font-weight: bold;', args);
            args = JSON.parse(JSON.stringify(args));
        }
        var def = this._performRpc(route, args);

        var abort = def.abort || def.reject;
        if (abort) {
            abort = abort.bind(def);
        } else {
            abort = function () {
                throw new Error("Can't abort this request");
            };
        }

        def = def.then(function (result) {
            var resultString = JSON.stringify(result || false);
            if (debug) {
                console.log('%c[rpc] response' + route, 'color: blue; font-weight: bold;', JSON.parse(resultString));
            }
            return JSON.parse(resultString);
        }, function (result, event) {
            var errorString = JSON.stringify(result || false);
            if (debug) {
                console.log('%c[rpc] response (error) ' + route, 'color: orange; font-weight: bold;', JSON.parse(errorString));
            }
            return $.Deferred().reject(errorString, event || $.Event());
        });

        var promise = def.promise();
        promise.abort = abort;
        return promise;
    },

    //--------------------------------------------------------------------------
    // Private
    //--------------------------------------------------------------------------

    /**
     * Apply the default values when creating an object in the local database.
     *
     * @private
     * @param {Object} model a model object from the local database
     * @param {Object} record
     */
    _applyDefaults: function (model, record) {
        record.display_name = record.display_name || record.name;
        for (var fieldName in model.fields) {
            if (fieldName === 'id') {
                continue;
            }
            if (!(fieldName in record)) {
                if ('default' in model.fields[fieldName]) {
                    record[fieldName] = model.fields[fieldName].default;
                } else if (_.contains(['one2many', 'many2many'], model.fields[fieldName].type)) {
                    record[fieldName] = [];
                } else {
                    record[fieldName] = false;
                }
            }
        }
    },
    /**
     * helper to evaluate a domain for given field values.
     * Currently, this is only a wrapper of the Domain.compute function in
     * "web.Domain".
     *
     * @param {Array} domain
     * @param {Object} fieldValues
     * @returns {boolean}
     */
    _evaluateDomain: function (domain, fieldValues) {
        return new Domain(domain).compute(fieldValues);
    },
    /**
     * helper: read a string describing an arch, and returns a simulated
     * 'fields_view_get' call to the server.
     *
     * @private
     * @param {string} arch a string OR a parsed xml document
     * @param {string} model a model name (that should be in this.data)
     * @param {Object} fields
     * @param {Object} context
     * @returns {Object} an object with 2 keys: arch and fields (the fields
     *   appearing in the views)
     */
    _fieldsViewGet: function (arch, model, fields, context) {
        var self = this;
        var modifiersNames = ['invisible', 'readonly', 'required'];
        var onchanges = this.data[model].onchanges || {};
        var fieldNodes = {};

        var doc;
        if (typeof arch === 'string') {
            doc = $.parseXML(arch).documentElement;
        } else {
            doc = arch;
        }

        var inTreeView = (doc.tagName === 'tree');

        this._traverse(doc, function (node) {
            if (node.nodeType === Node.TEXT_NODE) {
                return false;
            }
            var modifiers = {};

            var isField = (node.tagName === 'field');

            if (isField) {
                var fieldName = node.getAttribute('name');
                fieldNodes[fieldName] = node;

                // 'transfer_field_to_modifiers' simulation
                var field = fields[fieldName];

                if (!field) {
                    throw new Error("Field " + fieldName + " does not exist");
                }
                var defaultValues = {};
                var stateExceptions = {};
                _.each(modifiersNames, function (attr) {
                    stateExceptions[attr] = [];
                    defaultValues[attr] = !!field[attr];
                });
                _.each(field.states || {}, function (modifs, state) {
                    _.each(modifs, function (modif) {
                        if (defaultValues[modif[0]] !== modif[1]) {
                            stateExceptions[modif[0]].append(state);
                        }
                    });
                });
                _.each(defaultValues, function (defaultValue, attr) {
                    if (stateExceptions[attr].length) {
                        modifiers[attr] = [("state", defaultValue ? "not in" : "in", stateExceptions[attr])];
                    } else {
                        modifiers[attr] = defaultValue;
                    }
                });
            }

            // 'transfer_node_to_modifiers' simulation
            var attrs = node.getAttribute('attrs');
            if (attrs) {
                attrs = pyUtils.py_eval(attrs);
                _.extend(modifiers, attrs);
            }

            var states = node.getAttribute('states');
            if (states) {
                if (!modifiers.invisible) {
                    modifiers.invisible = [];
                }
                modifiers.invisible.push(["state", "not in", states.split(",")]);
            }
            _.each(modifiersNames, function (a) {
                var mod = node.getAttribute(a);
                if (mod) {
                    var pyevalContext = window.py.dict.fromJSON(context || {});
                    var v = pyUtils.py_eval(mod, {context: pyevalContext}) ? true: false;
                    if (inTreeView && a === 'invisible') {
                        modifiers.column_invisible = v;
                    } else if (v || !(a in modifiers) || !_.isArray(modifiers[a])) {
                        modifiers[a] = v;
                    }
                }
            });

            _.each(modifiersNames, function (a) {
                if (a in modifiers && (!!modifiers[a] === false || (_.isArray(modifiers[a]) && !modifiers[a].length))) {
                    delete modifiers[a];
                }
            });

            if (Object.keys(modifiers).length) {
                node.setAttribute('modifiers', JSON.stringify(modifiers));
            }

            return !isField;
        });

        var relModel, relFields;
        _.each(fieldNodes, function (node, name) {
            var field = fields[name];
            if (field.type === "many2one" || field.type === "many2many") {
                var canCreate = node.getAttribute('can_create');
                node.setAttribute('can_create', canCreate || "true");
                var canWrite = node.getAttribute('can_write');
                node.setAttribute('can_write', canWrite || "true");
            }
            if (field.type === "one2many" || field.type === "many2many") {
                field.views = {};
                _.each(node.childNodes, function (children) {
                    relModel = field.relation;
                    relFields = $.extend(true, {}, self.data[relModel].fields);
                    field.views[children.tagName] = self._fieldsViewGet(children, relModel,
                        relFields, context);
                });
            }

            // add onchanges
            if (name in onchanges) {
                node.setAttribute('on_change', "1");
            }
        });

        var xmlSerializer = new XMLSerializer();
        var processedArch = xmlSerializer.serializeToString(doc);
        return {
            arch: processedArch,
            fields: _.pick(fields, _.keys(fieldNodes)),
            model: model,
            type: doc.tagName === 'tree' ? 'list' : doc.tagName,
        };
    },
    /**
     * Get all records from a model matching a domain.  The only difficulty is
     * that if we have an 'active' field, we implicitely add active = true in
     * the domain.
     *
     * @private
     * @param {string} model a model name
     * @param {any[]} domain
     * @returns {Object[]} a list of records
     */
    _getRecords: function (model, domain) {
        if (!_.isArray(domain)) {
            throw new Error("MockServer._getRecords: given domain has to be an array.");
        }

        var self = this;
        var records = this.data[model].records;

        if ('active' in this.data[model].fields) {
            // add ['active', '=', true] to the domain if 'active' is not yet present in domain
            var activeInDomain = false;
            _.each(domain, function (subdomain) {
                activeInDomain = activeInDomain || subdomain[0] === 'active';
            });
            if (!activeInDomain) {
                domain = [['active', '=', true]].concat(domain);
            }
        }

        if (domain.length) {
            records = _.filter(records, function (record) {
                var fieldValues = _.mapObject(record, function (value) {
                    return value instanceof Array ? value[0] : value;
                });
                return self._evaluateDomain(domain, fieldValues);
            });
        }

        return records;
    },
    /**
     * Helper function, to find an available ID. The current algorithm is to add
     * all other IDS.
     *
     * @private
     * @param {string} modelName
     * @returns {integer} a valid ID (> 0)
     */
    _getUnusedID: function (modelName) {
        var model = this.data[modelName];
        return _.reduce(model.records, function (acc, record){
            return acc + record.id;
        }, 1);
    },
    /**
     * Simulate a 'copy' operation, so we simply try to duplicate a record in
     * memory
     *
     * @private
     * @param {string} modelName
     * @param {integer} id the ID of a valid record
     * @returns {integer} the ID of the duplicated record
     */
    _mockCopy: function (modelName, id) {
        var model = this.data[modelName];
        var newID = this._getUnusedID(modelName);
        var originalRecord = _.findWhere(model.records, {id: id});
        var duplicateRecord = _.extend({}, originalRecord, {id: newID});
        duplicateRecord.display_name = originalRecord.display_name + ' (copy)';
        model.records.push(duplicateRecord);
        return newID;
    },
    /**
     * Simulate a 'create' operation.  This is basically a 'write' with the
     * added work of getting a valid ID and applying default values.
     *
     * @private
     * @param {string} modelName
     * @param {Object} values
     * @returns {integer}
     */
    _mockCreate: function (modelName, values) {
        if ('id' in values) {
            throw "Cannot create a record with a predefinite id";
        }
        var model = this.data[modelName];
        var id = this._getUnusedID(modelName);
        var record = {id: id};
        model.records.push(record);
        this._applyDefaults(model, values);
        this._mockWrite(modelName, [[id], values]);
        return id;
    },
    /**
     * Simulate a 'default_get' operation
     *
     * @private
     * @param {string} modelName
     * @param {array[]} args a list with a list of fields in the first position
     * @param {Object} [kwargs]
     * @param {Object} [kwargs.context] the context to eventually read default
     *   values
     * @returns {Object}
     */
    _mockDefaultGet: function (modelName, args, kwargs) {
        var result = {};
        var fields = args[0];
        var model = this.data[modelName];
        _.each(fields, function (name) {
            var field = model.fields[name];
            if ('default' in field) {
                result[name] = field.default;
            }
        });
        if (kwargs && kwargs.context)
        _.each(kwargs.context, function (value, key) {
            if ('default_' === key.slice(0, 8)) {
                result[key.slice(8)] = value;
            }
        });
        return result;
    },
    /**
     * Simulate a 'fields_get' operation
     *
     * @private
     * @param {string} modelName
     * @param {any} args
     * @returns {Object}
     */
    _mockFieldsGet: function (modelName, args) {
        var modelFields = this.data[modelName].fields;
        // Get only the asked fields (args[0] could be the field names)
        if (args[0] && args[0].length) {
            modelFields = _.pick.apply(_, [modelFields].concat(args[0]));
        }
        // Get only the asked attributes (args[1] could be the attribute names)
        if (args[1] && args[1].length) {
            modelFields = _.mapObject(modelFields, function (field) {
                return _.pick.apply(_, [field].concat(args[1]));
            });
        }
        return modelFields;
    },
    /**
     * Simulate a call to the 'search_panel_select_range' method.
     *
     * Note that the implementation assumes that 'parent_id' is the field that
     * encodes the parent relationship.
     *
     * @private
     * @param {string} model
     * @param {Array} args
     * @returns {Object}
     */
    _mockSearchPanelSelectRange: function (model, args) {
        var fieldName = args[0];
        var field = this.data[model].fields[fieldName];

        if (field.type !== 'many2one') {
            throw new Error('Only fields of type many2one are handled');
        }

        var fields = ['display_name'];
        var parentField = this.data[field.relation].fields.parent_id;
        if (parentField) {
            fields.push('parent_id');
        }
        return {
            parent_field: parentField ? 'parent_id' : false,
            values: this._mockSearchRead(field.relation, [[], fields], {}),
        };
    },
    /**
     * Simulate a call to the 'search_panel_select_multi_range' method.
     *
     * Note that only the many2one and selection cases are handled by this
     * function.
     *
     * @param {string} model
     * @param {Array} args
     * @param {Object} kwargs
     * @returns {Object}
     */
    _mockSearchPanelSelectMultiRange: function (model, args, kwargs) {
        var fieldName = args[0];
        var field = this.data[model].fields[fieldName];
        var comodelDomain = kwargs.comodel_domain || [];

        if (!_.contains(['many2one', 'selection'], field.type)) {
            throw new Error('Only fields of type many2one and selection are handled');
        }

        var modelDomain;
        var disableCounters = kwargs.disable_counters || false;
        modelDomain = [[fieldName, '!=', false]]
                            .concat(kwargs.category_domain)
                            .concat(kwargs.filter_domain)
                            .concat(kwargs.search_domain);
        var groupBy = kwargs.group_by || false;
        var comodel = field.relation || false;
        var groupByField = groupBy && this.data[comodel].fields[groupBy];

        // get counters
        var groups;
        var counters = {};
        if (!disableCounters) {
            groups = this._mockReadGroup(model, {
                domain: modelDomain,
                fields: [fieldName],
                groupby: [fieldName],
            });
            groups.forEach(function (group) {
                var groupId = field.type === 'many2one' ? group[fieldName][0] : group[fieldName];
                counters[groupId] = group[fieldName + '_count'];
            });
        }

        // get filter values
        var filterValues = [];
        if (field.type === 'many2one') {
            var fields = groupBy ? ['display_name', groupBy] : ['display_name'];
            var records = this._mockSearchRead(comodel, [comodelDomain, fields], {});
            records.forEach(function (record) {
                var filterValue = {
                    count: counters[record.id] || 0,
                    id: record.id,
                    name: record.display_name,
                };
                if (groupBy) {
                    var id = record[groupBy];
                    var name = record[groupBy];
                    if (groupByField.type === 'many2one') {
                        name = id[1];
                        id = id[0];
                    } else if (groupByField.type === 'selection') {
                        name = _.find(field.selection, function (option) {
                            return option[0] === id;
                        })[1];
                    }
                    filterValue.group_id = id;
                    filterValue.group_name = name;
                }
                filterValues.push(filterValue);
            });
        } else if (field.type === 'selection') {
            field.selection.forEach(function (option) {
                filterValues.push({
                    count: counters[option[0]] || 0,
                    id: option[0],
                    name: option[1],
                });
            });
        }

        return filterValues;
    },
    /**
     * Simulate a call to the '/web/action/load' route
     *
     * @private
     * @param {Object} kwargs
     * @param {integer} kwargs.action_id
     * @returns {Object}
     */
    _mockLoadAction: function (kwargs) {
        var action = _.findWhere(this.actions, {id: parseInt(kwargs.action_id)});
        if (!action) {
            // when the action doesn't exist, the real server doesn't crash, it
            // simply returns false
            console.warn("No action found for ID " + kwargs.action_id);
        }
        return action || false;
    },
    /**
     * Simulate a 'load_views' operation
     *
     * @param {string} model
     * @param {Array} args
     * @param {Object} kwargs
     * @param {Array} kwargs.views
     * @param {Object} kwargs.options
     * @param {Object} kwargs.context
     * @returns {Object}
     */
    _mockLoadViews: function (model, kwargs) {
        var self = this;
        var views = {};
        _.each(kwargs.views, function (view_descr) {
            var viewID = view_descr[0] || false;
            var viewType = view_descr[1];
            if (!viewID) {
                var contextKey = viewType + '_view_ref';
                if (contextKey in kwargs.context) {
                    viewID = kwargs.context[contextKey];
                }
            }
            var key = [model, viewID, viewType].join(',');
            var arch = self.archs[key] || _.find(self.archs, function (_v, k) {
                var ka = k.split(',');
                viewID = parseInt(ka[1], 10);
                return ka[0] === model && ka[2] === viewType;
            });
            if (!arch) {
                throw new Error('No arch found for key ' + key);
            }
            views[viewType] = {
                arch: arch,
                view_id: viewID,
                model: model,
                viewOptions: {
                    context: kwargs.context,
                },
            };
        });
        return views;
    },
    /**
     * Simulate a 'name_get' operation
     *
     * @private
     * @param {string} model
     * @param {Array} args
     * @returns {Array[]} a list of [id, display_name]
     */
    _mockNameGet: function (model, args) {
        var ids = args[0];
        if (!_.isArray(ids)) {
            ids = [ids];
        }
        var records = this.data[model].records;
        var names = _.map(ids, function (id) {
            return [id, _.findWhere(records, {id: id}).display_name];
        });
        return names;
    },
    /**
     * Simulate a 'name_create' operation
     *
     * @private
     * @param {string} model
     * @param {Array} args
     * @returns {Array} a couple [id, name]
     */
    _mockNameCreate: function (model, args) {
        var name = args[0];
        var values = {
            name: name,
            display_name: name,
        };
        var id = this._mockCreate(model, values);
        return [id, name];
    },
    /**
     * Simulate a 'name_search' operation.
     *
     * not yet fully implemented (missing: limit, and evaluate operators)
     * domain works but only to filter on ids
     *
     * @private
     * @param {string} model
     * @param {Array} args
     * @param {string} args[0]
     * @param {Array} args[1], search domain
     * @param {Object} _kwargs
     * @returns {Array[]} a list of [id, display_name]
     */
    _mockNameSearch: function (model, args, _kwargs) {
        var str = args && typeof args[0] === 'string' ? args[0] : _kwargs.name;
        var domain = (args && args[1]) || _kwargs.args || [];
        var records = this._getRecords(model, domain);
        if (str.length) {
            records = _.filter(records, function (record) {
                return record.display_name.indexOf(str) !== -1;
            });
        }
        var result = _.map(records, function (record) {
            return [record.id, record.display_name];
        });
        if (_kwargs.limit) {
            return result.slice(0, _kwargs.limit);
        }
        return result;
    },
    /**
     * Simulate an 'onchange' rpc
     *
     * @private
     * @param {string} model
     * @param {string|string[]} args a list of field names, or just a field name
     * @returns {Object}
     */
    _mockOnchange: function (model, args) {
        var onchanges = this.data[model].onchanges || {};
        var record = args[1];
        var fields = args[2];
        if (!(fields instanceof Array)) {
            fields = [fields];
        }
        var result = {};
        _.each(fields, function (field) {
            if (field in onchanges) {
                var changes = _.clone(record);
                onchanges[field](changes);
                _.each(changes, function (value, key) {
                    if (record[key] !== value) {
                        result[key] = value;
                    }
                });
            }
        });
        return {value: result};
    },
    /**
     * Simulate a 'read' operation.
     *
     * @private
     * @param {string} model
     * @param {Array} args
     * @param {Object} _kwargs ignored... is that correct?
     * @returns {Object}
     */
    _mockRead: function (model, args, _kwargs) {
        var self = this;
        var ids = args[0];
        if (!_.isArray(ids)) {
            ids = [ids];
        }
        var fields = args[1] && args[1].length ? _.uniq(args[1].concat(['id'])) : Object.keys(this.data[model].fields);
        var records = _.reduce(ids, function (records, id) {
            if (!id) {
                throw "mock read: falsy value given as id, would result in an access error in actual server !";
            }
            var record =  _.findWhere(self.data[model].records, {id: id});
            return record ? records.concat(record) : records;
        }, []);
        var results = _.map(records, function (record) {
            var result = {};
            for (var i = 0; i < fields.length; i++) {
                var field = self.data[model].fields[fields[i]];
                if (!field) {
                    // the field doens't exist on the model, so skip it
                    continue;
                }
                if (field.type === 'float' ||
                    field.type === 'integer' ||
                    field.type === 'monetary') {
                    // read should return 0 for unset numeric fields
                    result[fields[i]] = record[fields[i]] || 0;
                } else if (field.type === 'many2one') {
                    var relatedRecord = _.findWhere(self.data[field.relation].records, {
                        id: record[fields[i]]
                    });
                    if (relatedRecord) {
                        result[fields[i]] =
                            [record[fields[i]], relatedRecord.display_name];
                    } else {
                        result[fields[i]] = false;
                    }
                } else if (field.type === 'one2many' || field.type === 'many2many') {
                    result[fields[i]] = record[fields[i]] || [];
                } else {
                    result[fields[i]] = record[fields[i]] || false;
                }
            }
            return result;
        });
        return results;
    },
    /**
     * Simulate a 'read_group' call to the server.
     *
     * Note: most of the keys in kwargs are still ignored
     *
     * @private
     * @param {string} model a string describing an existing model
     * @param {Object} kwargs various options supported by read_group
     * @param {string[]} kwargs.groupby fields that we are grouping
     * @param {string[]} kwargs.fields fields that we are aggregating
     * @param {Array} kwargs.domain the domain used for the read_group
     * @param {boolean} kwargs.lazy still mostly ignored
     * @param {integer} kwargs.limit ignored as well
     * @returns {Object[]}
     */
    _mockReadGroup: function (model, kwargs) {
        if (!('lazy' in kwargs)) {
            kwargs.lazy = true;
        }
        var self = this;
        var fields = this.data[model].fields;
        var aggregatedFields = [];
        _.each(kwargs.fields, function (field) {
            var split = field.split(":");
            var fieldName = split[0];
            if (kwargs.groupby.indexOf(fieldName) > 0) {
                // grouped fields are not aggregated
                return;
            }
            if (fields[fieldName] && (fields[fieldName].type === 'many2one') && split[1] !== 'count_distinct') {
                return;
            }
            aggregatedFields.push(fieldName);
        });
        var groupBy = [];
        if (kwargs.groupby.length) {
            groupBy = kwargs.lazy ? [kwargs.groupby[0]] : kwargs.groupby;
        }
        var records = this._getRecords(model, kwargs.domain);

        // if no fields have been given, the server picks all stored fields
        if (kwargs.fields.length === 0) {
            aggregatedFields = _.keys(this.data[model].fields);
        }

        var groupByFieldNames = _.map(groupBy, function (groupByField) {
            return groupByField.split(":")[0];
        });

        // filter out non existing fields
        aggregatedFields = _.filter(aggregatedFields, function (name) {
            return name in self.data[model].fields && !(_.contains(groupByFieldNames,name));
        });

        function aggregateFields(group, records) {
            var type;
            for (var i = 0; i < aggregatedFields.length; i++) {
                type = fields[aggregatedFields[i]].type;
                if (type === 'float' || type === 'integer') {
                    group[aggregatedFields[i]] = null;
                    for (var j = 0; j < records.length; j++) {
                        var value = group[aggregatedFields[i]] || 0;
                        group[aggregatedFields[i]] = value + records[j][aggregatedFields[i]];
                    }
                }
                if (type === 'many2one') {
                    var ids = _.pluck(records, aggregatedFields[i]); 
                    group[aggregatedFields[i]] = _.uniq(ids).length || null;
                }
            }
        }
        function formatValue(groupByField, val) {
            var fieldName = groupByField.split(':')[0];
            var aggregateFunction = groupByField.split(':')[1] || 'month';
            if (fields[fieldName].type === 'date') {
                if (!val) {
                    return false;
                } else if (aggregateFunction === 'day') {
                    return moment(val).format('YYYY-MM-DD');
                } else if (aggregateFunction === 'week') {
                    return moment(val).format('ww YYYY');
                } else if (aggregateFunction === 'year') {
                    return moment(val).format('Y');
                } else {
                    return moment(val).format('MMMM YYYY');
                }
            } else {
                return val instanceof Array ? val[0] : (val || false);
            }
        }
        function groupByFunction(record) {
            var value = '';
            _.each(groupBy, function (groupByField) {
                value = (value ? value + ',' : value) + groupByField + '#';
                var fieldName = groupByField.split(':')[0];
                if (fields[fieldName].type === 'date') {
                    value += formatValue(groupByField, record[fieldName]);
                } else {
                    value += record[groupByField];
                }
            });
            return value;
        }

        if (!groupBy.length) {
            var group = { __count: records.length };
            aggregateFields(group, records);
            return [group];
        }

        var groups = _.groupBy(records, groupByFunction);
        var result = _.map(groups, function (group) {
            var res = {
                __domain: kwargs.domain || [],
            };
            _.each(groupBy, function (groupByField) {
                var fieldName = groupByField.split(':')[0];
                var val = formatValue(groupByField, group[0][fieldName]);
                var field = self.data[model].fields[fieldName];
                if (field.type === 'many2one' && !_.isArray(val)) {
                    var related_record = _.findWhere(self.data[field.relation].records, {
                        id: val
                    });
                    if (related_record) {
                        res[groupByField] = [val, related_record.display_name];
                    } else {
                        res[groupByField] = false;
                    }
                } else {
                    res[groupByField] = val;
                }

                if (field.type === 'date') {
                    var aggregateFunction = groupByField.split(':')[1];
                    var startDate, endDate;
                    if (aggregateFunction === 'day') {
                        startDate = moment(val, 'YYYY-MM-DD');
                        endDate = startDate.clone().add(1, 'days');
                    } else if (aggregateFunction === 'week') {
                        startDate = moment(val, 'ww YYYY');
                        endDate = startDate.clone().add(1, 'weeks');
                    } else if (aggregateFunction === 'year') {
                        startDate = moment(val, 'Y');
                        endDate = startDate.clone().add(1, 'years');
                    } else {
                        startDate = moment(val, 'MMMM YYYY');
                        endDate = startDate.clone().add(1, 'months');
                    }
                    res.__domain = [[fieldName, '>=', startDate.format('YYYY-MM-DD')], [fieldName, '<', endDate.format('YYYY-MM-DD')]].concat(res.__domain);
                } else {
                    res.__domain = [[fieldName, '=', val]].concat(res.__domain);
                }

            });

            // compute count key to match dumb server logic...
            var countKey;
            if (kwargs.lazy) {
                countKey = groupBy[0].split(':')[0] + "_count";
            } else {
                countKey = "__count";
            }
            res[countKey] = group.length;
            aggregateFields(res, group);

            return res;
        });

        if (kwargs.orderby) {
            // only consider first sorting level
            kwargs.orderby = kwargs.orderby.split(',')[0];
            var fieldName = kwargs.orderby.split(' ')[0];
            var order = kwargs.orderby.split(' ')[1];
            result.sort(function (g1, g2) {
                if (g1[fieldName] < g2[fieldName]) {
                    return order === 'ASC' ? -1 : 1;
                }
                if (g1[fieldName] > g2[fieldName]) {
                    return order === 'ASC' ? 1 : -1;
                }
                return 0;
            });
        }

        return result;
    },
    /**
     * Simulates a 'read_progress_bar' operation
     *
     * @private
     * @param {string} model
     * @param {Object} kwargs
     * @returns {Object[][]}
     */
    _mockReadProgressBar: function (model, kwargs) {
        var domain = kwargs.domain;
        var groupBy = kwargs.group_by;
        var progress_bar = kwargs.progress_bar;

        var records = this._getRecords(model, domain || []);

        var data = {};
        _.each(records, function (record) {
            var groupByValue = record[groupBy]; // always technical value here

            if (!(groupByValue in data)) {
                data[groupByValue] = {};
                _.each(progress_bar.colors, function (val, key) {
                    data[groupByValue][key] = 0;
                });
            }

            var fieldValue = record[progress_bar.field];
            if (fieldValue in data[groupByValue]) {
                data[groupByValue][fieldValue]++;
            }
        });

        return data;
    },
    /**
     * Simulates a 'resequence' operation
     *
     * @private
     * @param {string} model
     * @param {string} field
     * @param {Array} ids
     */
    _mockResequence: function (args) {
        var offset = args.offset ? Number(args.offset) : 0;
        var field = args.field ? args.field : 'sequence';
        var records = this.data[args.model].records;
        if (!(field in this.data[args.model].fields)) {
            return false;
        }
        for (var i in args.ids) {
            var record = _.findWhere(records, {id: args.ids[i]});
            record[field] = Number(i) + offset;
        }
        return true;
    },
    /**
     * Simulate a 'search_count' operation
     *
     * @private
     * @param {string} model
     * @param {Array} args
     * @returns {integer}
     */
    _mockSearchCount: function (model, args) {
        return this._getRecords(model, args[0]).length;
    },
    /**
     * Simulate a 'search_read' operation on a model
     *
     * @private
     * @param {Object} args
     * @param {Array} args.domain
     * @param {string} args.model
     * @param {Array} [args.fields] defaults to the list of all fields
     * @param {integer} [args.limit]
     * @param {integer} [args.offset=0]
     * @param {string[]} [args.sort]
     * @returns {Object}
     */
    _mockSearchRead: function (model, args, kwargs) {
        var result = this._mockSearchReadController({
            model: model,
            domain: kwargs.domain || args[0],
            fields: kwargs.fields || args[1],
            offset: kwargs.offset || args[2],
            limit: kwargs.limit || args[3],
            order: kwargs.order || args[4],
            context: kwargs.context,
        });
        return result.records;
    },
    /**
     * Simulate a 'search_read' operation, from the controller point of view
     *
     * @private
     * @private
     * @param {Object} args
     * @param {Array} args.domain
     * @param {string} args.model
     * @param {Array} [args.fields] defaults to the list of all fields
     * @param {integer} [args.limit]
     * @param {integer} [args.offset=0]
     * @param {string[]} [args.sort]
     * @returns {Object}
     */
    _mockSearchReadController: function (args) {
        var self = this;
        var records = this._getRecords(args.model, args.domain || []);
        var fields = args.fields || _.keys(this.data[args.model].fields);
        var nbRecords = records.length;
        var offset = args.offset || 0;
        records = records.slice(offset, args.limit ? (offset + args.limit) : nbRecords);
        var processedRecords = _.map(records, function (r) {
            var result = {};
            _.each(_.uniq(fields.concat(['id'])), function (fieldName) {
                var field = self.data[args.model].fields[fieldName];
                if (field.type === 'many2one') {
                    var related_record = _.findWhere(self.data[field.relation].records, {
                        id: r[fieldName]
                    });
                    result[fieldName] =
                        related_record ? [r[fieldName], related_record.display_name] : false;
                } else {
                    result[fieldName] = r[fieldName];
                }
            });
            return result;
        });
        if (args.sort) {
            // deal with sort on multiple fields (i.e. only consider the first)
            args.sort = args.sort.split(',')[0];
            var fieldName = args.sort.split(' ')[0];
            var order = args.sort.split(' ')[1];
            processedRecords.sort(function (r1, r2) {
                if (r1[fieldName] < r2[fieldName]) {
                    return order === 'ASC' ? -1 : 1;
                }
                if (r1[fieldName] > r2[fieldName]) {
                    return order === 'ASC' ? 1 : -1;
                }
                return 0;
            });
        }
        var result = {
            length: nbRecords,
            records: processedRecords,
        };
        return $.extend(true, {}, result);
    },
    /**
     * Simulate a 'unlink' operation
     *
     * @private
     * @param {string} model
     * @param {Array} args
     * @returns {boolean} currently, always returns true
     */
    _mockUnlink: function (model, args) {
        var ids = args[0];
        if (!_.isArray(ids)) {
            ids = [ids];
        }
        this.data[model].records = _.reject(this.data[model].records, function (record) {
            return _.contains(ids, record.id);
        });

        // update value of one2many fields pointing to the deleted records
        _.each(this.data, function (d) {
            var relatedFields = _.pick(d.fields, function (field) {
                return field.type === 'one2many' && field.relation === model;
            });
            _.each(Object.keys(relatedFields), function (relatedField) {
                _.each(d.records, function (record) {
                    record[relatedField] = _.difference(record[relatedField], ids);
                });
            });
        });

        return true;
    },
    /**
     * Simulate a 'write' operation
     *
     * @private
     * @param {string} model
     * @param {Array} args
     * @returns {boolean} currently, always return 'true'
     */
    _mockWrite: function (model, args) {
        _.each(args[0], this._writeRecord.bind(this, model, args[1]));
        return true;
    },
    /**
     * Dispatch a RPC call to the correct helper function
     *
     * @see performRpc
     *
     * @private
     * @param {string} route
     * @param {Object} args
     * @returns {Deferred<any>}
     *          Resolved with the result of the RPC. If the RPC should fail, the
     *          deferred should either be rejected or the call should throw an
     *          exception (@see performRpc for error handling).
     */
    _performRpc: function (route, args) {
        switch (route) {
            case '/web/action/load':
                return $.when(this._mockLoadAction(args.kwargs));

            case '/web/dataset/search_read':
                return $.when(this._mockSearchReadController(args));

            case '/web/dataset/resequence':
                return $.when(this._mockResequence(args));
        }
        if (route.indexOf('/web/image') >= 0 || _.contains(['.png', '.jpg'], route.substr(route.length - 4))) {
            return $.when();
        }
        switch (args.method) {
            case 'copy':
                return $.when(this._mockCopy(args.model, args.args[0]));

            case 'create':
                return $.when(this._mockCreate(args.model, args.args[0]));

            case 'default_get':
                return $.when(this._mockDefaultGet(args.model, args.args, args.kwargs));

            case 'fields_get':
                return $.when(this._mockFieldsGet(args.model, args.args));

            case 'search_panel_select_range':
                return $.when(this._mockSearchPanelSelectRange(args.model, args.args, args.kwargs));

            case 'search_panel_select_multi_range':
                return $.when(this._mockSearchPanelSelectMultiRange(args.model, args.args, args.kwargs));

            case 'load_views':
                return $.when(this._mockLoadViews(args.model, args.kwargs));

            case 'name_get':
                return $.when(this._mockNameGet(args.model, args.args));

            case 'name_create':
                return $.when(this._mockNameCreate(args.model, args.args));

            case 'name_search':
                return $.when(this._mockNameSearch(args.model, args.args, args.kwargs));

            case 'onchange':
                return $.when(this._mockOnchange(args.model, args.args));

            case 'read':
                return $.when(this._mockRead(args.model, args.args, args.kwargs));

            case 'read_group':
                return $.when(this._mockReadGroup(args.model, args.kwargs));

            case 'read_progress_bar':
                return $.when(this._mockReadProgressBar(args.model, args.kwargs));

            case 'search_count':
                return $.when(this._mockSearchCount(args.model, args.args));

            case 'search_read':
                return $.when(this._mockSearchRead(args.model, args.args, args.kwargs));

            case 'unlink':
                return $.when(this._mockUnlink(args.model, args.args));

            case 'write':
                return $.when(this._mockWrite(args.model, args.args));
        }
        var model = this.data[args.model];
        if (model && typeof model[args.method] === 'function') {
            return $.when(this.data[args.model][args.method](args.args, args.kwargs));
        }

        throw new Error("Unimplemented route: " + route);
    },
    /**
     * helper function: traverse a tree and apply the function f to each of its
     * nodes.
     *
     * Note: this should be abstracted somewhere in web.utils, or in
     * web.tree_utils
     *
     * @param {Object} tree object with a 'children' key, which contains an
     *   array of trees.
     * @param {function} f
     */
    _traverse: function (tree, f) {
        var self = this;
        if (f(tree)) {
            _.each(tree.childNodes, function (c) { self._traverse(c, f); });
        }
    },
    /**
     * Write a record. The main difficulty is that we have to apply x2many
     * commands
     *
     * @private
     * @param {string} model
     * @param {Object} values
     * @param {integer} id
     */
    _writeRecord: function (model, values, id) {
        var self = this;
        var record = _.findWhere(this.data[model].records, {id: id});
        for (var field_changed in values) {
            var field = this.data[model].fields[field_changed];
            var value = values[field_changed];
            if (!field) {
                console.warn("Mock: Can't write on field '" + field_changed + "' on model '" + model + "' (field is undefined)");
                continue;
            }
            if (_.contains(['one2many', 'many2many'], field.type)) {
                var ids = _.clone(record[field_changed]) || [];
                // convert commands
                _.each(value, function (command) {
                    if (command[0] === 0) { // CREATE
                        var id = self._mockCreate(field.relation, command[2]);
                        ids.push(id);
                    } else if (command[0] === 1) { // UPDATE
                        self._mockWrite(field.relation, [[command[1]], command[2]]);
                    } else if (command[0] === 2) { // DELETE
                        ids = _.without(ids, command[1]);
                    } else if (command[0] === 3) { // FORGET
                        ids = _.without(ids, command[1]);
                    } else if (command[0] === 4) { // LINK_TO
                        if (!_.contains(ids, command[1])) {
                            ids.push(command[1]);
                        }
                    } else if (command[0] === 5) { // DELETE ALL
                        ids = [];
                    } else if (command[0] === 6) { // REPLACE WITH
                        ids = command[2];
                    } else {
                        console.error('Command ' + JSON.stringify(command) + ' not supported by the MockServer');
                    }
                });
                record[field_changed] = ids;
            } else if (field.type === 'many2one') {
                if (value) {
                    var relatedRecord = _.findWhere(this.data[field.relation].records, {
                        id: value
                    });
                    if (!relatedRecord) {
                        throw "Wrong id for a many2one";
                    } else {
                        record[field_changed] = value;
                    }
                } else {
                    record[field_changed] = false;
                }
            } else {
                record[field_changed] = value;
            }
        }
    },
});

return MockServer;

});
