/* 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";

/* 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 }
);

async function synthFocus(browser, container, item) {
  let focusPromise = waitForEvent(EVENT_FOCUS, item);
  await invokeContentTask(browser, [container], _container => {
    let elm = (
      content.document._testGetElementById || content.document.getElementById
    ).bind(content.document)(_container);
    elm.focus();
  });
  await focusPromise;
}

async function changeARIAActiveDescendant(
  browser,
  container,
  itemId,
  prevItemId,
  elementReflection
) {
  let expectedEvents = [[EVENT_FOCUS, itemId]];

  if (prevItemId) {
    info("A state change of the previous item precedes the new one.");
    expectedEvents.push(
      stateChangeEventArgs(prevItemId, EXT_STATE_ACTIVE, false, true)
    );
  }

  expectedEvents.push(
    stateChangeEventArgs(itemId, EXT_STATE_ACTIVE, true, true)
  );

  let expectedPromise = waitForEvents(expectedEvents);
  await invokeContentTask(
    browser,
    [container, itemId, elementReflection],
    (_container, _itemId, _elementReflection) => {
      let getElm = (
        content.document._testGetElementById || content.document.getElementById
      ).bind(content.document);
      let elm = getElm(_container);
      if (_elementReflection) {
        elm.ariaActiveDescendantElement = getElm(_itemId);
      } else {
        elm.setAttribute("aria-activedescendant", _itemId);
      }
    }
  );

  await expectedPromise;
}

async function clearARIAActiveDescendant(
  browser,
  container,
  prevItemId,
  defaultId,
  elementReflection
) {
  let expectedEvents = [[EVENT_FOCUS, defaultId || container]];
  if (prevItemId) {
    expectedEvents.push(
      stateChangeEventArgs(prevItemId, EXT_STATE_ACTIVE, false, true)
    );
  }

  if (defaultId) {
    expectedEvents.push(
      stateChangeEventArgs(defaultId, EXT_STATE_ACTIVE, true, true)
    );
  }

  let expectedPromise = waitForEvents(expectedEvents);
  await invokeContentTask(
    browser,
    [container, elementReflection],
    (_container, _elementReflection) => {
      let elm = (
        content.document._testGetElementById || content.document.getElementById
      ).bind(content.document)(_container);
      if (_elementReflection) {
        elm.ariaActiveDescendantElement = null;
      } else {
        elm.removeAttribute("aria-activedescendant");
      }
    }
  );

  await expectedPromise;
}

async function insertItemNFocus(
  browser,
  container,
  newItemID,
  prevItemId,
  elementReflection
) {
  let expectedEvents = [
    [EVENT_SHOW, newItemID],
    [EVENT_FOCUS, newItemID],
  ];

  if (prevItemId) {
    info("A state change of the previous item precedes the new one.");
    expectedEvents.push(
      stateChangeEventArgs(prevItemId, EXT_STATE_ACTIVE, false, true)
    );
  }

  expectedEvents.push(
    stateChangeEventArgs(newItemID, EXT_STATE_ACTIVE, true, true)
  );

  let expectedPromise = waitForEvents(expectedEvents);

  await invokeContentTask(
    browser,
    [container, newItemID, elementReflection],
    (_container, _newItemID, _elementReflection) => {
      let elm = (
        content.document._testGetElementById || content.document.getElementById
      ).bind(content.document)(_container);
      let itemElm = content.document.createElement("div");
      itemElm.setAttribute("id", _newItemID);
      itemElm.setAttribute("role", "listitem");
      itemElm.textContent = _newItemID;
      elm.appendChild(itemElm);
      if (_elementReflection) {
        elm.ariaActiveDescendantElement = itemElm;
      } else {
        elm.setAttribute("aria-activedescendant", _newItemID);
      }
    }
  );

  await expectedPromise;
}

async function moveARIAActiveDescendantID(browser, fromID, toID) {
  let expectedEvents = [
    [EVENT_FOCUS, toID],
    stateChangeEventArgs(toID, EXT_STATE_ACTIVE, true, true),
  ];

  let expectedPromise = waitForEvents(expectedEvents);
  await invokeContentTask(browser, [fromID, toID], (_fromID, _toID) => {
    let orig = (
      content.document._testGetElementById || content.document.getElementById
    ).bind(content.document)(_toID);
    if (orig) {
      orig.id = "";
    }
    (
      content.document._testGetElementById || content.document.getElementById
    ).bind(content.document)(_fromID).id = _toID;
  });
  await expectedPromise;
}

async function changeARIAActiveDescendantInvalid(
  browser,
  container,
  invalidID = "invalid",
  prevItemId = null
) {
  let expectedEvents = [[EVENT_FOCUS, container]];
  if (prevItemId) {
    expectedEvents.push(
      stateChangeEventArgs(prevItemId, EXT_STATE_ACTIVE, false, true)
    );
  }

  let expectedPromise = waitForEvents(expectedEvents);
  await invokeContentTask(
    browser,
    [container, invalidID],
    (_container, _invalidID) => {
      let elm = (
        content.document._testGetElementById || content.document.getElementById
      ).bind(content.document)(_container);
      elm.setAttribute("aria-activedescendant", _invalidID);
    }
  );

  await expectedPromise;
}

const LISTBOX_MARKUP = `
<div role="listbox" aria-activedescendant="item1" id="listbox" tabindex="1"
aria-owns="item3">
<div role="listitem" id="item1">item1</div>
<div role="listitem" id="item2">item2</div>
<div role="listitem" id="roaming" data-id="roaming">roaming</div>
<div role="listitem" id="roaming2" data-id="roaming2">roaming2</div>
</div>
<div role="listitem" id="item3">item3</div>
<div role="combobox" id="combobox">
<input id="combobox_entry">
<ul>
  <li role="option" id="combobox_option1">option1</li>
  <li role="option" id="combobox_option2">option2</li>
</ul>
</div>`;

async function basicListboxTest(browser, elementReflection) {
  await synthFocus(browser, "listbox", "item1");
  await changeARIAActiveDescendant(
    browser,
    "listbox",
    "item2",
    "item1",
    elementReflection
  );
  await changeARIAActiveDescendant(
    browser,
    "listbox",
    "item3",
    "item2",
    elementReflection
  );

  info("Focus out of listbox");
  await synthFocus(browser, "combobox_entry", "combobox_entry");
  await changeARIAActiveDescendant(
    browser,
    "combobox",
    "combobox_option2",
    null,
    elementReflection
  );
  await changeARIAActiveDescendant(
    browser,
    "combobox",
    "combobox_option1",
    null,
    elementReflection
  );

  info("Focus back in listbox");
  await synthFocus(browser, "listbox", "item3");
  await insertItemNFocus(
    browser,
    "listbox",
    "item4",
    "item3",
    elementReflection
  );

  await clearARIAActiveDescendant(
    browser,
    "listbox",
    "item4",
    null,
    elementReflection
  );
  await changeARIAActiveDescendant(
    browser,
    "listbox",
    "item1",
    null,
    elementReflection
  );
}

addAccessibleTask(
  LISTBOX_MARKUP,
  async function (browser) {
    info("Test aria-activedescendant content attribute");
    await basicListboxTest(browser, false);

    await changeARIAActiveDescendantInvalid(
      browser,
      "listbox",
      "invalid",
      "item1"
    );

    await changeARIAActiveDescendant(browser, "listbox", "roaming");
    await moveARIAActiveDescendantID(browser, "roaming2", "roaming");
    await changeARIAActiveDescendantInvalid(
      browser,
      "listbox",
      "roaming3",
      "roaming"
    );
    await moveARIAActiveDescendantID(browser, "roaming", "roaming3");
  },
  { topLevel: true, chrome: true }
);

addAccessibleTask(
  LISTBOX_MARKUP,
  async function (browser) {
    info("Test ariaActiveDescendantElement element reflection");
    await basicListboxTest(browser, true);
  },
  { topLevel: true, chrome: true }
);

addAccessibleTask(
  `
<input id="activedesc_nondesc_input" aria-activedescendant="activedesc_nondesc_option">
<div role="listbox">
  <div role="option" id="activedesc_nondesc_option">option</div>
</div>`,
  async function (browser) {
    info("Test aria-activedescendant non-descendant");
    await synthFocus(
      browser,
      "activedesc_nondesc_input",
      "activedesc_nondesc_option"
    );
  },
  { topLevel: true, chrome: true }
);

addAccessibleTask(
  `<div id="shadow"></div>`,
  async function (browser) {
    info("Test aria-activedescendant in shadow root");

    await invokeContentTask(browser, [], () => {
      const doc = content.document;

      let host = doc.getElementById("shadow");
      let shadow = host.attachShadow({ mode: "open" });
      let listbox = doc.createElement("div");
      listbox.id = "shadowListbox";
      listbox.setAttribute("role", "listbox");
      listbox.setAttribute("tabindex", "0");
      shadow.appendChild(listbox);
      let item = doc.createElement("div");
      item.id = "shadowItem1";
      item.setAttribute("role", "option");
      listbox.appendChild(item);
      listbox.setAttribute("aria-activedescendant", "shadowItem1");
      item = doc.createElement("div");
      item.id = "shadowItem2";
      item.setAttribute("role", "option");
      listbox.appendChild(item);

      // We want to retrieve elements using their IDs inside the shadow root, so
      // we define a custom get element by ID method that our utility functions
      // above call into if it exists.
      doc._testGetElementById = id =>
        doc.getElementById("shadow").shadowRoot.getElementById(id);
    });

    await synthFocus(browser, "shadowListbox", "shadowItem1");
    await changeARIAActiveDescendant(
      browser,
      "shadowListbox",
      "shadowItem2",
      "shadowItem1"
    );
    info("Do it again with element reflection");
    await changeARIAActiveDescendant(
      browser,
      "shadowListbox",
      "shadowItem1",
      "shadowItem2",
      true
    );
  },
  { topLevel: true, chrome: true }
);

addAccessibleTask(
  `
<div id="comboboxWithHiddenList" tabindex="0" role="combobox" aria-owns="hiddenList">
</div>
<div id="hiddenList" hidden role="listbox">
  <div id="hiddenListOption" role="option"></div>
</div>`,
  async function (browser, docAcc) {
    info("Test simultaneous insertion, relocation and aria-activedescendant");
    await synthFocus(
      browser,
      "comboboxWithHiddenList",
      "comboboxWithHiddenList"
    );

    testStates(
      findAccessibleChildByID(docAcc, "comboboxWithHiddenList"),
      STATE_FOCUSED
    );
    let evtProm = Promise.all([
      waitForEvent(EVENT_FOCUS, "hiddenListOption"),
      waitForStateChange("hiddenListOption", EXT_STATE_ACTIVE, true, true),
    ]);
    await invokeContentTask(browser, [], () => {
      info("hiddenList is owned, so unhiding causes insertion and relocation.");
      (
        content.document._testGetElementById || content.document.getElementById
      ).bind(content.document)("hiddenList").hidden = false;
      content.document
        .getElementById("comboboxWithHiddenList")
        .setAttribute("aria-activedescendant", "hiddenListOption");
    });
    await evtProm;
    testStates(
      findAccessibleChildByID(docAcc, "hiddenListOption"),
      STATE_FOCUSED
    );
  },
  { topLevel: true, chrome: true }
);

addAccessibleTask(
  `
<custom-listbox id="custom-listbox1">
  <div role="listitem" id="l1_1"></div>
  <div role="listitem" id="l1_2"></div>
  <div role="listitem" id="l1_3"></div>
</custom-listbox>

<custom-listbox id="custom-listbox2" aria-activedescendant="l2_1">
  <div role="listitem" id="l2_1"></div>
  <div role="listitem" id="l2_2"></div>
  <div role="listitem" id="l2_3"></div>
</custom-listbox>

<script>
customElements.define("custom-listbox",
  class extends HTMLElement {
    constructor() {
      super();
      this.tabIndex = "0"
      this._internals = this.attachInternals();
      this._internals.role = "listbox";
      this._internals.ariaActiveDescendantElement = this.lastElementChild;
    }
    get internals() {
      return this._internals;
    }
  }
);
</script>`,
  async function (browser) {
    await synthFocus(browser, "custom-listbox1", "l1_3");

    let evtProm = Promise.all([
      waitForEvent(EVENT_FOCUS, "l1_2"),
      waitForStateChange("l1_3", EXT_STATE_ACTIVE, false, true),
      waitForStateChange("l1_2", EXT_STATE_ACTIVE, true, true),
    ]);

    await invokeContentTask(browser, [], () => {
      content.document.getElementById(
        "custom-listbox1"
      ).internals.ariaActiveDescendantElement =
        content.document.getElementById("l1_2");
    });

    await evtProm;

    evtProm = Promise.all([
      waitForEvent(EVENT_FOCUS, "custom-listbox1"),
      waitForStateChange("l1_2", EXT_STATE_ACTIVE, false, true),
    ]);

    await invokeContentTask(browser, [], () => {
      content.document.getElementById(
        "custom-listbox1"
      ).internals.ariaActiveDescendantElement = null;
    });

    await evtProm;

    await synthFocus(browser, "custom-listbox2", "l2_1");
    await clearARIAActiveDescendant(browser, "custom-listbox2", "l2_1", "l2_3");
  }
);
