/*
 * Copyright 2012 Adobe Systems Incorporated. All Rights Reserved.
 */

/*jslint vars: true, plusplus: true, devel: true, browser: true, nomen: true, indent: 4, maxerr: 50 */
/*global define, $, brackets, FileError */

/**
 * FileSyncManager is a set of utilities to help track external modifications to the files and folders
 * in the currently open project.
 *
 * Currently, we look for external changes purely by checking file timestamps against the last-sync
 * timestamp recorded on Document. Later, we will use actual native directory-watching callbacks
 * instead.
 */
define(function (require, exports, module) {
    'use strict';

    // Load dependent modules
    var ProjectManager      = require("ProjectManager"),
        DocumentManager     = require("DocumentManager"),
        EditorManager       = require("EditorManager"),
        Commands            = require("Commands"),
        CommandManager      = require("CommandManager"),
        Async               = require("Async"),
        Dialogs             = require("Dialogs"),
        Strings             = require("strings"),
        FileUtils           = require("FileUtils");

    /**
     * Guard to spot re-entrancy while syncOpenDocuments() is still in progress
     * @type {boolean}
     */
    var _alreadyChecking = false;

    /**
     * If true, we should bail from the syncOpenDocuments() process and then re-run it. See
     * comments in syncOpenDocuments() for how this works.
     * @type {boolean}
     */
    var _restartPending = false;

    /** @type {Array.<Document>} */
    var toReload;
    /** @type {Array.<Document>} */
    var toClose;
    /** @type {Array.<Document>} */
    var editConflicts;
    /** @type {Array.<Document>} */
    var deleteConflicts;

    /**
     * Scans all the given Documents for changes on disk, and sorts them into four buckets,
     * populating the corresponding arrays:
     *  toReload        - changed on disk; unchanged within Brackets
     *  toClose         - deleted on disk; unchanged within Brackets
     *  editConflicts   - changed on disk; also dirty in Brackets
     *  deleteConflicts - deleted on disk; also dirty in Brackets
     *
     * @param {!Array.<Document>} docs
     * @return {$.Deferred}  Resolved when all scanning done, or rejected immediately if there's any
     *      error while reading file timestamps. Errors are logged but no UI is shown.
     */
    function findExternalChanges(docs) {

        toReload = [];
        toClose = [];
        editConflicts = [];
        deleteConflicts = [];

        function checkDoc(doc) {
            var result = new $.Deferred();

            // Check file timestamp / existence
            doc.file.getMetadata(
                function (metadata) {
                    // Does file's timestamp differ from last sync time on the Document?
                    if (metadata.modificationTime.getTime() !== doc.diskTimestamp.getTime()) {
                        if (doc.isDirty) {
                            editConflicts.push(doc);
                        } else {
                            toReload.push(doc);
                        }
                    }
                    result.resolve();
                },
                function (error) {
                    // File has been deleted externally
                    if (error.code === FileError.NOT_FOUND_ERR) {
                        if (doc.isDirty) {
                            deleteConflicts.push(doc);
                        } else {
                            toClose.push(doc);
                        }
                        result.resolve();
                    } else {
                        // Some other error fetching metadata: treat as a real error
                        console.log("Error checking modification status of " + doc.file.fullPath, error.code);
                        result.reject();
                    }
                }
            );
            return result.promise();
        }

        // Check all docs in parallel
        // (fail fast b/c we won't continue syncing if there was any error fetching timestamps)
        return Async.doInParallel(docs, checkDoc, true);
    }

    /**
     * Scans all the files in the working set that do not have Documents (and thus were not scanned
     * by findExternalChanges()). If any were deleted on disk, removes them from the working set.
     */
    function syncUnopenWorkingSet() {
        // We only care about working set entries that have never been open (have no Document).
        var unopenWorkingSetFiles = DocumentManager.getWorkingSet().filter(function (wsFile) {
            return !DocumentManager.getOpenDocumentForPath(wsFile.fullPath);
        });

        function checkWorkingSetFile(file) {
            var result = new $.Deferred();

            file.getMetadata(
                function (metadata) {
                    // File still exists
                    result.resolve();
                },
                function (error) {
                    // File has been deleted externally
                    if (error.code === FileError.NOT_FOUND_ERR) {
                        DocumentManager.closeFullEditor(file);
                        result.resolve();
                    } else {
                        // Some other error fetching metadata: treat as a real error
                        console.log("Error checking for deletion of " + file.fullPath, error.code);
                        result.reject();
                    }
                }
            );
            return result.promise();
        }

        // Check all these files in parallel
        return Async.doInParallel(unopenWorkingSetFiles, checkWorkingSetFile, false);
    }

    /**
     * Reloads the Document's contents from disk, discarding any unsaved changes in the editor.
     *
     * @param {!Document} doc
     * @return {$.Deferred} Resolved after editor has been refreshed; rejected if unable to load the
     *      file's new content. Errors are logged but no UI is shown.
     */
    function reloadDoc(doc) {

        var promise = FileUtils.readAsText(doc.file);

        promise.done(function (text, readTimestamp) {
            doc.refreshText(text, readTimestamp);
        });
        promise.fail(function (error) {
            console.log("Error reloading contents of " + doc.file.fullPath, error.code);
        });
        return promise;
    }

    /**
     * Reloads all the documents in "toReload" silently (no prompts). The operations are all run
     * in parallel.
     * @return {$.Deferred} Resolved/rejected after all reloads done; will be rejected if any one
     *      file's reload failed. Errors are logged (by reloadDoc()) but no UI is shown.
     */
    function reloadChangedDocs() {
        // Reload each doc in turn, and once all are (async) done, signal that we're done
        return Async.doInParallel(toReload, reloadDoc, false);
    }

    /**
     * @param {FileError} error
     * @param {!Document} doc
     * @return {$.Deferred}
     */
    function showReloadError(error, doc) {
        return Dialogs.showModalDialog(
            Dialogs.DIALOG_ID_ERROR,
            Strings.ERROR_RELOADING_FILE_TITLE,
            Strings.format(
                Strings.ERROR_RELOADING_FILE,
                doc.file.fullPath,
                FileUtils.getFileErrorString(error.code)
            )
        );
    }

    /**
     * Closes all the documents in "toClose" silently (no prompts). Completes synchronously.
     */
    function closeDeletedDocs() {
        toClose.forEach(function (doc) {
            // TODO (issue #474): should be Document.destroy() or something instead
            DocumentManager.closeFullEditor(doc.file);
        });
    }

    /**
     * Walks through all the documents in "editConflicts" & "deleteConflicts" and prompts the user
     * about each one. Processing is sequential: if the user chooses to reload a document, the next
     * prompt is not shown until after the reload has completed.
     *
     * @return {$.Deferred} Resolved/rejected after all documents have been prompted and (if
     *      applicable) reloaded (and any resulting error UI has been dismissed). Rejected if any
     *      one reload failed.
     */
    function presentConflicts() {

        var allConflicts = editConflicts.concat(deleteConflicts);

        function presentConflict(doc, i) {
            var result = new $.Deferred();

            // If window has been re-focused, skip all remaining conflicts so the sync can bail & restart
            if (_restartPending) {
                result.resolve();
                return result;
            }

            var message;
            var dialogId;
            var toClose;

            // Prompt UI varies depending on whether the file on disk was modified vs. deleted
            if (i < editConflicts.length) {
                toClose = false;
                dialogId = Dialogs.DIALOG_ID_EXT_CHANGED;
                message = Strings.format(
                    Strings.EXT_MODIFIED_MESSAGE,
                    ProjectManager.makeProjectRelativeIfPossible(doc.file.fullPath)
                );

            } else {
                toClose = true;
                dialogId = Dialogs.DIALOG_ID_EXT_DELETED;
                message = Strings.format(
                    Strings.EXT_DELETED_MESSAGE,
                    ProjectManager.makeProjectRelativeIfPossible(doc.file.fullPath)
                );
            }

            Dialogs.showModalDialog(dialogId, Strings.EXT_MODIFIED_TITLE, message)
                .done(function (id) {
                    if (id === Dialogs.DIALOG_BTN_DONTSAVE) {
                        if (toClose) {
                            // Discard - close editor
                            // TODO (issue #474): should be Document.destroy() or something instead
                            DocumentManager.closeFullEditor(doc.file);
                            result.resolve();
                        } else {
                            // Discard - load changes from disk
                            reloadDoc(doc)
                                .done(function () {
                                    result.resolve();
                                })
                                .fail(function (error) {
                                    // Unable to load changed version from disk - show error UI
                                    showReloadError(error, doc)
                                        .always(function () {
                                            // After user dismisses, move on to next conflict prompt
                                            result.reject();
                                        });
                                });
                        }

                    } else {
                        // Cancel - if user doesn't manually save or close, we'll prompt again next
                        // time window is reactivated;
                        // OR programmatically canceled due to _resetPending - we'll skip all
                        // remaining files in the conflicts list (see above)
                        result.resolve();
                    }
                });

            return result;
        }

        // Begin walking through the conflicts, one at a time
        return Async.doSequentially(allConflicts, presentConflict, false);
    }

    /**
     * Check to see whether any open files have been modified by an external app since the last time
     * Brackets synced up with the copy on disk (either by loading or saving the file). For clean
     * files, we silently upate the editor automatically. For files with unsaved changes, we prompt
     * the user.
     */
    function syncOpenDocuments() {

        // We can become "re-entrant" if the user leaves & then returns to Brackets before we're
        // done -- easy if a prompt dialog is left open. Since the user may have left Brackets to
        // revert some of the disk changes, etc. we want to cancel the current sync and immediately
        // begin a new one. We let the orig sync run until the user-visible dialog phase, then
        // bail; if we're already there we programmatically close the dialog to bail right away.
        if (_alreadyChecking) {
            _restartPending = true;

            // Close dialog if it was open. This will 'unblock' presentConflict(), which bails back
            // to us immediately upon seeing _restartPending. We then restart the sync - see below
            Dialogs.cancelModalDialogIfOpen(Dialogs.DIALOG_ID_EXT_CHANGED);
            Dialogs.cancelModalDialogIfOpen(Dialogs.DIALOG_ID_EXT_DELETED);

            return;
        }

        _alreadyChecking = true;

        // Syncing proceeds in four phases:
        //  1) Check all open files for external modifications
        //  2) Check any other working set entries (that are not open) for deletion, and remove
        //     from working set if deleted
        //  3) Refresh all Documents that are clean (if file changed on disk)
        //  4) Close all Documents that are clean (if file deleted on disk)
        //  5) Prompt about any Documents that are dirty (if file changed/deleted on disk)
        // Each phase fully completes (asynchronously) before the next one begins.

        // 1) Check for external modifications
        var allDocs = DocumentManager.getAllOpenDocuments();

        findExternalChanges(allDocs)
            .done(function () {
                // 2) Check un-open working set entries for deletion (& "close" if needed)
                syncUnopenWorkingSet()
                    .always(function () {
                        // If we were unable to check any un-open files for deletion, silently ignore
                        // (after logging to console). This doesn't have any bearing on syncing truly
                        // open Documents (which we've already successfully checked).

                        // 3) Reload clean docs as needed
                        reloadChangedDocs()
                            .always(function () {
                                // 4) Close clean docs as needed
                                // This phase completes synchronously
                                closeDeletedDocs();

                                // 5) Prompt for dirty editors (conflicts)
                                presentConflicts()
                                    .always(function () {
                                        if (_restartPending) {
                                            // Restart the sync if needed
                                            _restartPending = false;
                                            _alreadyChecking = false;
                                            syncOpenDocuments();
                                        } else {
                                            // We're really done!
                                            _alreadyChecking = false;

                                            // If we showed a dialog, restore focus to editor
                                            if (editConflicts.length > 0 || deleteConflicts.length > 0) {
                                                EditorManager.focusEditor();
                                            }

                                            // (Any errors that ocurred during presentConflicts() have already
                                            // shown UI & been dismissed, so there's no fail() handler here)
                                        }
                                    });
                            });
                            // Note: if any auto-reloads failed, we silently ignore (after logging to console)
                            // and we still continue onto phase 4 and try to process those files anyway.
                            // (We'll retry the auto-reloads next time window is activated... and evenually
                            // we'll also be double checking before each Save).
                    });
            }).fail(function () {
                // Unable to fetch timestamps for some reason - silently ignore (after logging to console)
                // (We'll retry next time window is activated... and evenually we'll also be double
                // checking before each Save).

                // We can't go on without knowing which files are dirty, so bail now
                _alreadyChecking = false;
            });

    }

    // Define public API
    exports.syncOpenDocuments = syncOpenDocuments;
});


// to make a block cursor, use something like this:

keep replace replace keep keep keep keep keep

/*
 * Copyright 2011 Adobe Systems Incorporated. All Rights Reserved.
 */

/*jslint vars: true, plusplus: true, devel: true, browser: true, nomen: true, indent: 4, maxerr: 50 */
/*global define, $ */

/*
* Displays an auto suggest popup list of files to allow the user to quickly navigate to a file.
* Uses FileIndexManger to supply the file list and registers Commands.FILE_QUICK_NAVIGATE with Brackets.
*
* TODO (issue 333) - currently jquery smart auto complete is used for the popup list. While it mostly works
* it has several issues, so it should be replace with an alternative. Issues:
* - only accepts an array of strings. A list of objects is preferred to avoid some work arounds to display
*   both the path and filename.
* - the popup position logic has flaws that require css work-arounds
* - the popup properties cannot be modified once the object is constructed
*/

define(function (require, exports, module) {
    'use strict';

    var FileIndexManager    = require("FileIndexManager"),
        DocumentManager     = require("DocumentManager"),
        CommandManager      = require("CommandManager"),
        EditorManager       = require("EditorManager"),
        Commands            = require("Commands"),
        ProjectManager      = require("ProjectManager");

    /**
    * QuickNavigateDialog class
    * @constructor
    *
    */
    function QuickNavigateDialog() {
        this.closed = false;
        this.result = null; // $.Deferred
    }

    /**
    * Creates a dialog div floating on top of the current code mirror editor
    */
    QuickNavigateDialog.prototype._createDialogDiv = function (template) {
        var wrap = $("#editorHolder")[0];
        this.dialog = wrap.insertBefore(document.createElement("div"), wrap.firstChild);
        this.dialog.className = "CodeMirror-dialog";
        this.dialog.innerHTML = '
' + template + '
';
    };

    function _filenameFromPath(path) {
        return path.slice(path.lastIndexOf("/") + 1, path.length);
    }

    /**
    * Closes the search dialog and resolves the promise that showDialog returned
    */
    QuickNavigateDialog.prototype._close = function (value) {
        if (this.closed) {
            return;
        }

        this.closed = true;

        this.dialog.parentNode.removeChild(this.dialog);

        $(".smart_autocomplete_container").remove();

        this.result.resolve(value);
    };

    /**
    * Shows the search dialog and initializes the auto suggestion list with filenames from the current project
    * @returns {$.Promise} a promise that is resolved when the dialgo closes with the string value from the search field
    */
    QuickNavigateDialog.prototype.showDialog = function () {
        var that = this;
        var fileInfoList;
        this.result = new $.Deferred();

        FileIndexManager.getFileInfoList("all")
            .done(function (filelistResult) {
                fileInfoList = filelistResult;
                var dialogHTML = 'Quick Open: <input type="text" autocomplete="off" id="quickFileOpenSearch" style="width: 30em">';
                that._createDialogDiv(dialogHTML);
                var closed = false;
                var searchField = $('input#quickFileOpenSearch');

                // auto suggest list helper function
                function _handleResultsFormatter(path) {
                    var filename = _filenameFromPath(path);
                    var rPath = ProjectManager.makeProjectRelativeIfPossible(path);
                    var boldName = filename.replace(new RegExp($('input#quickFileOpenSearch').val(), "gi"), "$&");
                    return "
" + boldName +
                        "
" + rPath + "
";
                }

                // auto suggest list helper function
                function _handleFilter(term, source) {
                    var filteredList = $.map(source, function (fileInfo) {
                        // match term again filename only (not the path)
                        var path = fileInfo.fullPath;
                        var filename = _filenameFromPath(path);
                        if (filename.toLowerCase().indexOf(term.toLowerCase()) !== -1) {
                            return fileInfo.fullPath;
                        } else {
                            return null;
                        }
                    }).sort(function (a, b) {
                        // sort by filename
                        var filenameA = _filenameFromPath(a).toLowerCase();
                        var filenameB = _filenameFromPath(b).toLowerCase();
                        if (filenameA < filenameB) {
                            return -1;
                        } else if (filenameA > filenameB) {
                            return 1;
                        } else {
                            return 0;
                        }
                    });

                    return filteredList;
                }

                // Create the auto suggest list of filenames
                searchField.smartAutoComplete({
                    source: fileInfoList,
                    maxResults: 10,
                    forceSelect: false,
                    typeAhead: true,
                    filter: _handleFilter,
                    resultFormatter: _handleResultsFormatter
                });

                searchField.bind({
                    // close the dialog when the user selects an item
                    itemSelect: function (ev, selected_item) {
                        var value = decodeURIComponent($(selected_item).attr("data-fullpath"));
                        that._close(value);
                    },

                    keydown: function (e) {
                        var query = searchField.val();

                        // close the dialog when the ENTER (23) or ESC (27) key is pressed
                        if ((e.keyCode === 13 && query.charAt(0) === ":") || e.keyCode === 27) {
                            e.stopPropagation();
                            e.preventDefault();

                            // clear the query on ESC key
                            if (e.keyCode === 27) {
                                query = null;
                            }

                            that._close(query);
                            EditorManager.focusEditor();
                        }
                    }

                });

                searchField.focus();
            });

        return this.result;
    };

    /**
    * Displays a non-modal embedded dialog above the code mirror edit that allows the user to quickly
    * navigate to different files and parts of the current file.
    * The search field lists files in the project. The user can enter ":" followed by a line
    * number to navigate to a specific line in the current file.
    */
    function doFileSearch() {

        var dialog = new QuickNavigateDialog();
        dialog.showDialog()
            .done(function (query) {
                if (query) {
                    if (query.charAt(0) === ":") {
                        var lineNumber = parseInt(query.slice(1, query.length), 10);
                        if (!isNaN(lineNumber)) {
                            var editor = EditorManager.getCurrentFullEditor();
                            editor.setCursorPos(lineNumber - 1, 0);
                        }
                    } else {
                        CommandManager.execute(Commands.FILE_OPEN, {fullPath: query});
                    }
                }
            });
    }

    CommandManager.register(Commands.FILE_QUICK_NAVIGATE, doFileSearch);
});


paranoid: false,
whereCollection: null,
schema: null,
schemaDelimiter: '',
defaultScope: null,
scopes: null,
hooks: {},
indexes: []
}, options || {});
this.associations = {};

keep keep keep keep keep replace keep keep keep keep keep

'use strict';

/* jshint -W030 */
var chai = require('chai')
  , expect = chai.expect
  , Support   = require(__dirname + '/../support')
  , current   = Support.sequelize;

describe(Support.getTestDialectTeaser('Model'), function() {
  var Project = current.define('project')
    , User = current.define('user')
    , Company;

  var scopes = {
    complexFunction: function(value) {
      return {
        where: [(value + ' IN (SELECT foobar FROM some_sql_function(foo.bar))')]
      };
    },
    somethingTrue: {
      where: {
        something: true,
        somethingElse: 42
      },
      limit: 5
    },
    somethingFalse: {
      where: {
        something: false
      }
    },
    users: {
      include: [
        { model: User }
      ]
    },
    alsoUsers: {
      include: [
        { model: User, where: { something: 42}}
      ]
    },
    projects: {
      include: [Project]
    },
    noArgs: function () {
      // This does not make much sense, since it does not actually need to be in a function,
      // In reality it could be used to do for example new Date or random in the scope - but we want it deterministic

      return {
        where: {
          other_value: 7
        }
      };
    },
    actualValue: function(value) {
      return {
        where: {
          other_value: value
        }
      };
    },
  };

  Company = current.define('company', {}, {
    defaultScope: {
      include: [Project],
      where: { active: true }
    },
    scopes: scopes
  });

  describe('.scope', function () {
    it('should apply default scope', function () {
      expect(Company.$scope).to.deep.equal({
        include: [{ model: Project }],
        where: { active: true }
      });
    });

    it('should be able to unscope', function () {
      expect(Company.scope(null).$scope).to.be.empty;
      expect(Company.unscoped().$scope).to.be.empty;
    });

    it('should be able to merge scopes', function() {
      expect(Company.scope('somethingTrue', 'somethingFalse').$scope).to.deep.equal({
        where: {
          something: false,
          somethingElse: 42
        },
        limit: 5
      });
    });

    it('should support multiple, coexistent scoped models', function () {
      var scoped1 = Company.scope('somethingTrue')
        , scoped2 = Company.scope('somethingFalse');

        expect(scoped1.$scope).to.deep.equal(scopes.somethingTrue);
        expect(scoped2.$scope).to.deep.equal(scopes.somethingFalse);
    });

    it('should work with function scopes', function () {
      expect(Company.scope({method: ['actualValue', 11]}).$scope).to.deep.equal({
        where: {
          other_value: 11
        }
      });

      expect(Company.scope('noArgs').$scope).to.deep.equal({
        where: {
          other_value: 7
        }
      });
    });

    it('should be able to merge two scoped includes', function () {
      expect(Company.scope('users', 'projects').$scope).to.deep.equal({
        include: [
          { model: User },
          { model: Project }
        ]
      });
    });

    it('should be able to override the default scope', function() {
      expect(Company.scope('somethingTrue').$scope).to.deep.equal(scopes.somethingTrue);
    });

    it('should be able to combine default with another scope', function () {
      expect(Company.scope(['defaultScope', {method: ['actualValue', 11]}]).$scope).to.deep.equal({
        include: [{ model: Project }],
        where: {
          active: true,
          other_value: 11
        }
      });
    });

    it('should be able to use raw queries', function () {
      expect(Company.scope([{method: ['complexFunction', 'qux']}]).$scope).to.deep.equal({
        where: [ 'qux IN (SELECT foobar FROM some_sql_function(foo.bar))' ]
      });
    });

    it('should override the default scope', function () {
      expect(Company.scope(['defaultScope', {method: ['complexFunction', 'qux']}]).$scope).to.deep.equal({
        include: [{ model: Project }],
        where: [ 'qux IN (SELECT foobar FROM some_sql_function(foo.bar))' ]
      });
    });

    it('should emit an error for scopes that dont exist', function() {
      expect(function () {
        Company.scope('doesntexist');
      }).to.throw('Invalid scope doesntexist called.');
    });
  });

  describe('addScope', function () {
    it('allows me to add a new scope', function ()  {
      expect(function () {
        Company.scope('newScope');
      }).to.throw('Invalid scope newScope called.');

      Company.addScope('newScope', {
        where: {
          this: 'that'
        },
        include: [Project]
      });

      expect(Company.scope('newScope').$scope).to.deep.equal({
        where: { this: 'that' },
        include: [{ model: Project }]
      });
    });

    it('warns me when overriding an existing scope', function () {
      expect(function () {
        Company.addScope('somethingTrue', {});
      }).to.throw('The scope somethingTrue already exists. Pass { override: true } as options to silence this error');
    });

    it('allows me to override an existing scope', function () {
      Company.addScope('somethingTrue', {
        where: {
          something: false
        }
      }, { override: true });

      expect(Company.scope('somethingTrue').$scope).to.deep.equal({
        where: { something: false },
      });
    });

    it('warns me when overriding an existing default scope', function () {
      expect(function () {
        Company.addScope('defaultScope', {});
      }).to.throw('The scope defaultScope already exists. Pass { override: true } as options to silence this error');
    });

    it('allows me to override a default scope', function () {
      Company.addScope('defaultScope', {
        include: [Project]
      }, { override: true });

      expect(Company.$scope).to.deep.equal({
        include: [{ model: Project }]
      });
    });
  });

  describe('$injectScope', function () {
    it('should be able to merge scope and where', function () {
      var scope = {
        where: {
          something: true,
          somethingElse: 42
        },
        limit: 15,
        offset: 3
      };

      var options = {
        where: {
          something: false
        },
        limit: 9
      };

      current.Model.$injectScope(scope, options);

      expect(options).to.deep.equal({
        where: {
          something: false,
          somethingElse: 42
        },
        limit: 9,
        offset: 3
      });
    });

    it('should be able to overwrite multiple scopes with the same include', function () {
      var scope = {
        include: [
          { model: Project, where: { something: false }},
          { model: Project, where: { something: true }}
        ]
      };

      var options = {};

      current.Model.$injectScope(scope, options);

      expect(options.include).to.have.length(1);
      expect(options.include[0]).to.deep.equal({ model: Project, where: { something: true }});
    });

    it('should be able to override scoped include', function () {
      var scope = {
        include: [{ model: Project, where: { something: false }}]
      };

      var options = {
        include: [{ model: Project, where: { something: true }}]
      };

      current.Model.$injectScope(scope, options);

      expect(options.include).to.have.length(1);
      expect(options.include[0]).to.deep.equal({ model: Project, where: { something: true }});
    });

    it('should be able to merge aliassed includes with the same model', function () {
      var scope = {
        include: [{model: User, as: 'someUser'}]
      };

      var options = {
        include: [{model: User, as: 'otherUser'}]
      };

      current.Model.$injectScope(scope, options);

      expect(options.include).to.have.length(2);
      expect(options.include[0]).to.deep.equal({model: User, as: 'otherUser'});
      expect(options.include[1]).to.deep.equal({model: User, as: 'someUser'});
    });

    it('should be able to merge scoped include with include in find', function () {
      var scope = {
        include: [
          { model: Project, where: { something: false }}
        ]
      };

      var options = {
        include: [
          { model: User, where: { something: true }}
        ]
      };

      current.Model.$injectScope(scope, options);

      expect(options.include).to.have.length(2);
      expect(options.include[0]).to.deep.equal({ model: User, where: { something: true }});
      expect(options.include[1]).to.deep.equal({ model: Project, where: { something: false }});
    });
  });
});


test*.js
*.swp
.idea
.DS_STORE
node_modules
npm-debug.log
*~
test/binary/tmp/*
test/tmp/*
test/dialects/sqlite/test.sqlite
test/sqlite/test.sqlite
coverage-*
site


# Mixin BelongsToMany
[View code](https://github.com/sequelize/sequelize/blob/f678009d7514b81a6f87e12b86360e9a597e3ca8/lib/associations/belongs-to-many.js#L36)
Many-to-many association with a join table.

When the join table has additional attributes, these can be passed in the options object:

```js
UserProject = sequelize.define('user_project', {
  role: Sequelize.STRING
});
User.belongsToMany(Project, { through: UserProject });

user.addProject(project, { role: 'manager', transaction: t });
```

All methods allow you to pass either a persisted instance, its primary key, or a mixture:

```js
Project.create({ id: 11 }).then(function (project) {
  user.addProjects([project, 12]);
});
```

In the API reference below, replace `Assocation(s)` with the actual name of your association, e.g. for `User.belongsToMany(Project)` the getter will be `user.getProjects()`.

***

## `getAssociations([options])` -> `Promise.<Array.<Instance>>`
[View code](https://github.com/sequelize/sequelize/blob/f678009d7514b81a6f87e12b86360e9a597e3ca8/lib/associations/belongs-to-many.js#L200)
Get everything currently associated with this, using an optional where clause

**Params:**

| Name | Type | Description |
| ---- | ---- | ----------- |
| [options] | Object |  |
| [options.where] | Object | An optional where clause to limit the associated models |
| [options.scope] | String | Boolean | Apply a scope on the related model, or remove its default scope by passing false |

***

## `setAssociations([newAssociations], [options])` -> `Promise`
[View code](https://github.com/sequelize/sequelize/blob/f678009d7514b81a6f87e12b86360e9a597e3ca8/lib/associations/belongs-to-many.js#L210)
Set the associated models by passing an array of instances or their primary keys. Everything that it not in the passed array will be un-associated

**Params:**

| Name | Type | Description |
| ---- | ---- | ----------- |
| [newAssociations] | Array.<Instance | String | Number> | An array of instances or primary key of instances to associate with this. Pass `null` or `undefined` to remove all associations. |
| [options] | Object | Options passed to `through.findAll`, `bulkCreate`, `update` and `destroy`. Can also hold additional attributes for the join table |
| [options.validate] | Object | Run validation for the join model |

***

## `addAssociations([newAssociations], [options])` -> `Promise`
[View code](https://github.com/sequelize/sequelize/blob/f678009d7514b81a6f87e12b86360e9a597e3ca8/lib/associations/belongs-to-many.js#L220)
Associate several instances with this

**Params:**

| Name | Type | Description |
| ---- | ---- | ----------- |
| [newAssociations] | Array.<Instance | String | Number> | An array of instances or primary key of instances to associate with this. Pass `null` or `undefined` to remove all associations. |
| [options] | Object | Options passed to `through.findAll`, `bulkCreate`, and `update` `destroy`. Can also hold additional attributes for the join table |
| [options.validate] | Object | Run validation for the join model |

***

## `addAssociation([newAssociation], [options])` -> `Promise`
[View code](https://github.com/sequelize/sequelize/blob/f678009d7514b81a6f87e12b86360e9a597e3ca8/lib/associations/belongs-to-many.js#L230)
Associate several instances with this

**Params:**

| Name | Type | Description |
| ---- | ---- | ----------- |
| [newAssociation] | Instance | String | Number | An array of instances or primary key of instances to associate with this. Pass `null` or `undefined` to remove all associations. |
| [options] | Object | Options passed to `through.findAll`, `bulkCreate` and `update`. Can also hold additional attributes for the join table |
| [options.validate] | Object | Run validation for the join model |

***

## `createAssociation([values], [options])` -> `Promise`
[View code](https://github.com/sequelize/sequelize/blob/f678009d7514b81a6f87e12b86360e9a597e3ca8/lib/associations/belongs-to-many.js#L239)
Create a new instance of the associated model and associate it with this.

**Params:**

| Name | Type | Description |
| ---- | ---- | ----------- |
| [values] | Object |  |
| [options] | Object | Options passed to create and add. Can also hold additional attributes for the join table |

***

## `removeAssociation([oldAssociated], [options])` -> `Promise`
[View code](https://github.com/sequelize/sequelize/blob/f678009d7514b81a6f87e12b86360e9a597e3ca8/lib/associations/belongs-to-many.js#L248)
Un-associate the instance

**Params:**

| Name | Type | Description |
| ---- | ---- | ----------- |
| [oldAssociated] | Instace | String | Number | Can be an Instance or its primary key |
| [options] | Object | Options passed to `through.destroy` |

***

## `removeAssociations([oldAssociated], [options])` -> `Promise`
[View code](https://github.com/sequelize/sequelize/blob/f678009d7514b81a6f87e12b86360e9a597e3ca8/lib/associations/belongs-to-many.js#L257)
Un-associate several instances

**Params:**

| Name | Type | Description |
| ---- | ---- | ----------- |
| [oldAssociated] | Array.<Instace | String | Number> | Can be an Instance or its primary key |
| [options] | Object | Options passed to `through.destroy` |

***

## `hasAssociation([instance], [options])` -> `Promise`
[View code](https://github.com/sequelize/sequelize/blob/f678009d7514b81a6f87e12b86360e9a597e3ca8/lib/associations/belongs-to-many.js#L266)
Check if an instance is associated with this.

**Params:**

| Name | Type | Description |
| ---- | ---- | ----------- |
| [instance] | Instace | String | Number | Can be an Instance or its primary key |
| [options] | Object | Options passed to getAssociations |

***

## `hasAssociations([instances], [options])` -> `Promise`
[View code](https://github.com/sequelize/sequelize/blob/f678009d7514b81a6f87e12b86360e9a597e3ca8/lib/associations/belongs-to-many.js#L275)
Check if all instances are associated with this.

**Params:**

| Name | Type | Description |
| ---- | ---- | ----------- |
| [instances] | Array.<Instace | String | Number> | Can be an Instance or its primary key |
| [options] | Object | Options passed to getAssociations |

***

_This document is automatically generated based on source code comments. Please do not edit it directly, as your changes will be ignored. Please write on IRC, open an issue or a create a pull request if you feel something can be improved. For help on how to write source code documentation see [JSDoc](http://usejsdoc.org) and [dox](https://github.com/tj/dox)_


## `Deferrable()` -> `object`
[View code](https://github.com/sequelize/sequelize/blob/f678009d7514b81a6f87e12b86360e9a597e3ca8/lib/deferrable.js#L39)
A collection of properties related to deferrable constraints. It can be used to
make foreign key constraints deferrable and to set the constaints within a
transaction. This is only supported in PostgreSQL.

The foreign keys can be configured like this. It will create a foreign key
that will check the constraints immediately when the data was inserted.

```js
sequelize.define('Model', {
  foreign_id: {
    type: Sequelize.INTEGER,
    references: {
      model: OtherModel,
      key: 'id',
      deferrable: Sequelize.Deferrable.INITIALLY_IMMEDIATE
    }
  }
});
```

The constraints can be configured in a transaction like this. It will
trigger a query once the transaction has been started and set the constraints
to be checked at the very end of the transaction.

```js
sequelize.transaction({
  deferrable: Sequelize.Deferrable.SET_DEFERRED
});
```

***

## `INITIALLY_DEFERRED()`
[View code](https://github.com/sequelize/sequelize/blob/f678009d7514b81a6f87e12b86360e9a597e3ca8/lib/deferrable.js#L59)
A property that will defer constraints checks to the end of transactions.

***

## `INITIALLY_IMMEDIATE()`
[View code](https://github.com/sequelize/sequelize/blob/f678009d7514b81a6f87e12b86360e9a597e3ca8/lib/deferrable.js#L76)
A property that will trigger the constraint checks immediately

***

## `NOT()`
[View code](https://github.com/sequelize/sequelize/blob/f678009d7514b81a6f87e12b86360e9a597e3ca8/lib/deferrable.js#L95)
A property that will set the constraints to not deferred. This is
the default in PostgreSQL and it make it impossible to dynamically
defer the constraints within a transaction.

***

## `SET_DEFERRED(constraints)`
[View code](https://github.com/sequelize/sequelize/blob/f678009d7514b81a6f87e12b86360e9a597e3ca8/lib/deferrable.js#L114)
A property that will trigger an additional query at the beginning of a
transaction which sets the constraints to deferred.

**Params:**

| Name | Type | Description |
| ---- | ---- | ----------- |
| constraints | Array | An array of constraint names. Will defer all constraints by default. |

***

## `SET_IMMEDIATE(constraints)`
[View code](https://github.com/sequelize/sequelize/blob/f678009d7514b81a6f87e12b86360e9a597e3ca8/lib/deferrable.js#L135)
A property that will trigger an additional query at the beginning of a
transaction which sets the constraints to immediately.

**Params:**

| Name | Type | Description |
| ---- | ---- | ----------- |
| constraints | Array | An array of constraint names. Will defer all constraints by default. |

***

_This document is automatically generated based on source code comments. Please do not edit it directly, as your changes will be ignored. Please write on IRC, open an issue or a create a pull request if you feel something can be improved. For help on how to write source code documentation see [JSDoc](http://usejsdoc.org) and [dox](https://github.com/tj/dox)_