// nsDoTestsForEditorWithAutoComplete tests basic functions of editor with autocomplete.
// Users must include SimpleTest.js and EventUtils.js, and register "Mozilla" to the autocomplete for the target.

async function waitForCondition(condition) {
  return new Promise(resolve => {
    var tries = 0;
    var interval = setInterval(function () {
      if (condition() || tries >= 60) {
        moveOn();
      }
      tries++;
    }, 100);
    var moveOn = function () {
      clearInterval(interval);
      resolve();
    };
  });
}

function nsDoTestsForEditorWithAutoComplete(
  aDescription,
  aWindow,
  aTarget,
  aAutoCompleteController,
  aIsFunc,
  aTodoIsFunc,
  aGetTargetValueFunc
) {
  this._description = aDescription;
  this._window = aWindow;
  this._target = aTarget;
  this._controller = aAutoCompleteController;

  this._is = aIsFunc;
  this._todo_is = aTodoIsFunc;
  this._getTargetValue = aGetTargetValueFunc;

  this._target.focus();

  this._DefaultCompleteDefaultIndex =
    this._controller.input.completeDefaultIndex;
}

nsDoTestsForEditorWithAutoComplete.prototype = {
  _window: null,
  _target: null,
  _controller: null,
  _DefaultCompleteDefaultIndex: false,
  _description: "",

  _is: null,
  _getTargetValue() {
    return "not initialized";
  },

  run: async function runTestsImpl() {
    for (let test of this._tests) {
      if (
        this._controller.input.completeDefaultIndex != test.completeDefaultIndex
      ) {
        this._controller.input.completeDefaultIndex = test.completeDefaultIndex;
      }

      let beforeInputEvents = [];
      let inputEvents = [];
      function onBeforeInput(aEvent) {
        beforeInputEvents.push(aEvent);
      }
      function onInput(aEvent) {
        inputEvents.push(aEvent);
      }
      this._target.addEventListener("beforeinput", onBeforeInput);
      this._target.addEventListener("input", onInput);

      if (test.execute(this._window, this._target) === false) {
        this._target.removeEventListener("beforeinput", onBeforeInput);
        this._target.removeEventListener("input", onInput);
        continue;
      }

      await waitForCondition(() => {
        return (
          this._controller.searchStatus >=
          Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH
        );
      });
      if (test.popup) {
        await waitForCondition(() => this._controller.input.popupOpen);
      }

      this._target.removeEventListener("beforeinput", onBeforeInput);
      this._target.removeEventListener("input", onInput);
      this._checkResult(test, beforeInputEvents, inputEvents);
    }
    this._controller.input.completeDefaultIndex =
      this._DefaultCompleteDefaultIndex;
  },

  _checkResult(aTest, aBeforeInputEvents, aInputEvents) {
    this._is(
      this._getTargetValue(),
      aTest.value,
      this._description + ", " + aTest.description + ": value"
    );
    this._is(
      this._controller.searchString,
      aTest.searchString,
      this._description + ", " + aTest.description + ": searchString"
    );
    this._is(
      this._controller.input.popupOpen,
      aTest.popup,
      this._description + ", " + aTest.description + ": popupOpen"
    );
    this._is(
      this._controller.searchStatus,
      Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH,
      this._description + ", " + aTest.description + ": status"
    );
    this._is(
      aBeforeInputEvents.length,
      aTest.inputEvents.length,
      this._description +
        ", " +
        aTest.description +
        ": number of beforeinput events wrong"
    );
    this._is(
      aInputEvents.length,
      aTest.inputEvents.length,
      this._description +
        ", " +
        aTest.description +
        ": number of input events wrong"
    );
    for (let events of [aBeforeInputEvents, aInputEvents]) {
      for (let i = 0; i < events.length; i++) {
        if (aTest.inputEvents[i] === undefined) {
          this._is(
            true,
            false,
            this._description +
              ", " +
              aTest.description +
              ': "beforeinput" and "input" event shouldn\'t be dispatched anymore'
          );
          return;
        }
        this._is(
          events[i] instanceof this._window.InputEvent,
          true,
          `${this._description}, ${aTest.description}: "${events[i].type}" event should be dispatched with InputEvent interface`
        );
        let expectCancelable =
          events[i].type === "beforeinput" &&
          (aTest.inputEvents[i].inputType !== "insertReplacementText" ||
            SpecialPowers.getBoolPref(
              "dom.input_event.allow_to_cancel_set_user_input"
            ));

        this._is(
          events[i].cancelable,
          expectCancelable,
          `${this._description}, ${aTest.description}: "${
            events[i].type
          }" event should ${expectCancelable ? "be" : "be never"} cancelable`
        );
        this._is(
          events[i].bubbles,
          true,
          `${this._description}, ${aTest.description}: "${events[i].type}" event should always bubble`
        );
        this._is(
          events[i].inputType,
          aTest.inputEvents[i].inputType,
          `${this._description}, ${aTest.description}: inputType of "${events[i].type}" event should be "${aTest.inputEvents[i].inputType}"`
        );
        this._is(
          events[i].data,
          aTest.inputEvents[i].data,
          `${this._description}, ${aTest.description}: data of "${events[i].type}" event should be ${aTest.inputEvents[i].data}`
        );
        this._is(
          events[i].dataTransfer,
          null,
          `${this._description}, ${aTest.description}: dataTransfer of "${events[i].type}" event should be null`
        );
        this._is(
          events[i].getTargetRanges().length,
          0,
          `${this._description}, ${aTest.description}: getTargetRanges() of "${events[i].type}" event should return empty array`
        );
      }
    }
  },

  _tests: [
    {
      description:
        "Undo/Redo behavior check when typed text exactly matches the case: type 'Mo'",
      completeDefaultIndex: false,
      execute(aWindow) {
        synthesizeKey("M", { shiftKey: true }, aWindow);
        synthesizeKey("o", {}, aWindow);
        return true;
      },
      popup: true,
      value: "Mo",
      searchString: "Mo",
      inputEvents: [
        { inputType: "insertText", data: "M" },
        { inputType: "insertText", data: "o" },
      ],
    },
    {
      description:
        "Undo/Redo behavior check when typed text exactly matches the case: select 'Mozilla' to complete the word",
      completeDefaultIndex: false,
      execute(aWindow) {
        synthesizeKey("KEY_ArrowDown", {}, aWindow);
        synthesizeKey("KEY_Enter", {}, aWindow);
        return true;
      },
      popup: false,
      value: "Mozilla",
      searchString: "Mozilla",
      inputEvents: [{ inputType: "insertReplacementText", data: "Mozilla" }],
    },
    {
      description:
        "Undo/Redo behavior check when typed text exactly matches the case: undo the word, but typed text shouldn't be canceled",
      completeDefaultIndex: false,
      execute(aWindow) {
        synthesizeKey("z", { accelKey: true }, aWindow);
        return true;
      },
      popup: true,
      value: "Mo",
      searchString: "Mo",
      inputEvents: [{ inputType: "historyUndo", data: null }],
    },
    {
      description:
        "Undo/Redo behavior check when typed text exactly matches the case: undo the typed text",
      completeDefaultIndex: false,
      execute(aWindow) {
        synthesizeKey("z", { accelKey: true }, aWindow);
        return true;
      },
      popup: false,
      value: "",
      searchString: "",
      inputEvents: [{ inputType: "historyUndo", data: null }],
    },
    {
      description:
        "Undo/Redo behavior check when typed text exactly matches the case: redo the typed text",
      completeDefaultIndex: false,
      execute(aWindow) {
        synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow);
        return true;
      },
      popup: true,
      value: "Mo",
      searchString: "Mo",
      inputEvents: [{ inputType: "historyRedo", data: null }],
    },
    {
      description:
        "Undo/Redo behavior check when typed text exactly matches the case: redo the word",
      completeDefaultIndex: false,
      execute(aWindow) {
        synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow);
        return true;
      },
      popup: true,
      value: "Mozilla",
      searchString: "Mozilla",
      inputEvents: [{ inputType: "historyRedo", data: null }],
    },
    {
      description:
        "Undo/Redo behavior check when typed text exactly matches the case: removing all text for next test...",
      completeDefaultIndex: false,
      execute(aWindow) {
        synthesizeKey("a", { accelKey: true }, aWindow);
        synthesizeKey("KEY_Backspace", {}, aWindow);
        return true;
      },
      popup: false,
      value: "",
      searchString: "",
      inputEvents: [{ inputType: "deleteContentBackward", data: null }],
    },

    {
      description:
        "Undo/Redo behavior check when typed text does not match the case: type 'mo'",
      completeDefaultIndex: false,
      execute(aWindow) {
        synthesizeKey("m", {}, aWindow);
        synthesizeKey("o", {}, aWindow);
        return true;
      },
      popup: true,
      value: "mo",
      searchString: "mo",
      inputEvents: [
        { inputType: "insertText", data: "m" },
        { inputType: "insertText", data: "o" },
      ],
    },
    {
      description:
        "Undo/Redo behavior check when typed text does not match the case: select 'Mozilla' to complete the word",
      completeDefaultIndex: false,
      execute(aWindow) {
        synthesizeKey("KEY_ArrowDown", {}, aWindow);
        synthesizeKey("KEY_Enter", {}, aWindow);
        return true;
      },
      popup: false,
      value: "Mozilla",
      searchString: "Mozilla",
      inputEvents: [{ inputType: "insertReplacementText", data: "Mozilla" }],
    },
    {
      description:
        "Undo/Redo behavior check when typed text does not match the case: undo the word, but typed text shouldn't be canceled",
      completeDefaultIndex: false,
      execute(aWindow) {
        synthesizeKey("z", { accelKey: true }, aWindow);
        return true;
      },
      popup: true,
      value: "mo",
      searchString: "mo",
      inputEvents: [{ inputType: "historyUndo", data: null }],
    },
    {
      description:
        "Undo/Redo behavior check when typed text does not match the case: undo the typed text",
      completeDefaultIndex: false,
      execute(aWindow) {
        synthesizeKey("z", { accelKey: true }, aWindow);
        return true;
      },
      popup: false,
      value: "",
      searchString: "",
      inputEvents: [{ inputType: "historyUndo", data: null }],
    },
    {
      description:
        "Undo/Redo behavior check when typed text does not match the case: redo the typed text",
      completeDefaultIndex: false,
      execute(aWindow) {
        synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow);
        return true;
      },
      popup: true,
      value: "mo",
      searchString: "mo",
      inputEvents: [{ inputType: "historyRedo", data: null }],
    },
    {
      description:
        "Undo/Redo behavior check when typed text does not match the case: redo the word",
      completeDefaultIndex: false,
      execute(aWindow) {
        synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow);
        return true;
      },
      popup: true,
      value: "Mozilla",
      searchString: "Mozilla",
      inputEvents: [{ inputType: "historyRedo", data: null }],
    },
    {
      description:
        "Undo/Redo behavior check when typed text does not match the case: removing all text for next test...",
      completeDefaultIndex: false,
      execute(aWindow) {
        synthesizeKey("a", { accelKey: true }, aWindow);
        synthesizeKey("KEY_Backspace", {}, aWindow);
        return true;
      },
      popup: false,
      value: "",
      searchString: "",
      inputEvents: [{ inputType: "deleteContentBackward", data: null }],
    },

    // Testing for nsIAutoCompleteInput.completeDefaultIndex being true.
    {
      description:
        "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): type 'Mo'",
      completeDefaultIndex: true,
      execute(aWindow) {
        synthesizeKey("M", { shiftKey: true }, aWindow);
        synthesizeKey("o", {}, aWindow);
        return true;
      },
      popup: true,
      value: "Mozilla",
      searchString: "Mo",
      inputEvents: [
        { inputType: "insertText", data: "M" },
        { inputType: "insertText", data: "o" },
        { inputType: "insertReplacementText", data: "Mozilla" },
      ],
    },
    {
      description:
        "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): select 'Mozilla' to complete the word",
      completeDefaultIndex: true,
      execute(aWindow) {
        synthesizeKey("KEY_ArrowDown", {}, aWindow);
        synthesizeKey("KEY_Enter", {}, aWindow);
        return true;
      },
      popup: false,
      value: "Mozilla",
      searchString: "Mozilla",
      inputEvents: [],
    },
    {
      description:
        "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): undo the word, but typed text shouldn't be canceled",
      completeDefaultIndex: true,
      execute(aWindow) {
        synthesizeKey("z", { accelKey: true }, aWindow);
        return true;
      },
      popup: true,
      value: "Mo",
      searchString: "Mo",
      inputEvents: [{ inputType: "historyUndo", data: null }],
    },
    {
      description:
        "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): undo the typed text",
      completeDefaultIndex: true,
      execute(aWindow) {
        synthesizeKey("z", { accelKey: true }, aWindow);
        return true;
      },
      popup: false,
      value: "",
      searchString: "",
      inputEvents: [{ inputType: "historyUndo", data: null }],
    },
    {
      description:
        "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): redo the typed text",
      completeDefaultIndex: true,
      execute(aWindow) {
        synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow);
        return true;
      },
      popup: true,
      value: "Mozilla",
      searchString: "Mo",
      inputEvents: [
        { inputType: "historyRedo", data: null },
        { inputType: "insertReplacementText", data: "Mozilla" },
      ],
    },
    {
      description:
        "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): redo the word",
      completeDefaultIndex: true,
      execute(aWindow) {
        synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow);
        return true;
      },
      popup: true,
      value: "Mozilla",
      searchString: "Mo",
      inputEvents: [],
    },
    {
      description:
        "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): removing all text for next test...",
      completeDefaultIndex: true,
      execute(aWindow) {
        synthesizeKey("a", { accelKey: true }, aWindow);
        synthesizeKey("KEY_Backspace", {}, aWindow);
        return true;
      },
      popup: false,
      value: "",
      searchString: "",
      inputEvents: [{ inputType: "deleteContentBackward", data: null }],
    },

    {
      description:
        "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): type 'mo'",
      completeDefaultIndex: true,
      execute(aWindow) {
        synthesizeKey("m", {}, aWindow);
        synthesizeKey("o", {}, aWindow);
        return true;
      },
      popup: true,
      value: "mozilla",
      searchString: "mo",
      inputEvents: [
        { inputType: "insertText", data: "m" },
        { inputType: "insertText", data: "o" },
        { inputType: "insertReplacementText", data: "mozilla" },
      ],
    },
    {
      description:
        "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): select 'Mozilla' to complete the word",
      completeDefaultIndex: true,
      execute(aWindow) {
        synthesizeKey("KEY_ArrowDown", {}, aWindow);
        synthesizeKey("KEY_Enter", {}, aWindow);
        return true;
      },
      popup: false,
      value: "Mozilla",
      searchString: "Mozilla",
      inputEvents: [{ inputType: "insertReplacementText", data: "Mozilla" }],
    },
    // Different from "exactly matches the case" case, modifying the case causes one additional transaction.
    // Although we could make this transaction ignored.
    {
      description:
        "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): undo the selected word, but typed text shouldn't be canceled",
      completeDefaultIndex: true,
      execute(aWindow) {
        synthesizeKey("z", { accelKey: true }, aWindow);
        return true;
      },
      popup: true,
      value: "mozilla",
      searchString: "mozilla",
      inputEvents: [{ inputType: "historyUndo", data: null }],
    },
    {
      description:
        "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): undo the word, but typed text shouldn't be canceled",
      completeDefaultIndex: true,
      execute(aWindow) {
        synthesizeKey("z", { accelKey: true }, aWindow);
        return true;
      },
      popup: true,
      value: "mo",
      searchString: "mo",
      inputEvents: [{ inputType: "historyUndo", data: null }],
    },
    {
      description:
        "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): undo the typed text",
      completeDefaultIndex: true,
      execute(aWindow) {
        synthesizeKey("z", { accelKey: true }, aWindow);
        return true;
      },
      popup: false,
      value: "",
      searchString: "",
      inputEvents: [{ inputType: "historyUndo", data: null }],
    },
    // XXX This is odd case.  Consistency with undo behavior, this should restore "mo".
    //     However, looks like that autocomplete automatically restores "mozilla".
    //     Additionally, looks like that it causes clearing the redo stack.
    //     Therefore, the following redo operations do nothing.
    {
      description:
        "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): redo the typed text",
      completeDefaultIndex: true,
      execute(aWindow) {
        synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow);
        return true;
      },
      popup: true,
      value: "mozilla",
      searchString: "mo",
      inputEvents: [
        { inputType: "historyRedo", data: null },
        { inputType: "insertReplacementText", data: "mozilla" },
      ],
    },
    {
      description:
        "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): redo the default index word",
      completeDefaultIndex: true,
      execute(aWindow) {
        synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow);
        return true;
      },
      popup: true,
      value: "mozilla",
      searchString: "mo",
      inputEvents: [],
    },
    {
      description:
        "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): redo the word",
      completeDefaultIndex: true,
      execute(aWindow) {
        synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow);
        return true;
      },
      popup: true,
      value: "mozilla",
      searchString: "mo",
      inputEvents: [],
    },
    {
      description:
        "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): removing all text for next test...",
      completeDefaultIndex: true,
      execute(aWindow) {
        synthesizeKey("a", { accelKey: true }, aWindow);
        synthesizeKey("KEY_Backspace", {}, aWindow);
        return true;
      },
      popup: false,
      value: "",
      searchString: "",
      inputEvents: [{ inputType: "deleteContentBackward", data: null }],
    },
  ],
};
