/**
 * This file provides set of helper functions to test nsIAccessibleTable
 * interface.
 *
 * Required:
 *   common.js
 *   role.js
 *   states.js
 */
/* import-globals-from common.js */
/* import-globals-from role.js */
/* import-globals-from states.js */

/**
 * Constants used to describe cells array.
 */
const kDataCell = 1; // Indicates the cell is origin data cell
const kRowHeaderCell = 2; // Indicates the cell is row header cell
const kColHeaderCell = 4; // Indicated the cell is column header cell
const kOrigin = kDataCell | kRowHeaderCell | kColHeaderCell;

const kRowSpanned = 8; // Indicates the cell is not origin and row spanned
const kColSpanned = 16; // Indicates the cell is not origin and column spanned
const kSpanned = kRowSpanned | kColSpanned;

/**
 * Constants to define column header type.
 */
const kNoColumnHeader = 0;
const kListboxColumnHeader = 1;
const kTreeColumnHeader = 2;

/**
 * Constants to define table type.
 */
const kTable = 0;
const kTreeTable = 1;
const kMathTable = 2;

/**
 * Test table structure and related methods.
 *
 * @param  aIdentifier     [in] table accessible identifier
 * @param  aCellsArray     [in] two dimensional array (row X columns) of
 *                          cell types (see constants defined above).
 * @param  aColHeaderType  [in] specifies wether column header cells are
 *                          arranged into the list.
 * @param  aCaption        [in] caption text if any
 * @param  aSummary        [in] summary text if any
 * @param  aTableType      [in] specifies the table type.
 * @param  aRowRoles       [in] array of row roles.
 */
function testTableStruct(
  aIdentifier,
  aCellsArray,
  aColHeaderType,
  aCaption,
  aSummary,
  aTableType,
  aRowRoles
) {
  var tableNode = getNode(aIdentifier);
  var isGrid =
    tableNode.getAttribute("role") == "grid" ||
    tableNode.getAttribute("role") == "treegrid" ||
    tableNode.localName == "tree";

  var rowCount = aCellsArray.length;
  var colsCount = aCellsArray[0] ? aCellsArray[0].length : 0;

  // Test table accessible tree.
  var tableObj = {
    children: [],
  };
  switch (aTableType) {
    case kTable:
      tableObj.role = ROLE_TABLE;
      break;
    case kTreeTable:
      tableObj.role = ROLE_TREE_TABLE;
      break;
    case kMathTable:
      tableObj.role = ROLE_MATHML_TABLE;
      break;
  }

  // caption accessible handling
  if (aCaption) {
    var captionObj = {
      role: ROLE_CAPTION,
      children: [
        {
          role: ROLE_TEXT_LEAF,
          name: aCaption,
        },
      ],
    };

    tableObj.children.push(captionObj);
  }

  // special types of column headers handling
  if (aColHeaderType) {
    var headersObj = {
      role: ROLE_LIST,
      children: [],
    };

    for (let idx = 0; idx < colsCount; idx++) {
      var headerCellObj = {
        role: ROLE_COLUMNHEADER,
      };
      headersObj.children.push(headerCellObj);
    }

    if (aColHeaderType == kTreeColumnHeader) {
      headersObj.children.push({
        role: ROLE_PUSHBUTTON,
      });
      headersObj.children.push({
        role: ROLE_MENUPOPUP,
      });
    }

    tableObj.children.push(headersObj);
  }

  // rows and cells accessibles
  for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
    let rowObj = {
      role: aRowRoles ? aRowRoles[rowIdx] : ROLE_ROW,
      children: [],
    };

    for (let colIdx = 0; colIdx < colsCount; colIdx++) {
      let celltype = aCellsArray[rowIdx][colIdx];

      var role = ROLE_NOTHING;
      switch (celltype) {
        case kDataCell:
          role =
            aTableType == kMathTable
              ? ROLE_MATHML_CELL
              : isGrid
                ? ROLE_GRID_CELL
                : ROLE_CELL;
          break;
        case kRowHeaderCell:
          role = ROLE_ROWHEADER;
          break;
        case kColHeaderCell:
          role = ROLE_COLUMNHEADER;
          break;
      }

      if (role != ROLE_NOTHING) {
        var cellObj = { role };
        rowObj.children.push(cellObj);
      }
    }

    tableObj.children.push(rowObj);
  }

  testAccessibleTree(aIdentifier, tableObj);

  // Test table table interface.
  var table = getAccessible(aIdentifier, [nsIAccessibleTable]);

  // summary
  if (aSummary) {
    is(
      table.summary,
      aSummary,
      "Wrong summary of the table " + prettyName(aIdentifier)
    );
  }

  // rowCount and columnCount
  is(
    table.rowCount,
    rowCount,
    "Wrong rows count of " + prettyName(aIdentifier)
  );
  is(
    table.columnCount,
    colsCount,
    "Wrong columns count of " + prettyName(aIdentifier)
  );

  // rows and columns extents
  for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
    for (let colIdx = 0; colIdx < colsCount; colIdx++) {
      let celltype = aCellsArray[rowIdx][colIdx];
      if (celltype & kOrigin) {
        // table getRowExtentAt
        var rowExtent = table.getRowExtentAt(rowIdx, colIdx);
        let idx;
        /* eslint-disable no-empty */
        for (
          idx = rowIdx + 1;
          idx < rowCount && aCellsArray[idx][colIdx] & kRowSpanned;
          idx++
        ) {}
        /* eslint-enable no-empty */

        var expectedRowExtent = idx - rowIdx;
        is(
          rowExtent,
          expectedRowExtent,
          "getRowExtentAt: Wrong number of spanned rows at (" +
            rowIdx +
            ", " +
            colIdx +
            ") for " +
            prettyName(aIdentifier)
        );

        // table getColumnExtentAt
        var colExtent = table.getColumnExtentAt(rowIdx, colIdx);
        /* eslint-disable no-empty */
        for (
          idx = colIdx + 1;
          idx < colsCount && aCellsArray[rowIdx][idx] & kColSpanned;
          idx++
        ) {}
        /* eslint-enable no-empty */

        var expectedColExtent = idx - colIdx;
        is(
          colExtent,
          expectedColExtent,
          "getColumnExtentAt: Wrong number of spanned columns at (" +
            rowIdx +
            ", " +
            colIdx +
            ") for " +
            prettyName(aIdentifier)
        );

        // cell rowExtent and columnExtent
        var cell = getAccessible(table.getCellAt(rowIdx, colIdx), [
          nsIAccessibleTableCell,
        ]);

        is(
          cell.rowExtent,
          expectedRowExtent,
          "rowExtent: Wrong number of spanned rows at (" +
            rowIdx +
            ", " +
            colIdx +
            ") for " +
            prettyName(aIdentifier)
        );

        is(
          cell.columnExtent,
          expectedColExtent,
          "columnExtent: Wrong number of spanned column at (" +
            rowIdx +
            ", " +
            colIdx +
            ") for " +
            prettyName(aIdentifier)
        );
      }
    }
  }
}

/**
 * Test table indexes.
 *
 * @param  aIdentifier  [in] table accessible identifier
 * @param  aIdxes       [in] two dimensional array of cell indexes
 */
function testTableIndexes(aIdentifier, aIdxes) {
  var tableAcc = getAccessible(aIdentifier, [nsIAccessibleTable]);
  if (!tableAcc) {
    return;
  }

  var obtainedRowIdx, obtainedColIdx, obtainedIdx;
  var cellAcc;

  var id = prettyName(aIdentifier);

  var rowCount = aIdxes.length;
  for (var rowIdx = 0; rowIdx < rowCount; rowIdx++) {
    var colCount = aIdxes[rowIdx].length;
    for (var colIdx = 0; colIdx < colCount; colIdx++) {
      var idx = aIdxes[rowIdx][colIdx];

      // getCellAt
      try {
        cellAcc = null;
        cellAcc = tableAcc.getCellAt(rowIdx, colIdx);
      } catch (e) {}

      ok(
        (idx != -1 && cellAcc) || (idx == -1 && !cellAcc),
        id +
          ": Can't get cell accessible at row = " +
          rowIdx +
          ", column = " +
          colIdx
      );

      if (idx != -1) {
        // getRowIndexAt
        var origRowIdx = rowIdx;
        while (
          origRowIdx > 0 &&
          aIdxes[rowIdx][colIdx] == aIdxes[origRowIdx - 1][colIdx]
        ) {
          origRowIdx--;
        }

        try {
          obtainedRowIdx = tableAcc.getRowIndexAt(idx);
        } catch (e) {
          ok(
            false,
            id + ": can't get row index for cell index " + idx + "," + e
          );
        }

        is(
          obtainedRowIdx,
          origRowIdx,
          id + ": row for index " + idx + " is not correct (getRowIndexAt)"
        );

        // getColumnIndexAt
        var origColIdx = colIdx;
        while (
          origColIdx > 0 &&
          aIdxes[rowIdx][colIdx] == aIdxes[rowIdx][origColIdx - 1]
        ) {
          origColIdx--;
        }

        try {
          obtainedColIdx = tableAcc.getColumnIndexAt(idx);
        } catch (e) {
          ok(
            false,
            id + ": can't get column index for cell index " + idx + "," + e
          );
        }

        is(
          obtainedColIdx,
          origColIdx,
          id +
            ": column  for index " +
            idx +
            " is not correct (getColumnIndexAt)"
        );

        // getRowAndColumnIndicesAt
        var obtainedRowIdxObj = {},
          obtainedColIdxObj = {};
        try {
          tableAcc.getRowAndColumnIndicesAt(
            idx,
            obtainedRowIdxObj,
            obtainedColIdxObj
          );
        } catch (e) {
          ok(
            false,
            id +
              ": can't get row and column indices for cell index " +
              idx +
              "," +
              e
          );
        }

        is(
          obtainedRowIdxObj.value,
          origRowIdx,
          id +
            ": row for index " +
            idx +
            " is not correct (getRowAndColumnIndicesAt)"
        );
        is(
          obtainedColIdxObj.value,
          origColIdx,
          id +
            ": column  for index " +
            idx +
            " is not correct (getRowAndColumnIndicesAt)"
        );

        if (cellAcc) {
          var cellId = prettyName(cellAcc);
          cellAcc = getAccessible(cellAcc, [nsIAccessibleTableCell]);

          // cell: 'table-cell-index' attribute
          var attrs = cellAcc.attributes;
          var strIdx = "";
          try {
            strIdx = attrs.getStringProperty("table-cell-index");
          } catch (e) {
            ok(
              false,
              cellId +
                ": no cell index from object attributes on the cell accessible at index " +
                idx +
                "."
            );
          }

          if (strIdx) {
            is(
              parseInt(strIdx),
              idx,
              cellId +
                ": cell index from object attributes of cell accessible isn't corrent."
            );
          }

          // cell: table
          try {
            is(
              cellAcc.table,
              tableAcc,
              cellId + ": wrong table accessible for the cell."
            );
          } catch (e) {
            ok(false, cellId + ": can't get table accessible from the cell.");
          }

          // cell: getRowIndex
          try {
            obtainedRowIdx = cellAcc.rowIndex;
          } catch (e) {
            ok(
              false,
              cellId +
                ": can't get row index of the cell at index " +
                idx +
                "," +
                e
            );
          }

          is(
            obtainedRowIdx,
            origRowIdx,
            cellId + ": row for the cell at index " + idx + " is not correct"
          );

          // cell: getColumnIndex
          try {
            obtainedColIdx = cellAcc.columnIndex;
          } catch (e) {
            ok(
              false,
              cellId +
                ": can't get column index of the cell at index " +
                idx +
                "," +
                e
            );
          }

          is(
            obtainedColIdx,
            origColIdx,
            id + ": column for the cell at index " + idx + " is not correct"
          );
        }
      }

      // getCellIndexAt
      try {
        obtainedIdx = tableAcc.getCellIndexAt(rowIdx, colIdx);
      } catch (e) {
        obtainedIdx = -1;
      }

      is(
        obtainedIdx,
        idx,
        id +
          ": row " +
          rowIdx +
          " /column " +
          colIdx +
          " and index " +
          obtainedIdx +
          " aren't inconsistent."
      );
    }
  }
}

/**
 * Test table getters selection methods.
 *
 * @param  aIdentifier  [in] table accessible identifier
 * @param  aCellsArray  [in] two dimensional array (row X columns) of cells
 *                       states (either boolean (selected/unselected) if cell is
 *                       origin, otherwise kRowSpanned or kColSpanned constant).
 * @param  aMsg         [in] text appended before every message
 */
function testTableSelection(aIdentifier, aCellsArray, aMsg) {
  var msg = aMsg ? aMsg : "";
  var acc = getAccessible(aIdentifier, [nsIAccessibleTable]);
  if (!acc) {
    return;
  }

  var rowCount = aCellsArray.length;
  var colsCount = aCellsArray[0].length;

  // Columns selection tests.
  var selCols = [];

  // isColumnSelected test
  for (let colIdx = 0; colIdx < colsCount; colIdx++) {
    var isColSelected = true;
    for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
      if (
        !aCellsArray[rowIdx][colIdx] ||
        aCellsArray[rowIdx][colIdx] == undefined
      ) {
        isColSelected = false;
        break;
      }
    }

    is(
      acc.isColumnSelected(colIdx),
      isColSelected,
      msg +
        "Wrong selection state of " +
        colIdx +
        " column for " +
        prettyName(aIdentifier)
    );

    if (isColSelected) {
      selCols.push(colIdx);
    }
  }

  // selectedColsCount test
  is(
    acc.selectedColumnCount,
    selCols.length,
    msg + "Wrong count of selected columns for " + prettyName(aIdentifier)
  );

  // getSelectedColumns test
  var actualSelCols = acc.getSelectedColumnIndices();

  var actualSelColsCount = actualSelCols.length;
  is(
    actualSelColsCount,
    selCols.length,
    msg +
      "Wrong count of selected columns for " +
      prettyName(aIdentifier) +
      "from getSelectedColumns."
  );

  for (let i = 0; i < actualSelColsCount; i++) {
    is(
      actualSelCols[i],
      selCols[i],
      msg + "Column at index " + selCols[i] + " should be selected."
    );
  }

  // Rows selection tests.
  var selRows = [];

  // isRowSelected test
  for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
    var isRowSelected = true;
    for (let colIdx = 0; colIdx < colsCount; colIdx++) {
      if (
        !aCellsArray[rowIdx][colIdx] ||
        aCellsArray[rowIdx][colIdx] == undefined
      ) {
        isRowSelected = false;
        break;
      }
    }

    is(
      acc.isRowSelected(rowIdx),
      isRowSelected,
      msg +
        "Wrong selection state of " +
        rowIdx +
        " row for " +
        prettyName(aIdentifier)
    );

    if (isRowSelected) {
      selRows.push(rowIdx);
    }
  }

  // selectedRowCount test
  is(
    acc.selectedRowCount,
    selRows.length,
    msg + "Wrong count of selected rows for " + prettyName(aIdentifier)
  );

  // getSelectedRows test
  var actualSelRows = acc.getSelectedRowIndices();

  var actualSelrowCount = actualSelRows.length;
  is(
    actualSelrowCount,
    selRows.length,
    msg +
      "Wrong count of selected rows for " +
      prettyName(aIdentifier) +
      "from getSelectedRows."
  );

  for (let i = 0; i < actualSelrowCount; i++) {
    is(
      actualSelRows[i],
      selRows[i],
      msg + "Row at index " + selRows[i] + " should be selected."
    );
  }

  // Cells selection tests.
  var selCells = [];

  // isCellSelected test
  for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
    for (let colIdx = 0; colIdx < colsCount; colIdx++) {
      if (aCellsArray[rowIdx][colIdx] & kSpanned) {
        continue;
      }

      var isSelected = !!aCellsArray[rowIdx][colIdx];
      is(
        acc.isCellSelected(rowIdx, colIdx),
        isSelected,
        msg +
          "Wrong selection state of cell at " +
          rowIdx +
          " row and " +
          colIdx +
          " column for " +
          prettyName(aIdentifier)
      );

      if (aCellsArray[rowIdx][colIdx]) {
        selCells.push(acc.getCellIndexAt(rowIdx, colIdx));
      }
    }
  }

  // selectedCellCount tests
  is(
    acc.selectedCellCount,
    selCells.length,
    msg + "Wrong count of selected cells for " + prettyName(aIdentifier)
  );

  // getSelectedCellIndices test
  var actualSelCells = acc.getSelectedCellIndices();

  var actualSelCellsCount = actualSelCells.length;
  is(
    actualSelCellsCount,
    selCells.length,
    msg +
      "Wrong count of selected cells for " +
      prettyName(aIdentifier) +
      "from getSelectedCells."
  );

  for (let i = 0; i < actualSelCellsCount; i++) {
    is(
      actualSelCells[i],
      selCells[i],
      msg +
        "getSelectedCellIndices: Cell at index " +
        selCells[i] +
        " should be selected."
    );
  }

  // selectedCells and isSelected tests
  var actualSelCellsArray = acc.selectedCells;
  for (let i = 0; i < actualSelCellsCount; i++) {
    var actualSelCellAccessible = actualSelCellsArray.queryElementAt(
      i,
      nsIAccessibleTableCell
    );

    let colIdx = acc.getColumnIndexAt(selCells[i]);
    let rowIdx = acc.getRowIndexAt(selCells[i]);
    var expectedSelCellAccessible = acc.getCellAt(rowIdx, colIdx);

    is(
      actualSelCellAccessible,
      expectedSelCellAccessible,
      msg +
        "getSelectedCells: Cell at index " +
        selCells[i] +
        " should be selected."
    );

    ok(
      actualSelCellAccessible.isSelected(),
      "isSelected: Cell at index " + selCells[i] + " should be selected."
    );
  }

  // selected states tests
  for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
    for (let colIdx = 0; colIdx < colsCount; colIdx++) {
      if (aCellsArray[rowIdx][colIdx] & kSpanned) {
        continue;
      }

      var cell = acc.getCellAt(rowIdx, colIdx);
      var isSel = aCellsArray[rowIdx][colIdx];
      if (isSel == undefined) {
        testStates(cell, 0, 0, STATE_SELECTABLE | STATE_SELECTED);
      } else if (isSel) {
        testStates(cell, STATE_SELECTED);
      } else {
        testStates(cell, STATE_SELECTABLE, 0, STATE_SELECTED);
      }
    }
  }
}

/**
 * Test columnHeaderCells and rowHeaderCells of accessible table.
 */
function testHeaderCells(aHeaderInfoMap) {
  for (var testIdx = 0; testIdx < aHeaderInfoMap.length; testIdx++) {
    var dataCellIdentifier = aHeaderInfoMap[testIdx].cell;
    var dataCell = getAccessible(dataCellIdentifier, [nsIAccessibleTableCell]);

    // row header cells
    var rowHeaderCells = aHeaderInfoMap[testIdx].rowHeaderCells;
    var rowHeaderCellsCount = rowHeaderCells.length;
    var actualRowHeaderCells = dataCell.rowHeaderCells;
    var actualRowHeaderCellsCount = actualRowHeaderCells.length;

    is(
      actualRowHeaderCellsCount,
      rowHeaderCellsCount,
      "Wrong number of row header cells for the cell " +
        prettyName(dataCellIdentifier)
    );

    if (actualRowHeaderCellsCount == rowHeaderCellsCount) {
      for (let idx = 0; idx < rowHeaderCellsCount; idx++) {
        var rowHeaderCell = getAccessible(rowHeaderCells[idx]);
        var actualRowHeaderCell = actualRowHeaderCells.queryElementAt(
          idx,
          nsIAccessible
        );
        isObject(
          actualRowHeaderCell,
          rowHeaderCell,
          "Wrong row header cell at index " +
            idx +
            " for the cell " +
            dataCellIdentifier
        );
      }
    }

    // column header cells
    var colHeaderCells = aHeaderInfoMap[testIdx].columnHeaderCells;
    var colHeaderCellsCount = colHeaderCells.length;
    var actualColHeaderCells = dataCell.columnHeaderCells;
    var actualColHeaderCellsCount = actualColHeaderCells.length;

    is(
      actualColHeaderCellsCount,
      colHeaderCellsCount,
      "Wrong number of column header cells for the cell " +
        prettyName(dataCellIdentifier)
    );

    if (actualColHeaderCellsCount == colHeaderCellsCount) {
      for (let idx = 0; idx < colHeaderCellsCount; idx++) {
        var colHeaderCell = getAccessible(colHeaderCells[idx]);
        var actualColHeaderCell = actualColHeaderCells.queryElementAt(
          idx,
          nsIAccessible
        );
        isObject(
          actualColHeaderCell,
          colHeaderCell,
          "Wrong column header cell at index " +
            idx +
            " for the cell " +
            dataCellIdentifier
        );
      }
    }
  }
}

// //////////////////////////////////////////////////////////////////////////////
// private implementation

/**
 * Return row and column of orig cell for the given spanned cell.
 */
function getOrigRowAndColumn(aCellsArray, aRowIdx, aColIdx) {
  var cellState = aCellsArray[aRowIdx][aColIdx];

  var origRowIdx = aRowIdx,
    origColIdx = aColIdx;
  if (cellState & kRowSpanned) {
    for (var prevRowIdx = aRowIdx - 1; prevRowIdx >= 0; prevRowIdx--) {
      let prevCellState = aCellsArray[prevRowIdx][aColIdx];
      if (!(prevCellState & kRowSpanned)) {
        origRowIdx = prevRowIdx;
        break;
      }
    }
  }

  if (cellState & kColSpanned) {
    for (var prevColIdx = aColIdx - 1; prevColIdx >= 0; prevColIdx--) {
      let prevCellState = aCellsArray[aRowIdx][prevColIdx];
      if (!(prevCellState & kColSpanned)) {
        origColIdx = prevColIdx;
        break;
      }
    }
  }

  return [origRowIdx, origColIdx];
}
