/*
 * 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, it, xit, expect, beforeEach, afterEach */

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

    // Modules from the SpecRunner window
    var SpecRunnerUtils = brackets.getModule("spec/SpecRunnerUtils"),
        Editor          = brackets.getModule("editor/Editor").Editor,
        HTMLCodeHints   = require("main");

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

        var defaultContent = "<!doctype html>\n" +
                             "<html>\n" +
                             "<style type=\"text/css\">\n" +
                             "</style>\n" +
                             "<body>\n" +
                             "  <h1 id='foo'>Heading</h1>\n" +       // tag without whitespace
                             "  <h3 id  = 'bar' >Subheading</h3>\n" + // tag with whitespace
                             "  <p></p>\n" +                         // tag without attributes
                             "  <h5 id='aaa' class='bbb'></h5>\n" +  // tag with two attributes
                             "  <div \n" +                           // incomplete tag
                             "</body>\n" +
                             "</html>\n";

        var testDocument, testEditor;

        beforeEach(function () {
            // create dummy Document for the Editor
            testDocument = SpecRunnerUtils.createMockDocument(defaultContent, "html");

            // create Editor instance (containing a CodeMirror instance)
            $("body").append("<div id='editor'/>");
            testEditor = new Editor(testDocument, true, $("#editor").get(0));
        });

        afterEach(function () {
            testEditor.destroy();
            testEditor = null;
            $("#editor").remove();
            testDocument = null;
        });


        // Ask provider for hints at current cursor position; expect it to return some
        function expectHints(provider) {
            expect(provider.hasHints(testEditor, null)).toBe(true);
            var hintsObj = provider.getHints();
            expect(hintsObj).toBeTruthy();
            return hintsObj.hints; // return just the array of hints
        }

        // Ask provider for hints at current cursor position; expect it NOT to return any
        function expectNoHints(provider) {
            expect(provider.hasHints(testEditor, null)).toBe(false);
        }

        // Expect hintList to contain tag names, starting with given value (if unspecified, expects the default unfiltered list)
        function verifyTagHints(hintList, expectedFirstHint) {
            expect(hintList.indexOf("id")).toBe(-1);   // make sure attribute names aren't sneaking in there

            expectedFirstHint = expectedFirstHint || "a";  // assume unfiltered lists always start with "a"
            expect(hintList[0]).toBe(expectedFirstHint);
        }

        // Expect hintList to contain attribute names, starting with given value (if unspecified, expects the default unfilered list)
        function verifyAttrHints(hintList, expectedFirstHint) {
            expect(hintList.indexOf("div")).toBe(-1);   // make sure tag names aren't sneaking in there

            expectedFirstHint = expectedFirstHint || "accesskey";  // assume unfiltered lists always start with "accesskey"
            expect(hintList[0]).toBe(expectedFirstHint);
        }


        describe("Tag hint provider", function () {

            it("should not hint within <style> block", function () {  // (bug #1277)
                // Replace default test content with code containing a <style> block
                testDocument.setText("<!doctype html>\n" +
                                     "<html>\n" +
                                     "<head>\n" +
                                     "  <style>\n" +
                                     "  </style>\n" +
                                     "</head>\n" +
                                     "<body>\n" +
                                     "</body>\n" +
                                     "</html>\n");

                testEditor.setCursorPos({ line: 3, ch: 9 });        // cursor after the > in "<style>"
                expectNoHints(HTMLCodeHints.tagHintProvider);
            });

            it("should hint for < just before existing tag", function () {  // (bug #1260)
                testDocument.replaceRange("<", { line: 5, ch: 2 }); // insert text: "<h1" -> "<<h1"

                testEditor.setCursorPos({ line: 5, ch: 3 });        // cursor between the two <s
                var hintList = expectHints(HTMLCodeHints.tagHintProvider);
                verifyTagHints(hintList);
                expect(hintList.indexOf("div")).not.toBe(-1);  // additional sanity check
            });

            it("should filter hints by prefix", function () {  // (bug #1260)
                testDocument.replaceRange("  <s\n", { line: 8, ch: 0 }); // insert new line "<s", after line "<p></p>"

                testEditor.setCursorPos({ line: 8, ch: 4 });        // cursor at end of line
                var hintList = expectHints(HTMLCodeHints.tagHintProvider);
                verifyTagHints(hintList, "samp");
                expect(hintList.indexOf("div")).toBe(-1);
                expect(hintList.indexOf("span")).not.toBe(-1);
            });

            it("should list hints between '<' and some trailing spaces", function () {  // (bug #1515)
                // Replace line 9 with a complete div tag and insert a blank line and the closing div tag.
                testDocument.replaceRange("<div>\n   \n</div>", { line: 9, ch: 2 });
                expect(testDocument.getLine(10)).toBe("   ");

                // Insert a < on line 10
                testDocument.replaceRange("<", { line: 10, ch: 0 });
                testEditor.setCursorPos({ line: 10, ch: 1 });   // cursor between < and some trailing whitespaces
                var hintList = expectHints(HTMLCodeHints.tagHintProvider);
                verifyTagHints(hintList);

                // Replace '< ' on line 10 with '<\t'
                testDocument.replaceRange("<\t", { line: 10, ch: 0 }, { line: 10, ch: 2 });
                testEditor.setCursorPos({ line: 10, ch: 1 });   // cursor between < and some trailing whitespaces
                hintList = expectHints(HTMLCodeHints.tagHintProvider);
                verifyTagHints(hintList);
            });

            //Test for issue #3339
            it("should show HTML hints after HTML Entity on same line", function () {
                testDocument.replaceRange("&nbsp; Test <  ", { line: 8, ch: 0 });
                testEditor.setCursorPos({ line: 8, ch: 13 });   // cursor between < and some trailing whitespaces
                var hintList = expectHints(HTMLCodeHints.tagHintProvider);
                verifyTagHints(hintList);
            });
        });

        describe("Attribute name hint provider", function () {

            it("should list hints at start of existing attribute", function () {
                testEditor.setCursorPos({ line: 5, ch: 6 });
                var hintList = expectHints(HTMLCodeHints.attrHintProvider);
                verifyAttrHints(hintList);  // expect no filtering (bug #1311)
            });
            it("should list hints within existing attribute", function () {
                testEditor.setCursorPos({ line: 5, ch: 7 });
                var hintList = expectHints(HTMLCodeHints.attrHintProvider);
                verifyAttrHints(hintList, "id");  // filtered on "i"
            });
            it("should list hints at end of existing attribute", function () {
                testEditor.setCursorPos({ line: 5, ch: 8 });
                var hintList = expectHints(HTMLCodeHints.attrHintProvider);
                verifyAttrHints(hintList, "id");  // filtered on "id"
            });
            it("should list hints at end of existing attribute with whitespace", function () {
                testEditor.setCursorPos({ line: 6, ch: 8 });        // cursor between end of attr ("d") and space
                var hintList = expectHints(HTMLCodeHints.attrHintProvider);
                verifyAttrHints(hintList, "id");  // filtered on "id"
            });
            it("should list hints to right of attribute value and a space", function () {
                testEditor.setCursorPos({ line: 6, ch: 18 });        // cursor between space (after attr value) and >
                var hintList = expectHints(HTMLCodeHints.attrHintProvider);
                verifyAttrHints(hintList);  // expect no filtering
            });

            it("should NOT list hints to right of '=' sign on id attr", function () {
                testEditor.setCursorPos({ line: 5, ch: 9 });
                expectNoHints(HTMLCodeHints.attrHintProvider);
            });
            it("should list hints to right of '=' sign", function () {
                testEditor.setCursorPos({ line: 2, ch: 12 });
                expectHints(HTMLCodeHints.attrHintProvider);
            });

            // TODO: Uncomment this after fixing issue #1521
            xit("should NOT list hints to left of '=' sign with whitespace", function () {
                testEditor.setCursorPos({ line: 6, ch: 9 });    // cursor between two spaces before =
                expectNoHints(HTMLCodeHints.attrHintProvider);
                testEditor.setCursorPos({ line: 6, ch: 10 });    // cursor between space and =
                expectNoHints(HTMLCodeHints.attrHintProvider);
            });
            it("should NOT list hints to right of '=' sign with whitespace on id attr", function () {
                testEditor.setCursorPos({ line: 6, ch: 11 });   // cursor between = and space
                expectNoHints(HTMLCodeHints.attrHintProvider);
                testEditor.setCursorPos({ line: 6, ch: 12 });   // cursor between space and '
                expectNoHints(HTMLCodeHints.attrHintProvider);
            });
            it("should list hints to right of '=' sign with whitespace", function () {
                testDocument.setText('<style type = "text/css">');
                testEditor.setCursorPos({ line: 0, ch: 13 });   // cursor between = and space
                expectHints(HTMLCodeHints.attrHintProvider);
                testEditor.setCursorPos({ line: 0, ch: 14 });   // cursor between space and "
                expectHints(HTMLCodeHints.attrHintProvider);
            });
            it("should NOT list hints to right of attribute value with no separating space", function () {
                testEditor.setCursorPos({ line: 5, ch: 14 });   // cursor between ' and >
                expectNoHints(HTMLCodeHints.attrHintProvider);
            });

            it("should NOT list hints within tag name", function () {
                testEditor.setCursorPos({ line: 5, ch: 3 });   // cursor between < and start of tag name ("h")
                expectNoHints(HTMLCodeHints.attrHintProvider);
                testEditor.setCursorPos({ line: 5, ch: 4 });   // cursor in middle of tag name
                expectNoHints(HTMLCodeHints.attrHintProvider);

                // two cases for end of tag name:
                testEditor.setCursorPos({ line: 5, ch: 5 });   // cursor between end of tag name ("1") and space
                expectNoHints(HTMLCodeHints.attrHintProvider);
                testEditor.setCursorPos({ line: 7, ch: 4 });   // cursor between end of tag name ("p") and >
                expectNoHints(HTMLCodeHints.attrHintProvider);
            });
            it("should list hints to right of tag name and a space", function () {  // (bug #1310)
                testDocument.replaceRange(" ", { line: 7, ch: 4 });  // insert a space: "<p>" -> "<p >"

                testEditor.setCursorPos({ line: 7, ch: 5 });   // cursor between space and >
                var hintList = expectHints(HTMLCodeHints.attrHintProvider);
                verifyAttrHints(hintList);  // expect no filtering
            });
            it("should list hints between tag name and attribute name (space on both sides of cursor)", function () {
                testDocument.replaceRange(" ", { line: 5, ch: 5 });  // insert a space: "<h1 id" -> "<h1  id"

                testEditor.setCursorPos({ line: 5, ch: 6 });   // cursor between two spaces, which are between end of tag name ("p") and start of attribute name
                var hintList = expectHints(HTMLCodeHints.attrHintProvider);
                verifyAttrHints(hintList);  // expect no filtering
            });
            it("should list hints between prev attribute value and next attribute name (space on both sides of cursor)", function () {
                testDocument.replaceRange(" ", { line: 8, ch: 14 });  // insert a space: "'aaa' class" -> "'aaa'  class"

                testEditor.setCursorPos({ line: 8, ch: 15 });   // cursor between two spaces, which are between end of attribute value ("p") and start of attribute name
                var hintList = expectHints(HTMLCodeHints.attrHintProvider);
                verifyAttrHints(hintList);  // expect no filtering
            });

            it("should NOT list hints to left of tag", function () {
                testEditor.setCursorPos({ line: 4, ch: 0 });    // tag starting at column zero
                expectNoHints(HTMLCodeHints.attrHintProvider);

                testEditor.setCursorPos({ line: 5, ch: 2 });    // tag with whitespace indent after it
                expectNoHints(HTMLCodeHints.attrHintProvider);
            });
            it("should NOT list hints within text content or whitespace", function () {
                testEditor.setCursorPos({ line: 5, ch: 0 });    // whitespace
                expectNoHints(HTMLCodeHints.attrHintProvider);

                testEditor.setCursorPos({ line: 5, ch: 18 });   // plain text content
                expectNoHints(HTMLCodeHints.attrHintProvider);
            });

            it("should NOT list hints within doctype 'tag'", function () {
                testEditor.setCursorPos({ line: 0, ch: 10 });
                expectNoHints(HTMLCodeHints.attrHintProvider);
            });

            it("should NOT list hints within closing tag", function () {
                testEditor.setCursorPos({ line: 5, ch: 23 });   // cursor between < and /
                expectNoHints(HTMLCodeHints.attrHintProvider);
                testEditor.setCursorPos({ line: 5, ch: 24 });   // cursor between / and start of tag name ("h")
                expectNoHints(HTMLCodeHints.attrHintProvider);
                testEditor.setCursorPos({ line: 5, ch: 25 });   // cursor in middle of tag name
                expectNoHints(HTMLCodeHints.attrHintProvider);
                testEditor.setCursorPos({ line: 5, ch: 26 });   // cursor between end of tag name ("1") and >  (bug #1335)
                expectNoHints(HTMLCodeHints.attrHintProvider);
            });

            it("should NOT list hints between begin 'div' and end 'div' tag", function () {  // (bug #1510)
                // replace line 9 with a complete div tag and insert a blank line and the closing div tag.
                testDocument.replaceRange("<div>\n   \n  </div>", { line: 9, ch: 2 });

                testEditor.setCursorPos({ line: 10, ch: 2 });   // cursor between whitespaces on the newly inserted blank line
                expectNoHints(HTMLCodeHints.attrHintProvider);

                testEditor.setCursorPos({ line: 9, ch: 7 });   // cursor to the right of <div>
                expectNoHints(HTMLCodeHints.attrHintProvider);

                testEditor.setCursorPos({ line: 11, ch: 2 });   // cursor to the left of </div>
                expectNoHints(HTMLCodeHints.attrHintProvider);
            });

            it("should NOT list hints between an empty tag and 'body' end tag", function () {  // (bug #1519)
                // replace line 9 with an input tag and insert two extra blank lines with some whitespaces.
                testDocument.replaceRange("<input type='button' />\n  \n   ", { line: 9, ch: 2 }, { line: 9, ch: 7 });

                // Set cursor between whitespaces on one of the newly inserted blank lines.
                testEditor.setCursorPos({ line: 11, ch: 2 });
                expectNoHints(HTMLCodeHints.attrHintProvider);
            });

            it("should NOT list hints between the 'body' begin tag and 'h1' begin tag", function () {  // (bug #1519)
                // Insert two blank lines with some whitespaces before line 5.
                testDocument.replaceRange("\n  \n   ", { line: 5, ch: 0 });

                // Set cursor between whitespaces on one of the newly inserted blank lines.
                testEditor.setCursorPos({ line: 7, ch: 2 });
                expectNoHints(HTMLCodeHints.attrHintProvider);
            });

            it("should NOT list hints between the 'h1' end tag and 'h3' begin tag", function () {  // (bug #1519)
                // Insert two blank lines with some whitespaces before line 5.
                testDocument.replaceRange("\n  \n   ", { line: 6, ch: 0 });

                // Set cursor between whitespaces on one of the newly inserted blank lines.
                testEditor.setCursorPos({ line: 8, ch: 2 });
                expectNoHints(HTMLCodeHints.attrHintProvider);
            });

            it("should NOT list hints after an HTML comment", function () {  // (bug #1440)
                // replace line 9 with an HTML comment and some spaces
                testDocument.replaceRange("<!-- some comments -->    ", { line: 9, ch: 2 });

                testEditor.setCursorPos({ line: 9, ch: 25 });   // cursor between whitespaces at the end of line 9
                expectNoHints(HTMLCodeHints.attrHintProvider);
            });

            it("should list hints on incomplete tag, after tag name", function () {
                testEditor.setCursorPos({ line: 9, ch: 7 });
                var hintList = expectHints(HTMLCodeHints.attrHintProvider);

                verifyAttrHints(hintList);  // expect no filtering
            });
            it("should NOT list hints on incomplete tag, at end of tag name", function () {
                testEditor.setCursorPos({ line: 9, ch: 5 });    // end of tag name
                expectNoHints(HTMLCodeHints.attrHintProvider);

                testEditor.setCursorPos({ line: 9, ch: 6 });    // within tag name
                expectNoHints(HTMLCodeHints.attrHintProvider);
            });
            it("should NOT list hints to left of incomplete tag", function () {
                testEditor.setCursorPos({ line: 9, ch: 2 });
                expectNoHints(HTMLCodeHints.attrHintProvider);
            });

            it("should list hints on incomplete attribute", function () {
                testDocument.replaceRange("i", { line: 9, ch: 7 });  // insert start of attribute: "<div " -> "<div i"

                testEditor.setCursorPos({ line: 9, ch: 8 });
                var hintList = expectHints(HTMLCodeHints.attrHintProvider);
                verifyAttrHints(hintList, "id");  // filtered on "i"
            });
        });

        describe("Attribute value hint provider", function () {

            it("should list attribute value hints for a single quote after the equal sign", function () {
                testDocument.replaceRange(" dir='", { line: 7, ch: 4 });  // insert dir=' between <p and >

                testEditor.setCursorPos({ line: 7, ch: 10 });       // set cursor right after dir='
                var hintList = expectHints(HTMLCodeHints.attrHintProvider);
                expect(hintList.indexOf("ltr")).not.toBe(-1);       // additional sanity check
            });
            it("should list attribute value hints within an existing attribute value", function () {
                testDocument.replaceRange(" dir=\"ltr\"", { line: 7, ch: 4 });  // insert dir="ltr" between <p and >

                testEditor.setCursorPos({ line: 7, ch: 11 });       // set cursor right after dir="l
                var hintList = expectHints(HTMLCodeHints.attrHintProvider);
                expect(hintList.indexOf("ltr")).not.toBe(-1);       // additional sanity check
            });
            it("should list attribute value hints within an unquoted attribute value", function () {
                testDocument.replaceRange(" dir=ltr", { line: 7, ch: 4 });  // insert dir=ltr between <p and >

                testEditor.setCursorPos({ line: 7, ch: 10 });       // set cursor right after dir=l
                var hintList = expectHints(HTMLCodeHints.attrHintProvider);
                expect(hintList.indexOf("ltr")).not.toBe(-1);       // additional sanity check
            });

            it("should list sorted boolean attribute value hints", function () {
                testDocument.replaceRange(" spellcheck=\"", { line: 7, ch: 4 });  // insert spellcheck=" between <p and >

                testEditor.setCursorPos({ line: 7, ch: 17 });       // set cursor right after spellcheck="
                var hintList = expectHints(HTMLCodeHints.attrHintProvider);
                expect(hintList.indexOf("false")).toBe(0);
                expect(hintList.indexOf("true")).toBe(1);
                expect(hintList.length).toBe(2);
            });

            it("should NOT list attribute value hints to the right of a closing double quote", function () {
                testDocument.replaceRange(" dir=\"ltr\"", { line: 7, ch: 4 });  // insert dir="ltr" between <p and >

                testEditor.setCursorPos({ line: 7, ch: 14 });       // set cursor right after dir="ltr"
                expectNoHints(HTMLCodeHints.attrHintProvider);
            });
            it("should NOT list attribute value hints to the right of a closing single quote", function () {
                testDocument.replaceRange(" dir='ltr'", { line: 6, ch: 5 });  // insert dir='ltr' between <h3 and id

                testEditor.setCursorPos({ line: 4, ch: 15 });       // set cursor right after dir='ltr'
                expectNoHints(HTMLCodeHints.attrHintProvider);
            });

            it("should list attribute value hints for type attribute of style tag", function () {
                // set cursor to the right of type attribute but before the closing quote
                testEditor.setCursorPos({ line: 2, ch: 21 });
                var hintList = expectHints(HTMLCodeHints.attrHintProvider);
                expect(hintList.indexOf("text/css")).not.toBe(-1);
            });
            it("should NOT list any attribute value for type attribute of embed tag", function () {
                // set cursor to the right of type attribute but before the closing quote
                testEditor.setCursorPos({ line: 9, ch: 3 });
                // Replace div on line 9 with embed type=' ("<div " --> "<embed type='")
                testDocument.replaceRange("embed type='", { line: 9, ch: 3 }, { line: 9, ch: 7 });
                testEditor.setCursorPos({ line: 9, ch: 15 });
                expectNoHints(HTMLCodeHints.attrHintProvider);
            });

            it("should NOT list any attribute value for an unknown attribute name", function () {
                testDocument.replaceRange("foo='", { line: 9, ch: 7 });  // insert foo=' after <div tag
                testEditor.setCursorPos({ line: 9, ch: 12 });
                expectNoHints(HTMLCodeHints.attrHintProvider);
            });
        });


        // CodeMirror doesn't correctly support valueless attributes, so none of them are included in the default
        // test content above
        describe("Valueless attributes", function () {

            it("should list hints after valueless attribute", function () {  // (bug #1313)
                testDocument.replaceRange("  <input checked >\n", { line: 9, ch: 0 });  // insert new line

                testEditor.setCursorPos({ line: 9, ch: 17 });   // cursor between space and >
                var hintList = expectHints(HTMLCodeHints.attrHintProvider);

                // Expect no filtering - however, we offer some attributes (including first in the list) that
                // are specific to the <input> tag, so we can't use the default "no filtering" empty arg here.
                // (This smart filtering isn't officially part of the release, so no unit tests specifically
                // targeting that functionality yet).
                verifyAttrHints(hintList, "accept");
            });

            it("should list hints after attribute that follows a valueless attribute", function () {  // (bug #1313)
                testDocument.replaceRange("  <input checked accept='' >\n", { line: 9, ch: 0 });  // insert new line

                testEditor.setCursorPos({ line: 9, ch: 27 });   // cursor between space and >
                var hintList = expectHints(HTMLCodeHints.attrHintProvider);

                // "accept" will get filtered and won't be the first one in the hints.
                verifyAttrHints(hintList);
            });

            it("should list attribute value hints for an attribute that follows a valueless attribute", function () {  // (bug #2804)
                testDocument.replaceRange("  <input checked accept='' >\n", { line: 9, ch: 0 });  // insert new line

                // cursor between quotes of accept attribute
                testEditor.setCursorPos({ line: 9, ch: 25 });
                var hintList = expectHints(HTMLCodeHints.attrHintProvider);

                verifyAttrHints(hintList, "application/msexcel");

                // cursor between equal sign and opening quote of accept attribute
                testEditor.setCursorPos({ line: 9, ch: 24 });
                hintList = expectHints(HTMLCodeHints.attrHintProvider);

                verifyAttrHints(hintList, "application/msexcel");
            });

            it("should NOT list attribute value hints when the cursor is after the end quote of an attribute value", function () {
                testDocument.replaceRange("  <input checked accept='' >\n", { line: 9, ch: 0 });  // insert new line

                // Set cursor after the closing quote of accept attribute
                testEditor.setCursorPos({ line: 9, ch: 26 });
                expectNoHints(HTMLCodeHints.attrHintProvider);
            });
        });


        describe("Attribute insertion", function () {

            function selectHint(provider, expectedHint) {
                var hintList = expectHints(provider);
                expect(hintList.indexOf(expectedHint)).not.toBe(-1);
                return provider.insertHint(expectedHint);
            }

            // Helper function for testing cursor position
            function fixPos(pos) {
                if (!("sticky" in pos)) {
                    pos.sticky = null;
                }
                return pos;
            }
            function expectCursorAt(pos) {
                var selection = testEditor.getSelection();
                expect(fixPos(selection.start)).toEqual(fixPos(selection.end));
                expect(fixPos(selection.start)).toEqual(fixPos(pos));
            }

            it("should insert =\"\" after attribute", function () {
                testEditor.setCursorPos({ line: 6, ch: 18 });   // cursor between space and >
                selectHint(HTMLCodeHints.attrHintProvider, "class");
                expect(testDocument.getLine(6)).toBe("  <h3 id  = 'bar' class=\"\">Subheading</h3>");
                expectCursorAt({ line: 6, ch: 25 });            // cursor between the two "s
            });

            it("should make explicit request for new hints after attribute name has been inserted", function () {
                testEditor.setCursorPos({ line: 6, ch: 18 });   // cursor between space and >
                expect(selectHint(HTMLCodeHints.attrHintProvider, "dir")).toBe(true); // returning 'true' from insertHint (which is called by selectHint helper) initiates a new explicit hint request
                expect(testDocument.getLine(6)).toBe("  <h3 id  = 'bar' dir=\"\">Subheading</h3>");
            });

            it("should NOT insert =\"\" after valueless attribute", function () {
                testDocument.replaceRange("  <input \n", { line: 9, ch: 0 });  // insert new line

                testEditor.setCursorPos({ line: 9, ch: 9 });    // cursor after trailing space
                selectHint(HTMLCodeHints.attrHintProvider, "checked");
                expect(testDocument.getLine(9)).toBe("  <input checked");
                expectCursorAt({ line: 9, ch: 16 });            // cursor at end of attr name
            });

            it("should insert a new attribute before the existing one that starts at cursor", function () {  // (bug #1312)
                testEditor.setCursorPos({ line: 5, ch: 6 });    // cursor between space and start of existing attribute name ("id")
                selectHint(HTMLCodeHints.attrHintProvider, "class");
                expect(testDocument.getLine(5)).toBe("  <h1 class=\"\"id='foo'>Heading</h1>");
                expectCursorAt({ line: 5, ch: 13 });            // cursor between two double-quotes
            });
            it("should change nothing when cursor at end", function () {  // (bug #1314)
                testEditor.setCursorPos({ line: 5, ch: 8 });    // cursor between end of existing attribute name ("id") and =
                selectHint(HTMLCodeHints.attrHintProvider, "id");
                expect(testDocument.getLine(5)).toBe("  <h1 id='foo'>Heading</h1>");
                expectCursorAt({ line: 5, ch: 8 });            // cursor stays at end of attr name
            });

            it("should insert a valueless attribute before the existing attribute that starts at cursor", function () {
                testDocument.replaceRange("  <input id='foo'>\n", { line: 9, ch: 0 });  // insert new line

                testEditor.setCursorPos({ line: 9, ch: 9 });    // cursor after between "i" and "d"
                selectHint(HTMLCodeHints.attrHintProvider, "checked");
                expect(testDocument.getLine(9)).toBe("  <input checkedid='foo'>");
                expectCursorAt({ line: 9, ch: 16 });            // cursor between end of attr name and =
            });

            it("should insert a new atribute with an empty value before the existing valueless attribute", function () {
                testDocument.replaceRange("  <input checked>\n", { line: 9, ch: 0 });  // insert new line

                testEditor.setCursorPos({ line: 9, ch: 9 });    // cursor after trailing space
                selectHint(HTMLCodeHints.attrHintProvider, "class");
                expect(testDocument.getLine(9)).toBe("  <input class=\"\"checked>");
                expectCursorAt({ line: 9, ch: 16 });            // cursor between the two "s
            });

            it("should insert the selected attribute value with the closing (single) quote", function () {
                testDocument.replaceRange("dir='", { line: 9, ch: 7 });  // insert dir=' after <div tag
                testEditor.setCursorPos({ line: 9, ch: 12 });
                selectHint(HTMLCodeHints.attrHintProvider, "rtl");
                expect(testDocument.getLine(9)).toBe("  <div dir='rtl'");
                expectCursorAt({ line: 9, ch: 16 });            // cursor after the closing quote
            });

            it("should insert the selected attribute value with the closing (double) quote", function () {
                testDocument.replaceRange("dir=\"", { line: 9, ch: 7 });  // insert dir=" after <div tag
                testEditor.setCursorPos({ line: 9, ch: 12 });
                selectHint(HTMLCodeHints.attrHintProvider, "rtl");
                expect(testDocument.getLine(9)).toBe("  <div dir=\"rtl\"");
                expectCursorAt({ line: 9, ch: 16 });            // cursor after the closing quote
            });

            it("should insert the selected attribute value inside the existing quotes", function () {
                testDocument.replaceRange("dir=\"\"", { line: 9, ch: 7 });  // insert dir="" after <div tag
                testEditor.setCursorPos({ line: 9, ch: 12 });
                selectHint(HTMLCodeHints.attrHintProvider, "rtl");
                expect(testDocument.getLine(9)).toBe("  <div dir=\"rtl\"");
                expectCursorAt({ line: 9, ch: 16 });            // cursor after the closing quote
            });

            it("should insert the selected attribute value wrapped in double quotes", function () {
                testDocument.replaceRange("dir=", { line: 9, ch: 7 });  // insert dir= after <div tag
                testEditor.setCursorPos({ line: 9, ch: 11 });
                selectHint(HTMLCodeHints.attrHintProvider, "rtl");
                expect(testDocument.getLine(9)).toBe("  <div dir=\"rtl\"");
                expectCursorAt({ line: 9, ch: 16 });            // cursor after the closing quote
            });

            it("should replace the partially typed attribute value with the selected attribute value", function () {
                testDocument.replaceRange("dir=\"lt", { line: 9, ch: 7 });  // insert dir="lt after <div tag
                testEditor.setCursorPos({ line: 9, ch: 14 });
                selectHint(HTMLCodeHints.attrHintProvider, "ltr");
                expect(testDocument.getLine(9)).toBe("  <div dir=\"ltr\"");
                expectCursorAt({ line: 9, ch: 16 });            // cursor after the closing quote
            });

            it("should replace the partially typed attribute value with the selected attribute value", function () {
                testDocument.replaceRange("dir=lt", { line: 9, ch: 7 });  // insert dir="lt after <div tag
                testEditor.setCursorPos({ line: 9, ch: 13 });
                selectHint(HTMLCodeHints.attrHintProvider, "ltr");
                expect(testDocument.getLine(9)).toBe("  <div dir=ltr");
                expectCursorAt({ line: 9, ch: 14 });            // cursor after the closing quote
            });

            it("should replace an existing attribute value with the selected attribute value", function () {
                // Intentionally change the type of style attribute to an invalid value by replacing
                // "css" with "javascript".
                testDocument.replaceRange("javascript", { line: 2, ch: 18 }, { line: 2, ch: 21 });
                testEditor.setCursorPos({ line: 2, ch: 18 });
                selectHint(HTMLCodeHints.attrHintProvider, "text/css");
                expect(testDocument.getLine(2)).toBe("<style type=\"text/css\">");
                expectCursorAt({ line: 2, ch: 22 });            // cursor after the closing quote
            });

            it("should replace a quoted attribute value and keep the preceding space and quotes", function () {
                // Insert an unquoted attribute between <div and id on line 5.
                testDocument.replaceRange("dir= \"ltr\" ", { line: 5, ch: 6 });
                testEditor.setCursorPos({ line: 5, ch: 10 });   // Set the cursor between = and the space
                // Select "rtl" to replace "ltr"
                selectHint(HTMLCodeHints.attrHintProvider, "rtl");
                expect(testDocument.getLine(5)).toBe("  <h1 dir= \"rtl\" id='foo'>Heading</h1>");
                expectCursorAt({ line: 5, ch: 16 });            // cursor after the closing quote

                testEditor.setCursorPos({ line: 5, ch: 11 });   // Set the cursor between the space and the begin quote
                // Select "ltr" to replace "rtl"
                selectHint(HTMLCodeHints.attrHintProvider, "ltr");
                expect(testDocument.getLine(5)).toBe("  <h1 dir= \"ltr\" id='foo'>Heading</h1>");
                expectCursorAt({ line: 5, ch: 16 });            // cursor after the closing quote
            });

            it("should replace the unquoted attribute value with the selected attribute value in quotes", function () {
                // Insert an unquoted attribute between <div and id on line 5.
                testDocument.replaceRange("dir=ltr ", { line: 5, ch: 6 });
                testEditor.setCursorPos({ line: 5, ch: 10 });
                // Select "rtl" to replace "ltr"
                selectHint(HTMLCodeHints.attrHintProvider, "rtl");
                expect(testDocument.getLine(5)).toBe("  <h1 dir=\"rtl\" id='foo'>Heading</h1>");
                expectCursorAt({ line: 5, ch: 15 });            // cursor after the inserted value
            });

            it("should replace an unquoted attribute value when the cursor is inside that value", function () {
                // Insert an unquoted attribute between <div and id on line 5.
                // intentionally have an invalid attribute lttr here
                testDocument.replaceRange("dir= lttr ", { line: 5, ch: 6 });
                testEditor.setCursorPos({ line: 5, ch: 12 });   // Set cursor after l in the invalid attribute value
                selectHint(HTMLCodeHints.attrHintProvider, "ltr");
                expect(testDocument.getLine(5)).toBe("  <h1 dir= ltr id='foo'>Heading</h1>");
                expectCursorAt({ line: 5, ch: 14 });            // cursor after the inserted value
            });

            it("should insert a quoted attribute value before an existing unquoted attribute value with preceding space character", function () {
                // Insert an unquoted attribute between <div and id on line 5.
                testDocument.replaceRange("dir= ltr ", { line: 5, ch: 6 });
                testEditor.setCursorPos({ line: 5, ch: 10 }); // Set cursor between = and the space
                selectHint(HTMLCodeHints.attrHintProvider, "rtl");
                expect(testDocument.getLine(5)).toBe("  <h1 dir=\"rtl\" ltr id='foo'>Heading</h1>");
                expectCursorAt({ line: 5, ch: 15 });          // cursor after the inserted value
            });

            it("should insert a quoted attribute value before an existing id attribute", function () {
                // Insert an unquoted attribute between <div and id on line 5.
                testDocument.replaceRange("dir= ", { line: 5, ch: 6 });
                testEditor.setCursorPos({ line: 5, ch: 10 }); // Set cursor between = and the space
                selectHint(HTMLCodeHints.attrHintProvider, "rtl");
                expect(testDocument.getLine(5)).toBe("  <h1 dir=\"rtl\" id='foo'>Heading</h1>");
                expectCursorAt({ line: 5, ch: 15 });          // cursor after the inserted value
            });

            it("should insert a quoted attribute value right before the closing > of the tag", function () {
                // Insert an unquoted attribute between <p and > on line 7.
                testDocument.replaceRange(" dir=", { line: 7, ch: 4 });
                testEditor.setCursorPos({ line: 7, ch: 9 }); // Set cursor between = and >
                selectHint(HTMLCodeHints.attrHintProvider, "rtl");
                expect(testDocument.getLine(7)).toBe("  <p dir=\"rtl\"></p>");
                expectCursorAt({ line: 7, ch: 14 });         // cursor after the inserted value
            });

            it("should insert a quoted attribute value without overwriting the closing > of the tag", function () {
                // Insert an attribute value right before > on line 7 with an opening double quote that
                // creates an inbalanced string up to the first attribute value in the next tag.
                testDocument.replaceRange("<a dir=\"><span class=\"foo\"></span></a>", { line: 7, ch: 2 }, { line: 7, ch: 9 });
                testEditor.setCursorPos({ line: 7, ch: 10 }); // Set cursor between dir=" and >
                selectHint(HTMLCodeHints.attrHintProvider, "rtl");
                expect(testDocument.getLine(7)).toBe("  <a dir=\"rtl\"><span class=\"foo\"></span></a>");
                expectCursorAt({ line: 7, ch: 14 });          // cursor after the inserted value
            });
        });


    }); // describe("HTML Code Hinting"
});
