/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */

const TEST_PAGE_BASENAME = "searchSuggestionUI.html";
const TEST_CONTENT_SCRIPT_BASENAME = "searchSuggestionUI.js";
const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";

const TEST_MSG = "SearchSuggestionUIControllerTest";

add_task(function* emptyInput() {
  yield setUp();

  let state = yield msg("key", { key: "x", waitForSuggestions: true });
  checkState(state, "x", ["xfoo", "xbar"], -1);

  state = yield msg("key", "VK_BACK_SPACE");
  checkState(state, "", [], -1);

  yield msg("reset");
});

add_task(function* blur() {
  yield setUp();

  let state = yield msg("key", { key: "x", waitForSuggestions: true });
  checkState(state, "x", ["xfoo", "xbar"], -1);

  state = yield msg("blur");
  checkState(state, "x", [], -1);

  yield msg("reset");
});

add_task(function* arrowKeys() {
  yield setUp();

  let state = yield msg("key", { key: "x", waitForSuggestions: true });
  checkState(state, "x", ["xfoo", "xbar"], -1);

  // Cycle down the suggestions starting from no selection.
  state = yield msg("key", "VK_DOWN");
  checkState(state, "xfoo", ["xfoo", "xbar"], 0);

  state = yield msg("key", "VK_DOWN");
  checkState(state, "xbar", ["xfoo", "xbar"], 1);

  state = yield msg("key", "VK_DOWN");
  checkState(state, "x", ["xfoo", "xbar"], -1);

  // Cycle up starting from no selection.
  state = yield msg("key", "VK_UP");
  checkState(state, "xbar", ["xfoo", "xbar"], 1);

  state = yield msg("key", "VK_UP");
  checkState(state, "xfoo", ["xfoo", "xbar"], 0);

  state = yield msg("key", "VK_UP");
  checkState(state, "x", ["xfoo", "xbar"], -1);

  yield msg("reset");
});

// The right arrow and return key function the same.
function rightArrowOrReturn(keyName) {
  return function* rightArrowOrReturnTest() {
    yield setUp();

    let state = yield msg("key", { key: "x", waitForSuggestions: true });
    checkState(state, "x", ["xfoo", "xbar"], -1);

    state = yield msg("key", "VK_DOWN");
    checkState(state, "xfoo", ["xfoo", "xbar"], 0);

    // This should make the xfoo suggestion sticky.  To make sure it sticks,
    // trigger suggestions again and cycle through them by pressing Down until
    // nothing is selected again.
    state = yield msg("key", keyName);
    checkState(state, "xfoo", [], -1);

    state = yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
    checkState(state, "xfoo", ["xfoofoo", "xfoobar"], -1);

    state = yield msg("key", "VK_DOWN");
    checkState(state, "xfoofoo", ["xfoofoo", "xfoobar"], 0);

    state = yield msg("key", "VK_DOWN");
    checkState(state, "xfoobar", ["xfoofoo", "xfoobar"], 1);

    state = yield msg("key", "VK_DOWN");
    checkState(state, "xfoo", ["xfoofoo", "xfoobar"], -1);

    yield msg("reset");
  };
}

add_task(rightArrowOrReturn("VK_RIGHT"));
add_task(rightArrowOrReturn("VK_RETURN"));

add_task(function* mouse() {
  yield setUp();

  let state = yield msg("key", { key: "x", waitForSuggestions: true });
  checkState(state, "x", ["xfoo", "xbar"], -1);

  // Mouse over the first suggestion.
  state = yield msg("mousemove", 0);
  checkState(state, "x", ["xfoo", "xbar"], 0);

  // Mouse over the second suggestion.
  state = yield msg("mousemove", 1);
  checkState(state, "x", ["xfoo", "xbar"], 1);

  // Click the second suggestion.  This should make it sticky.  To make sure it
  // sticks, trigger suggestions again and cycle through them by pressing Down
  // until nothing is selected again.
  state = yield msg("mousedown", 1);
  checkState(state, "xbar", [], -1);

  state = yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
  checkState(state, "xbar", ["xbarfoo", "xbarbar"], -1);

  state = yield msg("key", "VK_DOWN");
  checkState(state, "xbarfoo", ["xbarfoo", "xbarbar"], 0);

  state = yield msg("key", "VK_DOWN");
  checkState(state, "xbarbar", ["xbarfoo", "xbarbar"], 1);

  state = yield msg("key", "VK_DOWN");
  checkState(state, "xbar", ["xbarfoo", "xbarbar"], -1);

  yield msg("reset");
});

add_task(function* formHistory() {
  yield setUp();

  // Type an X and add it to form history.
  let state = yield msg("key", { key: "x", waitForSuggestions: true });
  checkState(state, "x", ["xfoo", "xbar"], -1);
  yield msg("addInputValueToFormHistory");

  // Wait for Satchel to say it's been added to form history.
  let deferred = Promise.defer();
  Services.obs.addObserver(function onAdd(subj, topic, data) {
    if (data == "formhistory-add") {
      Services.obs.removeObserver(onAdd, "satchel-storage-changed");
      executeSoon(() => deferred.resolve());
    }
  }, "satchel-storage-changed", false);
  yield deferred.promise;

  // Reset the input.
  state = yield msg("reset");
  checkState(state, "", [], -1);

  // Type an X again.  The form history entry should appear.
  state = yield msg("key", { key: "x", waitForSuggestions: true });
  checkState(state, "x", [{ str: "x", type: "formHistory" }, "xfoo", "xbar"],
             -1);

  // Select the form history entry and delete it.
  state = yield msg("key", "VK_DOWN");
  checkState(state, "x", [{ str: "x", type: "formHistory" }, "xfoo", "xbar"],
             0);

  state = yield msg("key", "VK_DELETE");
  checkState(state, "x", ["xfoo", "xbar"], -1);

  // Wait for Satchel.
  deferred = Promise.defer();
  Services.obs.addObserver(function onRemove(subj, topic, data) {
    if (data == "formhistory-remove") {
      Services.obs.removeObserver(onRemove, "satchel-storage-changed");
      executeSoon(() => deferred.resolve());
    }
  }, "satchel-storage-changed", false);
  yield deferred.promise;

  // Reset the input.
  state = yield msg("reset");
  checkState(state, "", [], -1);

  // Type an X again.  The form history entry should still be gone.
  state = yield msg("key", { key: "x", waitForSuggestions: true });
  checkState(state, "x", ["xfoo", "xbar"], -1);

  yield msg("reset");
});

add_task(function* composition() {
  yield setUp();

  let state = yield msg("startComposition", { data: "" });
  checkState(state, "", [], -1);
  state = yield msg("updateComposition", { data: "x" });
  checkState(state, "", [], -1);
  state = yield msg("changeComposition", { data: "x", waitForSuggestions: true });
  checkState(state, "x", ["xfoo", "xbar"], -1);

  // Mouse over the first suggestion.
  state = yield msg("mousemove", 0);
  checkState(state, "x", ["xfoo", "xbar"], 0);

  // Mouse over the second suggestion.
  state = yield msg("mousemove", 1);
  checkState(state, "x", ["xfoo", "xbar"], 1);

  // Click the second suggestion.  This should make it sticky.  To make sure it
  // sticks, trigger suggestions again and cycle through them by pressing Down
  // until nothing is selected again.
  state = yield msg("mousedown", 1);

  checkState(state, "xbar", [], -1);

  state = yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
  checkState(state, "xbar", ["xbarfoo", "xbarbar"], -1);

  state = yield msg("key", "VK_DOWN");
  checkState(state, "xbarfoo", ["xbarfoo", "xbarbar"], 0);

  state = yield msg("key", "VK_DOWN");
  checkState(state, "xbarbar", ["xbarfoo", "xbarbar"], 1);

  state = yield msg("key", "VK_DOWN");
  checkState(state, "xbar", ["xbarfoo", "xbarbar"], -1);

  yield msg("reset");
});


let gDidInitialSetUp = false;

function setUp() {
  return Task.spawn(function* () {
    if (!gDidInitialSetUp) {
      yield promiseNewEngine(TEST_ENGINE_BASENAME);
      yield promiseTab();
      gDidInitialSetUp = true;
    }
    yield msg("focus");
  });
}

function msg(type, data=null) {
  gMsgMan.sendAsyncMessage(TEST_MSG, {
    type: type,
    data: data,
  });
  let deferred = Promise.defer();
  gMsgMan.addMessageListener(TEST_MSG, function onMsg(msg) {
    gMsgMan.removeMessageListener(TEST_MSG, onMsg);
    deferred.resolve(msg.data);
  });
  return deferred.promise;
}

function checkState(actualState, expectedInputVal, expectedSuggestions,
                    expectedSelectedIdx) {
  expectedSuggestions = expectedSuggestions.map(sugg => {
    return typeof(sugg) == "object" ? sugg : {
      str: sugg,
      type: "remote",
    };
  });

  let expectedState = {
    selectedIndex: expectedSelectedIdx,
    numSuggestions: expectedSuggestions.length,
    suggestionAtIndex: expectedSuggestions.map(s => s.str),
    isFormHistorySuggestionAtIndex:
      expectedSuggestions.map(s => s.type == "formHistory"),

    tableHidden: expectedSuggestions.length == 0,
    tableChildrenLength: expectedSuggestions.length,
    tableChildren: expectedSuggestions.map((s, i) => {
      let expectedClasses = new Set([s.type]);
      if (i == expectedSelectedIdx) {
        expectedClasses.add("selected");
      }
      return {
        textContent: s.str,
        classes: expectedClasses,
      };
    }),

    inputValue: expectedInputVal,
    ariaExpanded: expectedSuggestions.length == 0 ? "false" : "true",
  };

  SimpleTest.isDeeply(actualState, expectedState, "State");
}

var gMsgMan;

function promiseTab() {
  let deferred = Promise.defer();
  let tab = gBrowser.addTab();
  registerCleanupFunction(() => gBrowser.removeTab(tab));
  gBrowser.selectedTab = tab;
  let pageURL = getRootDirectory(gTestPath) + TEST_PAGE_BASENAME;
  tab.linkedBrowser.addEventListener("load", function onLoad(event) {
    tab.linkedBrowser.removeEventListener("load", onLoad, true);
    gMsgMan = tab.linkedBrowser.messageManager;
    gMsgMan.sendAsyncMessage("ContentSearch", {
      type: "AddToWhitelist",
      data: [pageURL],
    });
    promiseMsg("ContentSearch", "AddToWhitelistAck", gMsgMan).then(() => {
      let jsURL = getRootDirectory(gTestPath) + TEST_CONTENT_SCRIPT_BASENAME;
      gMsgMan.loadFrameScript(jsURL, false);
      deferred.resolve();
    });
  }, true, true);
  openUILinkIn(pageURL, "current");
  return deferred.promise;
}

function promiseMsg(name, type, msgMan) {
  let deferred = Promise.defer();
  info("Waiting for " + name + " message " + type + "...");
  msgMan.addMessageListener(name, function onMsg(msg) {
    info("Received " + name + " message " + msg.data.type + "\n");
    if (msg.data.type == type) {
      msgMan.removeMessageListener(name, onMsg);
      deferred.resolve(msg);
    }
  });
  return deferred.promise;
}

function promiseNewEngine(basename) {
  info("Waiting for engine to be added: " + basename);
  let addDeferred = Promise.defer();
  let url = getRootDirectory(gTestPath) + basename;
  Services.search.addEngine(url, Ci.nsISearchEngine.TYPE_MOZSEARCH, "", false, {
    onSuccess: function (engine) {
      info("Search engine added: " + basename);
      registerCleanupFunction(() => Services.search.removeEngine(engine));
      addDeferred.resolve(engine);
    },
    onError: function (errCode) {
      ok(false, "addEngine failed with error code " + errCode);
      addDeferred.reject();
    },
  });
  return addDeferred.promise;
}
