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

function testRangeAtMarker(macDoc, marker, attribute, expected, msg) {
  let range = macDoc.getParameterizedAttributeValue(attribute, marker);
  is(stringForRange(macDoc, range), expected, msg);
}

function testUIElement(
  macDoc,
  marker,
  msg,
  expectedRole,
  expectedValue,
  expectedRange
) {
  let elem = macDoc.getParameterizedAttributeValue(
    "AXUIElementForTextMarker",
    marker
  );
  is(
    elem.getAttributeValue("AXRole"),
    expectedRole,
    `${msg}: element role matches`
  );
  is(elem.getAttributeValue("AXValue"), expectedValue, `${msg}: element value`);
  let elemRange = macDoc.getParameterizedAttributeValue(
    "AXTextMarkerRangeForUIElement",
    elem
  );
  is(
    stringForRange(macDoc, elemRange),
    expectedRange,
    `${msg}: element range matches element value`
  );
}

function testStyleRun(macDoc, marker, msg, expectedStyleRun) {
  testRangeAtMarker(
    macDoc,
    marker,
    "AXStyleTextMarkerRangeForTextMarker",
    expectedStyleRun,
    `${msg}: style run matches`
  );
}

function testParagraph(macDoc, marker, msg, expectedParagraph) {
  testRangeAtMarker(
    macDoc,
    marker,
    "AXParagraphTextMarkerRangeForTextMarker",
    expectedParagraph,
    `${msg}: paragraph matches`
  );
}

function testWords(macDoc, marker, msg, expectedLeft, expectedRight) {
  testRangeAtMarker(
    macDoc,
    marker,
    "AXLeftWordTextMarkerRangeForTextMarker",
    expectedLeft,
    `${msg}: left word matches`
  );

  testRangeAtMarker(
    macDoc,
    marker,
    "AXRightWordTextMarkerRangeForTextMarker",
    expectedRight,
    `${msg}: right word matches`
  );
}

function testLines(
  macDoc,
  marker,
  msg,
  expectedLine,
  expectedLeft,
  expectedRight
) {
  testRangeAtMarker(
    macDoc,
    marker,
    "AXLineTextMarkerRangeForTextMarker",
    expectedLine,
    `${msg}: line matches`
  );

  testRangeAtMarker(
    macDoc,
    marker,
    "AXLeftLineTextMarkerRangeForTextMarker",
    expectedLeft,
    `${msg}: left line matches`
  );

  testRangeAtMarker(
    macDoc,
    marker,
    "AXRightLineTextMarkerRangeForTextMarker",
    expectedRight,
    `${msg}: right line matches`
  );
}

function* markerIterator(macDoc, reverse = false) {
  let m = macDoc.getAttributeValue(
    reverse ? "AXEndTextMarker" : "AXStartTextMarker"
  );
  let c = 0;
  while (m) {
    yield [m, c++];
    m = macDoc.getParameterizedAttributeValue(
      reverse
        ? "AXPreviousTextMarkerForTextMarker"
        : "AXNextTextMarkerForTextMarker",
      m
    );
  }
}

// Tests consistency in text markers between:
// 1. "Linked list" forward navagation
// 2. Getting markers by index
// 3. "Linked list" reverse navagation
// For each iteration method check that the returned index is consistent
function testMarkerIntegrity(accDoc, expectedMarkerValues) {
  let macDoc = accDoc.nativeInterface.QueryInterface(
    Ci.nsIAccessibleMacInterface
  );

  // Iterate forward with "AXNextTextMarkerForTextMarker"
  let prevMarker;
  let count = 0;
  for (let [marker, index] of markerIterator(macDoc)) {
    count++;
    let markerIndex = macDoc.getParameterizedAttributeValue(
      "AXIndexForTextMarker",
      marker
    );
    is(
      markerIndex,
      index,
      `Correct index in "AXNextTextMarkerForTextMarker": ${index}`
    );
    if (prevMarker) {
      let range = macDoc.getParameterizedAttributeValue(
        "AXTextMarkerRangeForUnorderedTextMarkers",
        [prevMarker, marker]
      );
      is(
        macDoc.getParameterizedAttributeValue(
          "AXLengthForTextMarkerRange",
          range
        ),
        1,
        `[${index}] marker moved one character`
      );
    }
    prevMarker = marker;

    testWords(
      macDoc,
      marker,
      `At index ${index}`,
      ...expectedMarkerValues[index].words
    );
    testLines(
      macDoc,
      marker,
      `At index ${index}`,
      ...expectedMarkerValues[index].lines
    );
    testUIElement(
      macDoc,
      marker,
      `At index ${index}`,
      ...expectedMarkerValues[index].element
    );
    testParagraph(
      macDoc,
      marker,
      `At index ${index}`,
      expectedMarkerValues[index].paragraph
    );
    testStyleRun(
      macDoc,
      marker,
      `At index ${index}`,
      expectedMarkerValues[index].style
    );
  }

  is(expectedMarkerValues.length, count, `Correct marker count: ${count}`);

  // Use "AXTextMarkerForIndex" to retrieve all text markers
  for (let i = 0; i < count; i++) {
    let marker = macDoc.getParameterizedAttributeValue(
      "AXTextMarkerForIndex",
      i
    );
    let index = macDoc.getParameterizedAttributeValue(
      "AXIndexForTextMarker",
      marker
    );
    is(index, i, `Correct index in "AXTextMarkerForIndex": ${i}`);

    if (i == count - 1) {
      ok(
        !macDoc.getParameterizedAttributeValue(
          "AXNextTextMarkerForTextMarker",
          marker
        ),
        "Iterated through all markers"
      );
    }
  }

  count = expectedMarkerValues.length;

  // Iterate backward with "AXPreviousTextMarkerForTextMarker"
  for (let [marker] of markerIterator(macDoc, true)) {
    if (count <= 0) {
      ok(false, "Exceeding marker count");
      break;
    }
    count--;
    let index = macDoc.getParameterizedAttributeValue(
      "AXIndexForTextMarker",
      marker
    );
    is(
      index,
      count,
      `Correct index in "AXPreviousTextMarkerForTextMarker": ${count}`
    );
  }

  is(count, 0, "Iterated backward through all text markers");
}

addAccessibleTask("mac/doc_textmarker_test.html", async (browser, accDoc) => {
  await SpecialPowers.pushPrefEnv({
    set: [["layout.word_select.stop_at_punctuation", false]],
  });

  const expectedValues = await SpecialPowers.spawn(browser, [], async () => {
    return content.wrappedJSObject.getExpected(false);
  });

  testMarkerIntegrity(accDoc, expectedValues);

  await SpecialPowers.popPrefEnv();
});

addAccessibleTask("mac/doc_textmarker_test.html", async (browser, accDoc) => {
  await SpecialPowers.pushPrefEnv({
    set: [
      ["layout.word_select.stop_at_punctuation", true], // this is default
    ],
  });

  const expectedValues = await SpecialPowers.spawn(browser, [], async () => {
    return content.wrappedJSObject.getExpected(true);
  });

  testMarkerIntegrity(accDoc, expectedValues);

  await SpecialPowers.popPrefEnv();
});

// Test text marker lesser-than operator
addAccessibleTask(
  `<p id="p">hello <a id="a" href="#">goodbye</a> world</p>`,
  async (browser, accDoc) => {
    let macDoc = accDoc.nativeInterface.QueryInterface(
      Ci.nsIAccessibleMacInterface
    );

    let start = macDoc.getParameterizedAttributeValue(
      "AXTextMarkerForIndex",
      1
    );
    let end = macDoc.getParameterizedAttributeValue("AXTextMarkerForIndex", 10);

    let range = macDoc.getParameterizedAttributeValue(
      "AXTextMarkerRangeForUnorderedTextMarkers",
      [end, start]
    );
    is(stringForRange(macDoc, range), "ello good");
  }
);

addAccessibleTask(
  `<input id="input" value=""><a href="#">goodbye</a>`,
  async (browser, accDoc) => {
    let macDoc = accDoc.nativeInterface.QueryInterface(
      Ci.nsIAccessibleMacInterface
    );

    let input = getNativeInterface(accDoc, "input");

    let range = macDoc.getParameterizedAttributeValue(
      "AXTextMarkerRangeForUIElement",
      input
    );

    is(stringForRange(macDoc, range), "", "string value is correct");
  }
);

addAccessibleTask(
  `<div role="listbox" id="box">
    <input type="radio" name="test" role="option" title="First item"/>
    <input type="radio" name="test" role="option" title="Second item"/>
  </div>`,
  async (browser, accDoc) => {
    let box = getNativeInterface(accDoc, "box");
    const children = box.getAttributeValue("AXChildren");
    is(children.length, 2, "Listbox contains two items");
    is(children[0].getAttributeValue("AXValue"), "First item");
    is(children[1].getAttributeValue("AXValue"), "Second item");
  }
);

addAccessibleTask(
  `<div id="t">
    A link <b>should</b> explain <u>clearly</u> what information the <i>reader</i> will get by clicking on that link.
  </div>`,
  async (browser, accDoc) => {
    let t = getNativeInterface(accDoc, "t");
    const children = t.getAttributeValue("AXChildren");
    const expectedTitles = [
      "A link ",
      "should",
      " explain ",
      "clearly",
      " what information the ",
      "reader",
      " will get by clicking on that link. ",
    ];
    is(children.length, 7, "container has seven children");
    children.forEach((child, index) => {
      is(child.getAttributeValue("AXValue"), expectedTitles[index]);
    });
  }
);

addAccessibleTask(
  `<a href="#">link</a> <input id="input" value="hello">`,
  async (browser, accDoc) => {
    let macDoc = accDoc.nativeInterface.QueryInterface(
      Ci.nsIAccessibleMacInterface
    );

    let input = getNativeInterface(accDoc, "input");
    let range = macDoc.getParameterizedAttributeValue(
      "AXTextMarkerRangeForUIElement",
      input
    );

    let firstMarkerInInput = macDoc.getParameterizedAttributeValue(
      "AXStartTextMarkerForTextMarkerRange",
      range
    );

    let leftWordRange = macDoc.getParameterizedAttributeValue(
      "AXLeftWordTextMarkerRangeForTextMarker",
      firstMarkerInInput
    );
    let str = macDoc.getParameterizedAttributeValue(
      "AXStringForTextMarkerRange",
      leftWordRange
    );
    is(str, "hello", "Left word at start of input should be right word");
  }
);

addAccessibleTask(`<p id="p">hello world</p>`, async (browser, accDoc) => {
  let macDoc = accDoc.nativeInterface.QueryInterface(
    Ci.nsIAccessibleMacInterface
  );

  let p = getNativeInterface(accDoc, "p");
  let range = macDoc.getParameterizedAttributeValue(
    "AXTextMarkerRangeForUIElement",
    p
  );

  let bounds = macDoc.getParameterizedAttributeValue(
    "AXBoundsForTextMarkerRange",
    range
  );

  ok(bounds.origin && bounds.size, "Returned valid bounds");

  // Test bounds of collapsed range.

  let marker = macDoc.getParameterizedAttributeValue(
    "AXStartTextMarkerForTextMarkerRange",
    range
  );

  let collapsedRange = macDoc.getParameterizedAttributeValue(
    "AXTextMarkerRangeForUnorderedTextMarkers",
    [marker, marker]
  );

  let collapsedBounds = macDoc.getParameterizedAttributeValue(
    "AXBoundsForTextMarkerRange",
    collapsedRange
  );

  ok(
    collapsedBounds.origin && collapsedBounds.size,
    "Returned valid collapsed bounds"
  );

  let nextMarker = macDoc.getParameterizedAttributeValue(
    "AXNextTextMarkerForTextMarker",
    marker
  );

  range = macDoc.getParameterizedAttributeValue(
    "AXTextMarkerRangeForUnorderedTextMarkers",
    [marker, nextMarker]
  );

  is(stringForRange(macDoc, range), "h", "range is first letter");

  bounds = macDoc.getParameterizedAttributeValue(
    "AXBoundsForTextMarkerRange",
    range
  );

  Assert.less(
    collapsedBounds.size[0],
    bounds.size[0],
    "Collapsed range is smaller than character range"
  );
});
