var utils = {}; Components.utils.import(/([^ ]+\/)[^\/]+$/.exec(Components.stack.filename)[1] + "utils.jsm", utils);

var EXPORTED_SYMBOLS = ["Controller"];

const { module, NS } = utils;

var elementslib = module("resource://mozmill/modules/elementslib.js");
var frame = module("resource://mozmill/modules/frame.js");
var jumlib = module("resource://mozmill/modules/jum.js");

function wrapAssertNoErrors(func, message) {
    return function wrapped(arg) this.assertNoErrors(func, this, arguments, message || arg);
}

function assertMessage(funcName, want, got, message) {
    if (typeof want === "string")
        return utils.assertEqual(funcName, want, got, message);
    else if (typeof want === "function") {
        var res = want(got);
        if (res === undefined)
            return true;
        return utils.test(res, {
            function: funcName,
            want: want, got: got,
            comment: message
        });
    }
    else
        return utils.test(want.test(got), {
            function: funcName,
            want: want, got: got,
            comment: message
        });
}

/**
 * A controller for simulating Dactyl related user actions and for making
 * assertions about the expected outcomes of such actions.
 *
 * @param {MozMillController} controller The browser's MozMill controller.
 */
function Controller(controller) {
    this.controller = controller;

    /**
     * @property {object} The dactyl modules namespace, to be used
     * sparingly in tests.
     */
    this.modules = controller.window.dactyl.modules;

    this.errorCount = 0;

    this._countBeep = () => {
        this.beepCount++;
    };
    this.errors = [];
    this._countError = (message, highlight) => {
        if (/\b(Error|Warning)Msg\b/.test(highlight))
            this.errors.push(String(message));
    };
    this.modules.dactyl.registerObserver("beep", this._countBeep);
    this.modules.dactyl.registerObserver("echoLine", this._countError);
    this.modules.dactyl.registerObserver("echoMultiline", this._countError);

    this.resetErrorCount();
}

Controller.prototype = {

    teardown: function () {
        this.modules.dactyl.unregisterObserver("beep", this._countBeep);
        this.modules.dactyl.unregisterObserver("echoLine", this._countError);
        this.modules.dactyl.unregisterObserver("echoMultiline", this._countError);
    },

    beepCount: 0,
    errorCount: 0,

    /**
     * Asserts that an error message is displayed during the execution
     * of *func*.
     *
     * @param {function} func A function to call during before the
     *      assertion takes place.
     * @param {object} self The 'this' object to be used during the call
     *      of *func*. @optional
     * @param {Array} args Arguments to be passed to *func*. @optional
     * @param {string} message The message to display upon assertion failure. @optional
     */
    assertMessageError: function (func, self, args, message) {
        let errorCount = this.errors.length;
        this.assertNoErrors(func, self, args, message);
        // dump("assertMessageError " + errorCount + " " + this.errorMessageCount + "\n");
        return utils.assert('dactyl.assertMessageError', this.errors.length > errorCount,
                            "Expected error but got none" + (message ? ": " + message : ""));
    },

    /**
     * Asserts that any output message text content matches *text*.
     *
     * @param {string|RegExp|function} want The expected text of the expected message line.
     * @param {string} message The message to display upon assertion failure.
     */
    assertMessage: function (want, message) {
        return assertMessage('dactyl.assertMessage', want,
                             this.readMessageWindow() || this.readMessageLine(),
                             message);
    },

    /**
     * Asserts that the output message line text content matches *text*.
     *
     * @param {string|RegExp|function} want The expected text of the expected message line.
     * @param {string} message The message to display upon assertion failure.
     */
    assertMessageLine: function (want, message) {
        return assertMessage('dactyl.assertMessageLine', want,
                             this.readMessageLine(),
                             message);
    },

    /**
     * Asserts that the output message window text content matches *text*.
     *
     * @param {string|RegExp|function} want The expected text of the message window.
     * @param {string} message The message to display upon assertion failure.
     */
    assertMessageWindow: function (want, message) {
        return assertMessage('dactyl.assertMessageWindow', want,
                             this.readMessageWindow(),
                             message);
    },

    /**
     * Asserts that the output message line text is an error and content matches *text*.
     *
     * @param {string|RegExp|function} want The expected text of the expected message line.
     * @param {string} message The message to display upon assertion failure.
     */
    assertErrorMessage: function (want, message) {
        return assertMessage('dactyl.assertMessageError', want,
                             this.readMessageLine(),
                             message) &&
               assertMessage('dactyl.assertMessageError', /\bErrorMsg\b/,
                             this.elements.message.getAttributeNS(NS, "highlight"),
                             message);
    },

    /**
     * Asserts that the multi-line output window is in the given state.
     *
     * @param {boolean} open True if the window is expected to be open.
     * @param {string} message The message to display upon assertion failure. @optional
     */
    assertMessageWindowOpen: function (open, message) {
        return utils.assertEqual('dactyl.assertMessageWindowOpen', open,
                                 !this.elements.multilineContainer.collapsed,
                                 message || "Multi-line output not in the expected state");
    },

    /**
     * Asserts that the no errors have been reported since the last call
     * to resetErrorCount.
     *
     * @param {function} func A function to call during before the
     *      assertion takes place. When present, the current error count
     *      is reset before execution.
     *      @optional
     * @param {object} self The 'this' object to be used during the call
     *      of *func*. @optional
     * @param {Array} args Arguments to be passed to *func*. @optional
     * @param {string} message The message to display upon assertion failure. @optional
     */
    assertNoErrors: function (func, self, args, message) {
        let msg = message ? ": " + message : "";

        let beepCount = this.beepCount;
        let errorCount = this.errorCount;
        if (func) {
            errorCount = this.modules.util.errorCount;

            try {
                var returnVal = func.apply(self || this, args || []);
            }
            catch (e) {
                this.modules.util.reportError(e);
            }
        }

        if (this.beepCount > beepCount)
            frame.log({
                function: "dactyl.beepMonitor",
                want: beepCount, got: this.beepCount,
                comment: "Got " + (this.beepCount - beepCount) + " beeps during execution" + msg
            });

        if (errorCount != this.modules.util.errorCount)
            var errors = this.modules.util.errors.slice(errorCount - this.modules.util.errorCount)
                             .join("\n");

        var res = utils.assertEqual('dactyl.assertNoErrors',
                                    errorCount, this.modules.util.errorCount,
                                    "Errors were reported during the execution of this test" + msg + "\n" + errors);

        return returnVal === undefined ? res : returnVal;
    },

    /**
     * Asserts that the no error messages are reported during the call
     * of *func*.
     *
     * @param {function} func A function to call during before the
     *      assertion takes place. When present, the current error count
     *      is reset before execution.
     *      @optional
     * @param {object} self The 'this' object to be used during the call
     *      of *func*. @optional
     * @param {Array} args Arguments to be passed to *func*. @optional
     * @param {string} message The message to display upon assertion failure. @optional
     */
    assertNoErrorMessages: function (func, self, args, message) {
        let msg = message ? ": " + message : "";
        let count = this.errors.length;

        try {
            func.apply(self || this, args || []);
        }
        catch (e) {
            this.modules.util.reportError(e);
        }

        return utils.assertEqual('dactyl.assertNoErrorMessages', count, this.errors.length,
                                 "Error messsages were reported" + msg + ":\n\t" +
                                 this.errors.slice(count).join("\n\t"));
    },

    /**
     * Resets the error count used to determine whether new errors were
     * reported during the execution of a test.
     */
    resetErrorCount: function () {
        this.errorCount = this.modules.util.errorCount;
    },

    /**
     * Wraps the given function such that any errors triggered during
     * its execution will trigger a failed assertion.
     *
     * @param {function} func The function to wrap.
     * @param {string} message The message to display upon assertion failure. @optional
     */
    wrapAssertNoErrors: function (func, message) {
        return () => this.assertNoErrors(func, this, arguments, message);
    },

    /**
     * Asserts that the current window selection matches *text*.
     *
     * @param {string|RegExp|function} text The expected text of the current selection.
     * @param {string} message The message to display upon assertion failure.
     */
    assertSelection: function (want, message) {
        return assertMessage('dactyl.assertSelection', want,
                             String(this.controller.window.content.getSelection()),
                             message);
    },

    /**
     * @property {string} The name of dactyl's current key handling
     * mode.
     */
    get currentMode() this.modules.modes.main.name,

    /**
     * @property {object} A map of dactyl widgets to be used sparingly
     * for focus assertions.
     */
    get elements() let (self = this) ({
        /**
         * @property {HTMLInputElement} The command line's command input box
         */
        get commandInput() self.modules.commandline.widgets.active.command.inputField,
        /**
         * @property {Node|null} The currently focused node.
         */
        get focused() self.controller.window.document.commandDispatcher.focusedElement,
        /**
         * @property {HTMLInputElement} The message bar's command input box
         */
        get message() self.modules.commandline.widgets.active.message,
        /**
         * @property {Node} The multi-line output window.
         */
        get multiline() self.modules.commandline.widgets.multilineOutput,
        /**
         * @property {Node} The multi-line output container.
         */
        get multilineContainer() self.modules.commandline.widgets.mowContainer,
    }),

    /**
     * Returns dactyl to Normal mode.
     */
    setNormalMode: wrapAssertNoErrors(function () {
        // XXX: Normal mode test
        for (let i = 0; i < 15 && this.modules.modes.stack.length > 1; i++)
            this.controller.keypress(null, "VK_ESCAPE", {});

        this.controller.keypress(null, "l", { ctrlKey: true });

        utils.assert("dactyl.setNormalMode", this.modules.modes.stack.length == 1,
                     "Failed to return to Normal mode");

        this.assertMessageWindowOpen(false, "Returning to Normal mode: Multi-line output not closed");
        this.assertMessageLine(function (msg) !msg, "Returning to Normal mode: Message not cleared");
    }, "Returning to Normal mode"),

    /**
     * Returns dactyl to Ex mode.
     */
    setExMode: wrapAssertNoErrors(function () {
        if (this.currentMode !== "EX") {
            this.setNormalMode();
            this.controller.keypress(null, ":", {});
        }
        else {
            this.elements.commandInput.value = "";
        }
    }),

    /**
     * Runs a Vi command.
     *
     * @param {string|Array} keys Either a string of simple keys suitable for
     *     {@link MozMillController#type} or an array of keysym - modifier
     *     pairs suitable for {@link MozMillController#keypress}.
     */
    runViCommand: wrapAssertNoErrors(function (keys) {
        if (typeof keys == "string")
            keys = [[k] for each (k in keys)];
        keys.forEach(function ([key, modifiers]) { this.controller.keypress(null, key, modifiers || {}); }, this);
    }),

    /**
     * Runs an Ex command.
     *
     * @param {string} cmd The Ex command string as entered on the command
     *     line.
     * @param {object} args An args object by means of which to execute
     *     the command. If absent *cmd* is parsed as a complete
     *     arguments string. @optional
     */
    // TODO: Use execution code from commandline.js to catch more
    // possible errors without being insanely inefficient after the
    // merge.
    runExCommand: wrapAssertNoErrors(function (cmd, args) {
        this.setNormalMode();
        try {
            // Force async commands to wait for their output to be ready
            // before returning.
            this.modules.commandline.savingOutput = true;
            if (args)
                this.modules.ex[cmd](args);
            else if (true)
                this.modules.commands.execute(cmd, null, false, null,
                                             { file: "[Command Line]", line: 1 });
            else {
                var doc = this.controller.window.document;
                var event = doc.createEvent("Events");
                event.initEvent("dactyl.execute", false, false);
                doc.documentElement.setAttribute("dactyl-execute", cmd);
                doc.documentElement.dispatchEvent(event);
            }
        }
        finally {
            this.modules.commandline.savingOutput = false;
        }
    }),

    /**
     * Triggers a completion function with the given arguments an
     * ensures that no errors have occurred during the process.
     *
     * @param {object} self The 'this' object for which to trigger the
     *     completer.
     * @param {function|string} func The method or method name to call.
     * @param {string} string The method or method name to call. @optional
     * @param {string} message The message to display upon assertion failure. @optional
     * @param {...} Extra arguments are passed to the completion
     *     function directly.
     */
    testCompleter: wrapAssertNoErrors(function testCompleter(self, func, string, message, ...args) {
        var context = this.modules.CompletionContext(string || "");
        context.tabPressed = true;
        context.forkapply("completions", 0, self, func, args);

        utils.assert("dactyl.runCompletions", context.wait(5000),
                     message || "Completion failed: " + self + "." + func);

        for (var [, ctxt] in Iterator(context.contextList))
            for (var [, item] in Iterator(ctxt.items))
                ctxt.createRow(item);

        return context;
    }),

    /**
     * Triggers Ex completion for the given command string and ensures
     * that no errors have occurred during the process.
     *
     * @param {string} cmd The Ex command string as entered on the command
     *     line.
     * @param {boolean} longWay Whether to test the completion by
     *     entering it into the command line and dispatching a <Tab> key
     *     press.
     */
    runExCompletion: wrapAssertNoErrors(function (cmd, longWay) {
        // dump("runExCompletion " + cmd + "\n");
        if (!longWay) {
            var context = this.modules.CompletionContext(cmd);
            context.tabPressed = true;
            context.fork("ex", 0, this.modules.completion, "ex");

            utils.assert("dactyl.runCompletions", context.wait(5000),
                         "Completion failed: " + cmd.quote());

            for (var [, ctxt] in Iterator(context.contextList))
                for (var [, item] in Iterator(ctxt.items))
                    ctxt.createRow(item);

            return context;
        }
        else {
            this.setExMode();

            utils.assertEqual("dactyl.assertCommandLineFocused",
                              this.elements.commandInput,
                              this.elements.focused,
                              "Running Ex Completion: The command line is not focused");

            if (true) {
                let input = this.elements.commandInput;
                input.value = cmd;

                var event = input.ownerDocument.createEvent("Events");
                event.initEvent("change", true, false);
                input.dispatchEvent(event);
            }
            else {
                this.controller.type(null, cmd);

                utils.assertEqual("dactyl.runExCompletion", cmd,
                                  this.elements.commandInput.editor.rootElement.firstChild.textContent,
                                  "Command line does not have the expected value: " + cmd);
            }

            this.controller.keypress(null, "VK_TAB", {});

            // XXX
            if (this.modules.commandline._tabTimer)
                this.modules.commandline._tabTimer.flush();
            else if (this.modules.commandline.commandSession && this.modules.commandline.commandSession.completions)
                this.modules.commandline.commandSession.completions.tabTimer.flush();
        }
    }),

    /**
     * Returns the text content of the output message line.
     *
     * @returns {string} The message line text content.
     */
    readMessageLine: function () {
        return this.elements.message.value;
    },

    /**
     * Returns the text content of the output message window.
     *
     * @returns {string} The message window text content.
     */
    readMessageWindow: function () {
        if (!this.elements.multilineContainer.collapsed)
            return this.elements.multiline.contentDocument.body.textContent;
        return "";
    },

    /**
     * Opens the output message window by echoing a single newline character.
     */
    openMessageWindow: wrapAssertNoErrors(function () {
        this.modules.dactyl.echo("\n");
    }, "Opening message window"),

    /**
     * Clears the current message.
     */
    clearMessage: function () {
        this.elements.message.value = ""; // XXX
    },

    /**
     * Closes the output message window if open.
     */
    closeMessageWindow: wrapAssertNoErrors(function () {
        for (let i = 0; i < 15 && !this.elements.multilineContainer.collapsed; i++)
            this.controller.keypress(null, "VK_ESCAPE", {});
        this.assertMessageWindowOpen(false, "Clearing message window failed");
    }, "Clearing message window"),

    /**
     * @property {string} The specific Dactyl application. Eg. Pentadactyl
     */
    get applicationName() this.modules.config.appName // XXX
};

// vim: sw=4 ts=8 et ft=javascript:
