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

add_setup(async function () {
  await SpecialPowers.pushPrefEnv({
    set: [["test.wait300msAfterTabSwitch", true]],
  });
});

async function runTests(browser, accDoc) {
  const dpr = await getContentDPR(browser);

  await testChildAtPoint(
    dpr,
    3,
    3,
    findAccessibleChildByID(accDoc, "list"),
    findAccessibleChildByID(accDoc, "listitem"),
    findAccessibleChildByID(accDoc, "inner").firstChild
  );
  todo(
    false,
    "Bug 746974 - children must match on all platforms. On Windows, " +
      "ChildAtPoint with eDeepestChild is incorrectly ignoring MustPrune " +
      "for the graphic."
  );

  const txt = findAccessibleChildByID(accDoc, "txt");
  await testChildAtPoint(dpr, 1, 1, txt, txt, txt);

  info(
    "::MustPrune case, point is outside of textbox accessible but is in document."
  );
  await testChildAtPoint(dpr, -1, -1, txt, null, null);

  info("::MustPrune case, point is outside of root accessible.");
  await testChildAtPoint(dpr, -10000, -10000, txt, null, null);

  info("Not specific case, point is inside of btn accessible.");
  const btn = findAccessibleChildByID(accDoc, "btn");
  await testChildAtPoint(dpr, 1, 1, btn, btn, btn);

  info("Not specific case, point is outside of btn accessible.");
  await testChildAtPoint(dpr, -1, -1, btn, null, null);

  info(
    "Out of flow accessible testing, do not return out of flow accessible " +
      "because it's not a child of the accessible even though visually it is."
  );
  await invokeContentTask(browser, [], () => {
    const { CommonUtils } = ChromeUtils.importESModule(
      "chrome://mochitests/content/browser/accessible/tests/browser/Common.sys.mjs"
    );
    const doc = content.document;
    const rectArea = CommonUtils.getNode("area", doc).getBoundingClientRect();
    const outOfFlow = CommonUtils.getNode("outofflow", doc);
    outOfFlow.style.left = rectArea.left + "px";
    outOfFlow.style.top = rectArea.top + "px";
  });

  const area = findAccessibleChildByID(accDoc, "area");
  await testChildAtPoint(dpr, 1, 1, area, area, area);

  info("Test image maps. Their children are not in the layout tree.");
  await waitForImageMap(browser, accDoc);
  const imgmap = findAccessibleChildByID(accDoc, "imgmap");
  ok(imgmap, "Image map exists");
  const theLetterA = imgmap.firstChild;
  await hitTest(browser, imgmap, theLetterA, theLetterA);
  await hitTest(
    browser,
    findAccessibleChildByID(accDoc, "container"),
    imgmap,
    theLetterA
  );

  info("hit testing for element contained by zero-width element");
  const container2Input = findAccessibleChildByID(accDoc, "container2_input");
  await hitTest(
    browser,
    findAccessibleChildByID(accDoc, "container2"),
    container2Input,
    container2Input
  );

  info("hittesting table, row, cells -- rows are not in the layout tree");
  const table = findAccessibleChildByID(accDoc, "table");
  const row = findAccessibleChildByID(accDoc, "row");
  const cell1 = findAccessibleChildByID(accDoc, "cell1");

  await hitTest(browser, table, row, cell1);

  info("Testing that an inaccessible child doesn't break hit testing");
  const containerWithInaccessibleChild = findAccessibleChildByID(
    accDoc,
    "containerWithInaccessibleChild"
  );
  const containerWithInaccessibleChildP2 = findAccessibleChildByID(
    accDoc,
    "containerWithInaccessibleChild_p2"
  );
  await hitTest(
    browser,
    containerWithInaccessibleChild,
    containerWithInaccessibleChildP2,
    containerWithInaccessibleChildP2.firstChild
  );

  info("Testing wrapped text");
  const wrappedTextLinkFirstP = findAccessibleChildByID(
    accDoc,
    "wrappedTextLinkFirstP"
  );
  const wrappedTextLinkFirstA = findAccessibleChildByID(
    accDoc,
    "wrappedTextLinkFirstA"
  );
  await hitTest(
    browser,
    wrappedTextLinkFirstP,
    wrappedTextLinkFirstA,
    wrappedTextLinkFirstA.firstChild
  );
  const wrappedTextLeafFirstP = findAccessibleChildByID(
    accDoc,
    "wrappedTextLeafFirstP"
  );
  const wrappedTextLeafFirstMark = findAccessibleChildByID(
    accDoc,
    "wrappedTextLeafFirstMark"
  );
  await hitTest(
    browser,
    wrappedTextLeafFirstP,
    wrappedTextLeafFirstMark,
    wrappedTextLeafFirstMark.firstChild
  );
  const wrappedTextNestedInlineP = findAccessibleChildByID(
    accDoc,
    "wrappedTextNestedInlineP"
  );
  const wrappedTextNestedInlineEm = findAccessibleChildByID(
    accDoc,
    "wrappedTextNestedInlineEm"
  );
  const wrappedTextNestedInlineStrong = findAccessibleChildByID(
    accDoc,
    "wrappedTextNestedInlineStrong"
  );
  await hitTest(
    browser,
    wrappedTextNestedInlineP,
    wrappedTextNestedInlineEm,
    wrappedTextNestedInlineStrong.firstChild
  );
  const wrappedTextPre = findAccessibleChildByID(accDoc, "wrappedTextPre");
  const wrappedTextPreCode = findAccessibleChildByID(
    accDoc,
    "wrappedTextPreCode"
  );
  await hitTest(
    browser,
    wrappedTextPre,
    wrappedTextPreCode,
    wrappedTextPreCode.firstChild
  );
  // hitTest() can only test the first character. We need to test a subsequent
  // character for this case.
  let [x, y, w] = await getContentBoundsForDOMElm(
    browser,
    "wrappedTextPreCode"
  );
  // Use the top center of the element.
  x = x + w / 2;
  await untilCacheIs(
    () => getChildAtPoint(wrappedTextPre, x, y, true),
    wrappedTextPreCode.firstChild,
    `Wrong deepest child accessible at the point (${x}, ${y}) of wrappedTextPre, sought wrappedTextPreCode leaf`
  );

  info("Testing image");
  const imageP = findAccessibleChildByID(accDoc, "imageP");
  const image = findAccessibleChildByID(accDoc, "image");
  await hitTest(browser, imageP, image, image);

  info("Testing image map with 0-sized area");
  const mapWith0AreaP = findAccessibleChildByID(accDoc, "mapWith0AreaP");
  const mapWith0Area = findAccessibleChildByID(accDoc, "mapWith0Area");
  await hitTest(browser, mapWith0AreaP, mapWith0Area, mapWith0Area);
}

addAccessibleTask(
  `
  <div role="list" id="list">
    <div role="listitem" id="listitem"><span title="foo" id="inner">inner</span>item</div>
  </div>

  <span role="button">button1</span><span role="button" id="btn">button2</span>

  <span role="textbox">textbox1</span><span role="textbox" id="txt">textbox2</span>

  <div id="outofflow" style="width: 10px; height: 10px; position: absolute; left: 0px; top: 0px; background-color: yellow;">
  </div>
  <div id="area" style="width: 100px; height: 100px; background-color: blue;"></div>

  <map name="atoz_map">
    <area id="thelettera" href="http://www.bbc.co.uk/radio4/atoz/index.shtml#a"
          coords="0,0,15,15" alt="thelettera" shape="rect"/>
  </map>

  <div id="container">
    <img id="imgmap" width="447" height="15" usemap="#atoz_map" src="http://example.com/a11y/accessible/tests/mochitest/letters.gif"/>
  </div>

  <div id="container2" style="width: 0px">
    <input id="container2_input">
  </div>

  <table id="table" border>
    <tr id="row">
      <td id="cell1">hello</td>
      <td id="cell2">world</td>
    </tr>
  </table>

  <div id="containerWithInaccessibleChild">
    <p>hi</p>
    <p aria-hidden="true">hi</p>
    <p id="containerWithInaccessibleChild_p2">bye</p>
  </div>

  <p id="wrappedTextLinkFirstP" style="width: 3ch; font-family: monospace;">
    <a id="wrappedTextLinkFirstA" href="https://example.com/">a</a>b cd
  </p>

  <p id="wrappedTextLeafFirstP" style="width: 3ch; font-family: monospace;">
    <mark id="wrappedTextLeafFirstMark">a</mark><a href="https://example.com/">b cd</a>
  </p>

  <p id="wrappedTextNestedInlineP" style="width: 1ch; font-family: monospace;">
    <em id="wrappedTextNestedInlineEm"><strong id="wrappedTextNestedInlineStrong">y </strong>z</em>
  </p>

  <pre id="wrappedTextPre"><code id="wrappedTextPreCode">ab cd
e</pre>

  <p id="imageP">
    <img id="image" src="http://example.com/a11y/accessible/tests/mochitest/letters.gif">
  </p>

  <map id="0Area">
    <area shape="rect">
  </map>
  <p id="mapWith0AreaP">
    <img id="mapWith0Area" src="http://example.com/a11y/accessible/tests/mochitest/letters.gif" usemap="#0Area">
  </p>
  `,
  runTests,
  {
    iframe: true,
    remoteIframe: true,
    // Ensure that all hittest elements are in view.
    iframeAttrs: { style: "width: 600px; height: 600px; padding: 10px;" },
  }
);

addAccessibleTask(
  `
  <div id="container">
    <h1 id="a">A</h1><h1 id="b">B</h1>
  </div>
  `,
  async function (browser, accDoc) {
    const a = findAccessibleChildByID(accDoc, "a");
    const b = findAccessibleChildByID(accDoc, "b");
    const dpr = await getContentDPR(browser);
    // eslint-disable-next-line no-unused-vars
    const [x, y, w, h] = Layout.getBounds(a, dpr);
    // The point passed below will be made relative to `b`, but
    // we'd like to test a point within `a`. Pass `a`s negative
    // width for an x offset. Pass zero as a y offset,
    // assuming the headings are on the same line.
    await testChildAtPoint(dpr, -w, 0, b, null, null);
  },
  {
    iframe: true,
    remoteIframe: true,
    // Ensure that all hittest elements are in view.
    iframeAttrs: { style: "width: 600px; height: 600px; padding: 10px;" },
  }
);

addAccessibleTask(
  `
  <style>
    div {
      width: 50px;
      height: 50px;
      position: relative;
    }

    div > div {
      width: 30px;
      height: 30px;
      position: absolute;
      opacity: 0.9;
    }
  </style>
  <div id="a" style="background-color: orange;">
    <div id="aa" style="background-color: purple;"></div>
  </div>
  <div id="b" style="background-color: yellowgreen;">
    <div id="bb" style="top: -30px; background-color: turquoise"></div>
  </div>`,
  async function (browser, accDoc) {
    const a = findAccessibleChildByID(accDoc, "a");
    const aa = findAccessibleChildByID(accDoc, "aa");
    const dpr = await getContentDPR(browser);
    const [, , w, h] = Layout.getBounds(a, dpr);
    // test upper left of `a`
    await testChildAtPoint(dpr, 1, 1, a, aa, aa);
    // test upper right of `a`
    await testChildAtPoint(dpr, w - 1, 1, a, a, a);
    // test just outside upper left of `a`
    await testChildAtPoint(dpr, 1, -1, a, null, null);
    // test halfway down/left of `a`
    await testChildAtPoint(dpr, 1, Math.round(h / 2), a, a, a);
  },
  {
    chrome: true,
    topLevel: true,
    iframe: false,
    remoteIframe: false,
    // Ensure that all hittest elements are in view.
    iframeAttrs: { style: "width: 600px; height: 600px; padding: 10px;" },
  }
);

/**
 * Verify that hit testing returns the proper accessible when one acc content
 * is partially hidden due to overflow:hidden;
 */
addAccessibleTask(
  `
  <style>
    div div {
      overflow: hidden;
      font-family: monospace;
      width: 2ch;
    }
  </style>
  <div id="container" style="display: flex; flex-direction: row-reverse;">
    <div id="aNode">abcde</div><div id="fNode">fghij</div>
  </div>`,
  async function (browser, docAcc) {
    const container = findAccessibleChildByID(docAcc, "container");
    const aNode = findAccessibleChildByID(docAcc, "aNode");
    const fNode = findAccessibleChildByID(docAcc, "fNode");
    const dpr = await getContentDPR(browser);
    const [, , containerWidth] = Layout.getBounds(container, dpr);
    const [, , aNodeWidth] = Layout.getBounds(aNode, dpr);

    await testChildAtPoint(
      dpr,
      containerWidth - 1,
      1,
      container,
      aNode,
      aNode.firstChild
    );
    await testChildAtPoint(
      dpr,
      containerWidth - aNodeWidth - 1,
      1,
      container,
      fNode,
      fNode.firstChild
    );
  },
  { chrome: true, iframe: true, remoteIframe: true }
);

/**
 * Verify that hit testing is appropriately fuzzy when working with generics.
 * If we match on a generic which contains additional generics and a single text
 * leaf, we should return the text leaf as the deepest match instead of the
 * generic itself.
 */
addAccessibleTask(
  `
  <a href="example.com" id="link">
    <span style="overflow:hidden;" id="generic"><span aria-hidden="true" id="visible">I am some visible text</span><span id="invisible" style="overflow:hidden; height: 1px; width: 1px; position:absolute; clip: rect(0 0 0 0); display:block;">I am some invisible text</span></span>
  </a>`,
  async function (browser, docAcc) {
    const link = findAccessibleChildByID(docAcc, "link");
    const generic = findAccessibleChildByID(docAcc, "generic");
    const invisible = findAccessibleChildByID(docAcc, "invisible");
    const dpr = await getContentDPR(browser);

    await testChildAtPoint(
      dpr,
      1,
      1,
      link,
      generic, // Direct Child
      invisible.firstChild // Deepest Child
    );

    await testOffsetAtPoint(
      findAccessibleChildByID(docAcc, "invisible", [Ci.nsIAccessibleText]),
      1,
      1,
      COORDTYPE_PARENT_RELATIVE,
      0
    );
  },
  { chrome: false, iframe: true, remoteIframe: true }
);

/**
 * Verify that hit testing is appropriately fuzzy when working with generics with siblings.
 * We should return the deepest text leaf as the deepest match instead of the generic itself.
 */
addAccessibleTask(
  `
<div id="generic"><span aria-hidden="true" id="visible">Mozilla</span><span id="invisible" style="display: block !important;border: 0 !important;clip: rect(0 0 0 0) !important;height: 1px !important;margin: -1px !important;overflow: hidden !important;padding: 0 !important;position: absolute !important;white-space: nowrap !important;width: 1px !important;">hello world<br><div id="extraContainer">Mozilla</div></span><br>I am some other text</div>`,
  async function (browser, docAcc) {
    const generic = findAccessibleChildByID(docAcc, "generic");
    const invisible = findAccessibleChildByID(docAcc, "invisible");
    const dpr = await getContentDPR(browser);

    await testChildAtPoint(
      dpr,
      1,
      1,
      generic,
      invisible, // Direct Child
      invisible.firstChild // Deepest Child
    );
  },
  { chrome: false, iframe: true, remoteIframe: true }
);

/**
 * Verify that hit testing correctly ignores
 * elements with pointer-events: none;
 */
addAccessibleTask(
  `<div id="container" style="position:relative;"><button id="obscured">click me</button><div id="overlay" style="pointer-events:none; top:0; bottom:0; left:0; right:0; position: absolute;"></div></div><button id="clickable">I am clickable</button>`,
  async function (browser, docAcc) {
    const container = findAccessibleChildByID(docAcc, "container");
    const obscured = findAccessibleChildByID(docAcc, "obscured");
    const clickable = findAccessibleChildByID(docAcc, "clickable");
    const dpr = await getContentDPR(browser);
    let [targetX, targetY, targetW, targetH] = Layout.getBounds(obscured, dpr);
    const [x, y] = Layout.getBounds(docAcc, dpr);
    await testChildAtPoint(
      dpr,
      targetX - x + targetW / 2,
      targetY - y + targetH / 2,
      docAcc,
      container, // Direct Child
      obscured // Deepest Child
    );

    [targetX, targetY, targetW, targetH] = Layout.getBounds(clickable, dpr);
    await testChildAtPoint(
      dpr,
      targetX - x + targetW / 2,
      targetY - y + targetH / 2,
      docAcc,
      clickable, // Direct Child
      clickable // Deepest Child
    );
  },
  { chrome: false, iframe: true, remoteIframe: true }
);

/**
 * Test hit testing on elements which change to display: contents.
 */
addAccessibleTask(
  `
<style>
  p {
    font-family: monospace;
  }
</style>
<details id="detailsOpen" open>
  <summary>summary</summary>
  <p id="detailsOpenP">detailsOpenP</p>
</details>
<details id="details">
  <summary>summary</summary>
  <p id="detailsP">detailsP</p>
</details>
<div id="outer">
  <p>before</p>
  <div id="inner" role="group">
    <p id="innerP" hidden>innerP</p>
  </div>
</div>
  `,
  async function testChangeDisplayContents(browser, docAcc) {
    const detailsOpen = findAccessibleChildByID(docAcc, "detailsOpen");
    const detailsOpenP = findAccessibleChildByID(docAcc, "detailsOpenP");
    // Between the details element and its <p> exists
    // a pseudoelement, ::details-content. Use this as
    // our expected target for direct-child hittesting.
    await hitTest(
      browser,
      detailsOpen,
      detailsOpenP.parent,
      detailsOpenP.firstChild
    );

    info("Opening details");
    let shown = waitForEvent(EVENT_SHOW, "detailsP");
    await invokeContentTask(browser, [], () => {
      content.document.getElementById("details").open = true;
    });
    const detailsP = (await shown).accessible;
    // There is a <slot> between details and detailsP which has display:
    // contents.
    await hitTest(browser, detailsP.parent, detailsP, detailsP.firstChild);

    info("Setting display: contents on inner, showing innerP");
    shown = waitForEvent(EVENT_SHOW, "innerP");
    await invokeContentTask(browser, [], () => {
      content.document.getElementById("inner").style.display = "contents";
      content.document.getElementById("innerP").hidden = false;
    });
    const innerP = (await shown).accessible;
    const inner = findAccessibleChildByID(docAcc, "inner");
    await hitTest(browser, inner, innerP, innerP.firstChild);
  }
);
