/* global AJS:false */
AJS.test.require("com.atlassian.bitbucket.server.bitbucket-it-javascript:diff-tree-test", function() {
    /*jshint maxlen:450*/

    var $ = require('jquery');
    var _ = require('lodash');
    var pageState = require('bitbucket/internal/model/page-state');
    var difftree = require('bitbucket/internal/feature/commit/difftree');
    var Revision = require('bitbucket/internal/model/revision');
    var CommitRange = require('bitbucket/internal/model/commit-range');
    var nav = require('bitbucket/util/navbuilder');
    var computeTree = difftree.computeTree;
    var compareTreeNodes = difftree.compareTreeNodes;
    var getNodeToSelect = difftree.getNodeToSelect;
    var getPathFromRoot = difftree.getPathFromRoot;
    var DiffTree = difftree.DiffTree;

    sinon.stub(pageState, "getProject", function () {
        return {
            getKey: function() { return "PROJECT"; }
        };
    });
    sinon.stub(pageState, "getRepository", function () {
        return {
            getSlug : function() { return "REPO"; }
        };
    });

    function get$FileTreeContainer() {
        var container = $('#qunit-fixture').find('.file-tree-container');
        if (container.length === 1) {
            return container;
        } else {
            return $('#qunit-fixture').append('<div class="file-tree-container"></div>');
        }
    }

    function get$DiffTreeWrapper() {
        return $('<div class="file-tree-wrapper"><div class="file-tree"/></div></div>').appendTo(
            get$FileTreeContainer());
    }

    function get$ToolbarWrapper() {
        return $('<div class="diff-tree-toolbar"></div>').appendTo(get$FileTreeContainer());
    }

    module('difftree', {
        setup : function() {
        },
        teardown : function() {
            if (this.server) {
                this.server.restore();
                this.server = null;
            }
        }
    });


    test('compute single level tree', function() {
        var tree = computeTree(createChangeList(
            ['A', 'file1.txt'],
            ['A', 'file2.txt'],
            ['A', 'file3.txt'],
            ['A', 'file4.txt'],
            ['A', 'file5.txt']
        ));
        assertTreeStructure(tree, 1);
        strictEqual(tree.children.length, 5);
        strictEqual(tree.children[0].data.title, "file1.txt");
        strictEqual(tree.children[1].data.title, "file2.txt");
        strictEqual(tree.children[2].data.title, "file3.txt");
        strictEqual(tree.children[3].data.title, "file4.txt");
        strictEqual(tree.children[4].data.title, "file5.txt");
    });

    test('compute deep tree', function() {
        var tree = computeTree(createChangeList(
            ['A', 'file.txt'],
            ['A', 'path/to/very/deep/file.txt'],
            ['A', 'path/to/file.txt']
        ));
        assertTreeStructure(tree, 3);
        strictEqual(tree.children.length, 2);
        strictEqual(tree.children[0].data.title, "path/to");
        strictEqual(tree.children[1].data.title, "file.txt");

        strictEqual(tree.children[0].children.length, 2);
        strictEqual(tree.children[0].children[0].data.title, "very/deep");
        strictEqual(tree.children[0].children[1].data.title, "file.txt");

        strictEqual(tree.children[0].children[0].children.length, 1);
        strictEqual(tree.children[0].children[0].children[0].data.title, "file.txt");
    });

    test('compute conflicting tree', function() {
        var tree = computeTree(createChangeList(
            ['D', 'LICENSE'],
            ['A', 'LICENSE/GPL.txt'],
            ['A', 'LICENSE/MIT.txt']
        ));
        assertTreeStructure(tree, 2);
        strictEqual(tree.children.length, 2);
        strictEqual(tree.children[0].data.title, "LICENSE");
        strictEqual(tree.children[1].data.title, "LICENSE");

        strictEqual(tree.children[0].children.length, 2);
        strictEqual(tree.children[0].children[0].data.title, "GPL.txt");
        strictEqual(tree.children[0].children[1].data.title, "MIT.txt");
    });

    test('file count', function() {
        var tree = computeTree(createChangeList(
            ['A', 'file.txt'],
            ['A', 'path/to/very/deep/file.txt'],
            ['A', 'path/to/file.txt']
        ));
        strictEqual(DiffTree.prototype._getFileCount(tree, 1), 1);
        strictEqual(DiffTree.prototype._getFileCount(tree, 3), 3);
        strictEqual(DiffTree.prototype._getFileCount(tree, 100), 3);
    });

    test('open tree - open files', function() {
        function file(children) {
            return {
                metadata: {isFile: true},
                children: _.range(children).map(_.partial(file, 0))
            };
        }
        var tree = {
            children: [
                file(0), file(2), file(3), file(1), file(2)
            ],
            metadata: {isDirectory: true},
            data: {}
        };
        function checkOpenTree(expected, max) {
            var tree1 = difftree._openTree($.extend(true, {}, tree), max);
            var openFiles = _.reduce(tree1.children, function(count, child) {
                return count + (child.state === 'open' ? 1 : 0);
            }, 0);
            equal(openFiles, expected, "Open files expected");
            equal(tree1.state, 'open', "Root node always open");
        }

        [0, 1, 1, 2, 2, 2, 3, 4, 4].forEach(checkOpenTree);
    });


    function getNestedCollapsedTree() {
        return computeTree(createChangeList(
            ['A', 'path/a/change0.txt'],
            ['A', 'path/b/change1.txt'],
            ['A', 'path/b/change2.txt'],
            ['A', 'path/b/change3.txt'],
            ['A', 'path/c/aaa/dir/change4.txt'],
            ['A', 'path/c/dir/dir/dir/change5.txt'],
            ['A', 'path/c/dir/dir/change6.txt'],
            ['A', 'path/c/dir/eee/change7.txt'],
            ['A', 'path/c/change8.txt'],
            ['A', 'change9.txt']
        ), 1);
    }

    asyncTest('expand til next file', function() {

        var treeData = getNestedCollapsedTree();
        var commitRange = new CommitRange({
            untilRevision: new Revision({ id: "hash" }),
            sinceRevision: new Revision({ id: "parentHash"})
        });
        var tree = new DiffTree(get$DiffTreeWrapper(), get$ToolbarWrapper(), commitRange);
        var expectedVisibleLeaves = [2, 5, 5, 5, 7, 9, 9, 10, 10, 10];      //leaves will be visible as folders are opened

        tree.data = treeData;
        tree.init().always(function() {
            for (var i = 0; i < 10; i++) {
                strictEqual(tree.getSelectedFile().find('a').attr('id'), 'change' + i);
                strictEqual($('.jstree-leaf').length, expectedVisibleLeaves[i]);
                tree.openNextFile();
            }
            strictEqual(tree.getSelectedFile().find('a').attr('id'), 'change9');
            setTimeout(start);
        });
    });

    asyncTest('expand til prev file', function() {

        var treeData = getNestedCollapsedTree();
        var commitRange = new CommitRange({
            untilRevision: new Revision({ id: "hash" }),
            sinceRevision: new Revision({ id: "parentHash"})
        });
        var tree = new DiffTree(get$DiffTreeWrapper(), get$ToolbarWrapper(), commitRange);
        var expectedVisibleLeaves = [2, 3, 4, 5, 6, 7, 10, 10, 10, 10];
        var lastChangeId = 9;
        var expectedChangeId;

        tree.data = treeData;
        tree.init().always(function() {

            $('#change' + lastChangeId).click();// start from the bottom leaf

            for (var i = 0; i < 10; i++) {
                expectedChangeId = (lastChangeId - i);  // since we started with the last change
                strictEqual(tree.getSelectedFile().find('a').attr('id'), 'change' + expectedChangeId);
                strictEqual($('.jstree-leaf').length, expectedVisibleLeaves[i]);
                tree.openPrevFile();
            }
            strictEqual(tree.getSelectedFile().find('a').attr('id'), 'change0');
            setTimeout(start);
        });
    });

    asyncTest('create tree in dom', function() {
        function createUrlMatcher(callback) {
            function Matcher() {}
            Matcher.test = callback;
            return Matcher;
        }

        function generateChanges(n) {
            var changes = [];
            for (var i = 1; i <= n; i++) {
                [].push.apply(changes, createChangeList(['A', 'file' + i + '.txt']));
            }
            return changes;
        }
        this.server = sinon.fakeServer.create();

        this.server.respondWith("GET", createUrlMatcher(function(url) {
            /* global Uri:false */
            var uri = new Uri(url);
            return RegExp(nav.rest().project('PROJECT').repo("REPO").commit('hash').changes().build()).test(uri.path()) &&
                uri.query().getParamValue("since") === 'parentHash';
        }), [200, { "Content-Type": "application/json" }, JSON.stringify({
            start : 0,
            size : 200,
            isLastPage : true,
            values : generateChanges(200)
        })]);

        var commitRange = new CommitRange({
            untilRevision: new Revision({ id: "hash" }),
            sinceRevision: new Revision({ id: "parentHash"})
        });
        var tree = new DiffTree(get$DiffTreeWrapper(), get$ToolbarWrapper(), commitRange);

        var initPromise = tree.init();
        var wrapperDeferred = $.Deferred();
        initPromise.then(wrapperDeferred.resolve, wrapperDeferred.reject);

        this.server.respond();

        wrapperDeferred.done(function() {
            strictEqual($(".file-tree").children("ul").children("li").length, 200);
            setTimeout(start);
        });

        var timedOut = false;
        wrapperDeferred.fail(function() {
            ok(false, timedOut ? "Timed out" : "init failed.");
            setTimeout(start);
        });

        // if it hasn't resolved/rejected in 1s, kill it.
        setTimeout(function() {
            timedOut = true;
            wrapperDeferred.reject();
        }, 1000);
    });

    test('sort tree nodes', function() {
        var aDirectory = { children : [], data : { title : "dir" }, metadata : { isDirectory : true } };
        var bDirectory = { children : [], data : { title : "git" }, metadata : { isDirectory : true } };
        var aFile = { data : { title : "file.txt" }, metadata : { isFile : true } };
        var bFile = { data : { title : "git" }, metadata : { isFile : true } };

        ok(compareTreeNodes(aDirectory, bDirectory) < 0);
        ok(compareTreeNodes(aDirectory, aFile) < 0);
        ok(compareTreeNodes(bDirectory, aFile) < 0);
        ok(compareTreeNodes(bDirectory, bFile) < 0);
        ok(compareTreeNodes(aFile, bFile) < 0);
        ok(compareTreeNodes(bDirectory, aDirectory) > 0);
        ok(compareTreeNodes(aFile, aDirectory) > 0);
        ok(compareTreeNodes(aFile, bDirectory) > 0);
        ok(compareTreeNodes(bFile, bDirectory) > 0);
        ok(compareTreeNodes(bFile, aFile) > 0);
    });


    var simpleTestFlattenedTree = {
            children : [ {
                    data : { title : "dir/a/b/c" }, metadata : { isDirectory : true },
                    children : [ {
                            data : { title : "der" }, metadata : { isDirectory : true },
                            children : [
                                { data : { title : "file.txt" }, metadata : { isFile : true } }
                            ]
                        }, {
                            data : { title : "dar" }, metadata : { isDirectory : true },
                            children : [
                                { data : { title : "file.txt" }, metadata : { isFile : true } }
                            ]
                        }
                    ]
                }, {
                    data : { title : "git" }, metadata : { isDirectory : true },
                    children : [
                        { data : { title : "file.txt" }, metadata : { isFile : true } }
                    ]

                }, {
                    data : { title : "gat" }, metadata : { isDirectory : true },
                    children : [
                        { data : { title : "file.txt" }, metadata : { isFile : true } }
                    ]

                },
                { data : { title : "git" }, metadata : { isFile : true } },
                { data : { title : "file.txt" }, metadata : { isFile : true } }

            ]
        };
    var simpleMap = {
            _node : simpleTestFlattenedTree,
            dirabc : {
                _node : simpleTestFlattenedTree.children[0],
                der : {
                    _node : simpleTestFlattenedTree.children[0].children[0],
                    filetxt : { _node : simpleTestFlattenedTree.children[0].children[0].children[0] }
                },
                dar : {
                    _node : simpleTestFlattenedTree.children[0].children[1],
                    filetxt : { _node : simpleTestFlattenedTree.children[0].children[1].children[0] }
                }
            },
            gitDir : {
                _node : simpleTestFlattenedTree.children[1],
                filetxt : { _node : simpleTestFlattenedTree.children[1].children[0] }
            },
            gat : {
                _node : simpleTestFlattenedTree.children[2],
                filetxt : { _node : simpleTestFlattenedTree.children[2].children[0] }
            },
            git : { _node : simpleTestFlattenedTree.children[3] },
            filetxt : { _node : simpleTestFlattenedTree.children[4] }

        };

    test('get node to select - simple', function() {

        var fileOne = simpleMap.dirabc.der.filetxt._node;
        var fileTwo = simpleMap.gitDir.filetxt._node;
        var fileThree = simpleMap.git._node;
        var fileFour = simpleMap.filetxt._node;

        // no preferred
        strictEqual(getNodeToSelect(simpleTestFlattenedTree), fileOne, 'Selects first if no preferred passed in');
        strictEqual(getNodeToSelect(simpleTestFlattenedTree, []), fileOne, 'Selects first if empty preferred components passed in');

        // preferred hit
        strictEqual(getNodeToSelect(simpleTestFlattenedTree, ['dir', 'a', 'b', 'c', 'der', 'file.txt']), fileOne, 'Correctly selects through a collapsed node');
        strictEqual(getNodeToSelect(simpleTestFlattenedTree, ['git', 'file.txt']), fileTwo, 'Correctly selects directory instead of file of same name');
        strictEqual(getNodeToSelect(simpleTestFlattenedTree, ['git']), fileThree, 'Correctly selects file instead of directory of same name');
        strictEqual(getNodeToSelect(simpleTestFlattenedTree, ['file.txt']), fileFour, 'Correctly selects root file');

        //preferred miss
        strictEqual(getNodeToSelect(simpleTestFlattenedTree, ['dir', 'a', 'b', 'dar', 'file.txt']), fileOne, 'Correctly misses partial-match collapsed node');
        strictEqual(getNodeToSelect(simpleTestFlattenedTree, ['dir', 'a', 'b']), fileOne, 'Correctly misses directory level preferred through collapsed node');
        strictEqual(getNodeToSelect(simpleTestFlattenedTree, ['gat']), fileOne, 'Correctly misses directory-level preferred');
        strictEqual(getNodeToSelect(simpleTestFlattenedTree, ['gat', 'file.txt', 'wtf']), fileOne, 'Correctly misses too-long preferred');
        strictEqual(getNodeToSelect(simpleTestFlattenedTree, ['fail.txt']), fileOne, 'Correctly misses root-level non-existent');

    });

    test('get node to select - real-world', function() {
        var shouldSelect;
        var path = ["webapp","standalone","src","main","webapp","static","feature","commit","difftree","difftree.js"];
        var mockFlattenedTree = {
            "metadata":{"isDirectory":true},
            "children":[{
                    "data":{"title":"webapp/standalone/src/main","icon":"folder"},"state":"open",
                    "metadata":{"isDirectory":true},
                    "children":[{
                        "data":{"title":"resources","icon":"folder"},"state":"open",
                        "metadata":{"isDirectory":true},
                        "children":[{
                            "data":{"title":"atlassian-plugin.xml","icon":"file","attr":{"id":"change0","class":"change-type-MODIFY"}},
                            "metadata":{
                                "isFile":true,
                                "path":{
                                    "components":["webapp","standalone","src","main","resources","atlassian-plugin.xml"],
                                    "parent":"webapp/standalone/src/main/resources",
                                    "name":"atlassian-plugin.xml",
                                    "extension":"xml",
                                    "toString":"webapp/standalone/src/main/resources/atlassian-plugin.xml"
                                },
                                "changeType":"MODIFY",
                                "srcPath":null
                            }
                        }]
                    },{
                        "data":{"title":"webapp/static","icon":"folder"},"state":"open",
                        "metadata":{"isDirectory":true},
                        "children":[{
                            "data":{"title":"feature","icon":"folder"},"state":"open",
                            "metadata":{"isDirectory":true},
                            "children":[{
                                "data":{"title":"commit/difftree","icon":"folder"},"state":"open",
                                "metadata":{"isDirectory":true},
                                "children":[
                                    shouldSelect = {
                                        "data":{"title":"difftree.js","icon":"file","attr":{"id":"change1","class":"change-type-MODIFY"}},
                                        "metadata":{
                                            "isFile":true,
                                            "path":{
                                                "components":["webapp","standalone","src","main","webapp","static","feature","commit","difftree","difftree.js"],
                                                "parent":"webapp/standalone/src/main/webapp/static/feature/commit/difftree",
                                                "name":"difftree.js",
                                                "extension":"js",
                                                "toString":"webapp/standalone/src/main/webapp/static/feature/commit/difftree/difftree.js"
                                            },
                                            "changeType":"MODIFY",
                                            "srcPath":null
                                        }
                                    }
                                ]
                            },{
                                "data":{"title":"file-content","icon":"folder"},"state":"open",
                                "metadata":{"isDirectory":true},
                                "children":[{
                                    "data":{"title":"file-content.js","icon":"file","attr":{"id":"change2","class":"change-type-MODIFY"}},
                                    "metadata":{
                                        "isFile":true,
                                        "path":{
                                            "components":["webapp","standalone","src","main","webapp","static","feature","file-content","file-content.js"],
                                            "parent":"webapp/standalone/src/main/webapp/static/feature/file-content",
                                            "name":"file-content.js",
                                            "extension":"js",
                                            "toString":"webapp/standalone/src/main/webapp/static/feature/file-content/file-content.js"
                                        },
                                        "changeType":"MODIFY",
                                        "srcPath":null
                                    }
                                },{
                                    "data":{"title":"file-content.soy","icon":"file","attr":{"id":"change3","class":"change-type-MODIFY"}},
                                    "metadata":
                                    {
                                        "isFile":true,
                                        "path":{
                                            "components":["webapp","standalone","src","main","webapp","static","feature","file-content","file-content.soy"],
                                            "parent":"webapp/standalone/src/main/webapp/static/feature/file-content",
                                            "name":"file-content.soy",
                                            "extension":"soy",
                                            "toString":"webapp/standalone/src/main/webapp/static/feature/file-content/file-content.soy"
                                        },
                                        "changeType":"MODIFY",
                                        "srcPath":null
                                    }
                                }]
                            }]
                        },{
                            "data":{"title":"page/commit","icon":"folder"},"state":"open",
                            "metadata":{"isDirectory":true},
                            "children":[{
                                "data":{"title":"commit.js","icon":"file","attr":{"id":"change4","class":"change-type-MODIFY"}},
                                "metadata":{
                                    "isFile":true,
                                    "path":{
                                        "components":["webapp","standalone","src","main","webapp","static","page","commit","commit.js"],
                                        "parent":"webapp/standalone/src/main/webapp/static/page/commit",
                                        "name":"commit.js",
                                        "extension":"js",
                                        "toString":"webapp/standalone/src/main/webapp/static/page/commit/commit.js"
                                    },
                                    "changeType":"MODIFY",
                                    "srcPath":null
                                }
                            }]
                        }]
                    }]
            }], "state":"open"};

        strictEqual(getNodeToSelect(mockFlattenedTree, path), shouldSelect, 'Correctly finds file even if last child is reached');
    });

    test('get path from root', function() {

        strictEqual(getPathFromRoot(simpleTestFlattenedTree, {}), null, "Returns null when the node isn't in the tree.");

        deepEqual(getPathFromRoot(simpleTestFlattenedTree, simpleMap.dirabc.der.filetxt._node), [
            simpleMap._node,
            simpleMap.dirabc._node,
            simpleMap.dirabc.der._node,
            simpleMap.dirabc.der.filetxt._node
        ], "Correctly finds leaf node");

        deepEqual(getPathFromRoot(simpleTestFlattenedTree, simpleMap.dirabc.der._node), [
            simpleMap._node,
            simpleMap.dirabc._node,
            simpleMap.dirabc.der._node
        ], "Correctly finds non-leaf node");

        deepEqual(getPathFromRoot(simpleTestFlattenedTree, simpleMap.gitDir.filetxt._node), [
            simpleMap._node,
            simpleMap.gitDir._node,
            simpleMap.gitDir.filetxt._node
        ], "Correctly follows dir instead of file");

        deepEqual(getPathFromRoot(simpleTestFlattenedTree, simpleMap.git._node), [
            simpleMap._node,
            simpleMap.git._node
        ], "Correctly follows file instead of dir");

        deepEqual(getPathFromRoot(simpleTestFlattenedTree, simpleTestFlattenedTree), [simpleTestFlattenedTree], "Correctly handles root");
    });

    function createPath(str) {
        var path = {};
        path.toString = str;
        path.components = str.split("/");
        path.name = path.components[path.components.length - 1];
        path.parent = path.components.slice(0, path.components.length - 1).join("/");
        path.extension = path.name.lastIndexOf(".") !== -1 ? path.name.substring(path.name.lastIndexOf(".")) : null;
        return path;
    }

    function getChangeType(c) {
        switch(c.toUpperCase()) {
            case 'A':
                return 'ADD';
            case 'C':
                return 'COPY';
            case 'D':
                return 'DELETE';
            case 'M':
                return 'MODIFY';
            case 'R':
                return 'MOVE';
            default:
                return 'UNKNOWN';
        }
    }

    function createChangeList(/* [type, path[, srcPath]]... */) {
        return _.map(arguments, function(a) {
            return {
                type : getChangeType(a[0]),
                path : createPath(a[1]),
                srcPath : a.length === 3 ? createPath(a[2]) : undefined
            };
        });
    }

    function assertTreeStructure(tree, depthLimit) {
        depthLimit = depthLimit || Infinity;
        ok(tree);
        ok(tree.children);
        ok(tree.metadata);
        ok(tree.metadata.isDirectory);
        _.forEach(tree.children, function(child) {
            ok(child.data);
            ok(child.data.title);
            ok(child.metadata);
            ok(child.metadata.isFile ? !child.metadata.isDirectory : child.metadata.isDirectory);
            if (child.metadata.isFile) {
                ok(child.metadata.path);
                ok(child.metadata.changeType);
            } else {
                ok(depthLimit - 1); // Prevent recursion
                assertTreeStructure(child, depthLimit - 1);
            }
        });
        tree.children.sort(compareTreeNodes); // ensure it is sorted
    }


});