/* ***** BEGIN LICENSE BLOCK *****
 * Distributed under the BSD license:
 *
 * Copyright (c) 2010, Ajax.org B.V.
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of Ajax.org B.V. nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * ***** END LICENSE BLOCK ***** */

if (typeof process !== "undefined") {
    require("amd-loader");
    require("./test/mockdom");
}

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

var lang = require("./lib/lang");
var EditSession = require("./edit_session").EditSession;
var Editor = require("./editor").Editor;
var UndoManager = require("./undomanager").UndoManager;
var MockRenderer = require("./test/mockrenderer").MockRenderer;
var Range = require("./range").Range;
var assert = require("./test/assertions");
var JavaScriptMode = require("./mode/javascript").Mode;

function createFoldTestSession() {
    var lines = [
        "function foo(items) {",
        "    for (var i=0; i<items.length; i++) {",
        "        alert(items[i] + \"juhu\");",
        "    }    // Real Tab.",
        "}"
    ];
    var session = new EditSession(lines.join("\n"));
    session.setUndoManager(new UndoManager());
    session.addFold("args...", new Range(0, 13, 0, 18));
    session.addFold("foo...", new Range(1, 10, 2, 10));
    session.addFold("bar...", new Range(2, 20, 2, 25));
    return session;
}

function assertArray(a, b) {
    assert.equal(a+"", b+"");
    assert.ok(a.length == b.length);
    for (var i = 0; i < a.length; i++) {
        assert.equal(a[i], b[i]);
    }
}

module.exports = {

   "test: find matching opening bracket in Text mode" : function() {
        var session = new EditSession(["(()(", "())))"]);

        assert.position(session.findMatchingBracket({row: 0, column: 3}), 0, 1);
        assert.position(session.findMatchingBracket({row: 1, column: 2}), 1, 0);
        assert.position(session.findMatchingBracket({row: 1, column: 3}), 0, 3);
        assert.position(session.findMatchingBracket({row: 1, column: 4}), 0, 0);
        assert.equal(session.findMatchingBracket({row: 1, column: 5}), null);
    },

    "test: find matching closing bracket in Text mode" : function() {
        var session = new EditSession(["(()(", "())))"]);

        assert.position(session.findMatchingBracket({row: 1, column: 1}), 1, 1);
        assert.position(session.findMatchingBracket({row: 1, column: 1}), 1, 1);
        assert.position(session.findMatchingBracket({row: 0, column: 4}), 1, 2);
        assert.position(session.findMatchingBracket({row: 0, column: 2}), 0, 2);
        assert.position(session.findMatchingBracket({row: 0, column: 1}), 1, 3);
        assert.equal(session.findMatchingBracket({row: 0, column: 0}), null);
    },

    "test: find matching opening bracket in JavaScript mode" : function() {
        var lines = [
            "function foo() {",
            "    var str = \"{ foo()\";",
            "    if (debug) {",
            "        // write str (a string) to the console",
            "        console.log(str);",
            "    }",
            "    str += \" bar() }\";",
            "}"
        ];
        var session = new EditSession(lines.join("\n"), new JavaScriptMode());

        assert.position(session.findMatchingBracket({row: 0, column: 14}), 0, 12);
        assert.position(session.findMatchingBracket({row: 7, column: 1}), 0, 15);
        assert.position(session.findMatchingBracket({row: 6, column: 20}), 1, 15);
        assert.position(session.findMatchingBracket({row: 1, column: 22}), 1, 20);
        assert.position(session.findMatchingBracket({row: 3, column: 31}), 3, 21);
        assert.position(session.findMatchingBracket({row: 4, column: 24}), 4, 19);
        assert.equal(session.findMatchingBracket({row: 0, column: 1}), null);
    },

    "test: find matching closing bracket in JavaScript mode" : function() {
        var lines = [
            "function foo() {",
            "    var str = \"{ foo()\";",
            "    if (debug) {",
            "        // write str (a string) to the console",
            "        console.log(str);",
            "    }",
            "    str += \" bar() }\";",
            "}"
        ];
        var session = new EditSession(lines.join("\n"), new JavaScriptMode());

        assert.position(session.findMatchingBracket({row: 0, column: 13}), 0, 13);
        assert.position(session.findMatchingBracket({row: 0, column: 16}), 7, 0);
        assert.position(session.findMatchingBracket({row: 1, column: 16}), 6, 19);
        assert.position(session.findMatchingBracket({row: 1, column: 21}), 1, 21);
        assert.position(session.findMatchingBracket({row: 3, column: 22}), 3, 30);
        assert.position(session.findMatchingBracket({row: 4, column: 20}), 4, 23);
    },

    "test: handle unbalanced brackets in JavaScript mode" : function() {
        var lines = [
            "function foo() {",
            "    var str = \"{ foo()\";",
            "    if (debug) {",
            "        // write str a string) to the console",
            "        console.log(str);",
            "    ",
            "    str += \" bar() \";",
            "}"
        ];
        var session = new EditSession(lines.join("\n"), new JavaScriptMode());

        assert.equal(session.findMatchingBracket({row: 0, column: 16}), null);
        assert.equal(session.findMatchingBracket({row: 3, column: 30}), null);
        assert.equal(session.findMatchingBracket({row: 1, column: 16}), null);
    },

    "test: match different bracket types" : function() {
        var session = new EditSession(["({[", ")]}"]);

        assert.position(session.findMatchingBracket({row: 0, column: 1}), 1, 0);
        assert.position(session.findMatchingBracket({row: 0, column: 2}), 1, 2);
        assert.position(session.findMatchingBracket({row: 0, column: 3}), 1, 1);

        assert.position(session.findMatchingBracket({row: 1, column: 1}), 0, 0);
        assert.position(session.findMatchingBracket({row: 1, column: 2}), 0, 2);
        assert.position(session.findMatchingBracket({row: 1, column: 3}), 0, 1);
    },

    "test: move lines down" : function() {
        var session = new EditSession(["a1", "a2", "a3", "a4"]);

        session.moveLinesDown(0, 1);
        assert.equal(session.getValue(), ["a3", "a1", "a2", "a4"].join("\n"));

        session.moveLinesDown(1, 2);
        assert.equal(session.getValue(), ["a3", "a4", "a1", "a2"].join("\n"));

        session.moveLinesDown(2, 3);
        assert.equal(session.getValue(), ["a3", "a4", "a1", "a2"].join("\n"));

        session.moveLinesDown(2, 2);
        assert.equal(session.getValue(), ["a3", "a4", "a2", "a1"].join("\n"));
    },

    "test: move lines up" : function() {
        var session = new EditSession(["a1", "a2", "a3", "a4"]);

        session.moveLinesUp(2, 3);
        assert.equal(session.getValue(), ["a1", "a3", "a4", "a2"].join("\n"));

        session.moveLinesUp(1, 2);
        assert.equal(session.getValue(), ["a3", "a4", "a1", "a2"].join("\n"));

        session.moveLinesUp(0, 1);
        assert.equal(session.getValue(), ["a3", "a4", "a1", "a2"].join("\n"));

        session.moveLinesUp(2, 2);
        assert.equal(session.getValue(), ["a3", "a1", "a4", "a2"].join("\n"));
    },

    "test: duplicate lines" : function() {
        var session = new EditSession(["1", "2", "3", "4"]);

        session.duplicateLines(1, 2);
        assert.equal(session.getValue(), ["1", "2", "3", "2", "3", "4"].join("\n"));
    },

    "test: duplicate last line" : function() {
        var session = new EditSession(["1", "2", "3"]);

        session.duplicateLines(2, 2);
        assert.equal(session.getValue(), ["1", "2", "3", "3"].join("\n"));
    },

    "test: duplicate first line" : function() {
        var session = new EditSession(["1", "2", "3"]);

        session.duplicateLines(0, 0);
        assert.equal(session.getValue(), ["1", "1", "2", "3"].join("\n"));
    },

    "test: getScreenLastRowColumn": function() {
        var session = new EditSession([
            "juhu",
            "12\t\t34",
            "ぁぁa"
        ]);

        assert.equal(session.getScreenLastRowColumn(0), 4);
        assert.equal(session.getScreenLastRowColumn(1), 10);
        assert.equal(session.getScreenLastRowColumn(2), 5);
    },

    "test: convert document to screen coordinates" : function() {
        var session = new EditSession("01234\t567890\t1234");
        session.setTabSize(4);

        assert.equal(session.documentToScreenColumn(0, 0), 0);
        assert.equal(session.documentToScreenColumn(0, 4), 4);
        assert.equal(session.documentToScreenColumn(0, 5), 5);
        assert.equal(session.documentToScreenColumn(0, 6), 8);
        assert.equal(session.documentToScreenColumn(0, 12), 14);
        assert.equal(session.documentToScreenColumn(0, 13), 16);

        session.setTabSize(2);

        assert.equal(session.documentToScreenColumn(0, 0), 0);
        assert.equal(session.documentToScreenColumn(0, 4), 4);
        assert.equal(session.documentToScreenColumn(0, 5), 5);
        assert.equal(session.documentToScreenColumn(0, 6), 6);
        assert.equal(session.documentToScreenColumn(0, 7), 7);
        assert.equal(session.documentToScreenColumn(0, 12), 12);
        assert.equal(session.documentToScreenColumn(0, 13), 14);
    },

    "test: convert document to screen coordinates with leading tabs": function() {
        var session = new EditSession("\t\t123");
        session.setTabSize(4);

        assert.equal(session.documentToScreenColumn(0, 0), 0);
        assert.equal(session.documentToScreenColumn(0, 1), 4);
        assert.equal(session.documentToScreenColumn(0, 2), 8);
        assert.equal(session.documentToScreenColumn(0, 3), 9);
    },

    "test: documentToScreen without soft wrap": function() {
        var session = new EditSession([
            "juhu",
            "12\t\t34",
            "ぁぁa"
        ]);

        assert.position(session.documentToScreenPosition(0, 3), 0, 3);
        assert.position(session.documentToScreenPosition(1, 3), 1, 4);
        assert.position(session.documentToScreenPosition(1, 4), 1, 8);
        assert.position(session.documentToScreenPosition(2, 2), 2, 4);
    },

    "test: documentToScreen with soft wrap": function() {
        var session = new EditSession(["foo bar foo bar"]);
        session.setUseWrapMode(true);
        session.setWrapLimitRange(12, 12);
        session.adjustWrapLimit(80);

        assert.position(session.documentToScreenPosition(0, 11), 0, 11);
        assert.position(session.documentToScreenPosition(0, 12), 1, 0);
    },

    "test: documentToScreen with soft wrap and multibyte characters": function() {
        var session = new EditSession(["ぁぁa"]);
        session.setUseWrapMode(true);
        session.setWrapLimitRange(2, 2);
        session.adjustWrapLimit(80);

        assert.position(session.documentToScreenPosition(0, 1), 1, 0);
        assert.position(session.documentToScreenPosition(0, 2), 2, 0);
        assert.position(session.documentToScreenPosition(0, 4), 2, 1);
    },

    "test: documentToScreen should clip position to the document boundaries": function() {
        var session = new EditSession("foo bar\njuhu kinners");

        assert.position(session.documentToScreenPosition(-1, 4), 0, 0);
        assert.position(session.documentToScreenPosition(3, 0), 1, 12);
    },

    "test: convert screen to document coordinates" : function() {
        var session = new EditSession("01234\t567890\t1234");
        session.setTabSize(4);

        assert.equal(session.screenToDocumentColumn(0, 0), 0);
        assert.equal(session.screenToDocumentColumn(0, 4), 4);
        assert.equal(session.screenToDocumentColumn(0, 5), 5);
        assert.equal(session.screenToDocumentColumn(0, 6), 5);
        assert.equal(session.screenToDocumentColumn(0, 7), 5);
        assert.equal(session.screenToDocumentColumn(0, 8), 6);
        assert.equal(session.screenToDocumentColumn(0, 9), 7);
        assert.equal(session.screenToDocumentColumn(0, 15), 12);
        assert.equal(session.screenToDocumentColumn(0, 19), 16);

        session.setTabSize(2);

        assert.equal(session.screenToDocumentColumn(0, 0), 0);
        assert.equal(session.screenToDocumentColumn(0, 4), 4);
        assert.equal(session.screenToDocumentColumn(0, 5), 5);
        assert.equal(session.screenToDocumentColumn(0, 6), 6);
        assert.equal(session.screenToDocumentColumn(0, 12), 12);
        assert.equal(session.screenToDocumentColumn(0, 13), 12);
        assert.equal(session.screenToDocumentColumn(0, 14), 13);
    },

    "test: screenToDocument with soft wrap": function() {
        var session = new EditSession(["foo bar foo bar"]);
        session.setUseWrapMode(true);
        session.setWrapLimitRange(12, 12);
        session.adjustWrapLimit(80);

        assert.position(session.screenToDocumentPosition(1, 0), 0, 12);
        assert.position(session.screenToDocumentPosition(0, 11), 0, 11);
        // Check if the position is clamped the right way.
        assert.position(session.screenToDocumentPosition(0, 12), 0, 11);
        assert.position(session.screenToDocumentPosition(0, 20), 0, 11);
    },

    "test: screenToDocument with soft wrap and multi byte characters": function() {
        var session = new EditSession(["ぁ a"]);
        session.setUseWrapMode(true);
        session.adjustWrapLimit(80);

        assert.position(session.screenToDocumentPosition(0, 1), 0, 0);
        assert.position(session.screenToDocumentPosition(0, 2), 0, 1);
        assert.position(session.screenToDocumentPosition(0, 3), 0, 2);
        assert.position(session.screenToDocumentPosition(0, 4), 0, 3);
        assert.position(session.screenToDocumentPosition(0, 5), 0, 3);
    },

    "test: screenToDocument should clip position to the document boundaries": function() {
        var session = new EditSession("foo bar\njuhu kinners");

        assert.position(session.screenToDocumentPosition(-1, 4), 0, 0);
        assert.position(session.screenToDocumentPosition(0, -1), 0, 0);
        assert.position(session.screenToDocumentPosition(0, 30), 0, 7);
        assert.position(session.screenToDocumentPosition(2, 4), 1, 12);
        assert.position(session.screenToDocumentPosition(1, 30), 1, 12);
        assert.position(session.screenToDocumentPosition(20, 50), 1, 12);
        assert.position(session.screenToDocumentPosition(20, 5), 1, 12);

        // and the same for folded rows
        session.addFold("...", new Range(0,1,1,3));
        assert.position(session.screenToDocumentPosition(1, 2), 1, 12);
        // for wrapped rows
        session.setUseWrapMode(true);
        session.setWrapLimitRange(5,5);
        assert.position(session.screenToDocumentPosition(4, 1), 1, 12);
    },

    "test: wrapLine split function" : function() {
        function computeAndAssert(line, assertEqual, wrapLimit, tabSize) {
            wrapLimit = wrapLimit || 12;
            tabSize = tabSize || 4;
            line = lang.stringTrimRight(line);
            var tokens = EditSession.prototype.$getDisplayTokens(line);
            var splits = EditSession.prototype.$computeWrapSplits(tokens, wrapLimit, tabSize);
            // console.log("String:", line, "Result:", splits, "Expected:", assertEqual);
            assert.ok(splits.length == assertEqual.length);
            for (var i = 0; i < splits.length; i++) {
                assert.ok(splits[i] == assertEqual[i]);
            }
        }
        
        EditSession.prototype.$wrapAsCode = true;
        // Basic splitting.
        computeAndAssert("foo bar foo bar", [ 12 ]);
        computeAndAssert("foo bar f   bar", [ 12 ]);
        computeAndAssert("foo bar f     r", [ 12 ]); // 14 if we enable 
        computeAndAssert("foo bar foo bar foo bara foo", [12, 25]);

        // Don't split if there is only whitespaces/tabs at the end of the line.
        computeAndAssert("foo foo foo \t \t", [ ]);

        // If there is no space to split, force split.
        computeAndAssert("foooooooooooooo", [ 12 ]);
        computeAndAssert("fooooooooooooooooooooooooooo", [12, 24]);
        computeAndAssert("foo bar fooooooooooobooooooo", [8,  20]);

        // Basic splitting + tabs.
        computeAndAssert("foo \t\tbar", [ 6 ]);
        computeAndAssert("foo \t \tbar", [ 7 ]);

        // Ignore spaces/tabs at beginning of split.
        computeAndAssert("foo \t \t   \t \t bar", [ 7 ]); // 14

        // Test wrapping for asian characters.
        computeAndAssert("ぁぁ", [1], 2);
        computeAndAssert(" ぁぁ", [1, 2], 2);
        computeAndAssert(" ぁ\tぁ", [1, 3], 2);
        computeAndAssert(" ぁぁ\tぁ", [1, 4], 4);

        // Test wrapping for punctuation.
        computeAndAssert(" ab.c;ef++", [1, 3, 5, 7, 8], 2);
        computeAndAssert(" a.b", [1, 2, 3], 1);
        computeAndAssert("#>>", [1, 2], 1);
        
        // Test wrapping for punctuation in
        EditSession.prototype.$wrapAsCode = false;
        computeAndAssert("ab cde, Juhu kinners", [3, 8, 13, 19], 6);
    },

    "test get longest line" : function() {
        var session = new EditSession(["12"]);
        session.setTabSize(4);
        assert.equal(session.getScreenWidth(), 2);

        session.doc.insertNewLine({row: 0, column: Infinity});
        session.doc.insertLines(1, ["123"]);
        assert.equal(session.getScreenWidth(), 3);

        session.doc.insertNewLine({row: 0, column: Infinity});
        session.doc.insertLines(1, ["\t\t"]);

        assert.equal(session.getScreenWidth(), 8);

        session.setTabSize(2);
        assert.equal(session.getScreenWidth(), 4);
    },

    "test getDisplayString": function() {
        var session = new EditSession(["12"]);
        session.setTabSize(4);

        assert.equal(session.$getDisplayTokens("\t").length, 4);
        assert.equal(session.$getDisplayTokens("abc").length, 3);
        assert.equal(session.$getDisplayTokens("abc\t").length, 4);
    },

    "test issue 83": function() {
        var session = new EditSession("");
        var editor = new Editor(new MockRenderer(), session);
        var document = session.getDocument();

        session.setUseWrapMode(true);

        document.insertLines(0, ["a", "b"]);
        document.insertLines(2, ["c", "d"]);
        document.removeLines(1, 2);
    },

    "test wrapMode init has to create wrapData array": function() {
        var session = new EditSession("foo bar\nfoo bar");
        var editor = new Editor(new MockRenderer(), session);
        var document = session.getDocument();

        session.setUseWrapMode(true);
        session.setWrapLimitRange(3, 3);
        session.adjustWrapLimit(80);

        // Test if wrapData is there and was computed.
        assert.equal(session.$wrapData.length, 2);
        assert.equal(session.$wrapData[0].length, 1);
        assert.equal(session.$wrapData[1].length, 1);
    },

    "test first line blank with wrap": function() {
        var session = new EditSession("\nfoo");
        session.setUseWrapMode(true);
        assert.equal(session.doc.getValue(), ["", "foo"].join("\n"));
    },

    "test first line blank with wrap 2" : function() {
        var session = new EditSession("");
        session.setUseWrapMode(true);
        session.setValue("\nfoo");

        assert.equal(session.doc.getValue(), ["", "foo"].join("\n"));
    },

    "test fold getFoldDisplayLine": function() {
        var session = createFoldTestSession();
        function assertDisplayLine(foldLine, str) {
            var line = session.getLine(foldLine.end.row);
            var displayLine =
                session.getFoldDisplayLine(foldLine, foldLine.end.row, line.length);
            assert.equal(displayLine, str);
        }

        assertDisplayLine(session.$foldData[0], "function foo(args...) {")
        assertDisplayLine(session.$foldData[1], "    for (vfoo...ert(items[bar...\"juhu\");");
    },

    "test foldLine idxToPosition": function() {
        var session = createFoldTestSession();

        function assertIdx2Pos(foldLineIdx, idx, row, column) {
            var foldLine = session.$foldData[foldLineIdx];
            assert.position(foldLine.idxToPosition(idx), row, column);
        }

//        "function foo(items) {",
//        "    for (var i=0; i<items.length; i++) {",
//        "        alert(items[i] + \"juhu\");",
//        "    }    // Real Tab.",
//        "}"

        assertIdx2Pos(0, 12, 0, 12);
        assertIdx2Pos(0, 13, 0, 13);
        assertIdx2Pos(0, 14, 0, 13);
        assertIdx2Pos(0, 19, 0, 13);
        assertIdx2Pos(0, 20, 0, 18);

        assertIdx2Pos(1, 10, 1, 10);
        assertIdx2Pos(1, 11, 1, 10);
        assertIdx2Pos(1, 15, 1, 10);
        assertIdx2Pos(1, 16, 2, 10);
        assertIdx2Pos(1, 26, 2, 20);
        assertIdx2Pos(1, 27, 2, 20);
        assertIdx2Pos(1, 32, 2, 25);
    },

    "test fold documentToScreen": function() {
        var session = createFoldTestSession();
        function assertDoc2Screen(docRow, docCol, screenRow, screenCol) {
            assert.position(
                session.documentToScreenPosition(docRow, docCol),
                screenRow, screenCol
            );
        }

        // One fold ending in the same row.
        assertDoc2Screen(0,  0, 0, 0);
        assertDoc2Screen(0, 13, 0, 13);
        assertDoc2Screen(0, 14, 0, 13);
        assertDoc2Screen(0, 17, 0, 13);
        assertDoc2Screen(0, 18, 0, 20);

        // Fold ending on some other row.
        assertDoc2Screen(1,  0, 1, 0);
        assertDoc2Screen(1, 10, 1, 10);
        assertDoc2Screen(1, 11, 1, 10);
        assertDoc2Screen(1, 99, 1, 10);

        assertDoc2Screen(2,  0, 1, 10);
        assertDoc2Screen(2,  9, 1, 10);
        assertDoc2Screen(2, 10, 1, 16);
        assertDoc2Screen(2, 11, 1, 17);

        // Fold in the same row with fold over more then one row in the same row.
        assertDoc2Screen(2, 19, 1, 25);
        assertDoc2Screen(2, 20, 1, 26);
        assertDoc2Screen(2, 21, 1, 26);

        assertDoc2Screen(2, 24, 1, 26);
        assertDoc2Screen(2, 25, 1, 32);
        assertDoc2Screen(2, 26, 1, 33);
        assertDoc2Screen(2, 99, 1, 40);

        // Test one position after the folds. Should be all like normal.
        assertDoc2Screen(3,  0, 2,  0);
    },

    "test fold screenToDocument": function() {
        var session = createFoldTestSession();
        function assertScreen2Doc(docRow, docCol, screenRow, screenCol) {
            assert.position(
                session.screenToDocumentPosition(screenRow, screenCol),
                docRow, docCol
            );
        }

        // One fold ending in the same row.
        assertScreen2Doc(0,  0, 0, 0);
        assertScreen2Doc(0, 13, 0, 13);
        assertScreen2Doc(0, 13, 0, 14);
        assertScreen2Doc(0, 18, 0, 20);
        assertScreen2Doc(0, 19, 0, 21);

        // Fold ending on some other row.
        assertScreen2Doc(1,  0, 1, 0);
        assertScreen2Doc(1, 10, 1, 10);
        assertScreen2Doc(1, 10, 1, 11);

        assertScreen2Doc(1, 10, 1, 15);
        assertScreen2Doc(2, 10, 1, 16);
        assertScreen2Doc(2, 11, 1, 17);

        // Fold in the same row with fold over more then one row in the same row.
        assertScreen2Doc(2, 19, 1, 25);
        assertScreen2Doc(2, 20, 1, 26);
        assertScreen2Doc(2, 20, 1, 27);

        assertScreen2Doc(2, 20, 1, 31);
        assertScreen2Doc(2, 25, 1, 32);
        assertScreen2Doc(2, 26, 1, 33);
        assertScreen2Doc(2, 33, 1, 99);

        // Test one position after the folds. Should be all like normal.
        assertScreen2Doc(3,  0, 2,  0);
    },

    "test getFoldsInRange()": function() {
        var session = createFoldTestSession();
        var foldLines = session.$foldData;
        var folds = foldLines[0].folds.concat(foldLines[1].folds);

        function test(startRow, startColumn, endColumn, endRow, folds) {
            var r = new Range(startRow, startColumn, endColumn, endRow);
            var retFolds = session.getFoldsInRange(r);

            assert.ok(retFolds.length == folds.length);
            for (var i = 0; i < retFolds.length; i++) {
                assert.equal(retFolds[i].range + "", folds[i].range + "");
            }
        }

        test(0, 0, 0, 13,  [ ]);
        test(0, 0, 0, 14,  [ folds[0] ]);
        test(0, 0, 0, 18,  [ folds[0] ]);
        test(0, 0, 1, 10,  [ folds[0] ]);
        test(0, 0, 1, 11,  [ folds[0], folds[1] ]);
        test(0, 18, 1, 11, [ folds[1] ]);
        test(2, 0,  2, 13, [ folds[1] ]);
        test(2, 10, 2, 20, [ ]);
        test(2, 10, 2, 11, [ ]);
        test(2, 19, 2, 20, [ ]);
    },

    "test fold one-line text insert": function() {
        // These are mostly test for the FoldLine.addRemoveChars function.
        var session = createFoldTestSession();
        var undoManager = session.getUndoManager();
        var foldLines = session.$foldData;

        function insert(row, column, text) {
            session.insert({row: row, column: column}, text);

            // Force the session to store all changes made to the document NOW
            // on the undoManager's queue. Otherwise we can't undo in separate
            // steps later.
            session.$syncInformUndoManager();
        }

        var foldLine, fold, folds;
        // First line.
        foldLine = session.$foldData[0];
        fold = foldLine.folds[0];

        insert(0, 0, "0");
        assert.range(foldLine.range, 0, 14, 0, 19);
        assert.range(fold.range,     0, 14, 0, 19);
        insert(0, 14, "1");
        assert.range(foldLine.range, 0, 15, 0, 20);
        assert.range(fold.range,     0, 15, 0, 20);
        insert(0, 20, "2");
        assert.range(foldLine.range, 0, 15, 0, 20);
        assert.range(fold.range,     0, 15, 0, 20);

        // Second line.
        foldLine = session.$foldData[1];
        folds = foldLine.folds;

        insert(1, 0, "3");
        assert.range(foldLine.range, 1, 11, 2, 25);
        assert.range(folds[0].range, 1, 11, 2, 10);
        assert.range(folds[1].range, 2, 20, 2, 25);

        insert(1, 11, "4");
        assert.range(foldLine.range, 1, 12, 2, 25);
        assert.range(folds[0].range, 1, 12, 2, 10);
        assert.range(folds[1].range, 2, 20, 2, 25);

        insert(2, 10, "5");
        assert.range(foldLine.range, 1, 12, 2, 26);
        assert.range(folds[0].range, 1, 12, 2, 10);
        assert.range(folds[1].range, 2, 21, 2, 26);

        insert(2, 21, "6");
        assert.range(foldLine.range, 1, 12, 2, 27);
        assert.range(folds[0].range, 1, 12, 2, 10);
        assert.range(folds[1].range, 2, 22, 2, 27);

        insert(2, 27, "7");
        assert.range(foldLine.range, 1, 12, 2, 27);
        assert.range(folds[0].range, 1, 12, 2, 10);
        assert.range(folds[1].range, 2, 22, 2, 27);

        // UNDO = REMOVE
        undoManager.undo(); // 6
        assert.range(foldLine.range, 1, 12, 2, 27);
        assert.range(folds[0].range, 1, 12, 2, 10);
        assert.range(folds[1].range, 2, 22, 2, 27);

        undoManager.undo(); // 5
        assert.range(foldLine.range, 1, 12, 2, 26);
        assert.range(folds[0].range, 1, 12, 2, 10);
        assert.range(folds[1].range, 2, 21, 2, 26);

        undoManager.undo(); // 4
        assert.range(foldLine.range, 1, 12, 2, 25);
        assert.range(folds[0].range, 1, 12, 2, 10);
        assert.range(folds[1].range, 2, 20, 2, 25);

        undoManager.undo(); // 3
        assert.range(foldLine.range, 1, 11, 2, 25);
        assert.range(folds[0].range, 1, 11, 2, 10);
        assert.range(folds[1].range, 2, 20, 2, 25);

        undoManager.undo(); // Beginning first line.
        assert.equal(foldLines.length, 2);
        assert.range(foldLines[0].range, 0, 15, 0, 20);
        assert.range(foldLines[1].range, 1, 10, 2, 25);

        foldLine = session.$foldData[0];
        fold = foldLine.folds[0];

        undoManager.undo(); // 2
        assert.range(foldLine.range, 0, 15, 0, 20);
        assert.range(fold.range,     0, 15, 0, 20);

        undoManager.undo(); // 1
        assert.range(foldLine.range, 0, 14, 0, 19);
        assert.range(fold.range,     0, 14, 0, 19);

        undoManager.undo(); // 0
        assert.range(foldLine.range, 0, 13, 0, 18);
        assert.range(fold.range,     0, 13, 0, 18);
    },

    "test fold multi-line insert/remove": function() {
        var session = createFoldTestSession(),
            undoManager = session.getUndoManager(),
            foldLines = session.$foldData;
        function insert(row, column, text) {
            session.insert({row: row, column: column}, text);
            // Force the session to store all changes made to the document NOW
            // on the undoManager's queue. Otherwise we can't undo in separate
            // steps later.
            session.$syncInformUndoManager();
        }

        var foldLines = session.$foldData, foldLine, fold, folds;

        insert(0, 0, "\nfo0");
        assert.equal(foldLines.length, 2);
        assert.range(foldLines[0].range, 1, 16, 1, 21);
        assert.range(foldLines[1].range, 2, 10, 3, 25);

        insert(2, 0, "\nba1");
        assert.equal(foldLines.length, 2);
        assert.range(foldLines[0].range, 1, 16, 1, 21);
        assert.range(foldLines[1].range, 3, 13, 4, 25);

        insert(3, 10, "\nfo2");
        assert.equal(foldLines.length, 2);
        assert.range(foldLines[0].range, 1, 16, 1, 21);
        assert.range(foldLines[1].range, 4,  6, 5, 25);

        insert(5, 10, "\nba3");
        assert.equal(foldLines.length, 3);
        assert.range(foldLines[0].range, 1, 16, 1, 21);
        assert.range(foldLines[1].range, 4,  6, 5, 10);
        assert.range(foldLines[2].range, 6, 13, 6, 18);

        insert(6, 18, "\nfo4");
        assert.equal(foldLines.length, 3);
        assert.range(foldLines[0].range, 1, 16, 1, 21);
        assert.range(foldLines[1].range, 4,  6, 5, 10);
        assert.range(foldLines[2].range, 6, 13, 6, 18);

        undoManager.undo(); // 3
        assert.equal(foldLines.length, 3);
        assert.range(foldLines[0].range, 1, 16, 1, 21);
        assert.range(foldLines[1].range, 4,  6, 5, 10);
        assert.range(foldLines[2].range, 6, 13, 6, 18);

        undoManager.undo(); // 2
        assert.equal(foldLines.length, 2);
        assert.range(foldLines[0].range, 1, 16, 1, 21);
        assert.range(foldLines[1].range, 4,  6, 5, 25);

        undoManager.undo(); // 1
        assert.equal(foldLines.length, 2);
        assert.range(foldLines[0].range, 1, 16, 1, 21);
        assert.range(foldLines[1].range, 3, 13, 4, 25);

        undoManager.undo(); // 0
        assert.equal(foldLines.length, 2);
        assert.range(foldLines[0].range, 1, 16, 1, 21);
        assert.range(foldLines[1].range, 2, 10, 3, 25);

        undoManager.undo(); // Beginning
        assert.equal(foldLines.length, 2);
        assert.range(foldLines[0].range, 0, 13, 0, 18);
        assert.range(foldLines[1].range, 1, 10, 2, 25);
        // TODO: Add test for inseration inside of folds.
    },

    "test fold wrap data compution": function() {
        function assertWrap(line0, line1, line2) {
            line0 && assertArray(wrapData[0], line0);
            line1 && assertArray(wrapData[1], line1);
            line2 && assertArray(wrapData[2], line2);
        }

        function removeFoldAssertWrap(docRow, docColumn, line0, line1, line2) {
            session.removeFold(session.getFoldAt(docRow, docColumn));
            assertWrap(line0, line1, line2);
        }

        var lines = [
            "foo bar foo bar",
            "foo bar foo bar",
            "foo bar foo bar"
        ];

        var session = new EditSession(lines.join("\n"));
        session.setUseWrapMode(true);
        session.$wrapLimit = 7;
        session.$updateWrapData(0, 2);
        var wrapData = session.$wrapData;

        // Do a simple assertion without folds to check basic functionallity.
        assertWrap([8], [8], [8]);

        // --- Do in line folding ---

        // Adding a fold. The split position is inside of the fold. As placeholder
        // are not splitable, the split should be before the split.
        session.addFold("woot", new Range(0, 4, 0, 15));
        assertWrap([4], [8], [8]);

        // Remove the fold again which should reset the wrapData.
        removeFoldAssertWrap(0, 4, [8], [8], [8]);

        session.addFold("woot", new Range(0, 6, 0, 9));
        assertWrap([6, 13], [8], [8]);
        removeFoldAssertWrap(0, 6, [8], [8], [8]);

        // The fold fits into the wrap limit - no split expected.
        session.addFold("woot", new Range(0, 3, 0, 15));
        assertWrap([], [8], [8]);
        removeFoldAssertWrap(0, 4, [8], [8], [8]);

        // Fold after split position should be all fine.
        session.addFold("woot", new Range(0, 8, 0, 15));
        assertWrap([8], [8], [8]);
        removeFoldAssertWrap(0, 8, [8], [8], [8]);

        // Fold's placeholder is far too long for wrapSplit.
        session.addFold("woot0123456789", new Range(0, 8, 0, 15));
        assertWrap([8], [8], [8]);
        removeFoldAssertWrap(0, 8, [8], [8], [8]);

        // Fold's placeholder is far too long for wrapSplit
        // + content at the end of the line
        session.addFold("woot0123456789", new Range(0, 6, 0, 8));
        assertWrap([6, 20], [8], [8]);
        removeFoldAssertWrap(0, 8, [8], [8], [8]);

        session.addFold("woot0123456789", new Range(0, 6, 0, 8));
        session.addFold("woot0123456789", new Range(0, 8, 0, 10));
        assertWrap([6, 20, 34], [8], [8]);
        session.removeFold(session.getFoldAt(0, 7));
        removeFoldAssertWrap(0, 8, [8], [8], [8]);

        session.addFold("woot0123456789", new Range(0, 7, 0, 9));
        session.addFold("woot0123456789", new Range(0, 13, 0, 15));
        assertWrap([7, 21, 25], [8], [8]);
        session.removeFold(session.getFoldAt(0, 7));
        removeFoldAssertWrap(0, 14, [8], [8], [8]);

        // --- Do some multiline folding ---

        // Add a fold over two lines. Note, that the wrapData[1] stays the
        // same. This is an implementation detail and expected behavior.
        session.addFold("woot", new Range(0, 8, 1, 15));
        assertWrap([8], [8 /* See comments */], [8]);
        removeFoldAssertWrap(0, 8, [8], [8], [8]);

        session.addFold("woot", new Range(0, 9, 1, 11));
        assertWrap([8, 14], [8 /* See comments */], [8]);
        removeFoldAssertWrap(0, 9, [8], [8], [8]);

        session.addFold("woot", new Range(0, 9, 1, 15));
        assertWrap([8], [8 /* See comments */], [8]);
        removeFoldAssertWrap(0, 9, [8], [8], [8]);

        return session;
    },
    
    "test delete fold with wrap enabled": function() {
        var session = new EditSession("");
        session.setValue([
            "This is some placeholder text that will be folded inline.",
            "This is some placeholder text that will be folded inline.",
            "More text.",
            "<p>The cursor in this paragraph text will be offset by 1 row.<p>",
            "<p>Everything after this will be offset as well due to the folds in the row before too.</p>"
        ].join("\n"));
        session.addFold('...', new Range(0, 8, 0, 42));
        session.addFold('...', new Range(1, 8, 1, 42));
        session.addFold('...', new Range(3, 7, 3, 51));
        session.setOption("wrap", 40);
        session.remove(new Range(0,0, 2, 5));
        // needed because adjustWrapLimit is called async from renderer
        session.adjustWrapLimit(80);
        
        assert.equal(session.$wrapData + "", [[], [], [40, 76]] + "");
    },
        
    "test add fold": function() {
        var session = createFoldTestSession();
        var fold;

        function tryAddFold(placeholder, range, shouldFail) {
            var fail = false;
            try {
                fold = session.addFold(placeholder, range);
            } catch (e) {
                fail = true;
            }
            if (fail != shouldFail) {
                throw "Expected to get an exception";
            }
        }

        tryAddFold("foo", new Range(0, 13, 0, 17), false);
        tryAddFold("foo", new Range(0, 14, 0, 18), true);
        tryAddFold("foo", new Range(0, 13, 0, 18), false);
        assert.equal(session.$foldData[0].folds.length, 1);

        tryAddFold("f", new Range(0, 13, 0, 18), false);
        tryAddFold("foo", new Range(0, 18, 0, 21), false);
        assert.equal(session.$foldData[0].folds.length, 2);
        session.removeFold(fold);

        tryAddFold("foo", new Range(0, 18, 0, 22), false);
        tryAddFold("foo", new Range(0, 18, 0, 19), true);
        tryAddFold("foo", new Range(0, 22, 1, 10), false);
    },

    "test add subfolds": function() {
        var session = createFoldTestSession();
        var fold, oldFold;
        var foldData = session.$foldData;

        oldFold = foldData[0].folds[0];

        fold = session.addFold("fold0", new Range(0, 10, 0, 21));
        assert.equal(foldData[0].folds.length, 1);
        assert.equal(fold.subFolds.length, 1);
        assert.equal(fold.subFolds[0], oldFold);

        session.expandFold(fold);
        assert.equal(foldData[0].folds.length, 1);
        assert.equal(foldData[0].folds[0], oldFold);
        assert.equal(fold.subFolds.length, 0);

        fold = session.addFold("fold0", new Range(0, 13, 2, 10));
        assert.equal(foldData.length, 1);
        assert.equal(fold.subFolds.length, 2);
        assert.equal(fold.subFolds[0], oldFold);

        session.expandFold(fold);
        assert.equal(foldData.length, 2);
        assert.equal(foldData[0].folds.length, 1);
        assert.equal(foldData[0].folds[0], oldFold);
        assert.equal(fold.subFolds.length, 0);

        session.unfold(null, true);
        fold = session.addFold("fold0", new Range(0, 0, 0, 21));
        session.addFold("fold0", new Range(0, 1, 0, 5));
        session.addFold("fold0", new Range(0, 6, 0, 8));
        assert.equal(fold.subFolds.length, 2);
    },

    "test row cache": function() {
        var session = createFoldTestSession();

        session.screenToDocumentPosition(2,3);
        assertArray(session.$docRowCache, [1,3]);
        assertArray(session.$screenRowCache, [1,2]);

        session.screenToDocumentPosition(5,3);
        assertArray(session.$docRowCache, [1,3,4]);
        assertArray(session.$screenRowCache, [1,2,3]);

        session.screenToDocumentPosition(0,3);
        assertArray(session.$docRowCache, [1,3,4]);
        assertArray(session.$screenRowCache, [1,2,3]);

        var pos = session.screenToDocumentPosition(0,0);
        assert.equal(pos.row, 0);
        assertArray(session.$docRowCache, [1,3,4]);
        assertArray(session.$screenRowCache, [1,2,3]);
        
        session.screenToDocumentPosition(1,0);
        assertArray(session.$docRowCache, [1,3,4]);
        assertArray(session.$screenRowCache, [1,2,3]);

        session.$resetRowCache();
        assertArray(session.$docRowCache, []);
        assertArray(session.$screenRowCache, []);
        
        session.screenToDocumentPosition(1,3);
        assertArray(session.$docRowCache, [1]);
        assertArray(session.$screenRowCache, [1]);
       
        session.screenToDocumentPosition(5,3);
        assertArray(session.$docRowCache, [1,3,4]);
        assertArray(session.$screenRowCache, [1,2,3]);
        
        session = new EditSession(new Array(30).join("\n"));
        session.documentToScreenPosition(2,0);
        session.documentToScreenPosition(2,0);
        assertArray(session.$docRowCache, [1,2]);
        assertArray(session.$screenRowCache, [1,2]);
    },

    "test annotations": function() {
        var session = new EditSession([]),
            annotation = {row: 0, type: 'info', text: "This is a test."};

        session.clearAnnotations();
        assertArray(session.getAnnotations(), []);
        session.setAnnotations([annotation]);
        assertArray(session.getAnnotations(), [annotation]);
    },
    
    "test: mode loading" : function(next) {
        if (!require.undef) {
            console.log("Skipping test: This test only runs in the browser");
            next();
            return;
        }
        var session = new EditSession([]);
        session.setMode("ace/mode/javascript");
        assert.equal(session.$modeid, "ace/mode/javascript");
        session.on("changeMode", function() {
            assert.equal(session.$modeid, "ace/mode/javascript");
        });
        session.setMode("ace/mode/sh", function(mode) {
            assert.ok(!mode);
        });
        setTimeout(function() {
            session.setMode("ace/mode/javascript", function(mode) {
                session.setMode("ace/mode/javascript");
                assert.equal(session.$modeid, "ace/mode/javascript");
                next();
            });
        }, 0);
    }
};
});

if (typeof module !== "undefined" && module === require.main) {
    require("asyncjs").test.testcase(module.exports).exec();
}
