/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

const { SearchService } = ChromeUtils.importESModule(
  "moz-src:///toolkit/components/search/SearchService.sys.mjs"
);
const { UrlbarTestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/UrlbarTestUtils.sys.mjs"
);
/* import-globals-from ../../mochitest/role.js */
/* import-globals-from ../../mochitest/states.js */
loadScripts(
  { name: "role.js", dir: MOCHITESTS_DIR },
  { name: "states.js", dir: MOCHITESTS_DIR }
);

function getMacAccessible(accOrElmOrID) {
  return new Promise(resolve => {
    let intervalId = setInterval(() => {
      let acc = getAccessible(accOrElmOrID);
      if (acc) {
        clearInterval(intervalId);
        resolve(
          acc.nativeInterface.QueryInterface(Ci.nsIAccessibleMacInterface)
        );
      }
    }, 10);
  });
}

add_setup(async function () {
  await SpecialPowers.pushPrefEnv({
    set: [["browser.urlbar.trustPanel.featureGate", false]],
  });
});

/**
 * Test a11yUtils announcements are exposed to VO
 */
add_task(async () => {
  const tab = await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    "data:text/html,"
  );
  const alert = document.getElementById("a11y-announcement");
  ok(alert, "Found alert to send announcements");

  const alerted = waitForMacEvent("AXAnnouncementRequested", (iface, data) => {
    return data.AXAnnouncementKey == "hello world";
  });

  A11yUtils.announce({
    raw: "hello world",
  });
  await alerted;
  await BrowserTestUtils.removeTab(tab);
});

/**
 * Test browser tabs
 */
add_task(async () => {
  let newTabs = await Promise.all([
    BrowserTestUtils.openNewForegroundTab(
      gBrowser,
      "data:text/html,<title>Two</title>"
    ),
    BrowserTestUtils.openNewForegroundTab(
      gBrowser,
      "data:text/html,<title>Three</title>"
    ),
    BrowserTestUtils.openNewForegroundTab(
      gBrowser,
      "data:text/html,<title>Four</title>"
    ),
  ]);

  // Mochitests spawn with a tab, and we've opened 3 more for a total of 4 tabs
  is(gBrowser.tabs.length, 4, "We now have 4 open tabs");

  let tablist = await getMacAccessible("tabbrowser-tabs");
  is(
    tablist.getAttributeValue("AXRole"),
    "AXTabGroup",
    "Correct role for tablist"
  );

  let tabMacAccs = tablist.getAttributeValue("AXTabs");
  is(tabMacAccs.length, 4, "4 items in AXTabs");

  let selectedTabs = tablist.getAttributeValue("AXSelectedChildren");
  is(selectedTabs.length, 1, "one selected tab");

  let tab = selectedTabs[0];
  is(tab.getAttributeValue("AXRole"), "AXRadioButton", "Correct role for tab");
  is(
    tab.getAttributeValue("AXSubrole"),
    "AXTabButton",
    "Correct subrole for tab"
  );
  is(tab.getAttributeValue("AXTitle"), "Four", "Correct title for tab");

  let tabToSelect = tabMacAccs[2];
  is(
    tabToSelect.getAttributeValue("AXTitle"),
    "Three",
    "Correct title for tab"
  );

  let actions = tabToSelect.actionNames;
  ok(true, actions);
  ok(actions.includes("AXPress"), "Has switch action");

  // When tab is clicked selection of tab group changes,
  // and focus goes to the web area. Wait for both.
  let evt = Promise.all([
    waitForMacEvent("AXSelectedChildrenChanged"),
    waitForMacEvent(
      "AXFocusedUIElementChanged",
      iface => iface.getAttributeValue("AXRole") == "AXWebArea"
    ),
  ]);
  tabToSelect.performAction("AXPress");
  await evt;

  selectedTabs = tablist.getAttributeValue("AXSelectedChildren");
  is(selectedTabs.length, 1, "one selected tab");
  is(
    selectedTabs[0].getAttributeValue("AXTitle"),
    "Three",
    "Correct title for tab"
  );

  // Close all open tabs
  await Promise.all(newTabs.map(t => BrowserTestUtils.removeTab(t)));
});

/**
 * Test ignored invisible items in root
 */
add_task(async () => {
  await BrowserTestUtils.withNewTab(
    {
      gBrowser,
      url: "about:license",
    },
    async () => {
      const root = await getMacAccessible(document);
      let rootChildCount = () => root.getAttributeValue("AXChildren").length;

      // With no popups, the root accessible has 5 visible children:
      // 1. Tab bar (#TabsToolbar)
      // 2. Navigation bar (#nav-bar)
      // 3. Notifications toolbar (#notifications-toolbar)
      // 4. Content area (#tabbrowser-tabpanels)
      // 5. Accessibility announcements dialog (#a11y-announcement)
      let baseRootChildCount = 5;
      is(
        rootChildCount(),
        baseRootChildCount,
        `Root with no popups has ${baseRootChildCount} children`
      );

      // Open a context menu
      const menu = document.getElementById("contentAreaContextMenu");
      if (
        Services.prefs.getBoolPref("widget.macos.native-context-menus", false)
      ) {
        // Native context menu - do not expect accessibility notifications.
        let popupshown = BrowserTestUtils.waitForPopupEvent(menu, "shown");
        EventUtils.synthesizeMouseAtCenter(document.body, {
          type: "contextmenu",
        });
        await popupshown;

        is(
          rootChildCount(),
          baseRootChildCount,
          "Native context menus do not show up in the root children"
        );

        // Close context menu
        let popuphidden = BrowserTestUtils.waitForPopupEvent(menu, "hidden");
        menu.hidePopup();
        await popuphidden;
      } else {
        // Non-native menu
        EventUtils.synthesizeMouseAtCenter(document.body, {
          type: "contextmenu",
        });
        await waitForMacEvent("AXMenuOpened");

        // Now root has 1 more child
        is(rootChildCount(), baseRootChildCount + 1, "Root has 1 more child");

        // Close context menu
        let closed = waitForMacEvent("AXMenuClosed", "contentAreaContextMenu");
        EventUtils.synthesizeKey("KEY_Escape");
        await BrowserTestUtils.waitForPopupEvent(menu, "hidden");
        await closed;
      }

      // We're back to base child count
      is(rootChildCount(), baseRootChildCount, "Root has original child count");
    }
  );
});

/**
 * Tests for location bar
 */
add_task(async () => {
  await BrowserTestUtils.withNewTab(
    {
      gBrowser,
      // eslint-disable-next-line sdl/no-insecure-url
      url: "http://example.com",
    },
    async () => {
      let input = await getMacAccessible(gURLBar.inputField);
      is(
        input.getAttributeValue("AXValue"),
        // eslint-disable-next-line sdl/no-insecure-url
        UrlbarTestUtils.trimURL("http://example.com"),
        "Location bar has correct value"
      );
    }
  );
});

/**
 * Tests attributed text in nav bar has no invisible AXAttachments
 */
add_task(async () => {
  await BrowserTestUtils.withNewTab(
    {
      gBrowser,
      // eslint-disable-next-line sdl/no-insecure-url
      url: "http://example.com",
    },
    async () => {
      let root = await getMacAccessible(document);
      let navBar = await getMacAccessible("nav-bar");
      let elemRange = root.getParameterizedAttributeValue(
        "AXTextMarkerRangeForUIElement",
        navBar
      );
      let attributedString = root.getParameterizedAttributeValue(
        "AXAttributedStringForTextMarkerRange",
        elemRange
      );
      let attachmentRoles = attributedString.map(s =>
        s.AXAttachment ? s.AXAttachment.getAttributeValue("AXRole") : null
      );
      ok(
        !attachmentRoles.includes("AXMenu"),
        "Collapsed menu should be embedded in attributed text"
      );
    }
  );
});

/**
 * Test context menu
 */
add_task(async () => {
  if (Services.prefs.getBoolPref("widget.macos.native-context-menus", false)) {
    ok(true, "We cannot inspect native context menu contents; skip this test.");
    return;
  }

  await BrowserTestUtils.withNewTab(
    {
      gBrowser,
      url: 'data:text/html,<a id="exampleLink" href="https://example.com">link</a>',
    },
    async browser => {
      if (!SearchService.isInitialized) {
        let aStatus = await SearchService.init();
        Assert.ok(Components.isSuccessCode(aStatus));
        Assert.ok(SearchService.isInitialized);
      }

      const hasContainers =
        Services.prefs.getBoolPref("privacy.userContext.enabled") &&
        !!ContextualIdentityService.getPublicIdentities().length;
      info(`${hasContainers ? "Do" : "Don't"} expect containers item.`);
      const hasInspectA11y =
        Services.prefs.getBoolPref("devtools.everOpened", false) ||
        Services.prefs.getIntPref("devtools.selfxss.count", 0) > 0;
      info(`${hasInspectA11y ? "Do" : "Don't"} expect inspect a11y item.`);

      // synthesize a right click on the link to open the link context menu
      let menu = document.getElementById("contentAreaContextMenu");
      await BrowserTestUtils.synthesizeMouseAtCenter(
        "#exampleLink",
        { type: "contextmenu" },
        browser
      );
      await waitForMacEvent("AXMenuOpened");

      menu = await getMacAccessible(menu);
      let menuChildren = menu.getAttributeValue("AXChildren");
      const expectedChildCount = 12 + +hasContainers + +hasInspectA11y;
      is(
        menuChildren.length,
        expectedChildCount,
        `Context menu on link contains ${expectedChildCount} items.`
      );
      // items at indicies 3, 9, and 11 are the splitters when containers exist
      // everything else should be a menu item, otherwise indicies of splitters are
      // 3, 8, and 10
      const splitterIndicies = hasContainers ? [4, 9, 11] : [3, 8, 10];
      for (let i = 0; i < menuChildren.length; i++) {
        if (splitterIndicies.includes(i)) {
          is(
            menuChildren[i].getAttributeValue("AXRole"),
            "AXSplitter",
            "found splitter in menu"
          );
        } else {
          is(
            menuChildren[i].getAttributeValue("AXRole"),
            "AXMenuItem",
            "found menu item in menu"
          );
        }
      }

      // check the containers sub menu in depth if it exists
      if (hasContainers) {
        is(
          menuChildren[1].getAttributeValue("AXVisibleChildren"),
          null,
          "Submenu 1 has no visible chldren when hidden"
        );

        // focus the first submenu
        EventUtils.synthesizeKey("KEY_ArrowDown");
        EventUtils.synthesizeKey("KEY_ArrowDown");
        EventUtils.synthesizeKey("KEY_ArrowRight");
        await waitForMacEvent("AXMenuOpened");

        // after the submenu is opened, refetch it
        menu = document.getElementById("contentAreaContextMenu");
        menu = await getMacAccessible(menu);
        menuChildren = menu.getAttributeValue("AXChildren");

        // verify submenu-menuitem's attributes
        is(
          menuChildren[1].getAttributeValue("AXChildren").length,
          1,
          "Submenu 1 has one child when open"
        );
        const subMenu = menuChildren[1].getAttributeValue("AXChildren")[0];
        is(
          subMenu.getAttributeValue("AXRole"),
          "AXMenu",
          "submenu has role of menu"
        );
        const subMenuChildren = subMenu.getAttributeValue("AXChildren");
        is(subMenuChildren.length, 4, "sub menu has 4 children");
        is(
          subMenu.getAttributeValue("AXVisibleChildren").length,
          4,
          "submenu has 4 visible children"
        );

        // close context menu
        EventUtils.synthesizeKey("KEY_Escape");
        await waitForMacEvent("AXMenuClosed");
      }

      EventUtils.synthesizeKey("KEY_Escape");
      await waitForMacEvent("AXMenuClosed");
    }
  );
});

async function testPopover(native) {
  await SpecialPowers.pushPrefEnv({
    set: [["widget.macos.native-popovers", native]],
  });
  // Needs to happen in a separate window for the native/not-native pref to take effect.
  let win = await BrowserTestUtils.openNewBrowserWindow();

  await BrowserTestUtils.withNewTab(
    {
      gBrowser: win.gBrowser,
      url: "about:license",
    },
    async () => {
      let root = await getMacAccessible(win.document);
      let rootChildCount = () => root.getAttributeValue("AXChildren").length;

      let baseRootChildCount = 5;
      is(
        rootChildCount(),
        baseRootChildCount,
        `Root with no popups has ${baseRootChildCount} children`
      );

      // Open site identity popup
      win.document.getElementById("identity-icon-box").click();
      const identityPopup = win.document.getElementById("identity-popup");
      await BrowserTestUtils.waitForPopupEvent(identityPopup, "shown");

      if (native) {
        // With native popovers the popover will appear in a separate child window and
        // not in the main root group.
        is(
          rootChildCount(),
          baseRootChildCount,
          "Root does not have another child"
        );
      } else {
        // With non native popovers, the AXPopover will appear in-line with its markup
        // in the root group.
        is(rootChildCount(), baseRootChildCount + 1, "Root has another child");
      }

      let popupAcc = getAccessible(
        identityPopup
      ).nativeInterface.QueryInterface(Ci.nsIAccessibleMacInterface);
      is(
        popupAcc.getAttributeValue("AXSubrole"),
        "AXApplicationAlertDialog",
        "Popup has correct subrole"
      );

      let popupAccParent = popupAcc.getAttributeValue("AXParent");

      if (native) {
        // With native popovers, the popup's parent is the native popover.
        is(
          popupAccParent.getAttributeValue("AXRole"),
          "AXPopover",
          "Popup's parent is the popover window"
        );
      } else {
        // In non-native, the popup itself uses the role of popover
        is(
          popupAcc.getAttributeValue("AXRole"),
          "AXPopover",
          "Popup has correct role"
        );
      }

      let popupAccGrandparent = popupAccParent.getAttributeValue("AXParent");

      if (native) {
        is(
          popupAccGrandparent.getAttributeValue("AXRole"),
          "AXWindow",
          "native popup's grandparent is the app window"
        );
      } else {
        is(
          popupAccGrandparent.getAttributeValue("AXRole"),
          "AXWindow",
          "non-native popup's parent is the root group, and its grandparent is the main window"
        );
      }

      // Close popup
      let hide = waitForMacEvent("AXUIElementDestroyed");
      EventUtils.synthesizeKey("KEY_Escape", null, win);
      await BrowserTestUtils.waitForPopupEvent(identityPopup, "hidden");
      await hide;

      // We're back to the base child count
      is(rootChildCount(), baseRootChildCount, "Root has the base child count");
    }
  );
  await SpecialPowers.popPrefEnv();
  await BrowserTestUtils.closeWindow(win);
}

/**
 * Test popovers.
 */
add_task(async () => {
  if (Services.env.get("MOZ_HEADLESS")) {
    todo(false, "Popovers don't work quite right in headless mode");
    return;
  }
  // await testPopover(true);
  await testPopover(false);
});
