/*
 * Copyright (c) 2012 - 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.
 *
 */

/*global describe, beforeEach, afterEach, it, runs, waitsForDone, expect, beforeFirst, afterLast */

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

    // Load dependent modules
    var SpecRunnerUtils = require("spec/SpecRunnerUtils"),
        KeyEvent        = require("utils/KeyEvent"),
        Commands        = require("command/Commands"),
        EditorManager,      // loaded from brackets.test
        CommandManager,
        CodeHintManager,
        KeyBindingManager;

    var testPath = SpecRunnerUtils.getTestPath("/spec/CodeHint-test-files"),
        testWindow;

    describe("CodeHintManager", function () {
        this.category = "integration";

        /**
         * Performs setup for a code hint test. Opens a file and set pos.
         *
         * @param {!string} openFile Project relative file path to open in a main editor.
         * @param {!number|Array} openPos The pos within openFile to place the IP, or an array
         *      representing a multiple selection to set.
         */
        function initCodeHintTest(openFile, openPos) {
            SpecRunnerUtils.loadProjectInTestWindow(testPath);

            runs(function () {
                var promise = SpecRunnerUtils.openProjectFiles([openFile]);
                waitsForDone(promise);
            });

            runs(function () {
                var editor = EditorManager.getCurrentFullEditor();
                if (Array.isArray(openPos)) {
                    editor.setSelections(openPos);
                } else {
                    editor.setCursorPos(openPos.line, openPos.ch);
                }
            });
        }

        beforeFirst(function () {
            SpecRunnerUtils.createTestWindowAndRun(this, function (w) {
                testWindow = w;

                // uncomment this line to debug test window:
                //testWindow.brackets.app.showDeveloperTools();

                // Load module instances from brackets.test
                CodeHintManager     = testWindow.brackets.test.CodeHintManager;
                EditorManager       = testWindow.brackets.test.EditorManager;
                CommandManager      = testWindow.brackets.test.CommandManager;
                KeyBindingManager   = testWindow.brackets.test.KeyBindingManager;
            });
        });

        afterLast(function () {
            testWindow          = null;
            CodeHintManager     = null;
            EditorManager       = null;
            CommandManager      = null;
            KeyBindingManager   = null;
            SpecRunnerUtils.closeTestWindow();
        });

        afterEach(function () {
            runs(function () {
                testWindow.closeAllFiles();
            });
        });

        function invokeCodeHints() {
            CommandManager.execute(Commands.SHOW_CODE_HINTS);
        }

        // Note: these don't request hint results - they only examine hints that might already be open
        function expectNoHints() {
            var codeHintList = CodeHintManager._getCodeHintList();
            expect(codeHintList).toBeFalsy();
        }

        function expectSomeHints() {
            var codeHintList = CodeHintManager._getCodeHintList();
            expect(codeHintList).toBeTruthy();
            expect(codeHintList.isOpen()).toBe(true);
            return codeHintList;
        }

        // TODO: There seems to be an issue with CodeHintManager._removeHintProvider because of which the tests
        // added for "Hint Provider Registration" are interfering with this test case
        // (mock provider added for new language shows up for RegExp, even though no code hint should show up)
        // I am unable to reproduce this in a real scenario though/
        // This is the reason this test case is being added before the registration test cases.
        // We need to either figure out whats going wrong with register or change the RegExp code hint fix to bail
        // before HintUtils (Something like the fix for no code hints for multiple selection)
        describe("RegExp codehint tests", function () {
            it("should not show codehints for regular expression in a script block in html", function () {
                var editor,
                    pos = {line: 8, ch: 30};

                // Place cursor inside a sample regular expression
                // Note: line for pos is 0-based and editor lines numbers are 1-based
                initCodeHintTest("test1.html", pos);

                runs(function () {
                    editor = EditorManager.getCurrentFullEditor();
                    expect(editor).toBeTruthy();
                    invokeCodeHints();
                    expectNoHints();
                    editor = null;
                });
            });

            it("should not show codehints for regular expression in a Javascript file", function () {
                var editor,
                    pos = {line: 2, ch: 30};

                // Place cursor inside a sample regular expression
                // Note: line for pos is 0-based and editor lines numbers are 1-based
                initCodeHintTest("testRegExp.js", pos);

                runs(function () {
                    editor = EditorManager.getCurrentFullEditor();
                    expect(editor).toBeTruthy();
                    invokeCodeHints();
                    expectNoHints();
                    editor = null;
                });
            });
        });

        describe("Hint Provider Registration", function () {
            beforeEach(function () {
                initCodeHintTest("test1.html", {line: 0, ch: 0});
            });

            var mockProvider = {
                hasHints: function (editor, implicitChar) {
                    return true;
                },
                getHints: function (implicitChar) {
                    return { hints: ["mock hint"], match: null, selectInitial: false };
                },
                insertHint: function (hint) { }
            };

            function expectMockHints() {
                var codeHintList = expectSomeHints();
                expect(codeHintList.hints[0]).toBe("mock hint");
                expect(codeHintList.hints.length).toBe(1);
            }

            it("should register provider for a new language", function () {
                runs(function () {
                    CodeHintManager.registerHintProvider(mockProvider, ["clojure"], 0);

                    // Ensure no hints in language we didn't register for
                    invokeCodeHints();
                    expectNoHints();

                    // Expect hints in language we did register for
                    var promise = CommandManager.execute(Commands.FILE_OPEN, { fullPath: SpecRunnerUtils.makeAbsolute("test.clj") });
                    waitsForDone(promise);
                });
                runs(function () {
                    invokeCodeHints();
                    expectMockHints();

                    CodeHintManager._removeHintProvider(mockProvider, ["clojure"], 0);
                });
            });

            it("should register higher-priority provider for existing language", function () {
                runs(function () {
                    CodeHintManager.registerHintProvider(mockProvider, ["html"], 1);

                    // Expect hints to replace default HTML hints
                    var editor = EditorManager.getCurrentFullEditor();
                    editor.setCursorPos(3, 1);
                    invokeCodeHints();
                    expectMockHints();

                    CodeHintManager._removeHintProvider(mockProvider, ["html"], 1);
                });
            });

            it("should register \"all\" languages provider", function () {
                runs(function () {
                    CodeHintManager.registerHintProvider(mockProvider, ["all"], 0);

                    // Expect hints in language that already had hints (when not colliding with original provider)
                    invokeCodeHints();
                    expectMockHints();

                    // Expect hints in language that had no hints before
                    var promise = CommandManager.execute(Commands.FILE_OPEN, { fullPath: SpecRunnerUtils.makeAbsolute("test.clj") });
                    waitsForDone(promise);
                });
                runs(function () {
                    invokeCodeHints();
                    expectMockHints();

                    CodeHintManager._removeHintProvider(mockProvider, ["all"], 0);
                });
            });
        });


        describe("HTML Tests", function () {

            it("should show code hints menu and insert text at IP", function () {
                var editor,
                    pos = {line: 3, ch: 1},
                    lineBefore,
                    lineAfter;

                // minimal markup with an open '<' before IP
                // Note: line for pos is 0-based and editor lines numbers are 1-based
                initCodeHintTest("test1.html", pos);

                runs(function () {
                    editor = EditorManager.getCurrentFullEditor();
                    expect(editor).toBeTruthy();

                    // get text before insert operation
                    lineBefore = editor.document.getLine(pos.line);

                    invokeCodeHints();
                    expectSomeHints();
                });

                // simulate Enter key to insert code hint into doc
                runs(function () {
                    var e = $.Event("keydown");
                    e.keyCode = KeyEvent.DOM_VK_RETURN;

                    editor = EditorManager.getCurrentFullEditor();
                    expect(editor).toBeTruthy();

                    CodeHintManager._getCodeHintList()._keydownHook(e);

                    // doesn't matter what was inserted, but line should be different
                    var newPos = editor.getCursorPos();
                    lineAfter = editor.document.getLine(newPos.line);
                    expect(lineBefore).not.toEqual(lineAfter);

                    // and popup should auto-close
                    expectNoHints();

                    editor = null;
                });
            });

            it("should go to next hint with ctrl+space", function () {
                var editor,
                    pos = {line: 3, ch: 1},
                    hintBefore,
                    hintAfter;

                // minimal markup with an open '<' before IP
                // Note: line for pos is 0-based and editor lines numbers are 1-based
                initCodeHintTest("test1.html", pos);

                // simulate ctrl+space key to make sure it goes to next hint
                runs(function () {
                    var e = $.Event("keydown");
                    e.keyCode = KeyEvent.DOM_VK_SPACE;
                    e.ctrlKey = true;

                    editor = EditorManager.getCurrentFullEditor();
                    expect(editor).toBeTruthy();

                    invokeCodeHints();
                    var codeHintList = expectSomeHints();
                    hintBefore = codeHintList.selectedIndex;

                    // make sure hint list starts at 0
                    expect(hintBefore).toEqual(0);

                    // simulate ctrl+space keyhook
                    CodeHintManager._getCodeHintList()._keydownHook(e);
                    hintAfter = codeHintList.selectedIndex;

                    // selectedIndex should be one more after doing ctrl+space key event.
                    expect(hintBefore).toEqual(hintAfter-1);

                    editor = null;
                });
            });

            it("should loop to first hint when ctrl+space at last hint", function () {
                var editor,
                    pos = {line: 3, ch: 1},
                    hintBefore,
                    hintAfter;

                // minimal markup with an open '<' before IP
                // Note: line for pos is 0-based and editor lines numbers are 1-based
                initCodeHintTest("test1.html", pos);

                // simulate ctrl+space key to make sure it goes to next hint
                runs(function () {
                    var e = $.Event("keydown");
                    e.keyCode = KeyEvent.DOM_VK_UP;

                    editor = EditorManager.getCurrentFullEditor();
                    expect(editor).toBeTruthy();

                    invokeCodeHints();

                    // simulate up keyhook to send it to last hint
                    CodeHintManager._getCodeHintList()._keydownHook(e);

                    var codeHintList = expectSomeHints();
                    hintBefore = codeHintList.selectedIndex;
                    var numberOfHints = codeHintList.$hintMenu.find("li").length-1;

                    // should be at last hint
                    expect(hintBefore).toEqual(numberOfHints);

                    // call ctrl+space to loop it to first hint
                    e.keyCode = KeyEvent.DOM_VK_SPACE;
                    e.ctrlKey = true;

                    // simulate ctrl+space keyhook to send it to first hint
                    CodeHintManager._getCodeHintList()._keydownHook(e);
                    hintAfter = codeHintList.selectedIndex;

                    // should now be at hint 0
                    expect(hintAfter).toEqual(0);

                    editor = null;
                });
            });

            it("should not show code hints if there is a multiple selection", function () {
                // minimal markup with an open '<' before IP
                // Note: line for pos is 0-based and editor lines numbers are 1-based
                initCodeHintTest("test1.html", [
                    {start: {line: 3, ch: 1}, end: {line: 3, ch: 1}, primary: true},
                    {start: {line: 4, ch: 1}, end: {line: 4, ch: 1}}
                ]);

                runs(function () {
                    invokeCodeHints();
                    expectNoHints();
                });
            });

            it("should dismiss existing code hints if selection changes to a multiple selection", function () {
                var editor;

                initCodeHintTest("test1.html", {line: 3, ch: 1});

                runs(function () {
                    editor = EditorManager.getCurrentFullEditor();
                    expect(editor).toBeTruthy();

                    invokeCodeHints();
                    expectSomeHints();
                });

                runs(function () {
                    editor.setSelections([
                        {start: {line: 3, ch: 1}, end: {line: 3, ch: 1}, primary: true},
                        {start: {line: 4, ch: 1}, end: {line: 4, ch: 1}}
                    ]);
                    expectNoHints();
                });
            });

            it("should dismiss code hints menu with Esc key", function () {
                var pos = {line: 3, ch: 1};

                // minimal markup with an open '<' before IP
                // Note: line for pos is 0-based and editor lines numbers are 1-based
                initCodeHintTest("test1.html", pos);

                runs(function () {
                    invokeCodeHints();

                    // verify list is open
                    expectSomeHints();
                });

                // simulate Esc key to dismiss code hints menu
                runs(function () {
                    var key = KeyEvent.DOM_VK_ESCAPE,
                        element = testWindow.$(".dropdown.open")[0];
                    SpecRunnerUtils.simulateKeyEvent(key, "keydown", element);

                    // verify list is no longer open
                    expectNoHints();
                });
            });

            it("should dismiss code hints menu when launching a command", function () {
                var editor,
                    pos = {line: 3, ch: 1};

                // minimal markup with an open '<' before IP
                // Note: line for pos is 0-based and editor lines numbers are 1-based
                initCodeHintTest("test1.html", pos);

                runs(function () {
                    editor = EditorManager.getCurrentFullEditor();
                    expect(editor).toBeTruthy();

                    editor.document.replaceRange("di", pos);
                    invokeCodeHints();

                    // verify list is open
                    expectSomeHints();
                });

                // Call Undo command to remove "di" and then verify no code hints
                runs(function () {
                    CommandManager.execute(Commands.EDIT_UNDO);

                    // verify list is no longer open
                    expectNoHints();

                    editor = null;
                });
            });

            it("should stop handling keydowns if closed by a click outside", function () {
                var editor,
                    pos = {line: 3, ch: 1};

                // minimal markup with an open '<' before IP
                // Note: line for pos is 0-based and editor lines numbers are 1-based
                initCodeHintTest("test1.html", pos);

                runs(function () {
                    editor = EditorManager.getCurrentFullEditor();
                    expect(editor).toBeTruthy();

                    editor.document.replaceRange("di", pos);
                    invokeCodeHints();

                    // verify list is open
                    expectSomeHints();

                    // get the document text and make sure it doesn't change if we
                    // click outside and then keydown
                    var text = editor.document.getText();

                    testWindow.$("body").click();
                    KeyBindingManager._handleKeyEvent({
                        keyCode: KeyEvent.DOM_VK_ENTER,
                        stopImmediatePropagation: function () { },
                        stopPropagation: function () { },
                        preventDefault: function () { }
                    });

                    // Verify that after the keydown, the session is closed
                    // (not just the hint popup). Because of #1381, we don't
                    // actually have a way to close the session as soon as the
                    // popup is dismissed by Bootstrap, so we do so on the next
                    // keydown. Eventually, once that's fixed, we should be able
                    // to move this expectNoHints() up after the click.
                    expectNoHints();
                    expect(editor.document.getText()).toEqual(text);

                    editor = null;
                });
            });
        });
    });
});
