/*global describe it before beforeEach after bar =*/

"use client";


require(["lib/architect/architect", "lib/chai/chai", "/vfs-root", "ace/test/assertions"], function (architect, chai, baseProc) {
    var expect = chai.expect;
    
    expect.setupArchitectTest([
        {
            packagePath: "plugins/c9.core/c9",
            workspaceId: "johndoe/dev",
            startdate: new Date(),
            debug: true,
            hosted: true,
            local: false,
            davPrefix: "/"
        },
        
        "plugins/c9.core/ext",
        "plugins/c9.core/http-xhr",
        "plugins/c9.core/util",
        "plugins/c9.ide.ui/lib_apf",
        {
            packagePath: "plugins/c9.core/settings",
            testing: true
        },
        "plugins/c9.core/api.js",
        {
            packagePath: "plugins/c9.ide.ui/ui",
            staticPrefix: "plugins/c9.ide.ui"
        },
        "plugins/c9.ide.editors/document",
        "plugins/c9.ide.editors/undomanager",
        "plugins/c9.ide.editors/editors",
        "plugins/c9.ide.editors/editor",
        "plugins/c9.ide.editors/tabmanager",
        "plugins/c9.ide.ui/focus",
        "plugins/c9.ide.editors/pane",
        "plugins/c9.ide.editors/tab",
        "plugins/c9.ide.terminal/terminal",
        "plugins/c9.ide.terminal/predict_echo",
        "plugins/c9.vfs.client/vfs.ping",
        "plugins/c9.ide.preferences/preferences",
        "plugins/c9.ide.ui/forms",
        {
            packagePath: "plugins/c9.fs/proc",
            tmuxName: "cloud9test2"
        },
        "plugins/c9.vfs.client/vfs_client",
        "plugins/c9.vfs.client/endpoint",
        "plugins/c9.ide.auth/auth",
        {
            packagePath: "plugins/c9.fs/fs",
            baseProc: baseProc
        },
        
        // Mock plugins
        {
            consumes: ["apf", "ui", "Plugin"],
            provides: [
                "commands", "menus", "commands", "layout", "watcher", 
                "save", "anims", "clipboard", "dialog.alert", "auth.bootstrap",
                "info", "dialog.error"
            ],
            setup: expect.html.mocked
        },
        {
            consumes: ["tabManager", "proc", "terminal", "terminal.predict_echo", "c9"],
            provides: [],
            setup: main
        }
    ], architect);
    
    function main(options, imports, register) {
        var tabs = imports.tabManager;
        var proc = imports.proc;
        var predictor = imports["terminal.predict_echo"];
        var assert = require("ace/test/assertions");
        
        var ESC = "\u001B";
        var OUTPUT_CURSOR_START = ESC + "[H";
        var INPUT_LEFT = ESC + "[D";
        var INPUT_LEFT_ONCE = ESC + "[1D";
        var INPUT_LEFT_TWICE = ESC + "[2D";
        var INPUT_HOME = ESC + "[1~";
        var INPUT_END = "\u0005";
        var INPUT_RIGHT = ESC + "[C";
        var INPUT_BACKSPACE = "\u007F";
        var INPUT_CONTROL_C = "\u0003";
        var INPUT_DELETE = ESC + "[3~";
        var OUTPUT_BACKSPACE = "\b" + ESC + "[K";
        var OUTPUT_DELETE_CHAR = ESC + "[P";
        var STATE_WAIT_FOR_ECHO_OR_PROMPT = 1;
        var STATE_WAIT_FOR_ECHO = 2;
        var STATE_WAIT_FOR_PROMPT = 3;
        
        expect.html.setConstructor(function(tab) {
            if (typeof tab == "object")
                return tab.pane.aml.getPage("editor::" + tab.editorType).$ext;
        });
        
        describe('terminal.predict_echo', function() {
            this.timeout(30000);
                
            before(function(done) {
                this.timeout(45000);
                apf.config.setProperty("allow-select", false);
                apf.config.setProperty("allow-blur", false);
                
                bar.$ext.style.background = "rgba(220, 220, 220, 0.93)";
                bar.$ext.style.position = "fixed";
                bar.$ext.style.left = "20px";
                bar.$ext.style.right = "20px";
                bar.$ext.style.bottom = "20px";
                bar.$ext.style.height = "33%";
      
                document.body.style.marginBottom = "33%";
                
                predictor.$setTestTimeouts();
                predictor.DEBUG = true;
                
                proc.execFile("~/.c9/bin/tmux", { args: ["-L", "cloud9test", "kill-server"] }, function(err) {
                    tabs.once("ready", function(){
                        tabs.getPanes()[0].focus();
                        openTerminal(done);
                    });
                });
            });
            
            function openTerminal(done) {
                tabs.openEditor("terminal", function(err, tab) {
                    editor = tab.editor;
                    session = editor.ace.getSession().c9session;
                    send = session.send;

                    setTimeout(init);
                    
                    function init() {
                        afterPrompt(function() { setTimeout(start); });
                        // Make sure we have a prompt with a dollar for tests
                        // And terminal won't send rename commands in the middle of the test
                        // TODO: do we need to handle rename sequence in predict_echo instead?
                        editor.ace.onTextInput("PS1='. $ ';"
                            + "tmux setw automatic-rename off;"
                            + "printf '\\x1b]0;predict echo\\x07'\n");
                        // editor.ace.onTextInput("ssh lennart\n");
                        // editor.ace.onTextInput("ssh ubuntu@ci.c9.io\n");
                    }
                });
                function start() {
                    predictor.on("mispredict", function(e) {
                        console.error("MISPREDICTED", e)
                        delete e.session;
                        throw new Error("MISPREDICTED: " + JSON.stringify(e));
                    });
                    setTimeout(done)
                }
            }
            
            function peek(offset) {
                offset = offset || 0;
                var char = session.terminal.getCharAt(
                    session.terminal.y, session.terminal.x + offset);
                return char && char[1];
            }
            
            function afterPrompt(callback) {
                // Expect a prompt to appear
                editor.on("beforeWrite", function wait(e) {
                    if (!e.data.match(/\$ ([\s\S]*\u001B\[\d+;\d+H)?$/))
                        return;
                    if (session.$predictor.predictions.length
                        && !session.$predictor.predictions[session.$predictor.predictions.length - 1].optional)
                        return; // probably a false positive
                    editor.off("beforeWrite", wait);
                    
                    console.log("  ^ prompt; proceeding with test");
                    editor.once("afterWrite", function() {
                        // Make sure we're after predict_echo's afterWrite
                        callback();
                    });
                });
            }
            
            function afterPredict(text, callback) {
                predictor.on("predict", function wait(e) {
                    if (e.data.indexOf(text) === -1
                        && e.predictions.filter(function(p) {
                            return (p.$outputText || "").indexOf(text) !== -1
                        }).length === 0)
                        return;
                    predictor.off("predict", wait);
                    callback(e);
                });
            }
            
            var editor;
            var session;
            var send;
            
            function sendAll(keys, callback) {
                var key = keys.shift();
                if (!key)
                    return callback && callback();
                setTimeout(function() {
                    send(key);
                    sendAll(keys, callback);
                });
            }
            
            describe("predict_echo", function(){
                beforeEach(function(done) {
                    afterPredict("*", function() {
                        afterPrompt(function() {
                            session.$predictor.state = 0;
                            done();
                        });
                        send("\r");
                    });
                    session.$predictor.state = 0;
                    sendAll(" # next*".split(""));
                });
            
                it("should predict a single character", function(done) {
                    predictor.on("predict", function wait(e) {
                        if (!e.data.match(/:/))
                            return;
                        predictor.off("predict", wait);
                        done();
                    });
                    sendAll([":"]);
                });
            
                it("should predict multiple characters sent at the same time", function(done) {
                    afterPredict("!", function() {
                        afterPrompt(function() {
                            done();
                        })
                        send("\r");
                    });
                    sendAll([": test!"]);
                });
                
                it("should predict a single character straight from keyboard input", function(done) {
                    predictor.on("predict", function wait(e) {
                        if (!e.data.match(/:/))
                            return;
                        predictor.off("predict", wait);
                        done();
                    });
                    editor.ace.onTextInput(":");
                });
                
                
                it("should predict multiple character", function(done) {
                    predictor.on("predict", function wait(e) {
                        if (!e.data.match(/t/))
                            return;
                        predictor.off("predict", wait);
                        assert.equal(peek(-1), "t");
                        done();
                    });
                    
                    sendAll("ls -lt".split(""));
                });
                
                it("gracefully copes with a newline", function(done) {
                    // Expect \r
                    predictor.on("nopredict", function wait(e) {
                        if (e.data.indexOf("\r") === -1)
                            return;
                        predictor.off("nopredict", wait);
                        seenNewLine = true;
                    });
                    afterPrompt(function() {
                        assert(seenNewLine);
                        done();
                    });
                    
                    var seenNewLine;
                    
                    sendAll(["\r"]);
                });
                
                it("supports cursor key left/right", function(done) {
                    afterPredict("h", function() {
                        sendAll([INPUT_LEFT, "c"]);
                    });
                    afterPredict("c", function() {
                        sendAll([INPUT_RIGHT, "o"]);
                    });
                    afterPredict("o", function() {
                        assert.equal(peek(-4), "e");
                        assert.equal(peek(-3), "c");
                        assert.equal(peek(-2), "h");
                        assert.equal(peek(-1), "o");
                        
                        afterPrompt(done);
                        send("\r");
                    });
                    
                    sendAll(["e", "h"]);
                });
                
                // Fails on CI server (https://github.com/c9/newclient/issues/9550)
                it.skip("supports delete with repeated characters; stress test", function loop(done, attempt) {
                    this.timeout && this.timeout(60000);
                    session.$predictor.state = 0;
                    if (attempt === 5)
                        return done();
                    
                    afterPredict("[", function() {
                        afterPredict("[", function() {
                            assert.equal(peek(-1), " ");
                            assert.equal(peek(), "e");
                            assert.equal(peek(1), "c");

                            afterPrompt(loop.bind(null, done, (attempt || 0) + 1));
                            send("\r");
                        });
                        sendAll([INPUT_DELETE]);
                    })
                    
                    sendAll(["eecho blaaat", INPUT_HOME]);
                });
                
                // slow, useless; skip
                it.skip("supports long sequence of chars; stress test", function loop(done, attempt) {
                    this.timeout && this.timeout(60000);
                    session.$predictor.state = 0;
                    if (attempt === 4)
                        return done();
                    
                    afterPredict("?", function() {
                        send("\r");
                        afterPrompt(loop.bind(null, done, (attempt || 0) + 1));
                    });
                    
                    sendAll("echo this is a pretty long sequence of characters, right?".split(""));
                });
                
                it("supports short sequence of chars; stress test", function loop(done, attempt) {
                    this.timeout && this.timeout(90000);
                    session.$predictor.state = 0;
                    if (attempt === 5)
                        return done();
                    
                    afterPredict("?", function() {
                        send("\r");
                        afterPrompt(loop.bind(null, done, (attempt || 0) + 1));
                    });
                    
                    sendAll("echo ?".split(""));
                });
                
                it("supports short sequence of chars with a newline; stress test", function loop(done, attempt) {
                    this.timeout && this.timeout(90000);
                    session.$predictor.state = 0;
                    if (attempt === 5)
                        return done();
                    
                    afterPredict("?", function() {
                        afterPrompt(loop.bind(null, done, (attempt || 0) + 1));
                    });
                    
                    session.$predictor.state = 0;
                    sendAll("echo 2?\r".split(""));
                });
                
                it("supports backspace with repeated characters", function(done) {
                    afterPredict("[", function() {
                        afterPredict("[", function() {
                            afterPredict("[", function() {
                                assert.equal(peek(-1), " ");
                                assert.equal(peek(), "e");
                                assert.equal(peek(1), "c");

                                afterPrompt(done);
                                send("\r");
                            });
                            sendAll([INPUT_BACKSPACE]);
                        });
                        sendAll([INPUT_RIGHT]);
                    })
                    
                    sendAll(["eecho bleep", INPUT_HOME]);
                });
                
                it("supports insert with repeated characters; stress test", function loop(done, attempt) {
                    this.timeout && this.timeout(60000);
                    session.$predictor.state = 0;
                    if (attempt === 5)
                        return done();
                    
                    sendAll("echo blaat".split(""), function() {
                        var sawX;
                        
                        afterPredict("t", function() {
                            assert.equal(peek(-3), "a");
                            sendAll([INPUT_LEFT, INPUT_LEFT, INPUT_LEFT, "x", "a"]);
                        });
                        predictor.on("predict", function wait(e) {
                            sawX = sawX || e.data.match(/x/);
                            if (!sawX || e.data.match(/xaat/) || !e.data.match(/a/))
                                return; // console.log("  -", e.data, sawX)*
                            predictor.off("predict", wait);

                            assert.equal(peek(), "a");
                            assert.equal(peek(1), "a");
                            assert.equal(peek(-1), "a");

                            afterPrompt(loop.bind(null, done, (attempt || 0) + 1));
                            send("\r");
                        });
                    });
                });
                
                it("supports insert with home and repeated characters", function(done) {
                    afterPredict("t", function() {
                        assert.equal(peek(-3), "a");
                        sendAll(["x", INPUT_HOME, "e"])
                    });
                    predictor.on("predict", function wait(e) {
                        sawX = sawX || e.data.match(/x/);
                        if (!sawX || !e.data.match(/a/))
                            return;
                        predictor.off("predict", wait);
                        
                        assert.equal(peek(), "e");
                        assert.equal(peek(-1), "e");
                        
                        afterPrompt(done);
                        sendAll([INPUT_HOME, "#", "\r"]);
                    });
                    
                    var sawX;
                    
                    sendAll("echo blaat".split(""));
                });
                
                it("supports at least one spurious left cursor; stress test", function loop(done, attempt) {
                    this.timeout && this.timeout(60000);
                    session.$predictor.state = 0;
                    if (attempt === 10)
                        return done();
                    
                    afterPredict("c", function() {
                        assert.equal(peek(-1), "c");
                        assert.equal(peek(-2), "e");
                        assert.equal(peek(-3), " ");
                        
                        afterPrompt(function() {
                            loop(done, (attempt || 0) + 1)
                        });
                        send("\r");
                    });
                    
                    sendAll(["e", "h", "o", INPUT_LEFT, INPUT_LEFT, INPUT_LEFT, INPUT_RIGHT, "c"]);
                });
                
                it("supports home", function(done) {
                    predictor.on("predict", function wait(e) {
                        if (e.data.match(/^[a-z]*$/))
                            return;
                        predictor.off("predict", wait);
                        assert.equal(peek(), "e");
                        afterPrompt(done);
                        sendAll(["\r"]);
                    });
                    
                    sendAll(["e", "c", "h", "o", INPUT_HOME]);
                });
                
                // TODO: rewrite this to support delayed enabling?
                it.skip("enables and disables itself automatically; stress test", function loop(done, attempt) {
                    this.timeout && this.timeout(60000);
                    if (attempt === 5)
                        return done();
                    
                    predictor.once("nopredict", function() {
                        predictor.once("nopredict", function(e) {
                            assert.equal(e.data, ":");

                            afterPredict("i", function() {
                                afterPrompt(function() {
                                    loop(done, (attempt || 0) + 1);
                                });

                                send("\r");
                            });
                            
                            sendAll(" hoi".split(""));
                        });
                        
                        // sometimes backspace will re-enable state 0; we reset it here
                        session.$predictor.state = STATE_WAIT_FOR_ECHO_OR_PROMPT;
                        send(":");
                    });
                  
                    session.$predictor.state = STATE_WAIT_FOR_ECHO_OR_PROMPT;
                    send(INPUT_BACKSPACE);
                });
                
                it("correctly handles duplicate states", function(done) {
                    afterPredict("y", function() {
                        afterPrompt(done);
                        predictor.off("nopredict", fail);
                        send("\r");
                    });
                    predictor.on("nopredict", fail);
                    function fail(e) {
                        assert(false, "Prediction got disabled");
                    }
                    
                    var repeat = ["x", INPUT_BACKSPACE, "x", INPUT_BACKSPACE, "x", INPUT_BACKSPACE];
                    repeat = repeat.concat(repeat).concat(repeat);
                    sendAll(repeat, function() {
                        setTimeout(function() {
                            sendAll(repeat, function() {
                                setTimeout(function() {
                                    sendAll(repeat, function() {
                                        send("y");
                                    });
                                }, 300);
                            });
                        }, 200);
                    });
                });
                
                it("correctly handles repeated keys without pauses", function(done) {
                    afterPredict(":", function() {
                        afterPrompt(done);
                        send("\r");
                    });
                    sendAll("echo".split(""), function() {
                        send(INPUT_BACKSPACE);
                        send(INPUT_BACKSPACE);
                        send(INPUT_BACKSPACE);
                        send(INPUT_BACKSPACE);
                        send(":");
                        
                    });
                });
            });
            
            if (!onload.remain) {
                after(function(done) {
                    tabs.unload();
                    bar.destroy(true, true);
                    
                    document.body.style.marginBottom = "";
                    done();
                });
            }
        });
        
        onload && onload();
    }
});
