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

EXPECTED_BREACH = {
  AddedDate: "2018-12-20T23:56:26Z",
  BreachDate: "2018-12-16",
  Domain: "breached.example.com",
  Name: "Breached",
  PwnCount: 1643100,
  DataClasses: ["Email addresses", "Usernames", "Passwords", "IP addresses"],
  _status: "synced",
  id: "047940fe-d2fd-4314-b636-b4a952ee0043",
  last_modified: "1541615610052",
  schema: "1541615609018",
};

add_setup(async function () {
  TEST_LOGIN1 = await addLogin(TEST_LOGIN1);
  await BrowserTestUtils.openNewForegroundTab({
    gBrowser,
    url: "about:logins",
  });
  registerCleanupFunction(() => {
    BrowserTestUtils.removeTab(gBrowser.selectedTab);
    Services.logins.removeAllUserFacingLogins();
  });
});

add_task(async function test_tab_key_nav() {
  const browser = gBrowser.selectedBrowser;
  await SpecialPowers.spawn(browser, [], async () => {
    // Helper function for getting the resulting DOM element given a list of selectors possibly inside shadow DOM
    const selectWithShadowRootIfNeeded = (document, selectorsArray) =>
      selectorsArray.reduce(
        (selectionSoFar, currentSelector) =>
          selectionSoFar.shadowRoot
            ? selectionSoFar.shadowRoot.querySelector(currentSelector)
            : selectionSoFar.querySelector(currentSelector),
        document
      );

    const EventUtils = ContentTaskUtils.getEventUtils(content);
    // list [selector, shadow root selector] for each element
    // in the order we expect them to be navigated.
    const expectedElementsInOrder = [
      ["login-list", "login-filter", "input"],
      ["login-list", "button.create-login-button"],
      ["login-list", "select#login-sort"],
      ["login-list", "ol"],
      ["login-item", "button.edit-button"],
      ["login-item", "button.delete-button"],
      ["login-item", "a.origin-input"],
      ["login-item", "button.copy-username-button"],
      ["login-item", "input.reveal-password-checkbox"],
      ["login-item", "button.copy-password-button"],
    ];

    const firstElement = selectWithShadowRootIfNeeded(
      content.document,
      expectedElementsInOrder.at(0)
    );

    const lastElement = selectWithShadowRootIfNeeded(
      content.document,
      expectedElementsInOrder.at(-1)
    );

    async function tab() {
      EventUtils.synthesizeKey("KEY_Tab", {}, content);
      await new Promise(resolve => content.requestAnimationFrame(resolve));
      // The following line can help with focus trap debugging:
      // await new Promise(resolve => content.window.setTimeout(resolve, 500));
    }
    async function shiftTab() {
      EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }, content);
      await new Promise(resolve => content.requestAnimationFrame(resolve));
      // await new Promise(resolve => content.window.setTimeout(resolve, 500));
    }

    // Getting focused shadow DOM element itself instead of shadowRoot,
    // using recursion for any component-nesting level, as in:
    // document.activeElement.shadowRoot.activeElement.shadowRoot.activeElement
    function getFocusedElement() {
      let element = content.document.activeElement;
      const getShadowRootFocus = e => {
        if (e.shadowRoot) {
          return getShadowRootFocus(e.shadowRoot.activeElement);
        }
        return e;
      };
      return getShadowRootFocus(element);
    }

    // Ensure the test starts in a valid state
    firstElement.focus();
    // Assert that we tab navigate correctly
    for (let expectedSelector of expectedElementsInOrder) {
      const expectedElement = selectWithShadowRootIfNeeded(
        content.document,
        expectedSelector
      );

      // By default, MacOS will skip over certain text controls, such as links.
      if (
        content.window.navigator.platform.toLowerCase().includes("mac") &&
        expectedElement.tagName === "A"
      ) {
        continue;
      }

      const actualElement = getFocusedElement();

      Assert.equal(
        actualElement,
        expectedElement,
        "Actual focused element should equal the expected focused element"
      );
      await tab();
    }

    lastElement.focus();

    // Assert that we shift + tab navigate correctly starting from the last ordered element
    for (let expectedSelector of expectedElementsInOrder.reverse()) {
      const expectedElement = selectWithShadowRootIfNeeded(
        content.document,
        expectedSelector
      );
      // By default, MacOS will skip over certain text controls, such as links.
      if (
        content.window.navigator.platform.toLowerCase().includes("mac") &&
        expectedElement.tagName === "A"
      ) {
        continue;
      }

      const actualElement = getFocusedElement();
      Assert.equal(
        actualElement,
        expectedElement,
        "Actual focused element should equal the expected focused element"
      );
      await shiftTab();
    }
    await tab(); // tab back to the first element
  });
});

add_task(async function test_tab_to_create_button() {
  const browser = gBrowser.selectedBrowser;
  await SpecialPowers.spawn(browser, [], async () => {
    const EventUtils = ContentTaskUtils.getEventUtils(content);

    function waitForAnimationFrame() {
      return new Promise(resolve => content.requestAnimationFrame(resolve));
    }

    async function tab() {
      EventUtils.synthesizeKey("KEY_Tab", {}, content);
      await waitForAnimationFrame();
    }

    const loginList = content.document.querySelector("login-list");
    const loginFilter = loginList.shadowRoot.querySelector("login-filter");
    const loginSort = loginList.shadowRoot.getElementById("login-sort");
    const loginListbox = loginList.shadowRoot.querySelector("ol");
    const createButton = loginList.shadowRoot.querySelector(
      ".create-login-button"
    );

    const getFocusedElement = () => loginList.shadowRoot.activeElement;
    Assert.equal(getFocusedElement(), loginFilter, "login-filter is focused");

    await tab();
    Assert.equal(getFocusedElement(), createButton, "create button is focused");

    await tab();
    Assert.equal(getFocusedElement(), loginSort, "login sort is focused");

    await tab();
    Assert.equal(getFocusedElement(), loginListbox, "listbox is focused next");

    await tab();
    Assert.equal(getFocusedElement(), null, "login-list isn't focused again");
  });
});

add_task(async function test_tab_to_edit_button() {
  TEST_LOGIN3 = await addLogin(TEST_LOGIN3);
  let browser = gBrowser.selectedBrowser;
  await SpecialPowers.spawn(
    browser,
    [[TEST_LOGIN1.guid, TEST_LOGIN3.guid]],
    async ([testLoginNormalGuid, testLoginBreachedGuid]) => {
      const EventUtils = ContentTaskUtils.getEventUtils(content);

      function waitForAnimationFrame() {
        return new Promise(resolve => content.requestAnimationFrame(resolve));
      }

      async function tab() {
        EventUtils.synthesizeKey("KEY_Tab", {}, content);
        await waitForAnimationFrame();
      }

      const loginList = content.document.querySelector("login-list");
      const loginItem = content.document.querySelector("login-item");
      const loginFilter = loginList.shadowRoot.querySelector("login-filter");
      const createButton = loginList.shadowRoot.querySelector(
        ".create-login-button"
      );
      const loginSort = loginList.shadowRoot.getElementById("login-sort");
      const loginListbox = loginList.shadowRoot.querySelector("ol");
      const editButton = loginItem.shadowRoot.querySelector(".edit-button");
      const breachAlert =
        loginItem.shadowRoot.querySelector("login-breach-alert");
      const getFocusedElement = () => {
        if (content.document.activeElement == loginList) {
          return loginList.shadowRoot.activeElement;
        }
        if (content.document.activeElement == loginItem) {
          return loginItem.shadowRoot.activeElement;
        }
        if (content.document.activeElement == loginFilter) {
          return loginFilter.shadowRoot.activeElement;
        }
        Assert.ok(
          false,
          "not expecting a different element to get focused in this test: " +
            content.document.activeElement.outerHTML
        );
        return undefined;
      };

      for (let guidToSelect of [testLoginNormalGuid, testLoginBreachedGuid]) {
        let loginListItem = loginList.shadowRoot.querySelector(
          `.login-list-item[data-guid="${guidToSelect}"]`
        );
        loginListItem.click();
        await ContentTaskUtils.waitForCondition(() => {
          let waivedLoginItem = Cu.waiveXrays(loginItem);
          return (
            waivedLoginItem._login &&
            waivedLoginItem._login.guid == guidToSelect
          );
        }, "waiting for login-item to show the selected login");

        Assert.equal(
          breachAlert.hidden,
          guidToSelect == testLoginNormalGuid,
          ".breach-alert should be hidden if the login is not breached. current login breached? " +
            (guidToSelect == testLoginBreachedGuid)
        );

        createButton.focus();
        Assert.equal(
          getFocusedElement(),
          createButton,
          "create button is focused"
        );

        await tab();
        Assert.equal(getFocusedElement(), loginSort, "login sort is focused");

        await tab();
        Assert.equal(
          getFocusedElement(),
          loginListbox,
          "listbox is focused next"
        );

        await tab();
        Assert.equal(getFocusedElement(), editButton, "edit button is focused");
      }
    }
  );
});
