/* 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 */
loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });

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

/**
 * Helper function to test table consistency.
 */
function testTableConsistency(table, expectedRowCount, expectedColumnCount) {
  is(table.getAttributeValue("AXRole"), "AXTable", "Correct role for table");

  let tableChildren = table.getAttributeValue("AXChildren");
  // XXX: Should be expectedRowCount+ExpectedColumnCount+1 children, rows (incl headers) + cols + headers
  // if we're trying to match Safari.
  is(
    tableChildren.length,
    expectedRowCount + expectedColumnCount,
    "Table has children = rows (4) + cols (3)"
  );
  for (let i = 0; i < tableChildren.length; i++) {
    let currChild = tableChildren[i];
    if (i < expectedRowCount) {
      is(
        currChild.getAttributeValue("AXRole"),
        "AXRow",
        "Correct role for row"
      );
    } else {
      is(
        currChild.getAttributeValue("AXRole"),
        "AXColumn",
        "Correct role for col"
      );
      is(
        currChild.getAttributeValue("AXRoleDescription"),
        "column",
        "Correct role desc for col"
      );
    }
  }

  is(
    table.getAttributeValue("AXColumnCount"),
    expectedColumnCount,
    "Table has correct column count."
  );
  is(
    table.getAttributeValue("AXRowCount"),
    expectedRowCount,
    "Table has correct row count."
  );

  let cols = table.getAttributeValue("AXColumns");
  is(cols.length, expectedColumnCount, "Table has col list of correct length");
  for (let i = 0; i < cols.length; i++) {
    let currCol = cols[i];
    let currChildren = currCol.getAttributeValue("AXChildren");
    is(
      currChildren.length,
      expectedRowCount,
      "Column has correct number of cells"
    );
    for (let j = 0; j < currChildren.length; j++) {
      let currChild = currChildren[j];
      is(
        currChild.getAttributeValue("AXRole"),
        "AXCell",
        "Column child is cell"
      );
    }
  }

  let rows = table.getAttributeValue("AXRows");
  is(rows.length, expectedRowCount, "Table has row list of correct length");
  for (let i = 0; i < rows.length; i++) {
    let currRow = rows[i];
    let currChildren = currRow.getAttributeValue("AXChildren");
    is(
      currChildren.length,
      expectedColumnCount,
      "Row has correct number of cells"
    );
    for (let j = 0; j < currChildren.length; j++) {
      let currChild = currChildren[j];
      is(currChild.getAttributeValue("AXRole"), "AXCell", "Row child is cell");
    }
  }
}

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

/**
 * Test table, columns, rows
 */
addAccessibleTask(
  `<table id="customers">
    <tbody>
      <tr id="firstrow"><th>Company</th><th>Contact</th><th>Country</th></tr>
      <tr><td>Alfreds Futterkiste</td><td>Maria Anders</td><td>Germany</td></tr>
      <tr><td>Centro comercial Moctezuma</td><td>Francisco Chang</td><td>Mexico</td></tr>
      <tr><td>Ernst Handel</td><td>Roland Mendel</td><td>Austria</td></tr>
    </tbody>
  </table>`,
  async (browser, accDoc) => {
    let table = getNativeInterface(accDoc, "customers");
    testTableConsistency(table, 4, 3);

    const rowText = [
      "Madrigal Electromotive GmbH",
      "Lydia Rodarte-Quayle",
      "Germany",
    ];
    let reorder = waitForEvent(EVENT_REORDER, "customers");
    await SpecialPowers.spawn(browser, [rowText], _rowText => {
      let tr = content.document.createElement("tr");
      for (let t of _rowText) {
        let td = content.document.createElement("td");
        td.textContent = t;
        tr.appendChild(td);
      }
      content.document.getElementById("customers").appendChild(tr);
    });
    await reorder;

    let cols = table.getAttributeValue("AXColumns");
    is(cols.length, 3, "Table has col list of correct length");
    for (let i = 0; i < cols.length; i++) {
      let currCol = cols[i];
      let currChildren = currCol.getAttributeValue("AXChildren");
      is(currChildren.length, 5, "Column has correct number of cells");
      let lastCell = currChildren[currChildren.length - 1];
      let cellChildren = lastCell.getAttributeValue("AXChildren");
      is(cellChildren.length, 1, "Cell has a single text child");
      is(
        cellChildren[0].getAttributeValue("AXRole"),
        "AXStaticText",
        "Correct role for cell child"
      );
      is(
        cellChildren[0].getAttributeValue("AXValue"),
        rowText[i],
        "Correct text for cell"
      );
    }

    reorder = waitForEvent(EVENT_REORDER, "firstrow");
    await SpecialPowers.spawn(browser, [], () => {
      let td = content.document.createElement("td");
      td.textContent = "Ticker";
      content.document.getElementById("firstrow").appendChild(td);
    });
    await reorder;

    cols = table.getAttributeValue("AXColumns");
    is(cols.length, 4, "Table has col list of correct length");
    is(
      cols[cols.length - 1].getAttributeValue("AXChildren").length,
      1,
      "Last column has single child"
    );

    reorder = waitForEvent(
      EVENT_REORDER,
      e => e.accessible.role == ROLE_DOCUMENT
    );
    await SpecialPowers.spawn(browser, [], () => {
      content.document.getElementById("customers").remove();
    });
    await reorder;

    try {
      cols[0].getAttributeValue("AXChildren");
      ok(false, "Getting children from column of expired table should fail");
    } catch (e) {
      ok(true, "Getting children from column of expired table should fail");
    }
  }
);

addAccessibleTask(
  `<table id="table">
    <tr>
      <th colspan="2" id="header1">Header 1</th>
      <th id="header2">Header 2</th>
    </tr>
    <tr>
      <td id="cell1">one</td>
      <td id="cell2" rowspan="2">two</td>
      <td id="cell3">three</td>
    </tr>
    <tr>
    <td id="cell4">four</td>
    <td id="cell5">five</td>
  </tr>
  </table>`,
  (browser, accDoc) => {
    let table = getNativeInterface(accDoc, "table");

    let getCellAt = (col, row) =>
      table.getParameterizedAttributeValue("AXCellForColumnAndRow", [col, row]);

    function testCell(cell, expectedId, expectedColRange, expectedRowRange) {
      is(
        cell.getAttributeValue("AXDOMIdentifier"),
        expectedId,
        "Correct DOM Identifier"
      );
      Assert.deepEqual(
        cell.getAttributeValue("AXColumnIndexRange"),
        expectedColRange,
        "Correct column range"
      );
      Assert.deepEqual(
        cell.getAttributeValue("AXRowIndexRange"),
        expectedRowRange,
        "Correct row range"
      );
    }

    testCell(getCellAt(0, 0), "header1", [0, 2], [0, 1]);
    testCell(getCellAt(1, 0), "header1", [0, 2], [0, 1]);
    testCell(getCellAt(2, 0), "header2", [2, 1], [0, 1]);

    testCell(getCellAt(0, 1), "cell1", [0, 1], [1, 1]);
    testCell(getCellAt(1, 1), "cell2", [1, 1], [1, 2]);
    testCell(getCellAt(2, 1), "cell3", [2, 1], [1, 1]);

    testCell(getCellAt(0, 2), "cell4", [0, 1], [2, 1]);
    testCell(getCellAt(1, 2), "cell2", [1, 1], [1, 2]);
    testCell(getCellAt(2, 2), "cell5", [2, 1], [2, 1]);

    let colHeaders = table.getAttributeValue("AXColumnHeaderUIElements");
    Assert.deepEqual(
      colHeaders.map(c => c.getAttributeValue("AXDOMIdentifier")),
      ["header1", "header1", "header2"],
      "Correct column headers"
    );
  }
);

addAccessibleTask(
  `<table id="table">
    <tr>
      <td>Foo</td>
    </tr>
  </table>`,
  (browser, accDoc) => {
    // Make sure we guess this table to be a layout table.
    testAttrs(
      findAccessibleChildByID(accDoc, "table"),
      { "layout-guess": "true" },
      true
    );

    let table = getNativeInterface(accDoc, "table");
    is(
      table.getAttributeValue("AXRole"),
      "AXGroup",
      "Correct role (AXGroup) for layout table"
    );

    let children = table.getAttributeValue("AXChildren");
    is(
      children.length,
      1,
      "Layout table has single child (no additional columns)"
    );
  }
);

addAccessibleTask(
  `<div id="table" role="table">
    <span style="display: block;">
      <div role="row">
        <div role="cell">Cell 1</div>
        <div role="cell">Cell 2</div>
      </div>
    </span>
    <span style="display: block;">
      <div role="row">
        <span style="display: block;">
          <div role="cell">Cell 3</div>
          <div role="cell">Cell 4</div>
        </span>
      </div>
    </span>
  </div>`,
  async (browser, accDoc) => {
    let table = getNativeInterface(accDoc, "table");
    testTableConsistency(table, 2, 2);
  }
);

/*
 * After executing function 'change' which operates on 'elem', verify the specified
 * 'event' (if not null) is fired on elem. After the event, check if the given
 * native accessible 'table' is a layout or data table by role using 'isLayout'.
 */
async function testIsLayout(table, elem, event, change, isLayout) {
  info(
    "Changing " +
      elem +
      ", expecting table change to " +
      (isLayout ? "AXGroup" : "AXTable")
  );
  const toWait = event ? waitForEvent(event, elem) : null;
  await change();
  if (toWait) {
    await toWait;
  }
  let intendedRole = isLayout ? "AXGroup" : "AXTable";
  await untilCacheIs(
    () => table.getAttributeValue("AXRole"),
    intendedRole,
    "Table role correct after change"
  );
}

/*
 * The following attributes should fire an attribute changed
 * event, which in turn invalidates the layout-table cache
 * associated with the given table. After adding and removing
 * each attr, verify the table is a data or layout table,
 * appropriately. Attrs: summary, abbr, scope, headers
 */
addAccessibleTask(
  `<table id="table" summary="example summary">
    <tr role="presentation">
      <td id="cellOne">cell1</td>
      <td>cell2</td>
    </tr>
    <tr>
      <td id="cellThree">cell3</td>
      <td>cell4</td>
    </tr>
  </table>`,
  async (browser, accDoc) => {
    let table = getNativeInterface(accDoc, "table");
    // summary attr should take precedence over role="presentation" to make this
    // a data table
    is(table.getAttributeValue("AXRole"), "AXTable", "Table is data table");

    info("Removing summary attr");
    // after summary is removed, we should have a layout table
    await testIsLayout(
      table,
      "table",
      EVENT_OBJECT_ATTRIBUTE_CHANGED,
      async () => {
        await SpecialPowers.spawn(browser, [], () => {
          content.document.getElementById("table").removeAttribute("summary");
        });
      },
      true
    );

    info("Setting abbr attr");
    // after abbr is set we should have a data table again
    await testIsLayout(
      table,
      "cellThree",
      EVENT_OBJECT_ATTRIBUTE_CHANGED,
      async () => {
        await SpecialPowers.spawn(browser, [], () => {
          content.document
            .getElementById("cellThree")
            .setAttribute("abbr", "hello world");
        });
      },
      false
    );

    info("Removing abbr attr");
    // after abbr is removed we should have a layout table again
    await testIsLayout(
      table,
      "cellThree",
      EVENT_OBJECT_ATTRIBUTE_CHANGED,
      async () => {
        await SpecialPowers.spawn(browser, [], () => {
          content.document.getElementById("cellThree").removeAttribute("abbr");
        });
      },
      true
    );

    info("Setting scope attr");
    // after scope is set we should have a data table again
    await testIsLayout(
      table,
      "cellThree",
      EVENT_OBJECT_ATTRIBUTE_CHANGED,
      async () => {
        await SpecialPowers.spawn(browser, [], () => {
          content.document
            .getElementById("cellThree")
            .setAttribute("scope", "col");
        });
      },
      false
    );

    info("Removing scope attr");
    // remove scope should give layout
    await testIsLayout(
      table,
      "cellThree",
      EVENT_OBJECT_ATTRIBUTE_CHANGED,
      async () => {
        await SpecialPowers.spawn(browser, [], () => {
          content.document.getElementById("cellThree").removeAttribute("scope");
        });
      },
      true
    );

    info("Setting headers attr");
    // add headers attr should give data
    await testIsLayout(
      table,
      "cellThree",
      EVENT_OBJECT_ATTRIBUTE_CHANGED,
      async () => {
        await SpecialPowers.spawn(browser, [], () => {
          content.document
            .getElementById("cellThree")
            .setAttribute("headers", "cellOne");
        });
      },
      false
    );

    info("Removing headers attr");
    // remove headers attr should give layout
    await testIsLayout(
      table,
      "cellThree",
      EVENT_OBJECT_ATTRIBUTE_CHANGED,
      async () => {
        await SpecialPowers.spawn(browser, [], () => {
          content.document
            .getElementById("cellThree")
            .removeAttribute("headers");
        });
      },
      true
    );
  }
);

/*
 * The following style changes should fire a table style changed
 * event, which in turn invalidates the layout-table cache
 * associated with the given table.
 */
addAccessibleTask(
  `<table id="table">
    <tr id="rowOne">
      <td id="cellOne">cell1</td>
      <td>cell2</td>
    </tr>
    <tr>
      <td>cell3</td>
      <td>cell4</td>
    </tr>
  </table>`,
  async (browser, accDoc) => {
    let table = getNativeInterface(accDoc, "table");
    // we should start as a layout table
    is(table.getAttributeValue("AXRole"), "AXGroup", "Table is layout table");

    info("Adding cell border");
    // after cell border added, we should have a data table
    await testIsLayout(
      table,
      "cellOne",
      null,
      async () => {
        await SpecialPowers.spawn(browser, [], () => {
          content.document
            .getElementById("cellOne")
            .style.setProperty("border", "5px solid green");
        });
      },
      false
    );

    info("Removing cell border");
    // after cell border removed, we should have a layout table
    await testIsLayout(
      table,
      "cellOne",
      null,
      async () => {
        await SpecialPowers.spawn(browser, [], () => {
          content.document
            .getElementById("cellOne")
            .style.removeProperty("border");
        });
      },
      true
    );

    info("Adding row background");
    // after row background added, we should have a data table
    await testIsLayout(
      table,
      "rowOne",
      null,
      async () => {
        await SpecialPowers.spawn(browser, [], () => {
          content.document
            .getElementById("rowOne")
            .style.setProperty("background-color", "green");
        });
      },
      false
    );

    info("Removing row background");
    // after row background removed, we should have a layout table
    await testIsLayout(
      table,
      "rowOne",
      null,
      async () => {
        await SpecialPowers.spawn(browser, [], () => {
          content.document
            .getElementById("rowOne")
            .style.removeProperty("background-color");
        });
      },
      true
    );
  }
);

/*
 * thead/tbody elements with click handlers should:
 * (a) render as AXGroup elements
 * (b) expose their rows as part of their parent table's AXRows array
 */
addAccessibleTask(
  `<table id="table">
    <thead id="thead">
      <tr><td>head row</td></tr>
    </thead>
    <tbody id="tbody">
      <tr><td>body row</td></tr>
      <tr><td>another body row</td></tr>
    </tbody>
  </table>`,
  async (browser, accDoc) => {
    let table = getNativeInterface(accDoc, "table");

    // No click handlers present on thead/tbody
    let tableChildren = table.getAttributeValue("AXChildren");
    let tableRows = table.getAttributeValue("AXRows");

    is(tableChildren.length, 4, "Table has four children (3 row + 1 col)");
    is(tableRows.length, 3, "Table has three rows");

    for (let i = 0; i < tableChildren.length; i++) {
      const child = tableChildren[i];
      if (i < 3) {
        is(
          child.getAttributeValue("AXRole"),
          "AXRow",
          "Table's first 3 children are rows"
        );
      } else {
        is(
          child.getAttributeValue("AXRole"),
          "AXColumn",
          "Table's last child is a column"
        );
      }
    }
    const reorder = waitForEvent(EVENT_REORDER);
    await invokeContentTask(browser, [], () => {
      const head = content.document.getElementById("thead");
      const body = content.document.getElementById("tbody");

      head.addEventListener("click", function () {});
      body.addEventListener("click", function () {});
    });
    await reorder;

    // Click handlers present
    tableChildren = table.getAttributeValue("AXChildren");

    is(tableChildren.length, 3, "Table has three children (2 groups + 1 col)");
    is(
      tableChildren[0].getAttributeValue("AXRole"),
      "AXGroup",
      "Child one is a group"
    );
    is(
      tableChildren[0].getAttributeValue("AXChildren").length,
      1,
      "Child one has one child"
    );

    is(
      tableChildren[1].getAttributeValue("AXRole"),
      "AXGroup",
      "Child two is a group"
    );
    is(
      tableChildren[1].getAttributeValue("AXChildren").length,
      2,
      "Child two has two children"
    );

    is(
      tableChildren[2].getAttributeValue("AXRole"),
      "AXColumn",
      "Child three is a col"
    );

    tableRows = table.getAttributeValue("AXRows");
    is(tableRows.length, 3, "Table has three rows");
  }
);
