/* 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";
requestLongerTimeout(2);

/* import-globals-from ../../mochitest/role.js */
loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
/* import-globals-from ../../mochitest/states.js */
loadScripts({ name: "states.js", dir: MOCHITESTS_DIR });

/**
 * Test MEMBER_OF relation caching on HTML radio buttons
 */
addAccessibleTask(
  `
  <input type="radio" id="r1">I have no name<br>
  <input type="radio" id="r2">I also have no name<br>
  <input type="radio" id="r3" name="n">I have a name<br>
  <input type="radio" id="r4" name="a">I have a different name<br>
  <fieldset role="radiogroup">
    <input type="radio" id="r5" name="n">I have an already used name
     and am in a different part of the tree
    <input type="radio" id="r6" name="r">I have a different name but am
     in the same group
  </fieldset>`,
  async function (browser, accDoc) {
    const r1 = findAccessibleChildByID(accDoc, "r1");
    const r2 = findAccessibleChildByID(accDoc, "r2");
    const r3 = findAccessibleChildByID(accDoc, "r3");
    const r4 = findAccessibleChildByID(accDoc, "r4");
    const r5 = findAccessibleChildByID(accDoc, "r5");
    const r6 = findAccessibleChildByID(accDoc, "r6");

    await testCachedRelation(r1, RELATION_MEMBER_OF, []);
    await testCachedRelation(r2, RELATION_MEMBER_OF, []);
    await testCachedRelation(r3, RELATION_MEMBER_OF, [r3, r5]);
    await testCachedRelation(r4, RELATION_MEMBER_OF, r4);
    await testCachedRelation(r5, RELATION_MEMBER_OF, [r3, r5]);
    await testCachedRelation(r6, RELATION_MEMBER_OF, r6);

    await invokeContentTask(browser, [], () => {
      content.document.getElementById("r5").name = "a";
    });

    await testCachedRelation(r3, RELATION_MEMBER_OF, r3);
    await testCachedRelation(r4, RELATION_MEMBER_OF, [r5, r4]);
    await testCachedRelation(r5, RELATION_MEMBER_OF, [r5, r4]);
  },
  { chrome: true, iframe: true, remoteIframe: true }
);

/*
 * Test MEMBER_OF relation caching on aria radio buttons
 */
addAccessibleTask(
  `
  <div role="radio" id="r1">I have no radio group</div><br>
  <fieldset role="radiogroup" id="fs">
    <div role="radio" id="r2">hello</div><br>
    <div role="radio" id="r3">world</div><br>
  </fieldset>`,
  async function (browser, accDoc) {
    const r1 = findAccessibleChildByID(accDoc, "r1");
    const r2 = findAccessibleChildByID(accDoc, "r2");
    let r3 = findAccessibleChildByID(accDoc, "r3");

    await testCachedRelation(r1, RELATION_MEMBER_OF, []);
    await testCachedRelation(r2, RELATION_MEMBER_OF, [r2, r3]);
    await testCachedRelation(r3, RELATION_MEMBER_OF, [r2, r3]);
    const r = waitForEvent(EVENT_INNER_REORDER, "fs");
    await invokeContentTask(browser, [], () => {
      let innerRadio = content.document.getElementById("r3");
      content.document.body.appendChild(innerRadio);
    });
    await r;

    r3 = findAccessibleChildByID(accDoc, "r3");
    await testCachedRelation(r1, RELATION_MEMBER_OF, []);
    await testCachedRelation(r2, RELATION_MEMBER_OF, r2);
    await testCachedRelation(r3, RELATION_MEMBER_OF, []);
  },
  {
    chrome: true,
    iframe: true,
    remoteIframe: true,
  }
);

/*
 * Test mutation of LABEL relations via DOM ID reuse.
 */
addAccessibleTask(
  `
  <div id="label">before</div><input id="input" aria-labelledby="label">
  `,
  async function (browser, accDoc) {
    let label = findAccessibleChildByID(accDoc, "label");
    const input = findAccessibleChildByID(accDoc, "input");

    await testCachedRelation(label, RELATION_LABEL_FOR, input);
    await testCachedRelation(input, RELATION_LABELLED_BY, label);

    const r = waitForEvent(EVENT_REORDER, accDoc);
    await invokeContentTask(browser, [], () => {
      content.document.getElementById("label").remove();
      let l = content.document.createElement("div");
      l.id = "label";
      l.textContent = "after";
      content.document.body.insertBefore(
        l,
        content.document.getElementById("input")
      );
    });
    await r;
    label = findAccessibleChildByID(accDoc, "label");
    await testCachedRelation(label, RELATION_LABEL_FOR, input);
    await testCachedRelation(input, RELATION_LABELLED_BY, label);
  },
  {
    chrome: true,
    iframe: true,
    remoteIframe: true,
  }
);

/*
 * Test LINKS_TO relation caching an anchor with multiple hashes
 */
addAccessibleTask(
  `
  <a id="link" href="#foo#bar">Origin</a><br>
  <a id="anchor" name="foo#bar">Destination`,
  async function (browser, accDoc) {
    const link = findAccessibleChildByID(accDoc, "link");
    const anchor = findAccessibleChildByID(accDoc, "anchor");

    await testCachedRelation(link, RELATION_LINKS_TO, anchor);
  },
  {
    chrome: true,
    // IA2 doesn't have a LINKS_TO relation and Windows non-cached
    // RemoteAccessible uses IA2, so we can't run these tests in this case.
    topLevel: true,
    iframe: true,
    remoteIframe: true,
  }
);

/*
 * Test mutation of LABEL relations via accessible shutdown.
 */
addAccessibleTask(
  `
  <input id="d"></input>
  <label id="l">
    <select id="s"></select>
  </label>
  `,
  async function (browser, accDoc) {
    const label = findAccessibleChildByID(accDoc, "l");
    const select = findAccessibleChildByID(accDoc, "s");
    const input = findAccessibleChildByID(accDoc, "d");

    await testCachedRelation(label, RELATION_LABEL_FOR, select);
    await testCachedRelation(select, RELATION_LABELLED_BY, label);
    await testCachedRelation(input, RELATION_LABELLED_BY, []);
    await untilCacheOk(() => {
      if (!browser.isRemoteBrowser) {
        return true;
      }

      try {
        // We should get an acc ID back from this, but we don't have a way of
        // verifying its correctness -- it should be the ID of the select.
        return label.cache.getStringProperty("for");
      } catch (e) {
        ok(false, "Exception thrown while trying to read from the cache");
        return false;
      }
    }, "Label for relation exists");

    const r = waitForEvent(EVENT_INNER_REORDER, "l");
    await invokeContentTask(browser, [], () => {
      content.document.getElementById("s").remove();
    });
    await r;
    await untilCacheOk(() => {
      if (!browser.isRemoteBrowser) {
        return true;
      }

      try {
        label.cache.getStringProperty("for");
      } catch (e) {
        // This property should no longer exist in the cache, so we should
        // get an exception if we try to fetch it.
        return true;
      }
      return false;
    }, "Label for relation exists");

    await invokeContentTask(browser, [], () => {
      const l = content.document.getElementById("l");
      l.htmlFor = "d";
    });
    await testCachedRelation(label, RELATION_LABEL_FOR, input);
    await testCachedRelation(input, RELATION_LABELLED_BY, label);
  },
  {
    chrome: true,
    iframe: true,
    remoteIframe: true,
    topLevel: true,
  }
);

/**
 * Test label relations on HTML figure/figcaption.
 */
addAccessibleTask(
  `
<figure id="figure1">
  before
  <figcaption id="caption1">caption1</figcaption>
  after
</figure>
<figure id="figure2" aria-labelledby="label">
  <figcaption id="caption2">caption2</figure>
</figure>
<div id="label">label</div>
  `,
  async function (browser, docAcc) {
    const figure1 = findAccessibleChildByID(docAcc, "figure1");
    let caption1 = findAccessibleChildByID(docAcc, "caption1");
    await testCachedRelation(figure1, RELATION_LABELLED_BY, caption1);
    await testCachedRelation(caption1, RELATION_LABEL_FOR, figure1);

    info("Hiding caption1");
    let mutated = waitForEvent(EVENT_HIDE, caption1);
    await invokeContentTask(browser, [], () => {
      content.document.getElementById("caption1").hidden = true;
    });
    await mutated;
    await testCachedRelation(figure1, RELATION_LABELLED_BY, []);

    info("Showing caption1");
    mutated = waitForEvent(EVENT_SHOW, "caption1");
    await invokeContentTask(browser, [], () => {
      content.document.getElementById("caption1").hidden = false;
    });
    caption1 = (await mutated).accessible;
    await testCachedRelation(figure1, RELATION_LABELLED_BY, caption1);
    await testCachedRelation(caption1, RELATION_LABEL_FOR, figure1);

    const figure2 = findAccessibleChildByID(docAcc, "figure2");
    const caption2 = findAccessibleChildByID(docAcc, "caption2");
    const label = findAccessibleChildByID(docAcc, "label");
    await testCachedRelation(figure2, RELATION_LABELLED_BY, [label, caption2]);
    await testCachedRelation(caption2, RELATION_LABEL_FOR, figure2);
    await testCachedRelation(label, RELATION_LABEL_FOR, figure2);
  },
  { chrome: true, topLevel: true }
);

/**
 * Test aria-owns morphs to controls relationship when container is combobox.
 */
addAccessibleTask(
  `
  <div id="box" role="combobox"
         aria-owns="listbox"
         aria-expanded="true"
         aria-haspopup="listbox"
         aria-autocomplete="list"
         contenteditable="true"></div>
  <ul role="listbox" id="listbox">
    <li role="option">apple</li>
    <li role="option">peach</li>
  </ul>
`,
  async (browser, accDoc) => {
    const combobox = findAccessibleChildByID(accDoc, "box");
    const listbox = findAccessibleChildByID(accDoc, "listbox");

    testStates(combobox, 0, EXT_STATE_EDITABLE, 0, 0);
    is(combobox.childCount, 0, "combobox has no children");
    await testCachedRelation(combobox, RELATION_CONTROLLER_FOR, [listbox]);
    await testCachedRelation(listbox, RELATION_CONTROLLED_BY, [combobox]);

    let expectedEvents = Promise.all([
      waitForStateChange(combobox, EXT_STATE_EDITABLE, false, true),
      waitForEvent(EVENT_REORDER, accDoc),
    ]);
    await invokeContentTask(browser, [], () => {
      content.document.getElementById("box").contentEditable = false;
    });
    await expectedEvents;
    await testCachedRelation(combobox, RELATION_CONTROLLER_FOR, []);
    await testCachedRelation(listbox, RELATION_CONTROLLED_BY, []);
    is(combobox.childCount, 1, "combobox has listbox");
  }
);

/*
 * Test relocated child preserves relation.
 */
addAccessibleTask(
  `
  <div id="relocated">World</div>
  <div id="owner" aria-owns="relocated"></div>
  <button id="btn" aria-labelledby="relocated">Hello</button>
  `,
  async function testRelocationRelation(browser, docAcc) {
    const btn = findAccessibleChildByID(docAcc, "btn");
    const relocated = findAccessibleChildByID(docAcc, "relocated");

    await testCachedRelation(btn, RELATION_LABELLED_BY, relocated);
    await testCachedRelation(relocated, RELATION_LABEL_FOR, btn);
  },
  { chrome: true, topLevel: true }
);

/**
 * Test table caption.
 */
addAccessibleTask(
  `
<table id="table">
  <caption id="caption">caption</caption>
  <tr><th>a</th></tr>
</table>
  `,
  async function testTableCaption(browser, docAcc) {
    const table = findAccessibleChildByID(docAcc, "table");

    const caption = findAccessibleChildByID(docAcc, "caption");
    await testCachedRelation(table, RELATION_LABELLED_BY, caption);
    await testCachedRelation(caption, RELATION_LABEL_FOR, table);
  },
  { chrome: true, topLevel: true }
);

/**
 * Test elements that are not label by spec do not get LABEL relations
 */
addAccessibleTask(
  `
  <label id="label" for="btn">label</label>
  <div role="button" id="btn"></div>
  `,
  async function testLabelOnDiv(browser, docAcc) {
    const btn = findAccessibleChildByID(docAcc, "btn");

    const label = findAccessibleChildByID(docAcc, "label");
    await testCachedRelation(btn, RELATION_LABELLED_BY, []);
    await testCachedRelation(label, RELATION_LABEL_FOR, []);
  },
  { chrome: true, topLevel: true }
);
