/*
 * Copyright (c) 2013 - present Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*jslint regexp: true */
/*global describe, it, xit, expect, beforeEach, afterEach, waitsFor, runs, waitsForDone, waitsForFail, beforeFirst, afterLast */

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

    var Commands             = brackets.getModule("command/Commands"),
        CommandManager       = brackets.getModule("command/CommandManager"),
        MainViewManager      = brackets.getModule("view/MainViewManager"),
        DocumentManager      = brackets.getModule("document/DocumentManager"),
        EditorManager        = brackets.getModule("editor/EditorManager"),
        FileSystem           = brackets.getModule("filesystem/FileSystem"),
        FileUtils            = brackets.getModule("file/FileUtils"),
        PreferencesManager   = brackets.getModule("preferences/PreferencesManager"),
        SpecRunnerUtils      = brackets.getModule("spec/SpecRunnerUtils"),
        JSCodeHints          = require("main"),
        Preferences          = brackets.getModule("JSUtils/Preferences"),
        ScopeManager         = brackets.getModule("JSUtils/ScopeManager"),
        HintUtils            = brackets.getModule("JSUtils/HintUtils"),
        HintUtils2           = require("HintUtils2"),
        ParameterHintProvider = require("ParameterHintsProvider").JSParameterHintsProvider,
        phProvider            = new ParameterHintProvider();

    var extensionPath   = FileUtils.getNativeModuleDirectoryPath(module),
        testPath        = extensionPath + "/unittest-files/basic-test-files/file1.js",
        testHtmlPath    = extensionPath + "/unittest-files/basic-test-files/index.html",
        testDoc         = null,
        testEditor,
        preTestText;

    CommandManager.register("test-file-open", Commands.FILE_OPEN, function (fileInfo) {
        // Register a command for FILE_OPEN, which the jump to def code will call
        return DocumentManager.getDocumentForPath(fileInfo.fullPath).done(function (doc) {
            MainViewManager._edit(MainViewManager.ACTIVE_PANE, doc);
        });
    });

    describe("JavaScript Code Hinting", function () {

        // Helper function for testing cursor position
        function fixPos(pos) {
            if (!("sticky" in pos)) {
                pos.sticky = null;
            }
            return pos;
        }

        /*
         * Ask provider for hints at current cursor position; expect it to
         * return some
         *
         * @param {Object} provider - a CodeHintProvider object
         * @param {string} key - the charCode of a key press that triggers the
         *      CodeHint provider
         * @return {boolean} - whether the provider has hints in the context of
         *      the test editor
         */
        function expectHints(provider, key) {
            if (key === undefined) {
                key = null;
            }

            expect(provider.hasHints(testEditor, key)).toBe(true);
            return provider.getHints(null);
        }

        /*
         * Ask provider for hints at current cursor position; expect it NOT to
         * return any
         *
         * @param {Object} provider - a CodeHintProvider object
         * @param {string} key - the charCode of a key press that triggers the
         *      CodeHint provider
         */
        function expectNoHints(provider, key) {

            if (key === undefined) {
                key = null;
            }

            expect(provider.hasHints(testEditor, key)).toBe(false);
        }

        /*
         * Return the index at which hint occurs in hintList
         *
         * @param {Array.<Object>} hintList - the list of hints
         * @param {string} hint - the hint to search for
         * @return {number} - the index into hintList at which the hint occurs,
         * or -1 if it does not
         */
        function _indexOf(hintList, hint) {
            var index = -1,
                counter = 0;

            for (counter; counter < hintList.length; counter++) {
                if (hintList[counter].data("token").value === hint) {
                    index = counter;
                    break;
                }
            }
            return index;
        }

        /*
         * Wait for a hint response object to resolve, then apply a callback
         * to the result
         *
         * @param {Object + jQuery.Deferred} hintObj - a hint response object,
         *      possibly deferred
         * @param {Function} callback - the callback to apply to the resolved
         *      hint response object
         */
        function _waitForHints(hintObj, callback) {
            var complete = false,
                hintList = null;

            if (hintObj.hasOwnProperty("hints")) {
                complete = true;
                hintList = hintObj.hints;
            } else {
                hintObj.done(function (obj) {
                    complete = true;
                    hintList = obj.hints;
                });
            }

            waitsFor(function () {
                return complete;
            }, "Expected hints did not resolve", 3000);

            runs(function () { callback(hintList); });
        }

        /*
         * Test if hints should be closed or not closed at a given position.
         *
         * @param {Object} provider - a CodeHintProvider object
         * @param {Object + jQuery.Deferred} hintObj - a hint response object,
         *      possibly deferred
         * @param {line: number, ch: number} newPos - new position to move to
         * after hints are received.
         * @param {boolean} expectedValue - true if hints should close,
         * false otherwise.
         */
        function expectCloseHints(provider, hintObj, newPos, expectedValue) {
            _waitForHints(hintObj, function (hintList) {
                testEditor.setCursorPos(newPos);
                expect(provider.shouldCloseHints(JSCodeHints.getSession())).toBe(expectedValue);
            });
        }

        /*
         * Expect a given list of hints to be absent from a given hint
         * response object
         *
         * @param {Object + jQuery.Deferred} hintObj - a hint response object,
         *      possibly deferred
         * @param {Array.<string>} absentHints - a list of hints that should not
         *      be present in the hint response
         */
        function hintsAbsent(hintObj, absentHints) {
            _waitForHints(hintObj, function (hintList) {
                expect(hintList).toBeTruthy();
                absentHints.forEach(function (absentHint) {
                    expect(_indexOf(hintList, absentHint)).toBe(-1);
                });
            });
        }

        /*
         * Expect a given list of hints to be present in a given hint
         * response object
         *
         * @param {Object + jQuery.Deferred} hintObj - a hint response object,
         *      possibly deferred
         * @param {Array.<string>} expectedHints - a list of hints that should be
         *      present in the hint response
         */
        function hintsPresent(hintObj, expectedHints) {
            _waitForHints(hintObj, function (hintList) {
                expect(hintList).toBeTruthy();
                expectedHints.forEach(function (expectedHint) {
                    expect(_indexOf(hintList, expectedHint)).not.toBe(-1);
                });
            });
        }

        /*
         * Expect a given list of hints to be present in the given order in a
         * given hint response object
         *
         * @param {Object + jQuery.Deferred} hintObj - a hint response object,
         *      possibly deferred
         * @param {Array.<string>} expectedHints - a list of hints that should be
         *      present in the given order in the hint response
         */
        function hintsPresentOrdered(hintObj, expectedHints) {
            var prevIndex = -1,
                currIndex;

            _waitForHints(hintObj, function (hintList) {
                expect(hintList).toBeTruthy();
                expectedHints.forEach(function (expectedHint) {
                    currIndex = _indexOf(hintList, expectedHint);
                    expect(currIndex).toBeGreaterThan(prevIndex);
                    prevIndex = currIndex;
                });
            });
        }

        /*
         * Expect a given list of hints to be present in a given hint
         * response object, and no more.
         *
         * @param {Object + jQuery.Deferred} hintObj - a hint response object,
         *      possibly deferred
         * @param {Array.<string>} expectedHints - a list of hints that should be
         *      present in the hint response, and no more.
         */
        function hintsPresentExact(hintObj, expectedHints) {
            _waitForHints(hintObj, function (hintList) {
                expect(hintList).toBeTruthy();
                expect(hintList.length).toBe(expectedHints.length);
                expectedHints.forEach(function (expectedHint, index) {
                    expect(hintList[index].data("token").value).toBe(expectedHint);
                });
            });
        }

        /**
         * Find the index of a string in a list of hints.
         * @param {Array} hintList - the list of hints
         * @param {string} hintSelection - the string represenation of the hint
         *  to find the index of
         * @return {number} the index of the hint corresponding to the hintSelection
         */
        function findHint(hintList, hintSelection) {
            var i, l;
            for (i = 0, l = hintList.length; i < l; ++i) {
                var current = hintList[i].data("token");
                if (hintSelection === current.value) {
                    return i;
                }
            }
            return -1;
        }
        /*
         * Simulation of selection of a particular hint in a hint list.
         * Presumably results in side effects in the hint provider's
         * current editor context.
         *
         * @param {Object} provider - a CodeHint provider object
         * @param {Object} hintObj - a hint response object from that provider,
         *      possibly deferred
         * @param {string} hintSelection - the hint to select
         */
        function selectHint(provider, hintObj, hintSelection) {
            expectHints(provider);
            _waitForHints(hintObj, function (hintList) {
                expect(hintList).toBeTruthy();
                var index = findHint(hintList, hintSelection);
                expect(hintList[index].data("token")).toBeTruthy();
                expect(provider.insertHint(hintList[index])).toBe(false);
            });
        }

        /**
         * Wait for the editor to change positions, such as after a jump to
         * definition has been triggered.  Will timeout after 3 seconds
         *
         * @param {{line:number, ch:number}} oldLocation - the original line/col
         * @param {Function} callback - the callback to apply once the editor has changed position
         */
        function _waitForJump(jumpPromise, callback) {
            var cursor = null,
                complete = false;

            jumpPromise.done(function () {
                complete = true;
            });

            waitsFor(function () {
                var activeEditor = EditorManager.getActiveEditor();
                cursor = activeEditor.getCursorPos();
                return complete;
            }, "Expected jump did not occur", 3000);

            runs(function () { callback(cursor); });
        }

        /**
         * Trigger a jump to definition, and verify that the editor jumped to
         * the expected location. The new location is the variable definition
         * or function definition of the variable or function at the current
         * cursor location. Jumping to the new location will cause a new editor
         * to be opened or open an existing editor.
         *
         * @param {{line:number, ch:number, file:string}} expectedLocation - the
         *  line, column, and optionally the new file the editor should jump to.  If the
         *  editor is expected to stay in the same file, then file may be omitted.
         */
        function editorJumped(expectedLocation) {
            var jumpPromise = JSCodeHints.handleJumpToDefinition();


            _waitForJump(jumpPromise, function (newCursor) {
                expect(newCursor.line).toBe(expectedLocation.line);
                expect(newCursor.ch).toBe(expectedLocation.ch);
                if (expectedLocation.file) {
                    var activeEditor = EditorManager.getActiveEditor();
                    expect(activeEditor.document.file.name).toBe(expectedLocation.file);
                }
            });

        }

        /**
         * Verify there is no parameter hint at the current cursor.
         */
        function expectNoParameterHint() {
            var requestStatus = undefined;
            runs(function () {
                var request = phProvider._getParameterHint();
                request.fail(function (status) {
                    requestStatus = status;
                });

                waitsForFail(request, "ParameterHints");
            });
         
            runs(function () {
                expect(requestStatus).toBe(null);
            });  
        }

        /**
         * Verify the parameter hint is not visible.
         */
        function expectParameterHintClosed() {
            expect(phProvider.isHintDisplayed()).toBe(false);
        }

        /**
         * Show a function hint based on the code at the cursor. Verify the
         * hint matches the passed in value.
         *
         * @param {Array<{name: string, type: string, isOptional: boolean}>}
         * expectedParams - array of records, where each element of the array
         * describes a function parameter. If null, then no hint is expected.
         * @param {number} expectedParameter - the parameter at cursor.
         */
        function expectParameterHint(expectedParams, expectedParameter) {
            var requestHints = undefined,
                request = null;
            
            function expectHint(hint) {
                var params = hint.parameters,
                    n = params.length,
                    i;

                // compare params to expected params
                expect(params.length).toBe(expectedParams.length);
                expect(hint.currentIndex).toBe(expectedParameter);

                for (i = 0; i < n; i++) {

                    expect(params[i].name).toBe(expectedParams[i].name);
                    expect(params[i].type).toBe(expectedParams[i].type);
                    if (params[i].isOptional) {
                        expect(expectedParams[i].isOptional).toBeTruthy();
                    } else {
                        expect(expectedParams[i].isOptional).toBeFalsy();
                    }
                }

            }
            
            runs(function () {
                request = phProvider._getParameterHint();
                
                if (expectedParams === null) {
                    request.fail(function (result) {
                        requestHints = result;
                    });

                    waitsForFail(request, "ParameterHints");
                } else {
                    request.done(function (result) {
                        requestHints = result;
                    });

                    waitsForDone(request, "ParameterHints");
                }
            });

            if (expectedParams === null) {
                expect(requestHints).toBe(null);
            } else {
                expectHint(requestHints);
            }
        }

        function setupTest(path, primePump) { // FIXME: primePump argument ignored even though used below
            DocumentManager.getDocumentForPath(path).done(function (doc) {
                testDoc = doc;
            });

            waitsFor(function () {
                return testDoc !== null;
            }, "Unable to open test document", 10000);

            // create Editor instance (containing a CodeMirror instance)
            runs(function () {
                testEditor = SpecRunnerUtils.createMockEditorForDocument(testDoc);
                preTestText = testDoc.getText();
                waitsForDone(ScopeManager._readyPromise());
                waitsForDone(ScopeManager._maybeReset(JSCodeHints.getSession(), testDoc, true));
            });
        }

        function tearDownTest() {
            // Restore the pre-test version of the text here because the hinter
            // will update the contents of the previous document in tern.
            testDoc.setText(preTestText);

            // The following call ensures that the document is reloaded
            // from disk before each test
            MainViewManager._closeAll(MainViewManager.ALL_PANES);
            SpecRunnerUtils.destroyMockEditor(testDoc);
            testEditor = null;
            testDoc = null;
        }

        describe("JavaScript Code Hinting Basic", function () {
            beforeFirst(function () {
                brackets._configureJSCodeHints({
                    noReset: true
                });
            });

            afterLast(function () {
                brackets._configureJSCodeHints({
                    noReset: false
                });
            });

            beforeEach(function () {
                setupTest(testPath, false);
            });

            afterEach(function () {
                tearDownTest();
            });

            it("should not list hints in string literal", function () {
                testEditor.setCursorPos({ line: 20, ch: 22 });
                expectNoHints(JSCodeHints.jsHintProvider);
            });

            it("should list declared variable and function names in outer scope", function () {
                testEditor.setCursorPos({ line: 6, ch: 0 });
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                hintsPresent(hintObj, ["A2", "A3", "funB", "A1"]);
            });

            it("should filter hints by query", function () {
                testEditor.setCursorPos({ line: 5, ch: 10 });
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                hintsPresent(hintObj, ["A1", "A2", "A3"]);
                hintsAbsent(hintObj, ["funB"]);
            });

            it("should list keywords", function () {
                testEditor.setCursorPos({ line: 6, ch: 0 });
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                hintsPresent(hintObj, ["break", "case", "catch"]);
            });

            xit("should list explicitly defined globals from JSLint annotations", function () {
                testEditor.setCursorPos({ line: 6, ch: 0 });
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                hintsPresent(hintObj, ["brackets", "$"]);
            });

            xit("should list implicitly defined globals from JSLint annotations", function () {
                testEditor.setCursorPos({ line: 6, ch: 0 });
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                hintsPresent(hintObj, ["alert", "console", "confirm", "navigator", "window", "frames"]);
            });

            it("should NOT list implicitly defined globals from missing JSLint annotations", function () {
                testEditor.setCursorPos({ line: 6, ch: 0 });
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                hintsAbsent(hintObj, ["ActiveXObject", "CScript", "VBArray"]);
            });

            it("should NOT list explicitly defined globals from JSLint annotations in other files", function () {
                testEditor.setCursorPos({ line: 6, ch: 0 });
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                hintsAbsent(hintObj, ["crazyGlobal", "anotherCrazyGlobal"]);
            });

            it("should NOT list implicitly defined globals from JSLint annotations in other files", function () {
                testEditor.setCursorPos({ line: 6, ch: 0 });
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                hintsAbsent(hintObj, ["spawn", "version", "toint32"]);
            });

            it("should list literal constants", function () {
                testEditor.setCursorPos({ line: 6, ch: 0 });
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                hintsPresent(hintObj, ["null", "undefined", "true", "false"]);
            });

            it("should NOT list variables, function names and parameter names out of scope", function () {
                testEditor.setCursorPos({ line: 6, ch: 0 });
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                hintsAbsent(hintObj, ["paramB2", "paramB1"]);
            });

            it("should NOT list variables, function names and parameter names in other files", function () {
                testEditor.setCursorPos({ line: 6, ch: 0 });
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                hintsAbsent(hintObj, ["E1", "E2"]);
            });

            it("should NOT list property names on value lookups", function () {
                testEditor.setCursorPos({ line: 6, ch: 0 });
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                hintsAbsent(hintObj, ["propA", "propB", "propC"]);
            });

            it("should list declared variable, function and parameter names in inner scope", function () {
                testEditor.setCursorPos({ line: 12, ch: 0 });
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                hintsPresent(hintObj, ["B1", "B2", "funC", "paramB1", "paramB2", "funB", "A1", "A2", "A3"]);
            });

            xit("should list string literals that occur in the file", function () {
                testEditor.setCursorPos({ line: 12, ch: 0 });
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                hintsPresent(hintObj, ["use strict"]);
            });

            it("should NOT list string literals from other files", function () {
                testEditor.setCursorPos({ line: 6, ch: 0 });
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                hintsAbsent(hintObj, ["a very nice string"]);
            });

            it("should list property names that have been declared in the file", function () {
                testEditor.setCursorPos({ line: 17, ch: 11 });
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                hintsPresent(hintObj, ["propB"]);
            });

            it("should list identifier names that occur in other files", function () {
                testEditor.setCursorPos({ line: 16, ch: 0 });
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                hintsPresent(hintObj, ["D1", "D2"]);
            });

            it("should NOT list variable, parameter or function names on property lookups", function () {
                testEditor.setCursorPos({ line: 17, ch: 11 });
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                hintsAbsent(hintObj, ["A1", "A2", "funB", "paramB1", "paramB2", "B1", "B2", "funC", "paramC1", "paramC2"]);
            });

            it("should NOT list keywords on property lookups", function () {
                testEditor.setCursorPos({ line: 17, ch: 11 });
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                hintsAbsent(hintObj, ["case", "function", "var"]);
            });

            it("should close hints when move over '.' ", function () {
                testEditor.setCursorPos({ line: 17, ch: 11 });
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                expectCloseHints(JSCodeHints.jsHintProvider, hintObj,
                    { line: 17, ch: 10 }, true);
            });

            it("should close hints only when move off the end of a property ", function () {
                testEditor.setCursorPos({ line: 17, ch: 11 });
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                expectCloseHints(JSCodeHints.jsHintProvider, hintObj,
                    { line: 17, ch: 12 }, false);
                expectCloseHints(JSCodeHints.jsHintProvider, hintObj,
                    { line: 17, ch: 13 }, false);
                expectCloseHints(JSCodeHints.jsHintProvider, hintObj,
                    { line: 17, ch: 14 }, false);
                expectCloseHints(JSCodeHints.jsHintProvider, hintObj,
                    { line: 17, ch: 15 }, false);
                expectCloseHints(JSCodeHints.jsHintProvider, hintObj,
                    { line: 17, ch: 16 }, false);
                expectCloseHints(JSCodeHints.jsHintProvider, hintObj,
                    { line: 17, ch: 17 }, true);
            });

            it("should close hints only when move off the beginning of an identifier ", function () {
                testEditor.setCursorPos({ line: 17, ch: 10 });
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                expectCloseHints(JSCodeHints.jsHintProvider, hintObj,
                    { line: 17, ch: 9 }, false);
                expectCloseHints(JSCodeHints.jsHintProvider, hintObj,
                    { line: 17, ch: 8 }, false);
                expectCloseHints(JSCodeHints.jsHintProvider, hintObj,
                    { line: 17, ch: 7 }, true);
            });

            it("should close hints only when move off the beginning of a keyword ", function () {
                testEditor.setCursorPos({ line: 24, ch: 7 });
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                hintsPresent(hintObj, ["var"]);
                expectCloseHints(JSCodeHints.jsHintProvider, hintObj,
                    { line: 24, ch: 6 }, false);
                expectCloseHints(JSCodeHints.jsHintProvider, hintObj,
                    { line: 24, ch: 5 }, false);
                expectCloseHints(JSCodeHints.jsHintProvider, hintObj,
                    { line: 24, ch: 4 }, false);
                expectCloseHints(JSCodeHints.jsHintProvider, hintObj,
                    { line: 24, ch: 3 }, true);
            });

            it("should NOT list implicit hints on left-brace", function () {
                testEditor.setCursorPos({ line: 6, ch: 0 });
                expectNoHints(JSCodeHints.jsHintProvider, "{");
            });

            it("should list explicit hints for variable and function names", function () {
                testEditor.setCursorPos({ line: 6, ch: 0 });
                var hintObj = expectHints(JSCodeHints.jsHintProvider, null);
                hintsPresent(hintObj, ["A2", "A3", "funB", "A1"]);
            });

            it("should list implicit hints when typing property lookups", function () {
                testEditor.setCursorPos({ line: 17, ch: 10 });
                var hintObj = expectHints(JSCodeHints.jsHintProvider, ".");
                hintsPresent(hintObj, ["B1", "paramB1"]);
            });

            it("should give priority to identifier names associated with the current context", function () {
                testEditor.setCursorPos({ line: 16, ch: 0 });
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                hintsPresentOrdered(hintObj, ["C1", "B1"]);
                hintsPresentOrdered(hintObj, ["C2", "B2"]);
            });

            it("should give priority to property names associated with the current context from other files", function () {
                testEditor.setCursorPos({ line: 16, ch: 0 });
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                hintsPresentOrdered(hintObj, ["C1", "D1"]);
                hintsPresentOrdered(hintObj, ["B1", "D1"]);
                hintsPresentOrdered(hintObj, ["A1", "D1"]);
                hintsPresentOrdered(hintObj, ["funB", "funE"]);
            });

            xit("should choose the correct delimiter for string literal hints with no query", function () {
                var start = { line: 18, ch: 0 },
                    end   = { line: 18, ch: 18 };

                testEditor.setCursorPos(start);
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                selectHint(JSCodeHints.jsHintProvider, hintObj, 13); // hint 13 is "hello\\\"world!"
                runs(function () {
                    expect(testEditor.getCursorPos()).toEqual(end);
                    expect(testDoc.getRange(start, end)).toEqual('"hello\\\\\\" world!"');
                });
            });

            it("should insert value hints with no current query", function () {
                var start = { line: 6, ch: 0 },
                    end   = { line: 6, ch: 2 };

                testEditor.setCursorPos(start);
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                selectHint(JSCodeHints.jsHintProvider, hintObj, "A2");
                runs(function () {
                    //expect(testEditor.getCursorPos()).toEqual(end);
                    expect(testDoc.getRange(start, end)).toEqual("A2");
                });
            });

            it("should insert value hints replacing the current query", function () {
                var start   = { line: 5, ch: 10 }, // A3 = A<here>2;
                    before  = { line: 5, ch: 9 },
                    end     = { line: 5, ch: 11 };

                testEditor.setCursorPos(start);
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                hintsPresent(hintObj, ["A1", "A2", "A3"]);
                selectHint(JSCodeHints.jsHintProvider, hintObj, "A1");
                runs(function () {
                    //expect(testEditor.getCursorPos()).toEqual(end);
                    expect(testDoc.getRange(before, end)).toEqual("A1");
                });
            });

            it("should insert property hints with no current query", function () {
                var start   = { line: 6, ch: 0 },
                    middle  = { line: 6, ch: 3 },
                    end     = { line: 6, ch: 8 };

                testDoc.replaceRange("A1.", start, start);
                testEditor.setCursorPos(middle);
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                selectHint(JSCodeHints.jsHintProvider, hintObj, "propA");

                runs(function () {
                    expect(fixPos(testEditor.getCursorPos())).toEqual(fixPos(end));
                    expect(testDoc.getRange(start, end)).toEqual("A1.propA");
                    expect(testDoc.getLine(end.line).length).toEqual(8);
                });
            });

            it("should insert, not replace, property hints with no current query", function () {
                var start   = { line: 6, ch: 0 },
                    middle  = { line: 6, ch: 3 },
                    end     = { line: 6, ch: 8 },
                    endplus = { line: 6, ch: 12 };

                testDoc.replaceRange("A1.prop", start, start);
                testEditor.setCursorPos(middle);
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                selectHint(JSCodeHints.jsHintProvider, hintObj, "propA");

                runs(function () {
                    expect(fixPos(testEditor.getCursorPos())).toEqual(fixPos(end));
                    expect(testDoc.getRange(start, endplus)).toEqual("A1.propAprop");
                    expect(testDoc.getLine(end.line).length).toEqual(12);
                });
            });

            it("should insert, not replace, property hints with a partial current query", function () {
                var start   = { line: 6, ch: 0 },
                    middle  = { line: 6, ch: 6 },
                    end     = { line: 6, ch: 8 };

                testDoc.replaceRange("A1.pro", start, start);
                testEditor.setCursorPos(middle);
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                selectHint(JSCodeHints.jsHintProvider, hintObj, "propA");
                runs(function () {
                    expect(fixPos(testEditor.getCursorPos())).toEqual(fixPos(end));
                    expect(testDoc.getRange(start, end)).toEqual("A1.propA");
                    expect(testDoc.getLine(end.line).length).toEqual(8);
                });
            });

            it("should replace property hints replacing a partial current query", function () {
                var start   = { line: 6, ch: 0 },
                    middle  = { line: 6, ch: 6 },
                    end     = { line: 6, ch: 8 },
                    endplus = { line: 6, ch: 10 };

                testDoc.replaceRange("A1.propB", start, start);
                testEditor.setCursorPos(middle);
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                selectHint(JSCodeHints.jsHintProvider, hintObj, "propA");
                runs(function () {
                    expect(fixPos(testEditor.getCursorPos())).toEqual(fixPos(end));
                    expect(testDoc.getRange(start, endplus)).toEqual("A1.propApB");
                    expect(testDoc.getLine(end.line).length).toEqual(10);
                });
            });

            it("should replace property hints but not following delimiters", function () {
                var start   = { line: 6, ch: 0 },
                    middle  = { line: 6, ch: 4 },
                    end     = { line: 6, ch: 9 },
                    endplus = { line: 6, ch: 14 };

                testDoc.replaceRange("(A1.prop)", start, start);
                testEditor.setCursorPos(middle);
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                selectHint(JSCodeHints.jsHintProvider, hintObj, "propA");

                runs(function () {
                    expect(fixPos(testEditor.getCursorPos())).toEqual(fixPos(end));
                    expect(testDoc.getRange(start, endplus)).toEqual("(A1.propAprop)");
                    expect(testDoc.getLine(endplus.line).length).toEqual(14);
                });
            });

            it("should list hints for string, as string assigned to 's', 's' assigned to 'r' and 'r' assigned to 't'", function () {
                var start = { line: 26, ch: 0 },
                    middle = { line: 26, ch: 6 };

                // pad spaces here as tern has issue,without space, no code hint
                testDoc.replaceRange("    t.", start);
                testEditor.setCursorPos(middle);
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                runs(function () {
                    hintsPresentOrdered(hintObj, ["charAt", "charCodeAt", "concat", "indexOf"]);
                });
            });

            it("should list function type", function () {
                var start = { line: 37, ch: 0 },
                    middle = { line: 37, ch: 5 };

                testDoc.replaceRange("funD(", start, start);
                testEditor.setCursorPos(middle);
                runs(function () {
                    expectParameterHint([{name: "a", type: "String"},
                        {name: "b", type: "Number"}], 0);
                });
            });

            it("should list exports from a requirejs module", function () {
                var start = { line: 40, ch: 21 };

                testEditor.setCursorPos(start);
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                runs(function () {
                    hintsPresentExact(hintObj, ["a", "b", "c", "j"]);
                });
            });

            it("should list later defined property names", function () {
                var start = { line: 17, ch: 11 };

                testEditor.setCursorPos(start);
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                runs(function () {
                    hintsPresentExact(hintObj, ["foo", "propB"]);
                });
            });

            it("should list matching property names", function () {
                var cursor1 = { line: 12, ch: 0 },
                    cursor2 = { line: 12, ch: 6 };

                testDoc.replaceRange("paramB", cursor1, cursor1);
                testEditor.setCursorPos(cursor2);
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                runs(function () {
                    hintsPresentExact(hintObj, ["paramB1", "paramB2"]);
                });
            });

            it("should take anotation parameter type:String", function () {
                var start = { line: 37, ch: 21 };

                testDoc.replaceRange("var k= funD(10,11).x.", start, start);
                testEditor.setCursorPos(start);
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                runs(function () {
                    hintsPresentOrdered(hintObj, ["charAt", "charCodeAt", "concat", "indexOf"]);
                });
            });

            it("should take anotation parameter type:Number", function () {
                var start = { line: 37, ch: 21 };

                testDoc.replaceRange("var k= funD(10,11).y.", start, start);
                testEditor.setCursorPos(start);
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                runs(function () {
                    hintsPresentOrdered(hintObj, ["toExponential", "toFixed", "toString"]);
                });
            });

            it("should add new method on String .prototype", function () {
                var start = { line: 37, ch: 0 };
                var testPos = { line: 40, ch: 12 };
                testDoc.replaceRange("String.prototype.times = function (count) {\n" + "\treturn count < 1 ? '' : new Array[count + 1].join(this);\n};\n\"hello\".tim", start, start);
                testEditor.setCursorPos(testPos);
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                runs(function () {
                    hintsPresentOrdered(hintObj, ["times", "trim"]);
                });
            });

            it("should list function defined from .prototype", function () {
                var start = { line: 59, ch: 5 };

                testEditor.setCursorPos(start);
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                runs(function () {
                    hintsPresentExact(hintObj, ["calc"]);
                });
            });

            it("should list function type defined from .prototype", function () {
                var start = { line: 59, ch: 10 };
                testEditor.setCursorPos(start);
                runs(function () {
                    expectParameterHint([{name: "a4", type: "Number"}, {name: "b4", type: "Number"}], 0);
                });
            });

            it("should list function inherited from super class", function () {
                var start = { line: 79, ch: 11 };
                testEditor.setCursorPos(start);
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                runs(function () {
                    hintsPresentExact(hintObj, ["amountDue", "getAmountDue", "getName", "name", "setAmountDue"]);
                });
            });

            it("should show argument from from .prototype.Method", function () {
                var start = { line: 80, ch: 0 },
                    testPos = { line: 80, ch: 24 };

                testDoc.replaceRange("myCustomer.setAmountDue(", start);
                testEditor.setCursorPos(testPos);
                runs(function () {
                    expectParameterHint([{name: "amountDue", type: "Object"}], 0);
                });
            });

            it("should show inner function type", function () {
                var testPos = { line: 96, ch: 23 };

                testEditor.setCursorPos(testPos);
                runs(function () {
                    expectParameterHint([{name: "arg", type: "String"}], 0);
                });
            });

            it("should show type for inner function returned function", function () {
                var testPos = { line: 96, ch: 33 };

                testEditor.setCursorPos(testPos);
                expectHints(JSCodeHints.jsHintProvider);
                runs(function () {
                    expectParameterHint([], 0);
                });

            });

            // parameter type anotation tests, due to another bug #3670: first argument has ?
            xit("should list parameter Date,boolean type", function () {
                var start = { line: 109, ch: 0 },
                    testPos = { line: 109, ch: 11 };

                testDoc.replaceRange("funTypeAn1(", start);
                testEditor.setCursorPos(testPos);
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                runs(function () {
                    hintsPresentExact(hintObj, ["funTypeAn1((a: bool, b: Date) -> {x, y}"]);
                });
            });

            // parameter type anotation tests, due to another bug #3670: first argument has ?
            xit("should list parameter function type and best guess for its argument/return types", function () {
                var testPos = { line: 123, ch: 11 };

                testEditor.setCursorPos(testPos);
                runs(function () {
                    expectParameterHint([{name: "f", type: "function(): number"}], 0);
                });
            });

            // parameter type annotation tests
            it("should list parameter function type and best guess for function call/return types", function () {
                var testPos = { line: 139, ch: 12 };

                testEditor.setCursorPos(testPos);
                runs(function () {
                    expectParameterHint([{name: "f", type: "function(String, Number):String"}], 0);
                });
            });

            it("should list array containing functions", function () {
                var testPos = { line: 142, ch: 7 };

                testEditor.setCursorPos(testPos);
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                runs(function () {
                    hintsPresent(hintObj, ["index1", "index2"]);
                });
            });

            it("should list function reference", function () {
                var start = { line: 144, ch: 0 },
                    testPos = { line: 144, ch: 14 };

                testDoc.replaceRange("funArr.index1(", start);
                testEditor.setCursorPos(testPos);
                runs(function () {
                    expectParameterHint([], 0);
                });
            });

            it("should insert hint as [\"my-key\"] since 'my-key' is not a valid property name", function () {
                var start = { line: 49, ch: 0 },
                    middle = { line: 49, ch: 5 },
                    end = { line: 49, ch: 13 };

                testDoc.replaceRange("arr.m", start, start);
                testEditor.setCursorPos(middle);
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                selectHint(JSCodeHints.jsHintProvider, hintObj, "my-key");
                runs(function () {
                    expect(fixPos(testEditor.getCursorPos())).toEqual(fixPos(end));
                    expect(testDoc.getRange(start, end)).toEqual("arr[\"my-key\"]");
                    expect(testDoc.getLine(end.line).length).toEqual(13);
                });
            });

            it("should insert hint as [\"my-key\"] make sure this works if nothing is typed after the '.'", function () {
                var start = { line: 49, ch: 0 },
                    middle = { line: 49, ch: 4 },
                    end = { line: 49, ch: 13 };

                testDoc.replaceRange("arr.", start, start);
                testEditor.setCursorPos(middle);
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                selectHint(JSCodeHints.jsHintProvider, hintObj, "my-key");
                runs(function () {
                    expect(fixPos(testEditor.getCursorPos())).toEqual(fixPos(end));
                    expect(testDoc.getRange(start, end)).toEqual("arr[\"my-key\"]");
                    expect(testDoc.getLine(end.line).length).toEqual(13);
                });
            });

            it("should insert hint as '.for' since keywords can be used as property names", function () {
                var start = { line: 49, ch: 0 },
                    middle = { line: 49, ch: 5 },
                    end = { line: 49, ch: 7 };

                testDoc.replaceRange("arr.f", start, start);
                testEditor.setCursorPos(middle);
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                selectHint(JSCodeHints.jsHintProvider, hintObj, "for");
                runs(function () {
                    expect(fixPos(testEditor.getCursorPos())).toEqual(fixPos(end));
                    expect(testDoc.getRange(start, end)).toEqual("arr.for");
                    expect(testDoc.getLine(end.line).length).toEqual(7);
                });
            });

            it("should jump to function", function () {
                var start = { line: 43, ch: 0 };

                testEditor.setCursorPos(start);
                runs(function () {
                    editorJumped({line: 7, ch: 13});
                });
            });

            it("should jump to var", function () {
                var start = { line: 44, ch: 10 };

                testEditor.setCursorPos(start);
                runs(function () {
                    editorJumped({line: 3, ch: 6});
                });
            });

            it("should jump to closure, early defined var", function () {
                var start = { line: 17, ch: 9 };

                testEditor.setCursorPos(start);
                runs(function () {
                    editorJumped({line: 10, ch: 10});
                });
            });

            it("should jump to the definition in new module file", function () {
                var start = { line: 40, ch: 22 };

                testEditor.setCursorPos(start);
                runs(function () {
                    editorJumped({line: 4, ch: 13, file: "MyModule.js"}); //jump to another file
                });
            });

            it("should jump to the method definition in .prototype", function () {
                var start = { line: 59, ch: 8 };

                testEditor.setCursorPos(start);
                runs(function () {
                    editorJumped({line: 53, ch: 21}); //jump to prototype.calc
                });
            });

            it("should jump to parameter passed in the method", function () {
                var start = { line: 63, ch: 20 };

                testEditor.setCursorPos(start);
                runs(function () {
                    editorJumped({line: 61, ch: 27});
                });
            });

            it("should jump to parameter passed in anonymous method", function () {
                var start = { line: 83, ch: 25 };

                testEditor.setCursorPos(start);
                runs(function () {
                    editorJumped({line: 81, ch: 53});
                });
            });

            it("should jump to inner method", function () {
                var start = { line: 96, ch: 32 };

                testEditor.setCursorPos(start);
                runs(function () {
                    editorJumped({line: 94, ch: 17});
                });
            });

            it("should jump to the actual function definition, and not the exports line", function () {
                var start = { line: 159, ch: 22 };

                testEditor.setCursorPos(start);
                runs(function () {
                    editorJumped({line: 11, ch: 14, file: "MyModule.js"}); //jump to another file
                });
            });

            it("should not hint function, variable, or param decls", function () {
                var func = { line: 7, ch: 12 },
                    param = { line: 7, ch: 18 },
                    variable = { line: 10, ch: 10 };

                runs(function () {
                    testEditor.setCursorPos(func);
                    expectNoParameterHint();
                    testEditor.setCursorPos(param);
                    expectNoHints(JSCodeHints.jsHintProvider);
                    testEditor.setCursorPos(variable);
                    expectNoHints(JSCodeHints.jsHintProvider);
                });
            });

            it("should sort underscore names to the bottom", function () {
                testEditor.setCursorPos({ line: 146, ch: 0 });
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                hintsPresentOrdered(hintObj, ["A1", "A2", "A3", "funB", "_A1"]);
            });

            it("should list all properties for unknown type", function () {
                var start = { line: 149, ch: 0 },
                    end   = { line: 149, ch: 5 };

                testDoc.replaceRange("help.", start, start);
                testEditor.setCursorPos(end);
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                // check we have a properties from "Function", "String", and "Array"
                hintsPresentOrdered(hintObj, ["apply", "charCodeAt", "concat"]);
            });

            it("should switch to guesses after typing a query that does not match any hints", function () {
                var start = { line: 150, ch: 0 },
                    end   = { line: 150, ch: 5 };

                testDoc.replaceRange("s.shift", start, start);
                testEditor.setCursorPos(end);
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                // check we have a properties that start with "shift"
                hintsPresentOrdered(hintObj, ["shift", "shiftKey"]);
            });

            it("should handle valid non-ascii characters in a property name", function () {
                var start = { line: 153, ch: 0 },
                    end   = { line: 153, ch: 13 };

                testDoc.replaceRange("hope.frenchçP", start, start);
                testEditor.setCursorPos(end);
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                // check we have a properties that start with "shift"
                hintsPresentOrdered(hintObj, ["frenchçProp"]);
            });

            it("should show guessed argument type from current passing parameter", function () {
                var start = { line: 80, ch: 0 },
                    testPos = { line: 80, ch: 24 };
                testDoc.replaceRange("myCustomer.setAmountDue(10)", start);
                testEditor.setCursorPos(testPos);
                runs(function () {
                    expectParameterHint([{name: "amountDue", type: "Number"}], 0);
                });
            });

            it("should list parameter hint for record type annotation", function () {
                var testPos = { line: 178, ch: 25 };

                testEditor.setCursorPos(testPos);
                runs(function () {
                    expectParameterHint([{name: "t", type: "{index: Number, name: String}"}], -1);
                });
            });

            it("should list parameter hint for optional parameters", function () {
                var testPos = { line: 214, ch: 17 };

                testEditor.setCursorPos(testPos);
                runs(function () {
                    expectParameterHint([{name: "a", type: "Number", isOptional: true}, {name: "b", type: "String", isOptional: true}], 0);
                });
            });

            it("should list parameter hint for a function parameter", function () {
                var testPos = { line: 181, ch: 12 };

                testEditor.setCursorPos(testPos);
                runs(function () {
                    expectParameterHint([{name: "compare",
                        type: "function(Object, Object):Number",
                        isOptional: true}], -1);
                });
            });

            it("should list parameter hint for an array parameter", function () {
                var testPos = { line: 184, ch: 12 };
                testEditor.setCursorPos(testPos);
                runs(function () {
                    expectParameterHint([{name: "other", type: "Array.<Object>"}], -1);
                });
            });

            it("should list parameter hint for a source array annotation", function () {
                var testPos = { line: 200, ch: 20 };
                testEditor.setCursorPos(testPos);
                runs(function () {
                    expectParameterHint([{name: "a", type: "Array.<String>"}], 0);
                });
            });

            it("should close parameter hint when move off function", function () {
                var testPos = { line: 184, ch: 12 },
                    endPos  = { line: 184, ch: 19 };
                testEditor.setCursorPos(testPos);
                runs(function () {
                    expectParameterHint([{name: "other", type: "Array.<Object>"}], -1);
                });

                runs(function () {
                    testEditor.setCursorPos(endPos);
                    expectParameterHintClosed();
                });
            });

            it("should close parameter hint when move off function to another function", function () {
                var testPos = { line: 184, ch: 12 },
                    newPos  = { line: 181, ch: 12 };
                testEditor.setCursorPos(testPos);
                runs(function () {
                    expectParameterHint([{name: "other", type: "Array.<Object>"}], -1);
                });

                runs(function () {
                    testEditor.setCursorPos(newPos);
                    expectParameterHintClosed();
                });
            });

            it("should update current parameter as the cursor moves", function () {
                var testPos = { line: 186, ch: 19 },
                    newPos  = { line: 186, ch: 20 };
                testEditor.setCursorPos(testPos);
                runs(function () {
                    expectParameterHint([{name: "char", type: "String"},
                        {name: "from", type: "Number", isOptional: true}], 0);
                });

                runs(function () {
                    testEditor.setCursorPos(newPos);
                    expectParameterHint([{name: "char", type: "String"},
                        {name: "from", type: "Number", isOptional: true}], 1);
                });
            });

            // Test `jscodehints.noHintsOnDot` preference
            it("should consider dot a hintable key based on preference", function () {
                var noHintsOnDot = PreferencesManager.get("jscodehints.noHintsOnDot");

                testEditor.setCursorPos({ line: 44, ch: 10 });

                // Default is falsey
                expect(noHintsOnDot).toBeFalsy();

                // Should get hints after dot
                expectHints(JSCodeHints.jsHintProvider, ".");

                // Set preference to true
                PreferencesManager.set("jscodehints.noHintsOnDot", true);

                // Should no longer get hints after dot
                expectNoHints(JSCodeHints.jsHintProvider, ".");

                // Set preference back to original value (converted to boolean)
                PreferencesManager.set("jscodehints.noHintsOnDot", !!noHintsOnDot);
            });
        });

        describe("JavaScript Code Hinting in a HTML file", function () {

            beforeEach(function () {
                setupTest(testHtmlPath, false);
            });

            afterEach(function () {
                tearDownTest();
            });

            it("basic codehints in html file", function () {
                var start = { line: 37, ch: 9 },
                    end   = { line: 37, ch: 13};

                testDoc.replaceRange("x100.", start);
                testEditor.setCursorPos(end);
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                runs(function () {
                    hintsPresentOrdered(hintObj, ["charAt", "charCodeAt", "concat", "indexOf"]);
                });
            });

            it("function type hint in html file", function () {
                var start = { line: 36, ch: 12 };

                testEditor.setCursorPos(start);
                runs(function () {
                    expectParameterHint([{name: "a", type: "Number"}], 0);
                });
            });

            it("should show function type code hint for function in script file inside html file", function () {
                var start = { line: 22, ch: 17 };

                testEditor.setCursorPos(start);
                runs(function () {
                    expectParameterHint([{name: "a", type: "String"}, {name: "b", type: "Number"}], 0);
                });
            });

            it("should show function type code hint for function in another script file inside html file", function () {
                var start = { line: 23, ch: 17 };

                testEditor.setCursorPos(start);
                runs(function () {
                    expectParameterHint([{name: "paramE1", type: "D1"}, {name: "paramE2", type: "Number"}], 0);
                });
            });

            it("should show global variable in another script file inside html file", function () {
                var start        = { line: 27, ch: 8 },
                    end          = { line: 27, ch: 13},
                    testPosStart = { line: 27, ch: 11},
                    testPosEnd   = { line: 27, ch: 21};

                testDoc.replaceRange("arr.m", start);

                testEditor.setCursorPos(end);
                var hintObj = expectHints(JSCodeHints.jsHintProvider);
                runs(function () {
                    hintsPresent(hintObj, ["my-key"]);
                });
                selectHint(JSCodeHints.jsHintProvider, hintObj, "my-key");
                runs(function () {
                    expect(testDoc.getRange(testPosStart, testPosEnd)).toEqual("[\"my-key\"]");
                });
            });

            it("should jump to definition inside html file", function () {
                var start = { line: 36, ch: 10 };

                testEditor.setCursorPos(start);
                runs(function () {
                    editorJumped({line: 19, ch: 20});
                });
            });

            it("should jump to funtion definition to loaded file1", function () {
                var start = { line: 22, ch: 15 };

                testEditor.setCursorPos(start);
                runs(function () {
                    editorJumped({line: 33, ch: 13});
                });
            });

            it("should jump to funtion definition to loaded file2", function () {
                var start = { line: 23, ch: 15 };

                testEditor.setCursorPos(start);
                runs(function () {
                    editorJumped({line: 6, ch: 13});
                });
            });

            it("should jump to property definition to loaded file1", function () {
                var start = { line: 23, ch: 28 };

                testEditor.setCursorPos(start);
                runs(function () {
                    editorJumped({line: 4, ch: 16});
                });
            });

            it("should jump to property definition to loaded file2", function () {
                var start = { line: 23, ch: 18 };

                testEditor.setCursorPos(start);
                runs(function () {
                    editorJumped({line: 3, ch: 6});
                });
            });


        });

        describe("JavaScript Code Hinting without modules", function () {
            var testPath = extensionPath + "/unittest-files/non-module-test-files/app.js";
            ScopeManager.handleProjectOpen(extensionPath + "/unittest-files/non-module-test-files/");

            beforeEach(function () {
                setupTest(testPath, true);
            });

            afterEach(function () {
                tearDownTest();
            });

            // Test reading multiple files and subdirectories
            // Turned for per #7646
            xit("should handle reading all files when modules not used", function () {
                var start = { line: 8, ch: 8 };

                runs(function () {
                    testEditor.setCursorPos(start);
                    var hintObj = expectHints(JSCodeHints.jsHintProvider);
                    hintsPresentExact(hintObj, ["a", "b", "b1", "c", "d"]);
                });
            });
        });

        describe("JavaScript Code Hinting with modules", function () {
            var testPath = extensionPath + "/unittest-files/module-test-files/module_tests.js";

            beforeEach(function () {
                setupTest(testPath, true);
            });

            afterEach(function () {
                tearDownTest();
            });

            it("should read methods created in submodule on this", function () {
                var start = { line: 8, ch: 17 };

                runs(function () {
                    testEditor.setCursorPos(start);
                    var hintObj = expectHints(JSCodeHints.jsHintProvider);
                    hintsPresentExact(hintObj, ["addMessage", "name", "privilegedMethod", "publicMethod1"]);
                });
            });

            // bug: wait for fix in tern
            xit("should read methods created in submodule", function () {
                var start = { line: 19, ch: 15 };

                runs(function () {
                    testEditor.setCursorPos(start);
                    var hintObj = expectHints(JSCodeHints.jsHintProvider);
                    hintsPresentExact(hintObj, ["addMessage", "name", "privilegedMethod", "publicMethod1"]);
                });
            });

            it("should read properties created in parent module", function () {
                var start        = { line: 30, ch: 8 },
                    testPos          = { line: 30, ch: 15};

                testDoc.replaceRange("parent.", start);
                runs(function () {
                    testEditor.setCursorPos(testPos);
                    var hintObj = expectHints(JSCodeHints.jsHintProvider);
                    hintsPresentExact(hintObj, ["addMessage", "name", "privilegedMethod", "publicMethod1"]);
                });
            });

            // bug: wait for tern
            xit("should read methods created in submodule module", function () {
                var start        = { line: 62, ch: 0 },
                    testPos          = { line: 62, ch: 13};

                testDoc.replaceRange("SearchEngine.", start);
                runs(function () {
                    testEditor.setCursorPos(testPos);
                    var hintObj = expectHints(JSCodeHints.jsHintProvider);
                    hintsPresentExact(hintObj, ["getYourLuckyNumber", "subSearch"]);
                });
            });

            it("should read methods created in parent module", function () {
                var start        = { line: 78, ch: 41 };

                runs(function () {
                    testEditor.setCursorPos(start);
                    var hintObj = expectHints(JSCodeHints.jsHintProvider);
                    hintsPresentExact(hintObj, ["getYourLuckyNumber", "subSearch"]);
                });
            });

            it("should load module by file path from require", function () {
                var start        = { line: 88, ch: 20 };

                runs(function () {
                    testEditor.setCursorPos(start);
                    var hintObj = expectHints(JSCodeHints.jsHintProvider);
                    hintsPresentExact(hintObj, ["color", "material", "size"]);
                });
            });

            // tern bug: https://github.com/marijnh/tern/issues/147
            xit("should read properties from exported module", function () {
                var start        = { line: 96, ch: 0 },
                    testPos          = { line: 96, ch: 9};

                testDoc.replaceRange("hondaCar.", start);
                runs(function () {
                    testEditor.setCursorPos(testPos);
                    var hintObj = expectHints(JSCodeHints.jsHintProvider);
                    hintsPresentExact(hintObj, ["model", "name"]);
                });
            });

            // bug in test framework? can't run sequential jump, verification is wrong
            xit("should jump to a module, depending module", function () {
                var start        = { line: 93, ch: 25 },
                    testPos      = { line: 8, ch: 35 };

                testEditor.setCursorPos(start);
                runs(function () {
                    editorJumped({line: 5, ch: 23});
                });
                testEditor.setCursorPos(testPos);
                runs(function () {
                    editorJumped({line: 5, ch: 23});
                });
            });
        });

        describe("JavaScript Code Hinting preference tests", function () {
            var testPath = extensionPath + "/unittest-files/preference-test-files/",
                preferences;

            function getPreferences(path) {
                preferences = null;

                FileSystem.resolve(path, function (err, file) {
                    if (!err) {
                        FileUtils.readAsText(file).done(function (text) {
                            var configObj = null;
                            try {
                                configObj = JSON.parse(text);
                            } catch (e) {
                                // continue with null configObj
                                console.log(e);
                            }
                            preferences = new Preferences(configObj);
                        }).fail(function (error) {
                            preferences = new Preferences();
                        });
                    } else {
                        preferences = new Preferences();
                    }
                });
            }

            // Test preferences file with no entries. Preferences should contain
            // default values.
            it("should handle reading an empty configuration file", function () {
                getPreferences(testPath + "defaults-test/.jscodehints");
                waitsFor(function () {
                    return preferences !== null;
                });

                runs(function () {
                    expect(preferences.getExcludedDirectories()).toEqual(/node_modules/);
                    expect(preferences.getExcludedFiles().source).
                        toBe(/^require.*\.js$|^jquery.*\.js$/.source);
                    expect(preferences.getMaxFileCount()).toBe(100);
                    expect(preferences.getMaxFileSize()).toBe(512 * 1024);
                });
            });

            // Test preferences file with empty or out of ranges values. Preferences
            // should contain default values.
            it("should handle reading an invalid configuration file", function () {
                getPreferences(testPath + "negative-test/.jscodehints");
                waitsFor(function () {
                    return preferences !== null;
                });

                runs(function () {
                    expect(preferences.getExcludedDirectories()).toEqual(/node_modules/);
                    expect(preferences.getExcludedFiles().source).
                        toBe(/^require.*\.js$|^jquery.*\.js$/.source);
                    expect(preferences.getMaxFileCount()).toBe(100);
                    expect(preferences.getMaxFileSize()).toBe(512 * 1024);
                });
            });

            // Positive test. Test pattern matching.
            it("should handle a valid configuration file", function () {
                getPreferences(testPath + "positive-test/.jscodehints");
                waitsFor(function () {
                    return preferences !== null;
                });

                runs(function () {
                    var excludedDirs = preferences.getExcludedDirectories(),
                        excludedFiles = preferences.getExcludedFiles();

                    // test "excluded-dir1"
                    expect(excludedDirs.test("excluded-dir1")).toBeTruthy();
                    expect(excludedDirs.test("xexcluded-dir1")).toBeFalsy();

                    // test "excluded-dir2-*"
                    expect(excludedDirs.test("excluded-dir2-1")).toBeTruthy();
                    expect(excludedDirs.test("excluded-dir2-12")).toBeFalsy();
                    expect(excludedDirs.test("excluded-dir2-z")).toBeFalsy();
                    expect(excludedDirs.test("excluded-dir2-")).toBeFalsy();
                    expect(excludedDirs.test("xexcluded-dir2-1")).toBeFalsy();

                    // test "file1?.js"
                    expect(excludedFiles.test("file1.js")).toBeTruthy();
                    expect(excludedFiles.test("file12.js")).toBeTruthy();
                    expect(excludedFiles.test("file123.js")).toBeFalsy();

                    // test "file2*.js"
                    expect(excludedFiles.test("file2.js")).toBeTruthy();
                    expect(excludedFiles.test("file2xxx.js")).toBeTruthy();
                    expect(excludedFiles.test("filexxxx.js")).toBeFalsy();

                    // test "file3.js"
                    expect(excludedFiles.test("file3.js")).toBeTruthy();
                    expect(excludedFiles.test("xfile3.js")).toBeFalsy();

                    // test "/file4[x|y|z]?.js/"
                    expect(excludedFiles.test("file4.js")).toBeTruthy();
                    expect(excludedFiles.test("file4x.js")).toBeTruthy();
                    expect(excludedFiles.test("file4y.js")).toBeTruthy();
                    expect(excludedFiles.test("file4z.js")).toBeTruthy();
                    expect(excludedFiles.test("file4b.js")).toBeFalsy();
                    expect(excludedFiles.test("file4xyz.js")).toBeFalsy();
                    expect(excludedFiles.test("xfile4.js")).toBeTruthy();

                    // test builtin exclusions are also present
                    expect(excludedFiles.test("require.js")).toBeTruthy();
                    expect(excludedFiles.test("jquery.js")).toBeTruthy();

                    expect(preferences.getMaxFileCount()).toBe(512);
                    expect(preferences.getMaxFileSize()).toBe(100000);
                });
            });
        });

        describe("regression tests", function () {

            it("should return true for valid identifier, false for invalid one", function () {
                var identifierList = ["ᾩ", "ĦĔĽĻŎ", "〱〱〱〱", "जावास्क्रि",
                                      "KingGeorgeⅦ", "π", "ಠ_ಠ",
                                      "price_9̶9̶_89", "$_3423", "TRUE", "FALSE", "IV"];
                var invalidIdentifierList = [" break", "\tif", "\ntrade"];

                invalidIdentifierList.forEach(function (element) {
                    var result = HintUtils.maybeIdentifier(element);
                    expect(result).toBe(false);
                });

                identifierList.forEach(function (element) {
                    var result = HintUtils.maybeIdentifier(element);
                    expect(result).toBe(true);
                });
            });
        });

        describe("JavaScript Code Hinting with test.html file", function () {
            var testFile = extensionPath + "/unittest-files/basic-test-files/test.html";

            beforeEach(function () {
                setupTest(testFile, true);
            });

            afterEach(function () {
                tearDownTest();

            });

            // FIXME (issue #3915)
            xit("should read function name has double byte chars", function () {
                var start   = { line: 15, ch: 8 },
                    testPos = { line: 15, ch: 10 };

                runs(function () {
                    testEditor.setCursorPos(start);
                    var hintObj = expectHints(JSCodeHints.jsHintProvider);
                    hintsPresentExact(hintObj, ["fun測试"]);
                });
                runs(function () {
                    testEditor.setCursorPos(testPos);
                    var hintObj = expectHints(JSCodeHints.jsHintProvider);
                    hintsPresentExact(hintObj, ["fun測试()"]);
                });
            });

            it("should jump to function name with double byte chars", function () {
                var start        = { line: 16, ch: 9 };

                testEditor.setCursorPos(start);
                runs(function () {
                    editorJumped({line: 12, ch: 20});
                });
            });

            // FIXME (issue #3915)
            xit("should read function name has non ascii chars", function () {
                var start = { line: 16, ch: 16 };

                runs(function () {
                    testEditor.setCursorPos(start);
                    var hintObj = expectHints(JSCodeHints.jsHintProvider);
                    hintsPresentExact(hintObj, ["frenchçProp()"]);
                });
            });

            it("should jump to function name with non ascii chars", function () {
                var start        = { line: 16, ch: 12 };

                testEditor.setCursorPos(start);
                runs(function () {
                    editorJumped({line: 12, ch: 20});
                });
            });
        });

        describe("Code Hinting Regression", function () {
            var testFile = extensionPath + "/unittest-files/module-test-files/china/cupFiller.js";

            beforeEach(function () {
                setupTest(testFile, true);
            });

            afterEach(function () {
                tearDownTest();
            });

            // The test is disabled, because the TernWorker will consult the ProjectManager to
            // determine all the files in the project root. We don't have a project root for this
            // testcase. Perhaps we need to change the testsetup or find another way of dealing with this
            // Test makes sure that http://github.com/adobe/brackets/issue/6931 doesn't show up
            xit("should show hints for members of referenced class", function () {
                var start = { line: 8, ch: 15 };

                runs(function () {
                    testEditor.setCursorPos(start);
                    var hintObj = expectHints(JSCodeHints.jsHintProvider);
                    hintsPresentExact(hintObj, ["empty", "emptyIt", "fill", "full"]);
                });
            });
        });

        describe("JavaScript Code Hinting format parameters tests", function () {

            it("should format parameters with no params", function () {
                var params = [];

                expect(HintUtils2.formatParameterHint(params)).toBe("");
            });

            it("should format parameters with one param", function () {
                var params = [{name: "param1", type: "String"}];

                expect(HintUtils2.formatParameterHint(params)).toBe("String param1");
            });

            it("should format parameters with one optional param", function () {
                var params = [{name: "param1", type: "String", isOptional: true}];

                expect(HintUtils2.formatParameterHint(params)).toBe("[String param1]");
            });

            it("should format parameters with one required, one optional param", function () {
                var params = [{name: "param1", type: "String"},
                              {name: "param2", type: "String", isOptional: true}];

                expect(HintUtils2.formatParameterHint(params)).toBe("String param1, [String param2]");
            });

            it("should format parameters with required param following an optional param", function () {
                var params = [{name: "param1", type: "String"},
                    {name: "param2", type: "String", isOptional: true},
                    {name: "param3", type: "String"}];

                expect(HintUtils2.formatParameterHint(params)).toBe("String param1, [String param2, String param3]");
            });

            it("should format parameters with optional param following an optional param", function () {
                var params = [{name: "param1", type: "String"},
                    {name: "param2", type: "String", isOptional: true},
                    {name: "param3", type: "String", isOptional: true}];

                expect(HintUtils2.formatParameterHint(params)).toBe("String param1, [String param2], [String param3]");
            });

            it("should format parameters with optional param following optional and required params", function () {
                var params = [{name: "param1", type: "String"},
                    {name: "param2", type: "String", isOptional: true},
                    {name: "param3", type: "String"},
                    {name: "param4", type: "String", isOptional: true}];

                expect(HintUtils2.formatParameterHint(params)).toBe("String param1, [String param2, String param3], [String param4]");
            });

        });
    });
});
