/* 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/. */

const { TreeSelection } = ChromeUtils.importESModule(
  "chrome://messenger/content/TreeSelection.mjs"
);

var fakeView = {
  rowCount: 101,
  selectionChanged() {},
  QueryInterface: ChromeUtils.generateQI(["nsITreeView"]),
};

var sel = new TreeSelection(null);
sel.view = fakeView;

var tree = {
  view: fakeView,

  _invalidationCount: 0,
  invalidate() {
    this._invalidationCount++;
  },
  invalidateRange(startIndex, endIndex) {
    for (let index = startIndex; index <= endIndex; index++) {
      this.invalidateRow(index);
    }
  },
  _invalidatedRows: [],
  invalidateRow(index) {
    this._invalidatedRows.push(index);
  },

  assertInvalidated() {
    Assert.equal(this._invalidationCount, 1, "invalidated once");
    this._invalidationCount = 0;
    this.assertInvalidatedRows();
  },
  assertDidntInvalidate() {
    Assert.equal(this._invalidationCount, 0, "didn't invalidate");
  },
  assertInvalidatedRows(expected) {
    if (expected) {
      this.assertDidntInvalidate();
    } else {
      expected = [];
    }
    const numericSort = (a, b) => a - b;
    Assert.deepEqual(
      this._invalidatedRows.sort(numericSort),
      expected.sort(numericSort),
      "invalidated rows"
    );
    this._invalidatedRows.length = 0;
  },
};
sel.tree = tree;

function createRangeArray(low, high) {
  const array = [];
  for (let i = low; i <= high; i++) {
    array.push(i);
  }
  return array;
}

function assertSelectionRanges(expected) {
  Assert.deepEqual(sel._ranges, expected, "selected ranges");
}

function assertCurrentIndex(index) {
  Assert.equal(sel.currentIndex, index, `current index should be ${index}`);
}

function assertShiftPivot(index) {
  Assert.equal(
    sel.shiftSelectPivot,
    index,
    `shift select pivot should be ${index}`
  );
}

function assertSelected(index) {
  Assert.ok(sel.isSelected(index), `${index} should be selected`);
}

function assertNotSelected(index) {
  Assert.ok(!sel.isSelected(index), `${index} should not be selected`);
}

function run_test() {
  // -- select
  sel.select(1);
  tree.assertInvalidatedRows([1]);
  assertSelected(1);
  assertNotSelected(0);
  assertNotSelected(2);
  assertSelectionRanges([[1, 1]]);
  assertCurrentIndex(1);

  sel.select(2);
  tree.assertInvalidatedRows([1, 2]);
  assertSelected(2);
  assertNotSelected(1);
  assertNotSelected(3);
  assertSelectionRanges([[2, 2]]);
  assertCurrentIndex(2);

  // -- clearSelection
  sel.clearSelection();
  tree.assertInvalidatedRows([2]);
  assertSelectionRanges([]);
  assertCurrentIndex(2); // should still be the same...

  // -- toggleSelect
  // start from nothing
  sel.clearSelection();
  tree.assertInvalidatedRows([]);
  sel.toggleSelect(1);
  tree.assertInvalidatedRows([1]);
  assertSelectionRanges([[1, 1]]);
  assertCurrentIndex(1);

  // lower fusion
  sel.select(2);
  tree.assertInvalidatedRows([1, 2]);
  sel.toggleSelect(1);
  tree.assertInvalidatedRows([1]);
  assertSelectionRanges([[1, 2]]);
  assertCurrentIndex(1);

  // upper fusion
  sel.toggleSelect(3);
  tree.assertInvalidatedRows([3]);
  assertSelectionRanges([[1, 3]]);
  assertCurrentIndex(3);

  // splitting
  sel.toggleSelect(2);
  tree.assertInvalidatedRows([2]);
  assertSelectionRanges([
    [1, 1],
    [3, 3],
  ]);
  assertSelected(1);
  assertSelected(3);
  assertNotSelected(0);
  assertNotSelected(2);
  assertNotSelected(4);
  assertCurrentIndex(2);

  // merge
  sel.toggleSelect(2);
  tree.assertInvalidatedRows([2]);
  assertSelectionRanges([[1, 3]]);
  assertCurrentIndex(2);

  // lower shrinkage
  sel.toggleSelect(1);
  tree.assertInvalidatedRows([1]);
  assertSelectionRanges([[2, 3]]);
  assertCurrentIndex(1);

  // upper shrinkage
  sel.toggleSelect(3);
  tree.assertInvalidatedRows([3]);
  assertSelectionRanges([[2, 2]]);
  assertCurrentIndex(3);

  // nukage
  sel.toggleSelect(2);
  tree.assertInvalidatedRows([2]);
  assertSelectionRanges([]);
  assertCurrentIndex(2);

  // -- rangedSelect
  // simple non-augment
  sel.rangedSelect(0, 0, false);
  tree.assertInvalidatedRows([0]);
  assertSelectionRanges([[0, 0]]);
  assertShiftPivot(0);
  assertCurrentIndex(0);

  // slightly less simple non-augment
  sel.rangedSelect(2, 4, false);
  tree.assertInvalidatedRows([0, 2, 3, 4]);
  assertSelectionRanges([[2, 4]]);
  assertShiftPivot(2);
  assertCurrentIndex(4);

  // higher distinct range
  sel.rangedSelect(7, 9, true);
  tree.assertInvalidatedRows([7, 8, 9]);
  assertSelectionRanges([
    [2, 4],
    [7, 9],
  ]);
  assertShiftPivot(7);
  assertCurrentIndex(9);

  // lower distinct range
  sel.rangedSelect(0, 0, true);
  tree.assertInvalidatedRows([0]);
  assertSelectionRanges([
    [0, 0],
    [2, 4],
    [7, 9],
  ]);
  assertShiftPivot(0);
  assertCurrentIndex(0);

  // lower fusion
  sel.rangedSelect(6, 6, true);
  tree.assertInvalidatedRows([6, 7, 8, 9]); // Ideally this would just be 6.
  assertSelectionRanges([
    [0, 0],
    [2, 4],
    [6, 9],
  ]);
  assertShiftPivot(6);
  assertCurrentIndex(6);

  // upper fusion
  sel.rangedSelect(10, 11, true);
  tree.assertInvalidatedRows([6, 7, 8, 9, 10, 11]); // 10, 11
  assertSelectionRanges([
    [0, 0],
    [2, 4],
    [6, 11],
  ]);
  assertShiftPivot(10);
  assertCurrentIndex(11);

  // notch merge
  sel.rangedSelect(5, 5, true);
  tree.assertInvalidatedRows([2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); // 5
  assertSelectionRanges([
    [0, 0],
    [2, 11],
  ]);
  assertShiftPivot(5);
  assertCurrentIndex(5);

  // ambiguous consume with merge
  sel.rangedSelect(0, 5, true);
  tree.assertInvalidatedRows(createRangeArray(0, 11)); // 1
  assertSelectionRanges([[0, 11]]);
  assertShiftPivot(0);
  assertCurrentIndex(5);

  // aligned consumption
  sel.rangedSelect(0, 15, true);
  tree.assertInvalidatedRows(createRangeArray(0, 15)); // 12, 13, 14, 15
  assertSelectionRanges([[0, 15]]);
  assertShiftPivot(0);
  assertCurrentIndex(15);

  // excessive consumption
  sel.rangedSelect(5, 7, false);
  tree.assertInvalidatedRows(createRangeArray(0, 15)); // 0 to 4, 8 to 15
  sel.rangedSelect(3, 10, true);
  tree.assertInvalidatedRows([3, 4, 5, 6, 7, 8, 9, 10]); // 3, 4, 8, 9, 10
  assertSelectionRanges([[3, 10]]);
  assertShiftPivot(3);
  assertCurrentIndex(10);

  // overlap merge
  sel.rangedSelect(5, 10, false);
  tree.assertInvalidatedRows([3, 4, 5, 6, 7, 8, 9, 10]); // 3, 4
  sel.rangedSelect(15, 20, true);
  tree.assertInvalidatedRows([15, 16, 17, 18, 19, 20]);
  sel.rangedSelect(7, 17, true);
  tree.assertInvalidatedRows(createRangeArray(5, 20)); // 11, 12, 13, 14
  assertSelectionRanges([[5, 20]]);
  assertShiftPivot(7);
  assertCurrentIndex(17);

  // big merge and consume
  sel.rangedSelect(5, 10, false);
  tree.assertInvalidatedRows(createRangeArray(5, 20)); // 11 to 20
  sel.rangedSelect(15, 20, true);
  tree.assertInvalidatedRows([15, 16, 17, 18, 19, 20]);
  sel.rangedSelect(25, 30, true);
  tree.assertInvalidatedRows([25, 26, 27, 28, 29, 30]);
  sel.rangedSelect(35, 40, true);
  tree.assertInvalidatedRows([35, 36, 37, 38, 39, 40]);
  sel.rangedSelect(7, 37, true);
  tree.assertInvalidatedRows(createRangeArray(5, 40)); // 11 to 14, 21 to 24, 31 to 34
  assertSelectionRanges([[5, 40]]);
  assertShiftPivot(7);
  assertCurrentIndex(37);

  // broad lower fusion
  sel.rangedSelect(10, 20, false);
  tree.assertInvalidatedRows(createRangeArray(5, 40)); // 5 to 9, 21 to 40
  sel.rangedSelect(3, 15, true);
  tree.assertInvalidatedRows(createRangeArray(3, 20)); // 3 to 9
  assertSelectionRanges([[3, 20]]);
  assertShiftPivot(3);
  assertCurrentIndex(15);

  // -- clearRange
  sel.rangedSelect(10, 30, false);
  tree.assertInvalidatedRows(createRangeArray(3, 30)); // 3 to 9, 21 to 30

  // irrelevant low
  sel.clearRange(0, 5);
  tree.assertInvalidatedRows([]);
  assertSelectionRanges([[10, 30]]);

  // irrelevant high
  sel.clearRange(40, 45);
  tree.assertInvalidatedRows([]);
  assertSelectionRanges([[10, 30]]);

  // lower shrinkage tight
  sel.clearRange(10, 10);
  tree.assertInvalidatedRows([10]);
  assertSelectionRanges([[11, 30]]);

  // lower shrinkage broad
  sel.clearRange(0, 13);
  tree.assertInvalidatedRows(createRangeArray(0, 13)); // 11, 12, 13
  assertSelectionRanges([[14, 30]]);

  // upper shrinkage tight
  sel.clearRange(30, 30);
  tree.assertInvalidatedRows([30]);
  assertSelectionRanges([[14, 29]]);

  // upper shrinkage broad
  sel.clearRange(27, 50);
  tree.assertInvalidatedRows(createRangeArray(27, 50)); // 27, 28, 29
  assertSelectionRanges([[14, 26]]);

  // split tight
  sel.clearRange(20, 20);
  tree.assertInvalidatedRows([20]);
  assertSelectionRanges([
    [14, 19],
    [21, 26],
  ]);

  // split broad
  sel.toggleSelect(20);
  tree.assertInvalidatedRows([20]);
  sel.clearRange(19, 21);
  tree.assertInvalidatedRows([19, 20, 21]);
  assertSelectionRanges([
    [14, 18],
    [22, 26],
  ]);

  // hit two with tight shrinkage
  sel.clearRange(18, 22);
  tree.assertInvalidatedRows([18, 19, 20, 21, 22]); // 18, 22
  assertSelectionRanges([
    [14, 17],
    [23, 26],
  ]);

  // hit two with broad shrinkage
  sel.clearRange(15, 25);
  tree.assertInvalidatedRows(createRangeArray(15, 25)); // 15, 16, 17, 23, 24, 25
  assertSelectionRanges([
    [14, 14],
    [26, 26],
  ]);

  // obliterate
  sel.clearRange(0, 100);
  tree.assertInvalidatedRows(createRangeArray(0, 100)); // 14, 26
  assertSelectionRanges([]);

  // multi-obliterate
  sel.rangedSelect(10, 20, true);
  tree.assertInvalidatedRows(createRangeArray(10, 20));
  sel.rangedSelect(30, 40, true);
  tree.assertInvalidatedRows(createRangeArray(30, 40));
  sel.clearRange(0, 100);
  tree.assertInvalidatedRows(createRangeArray(0, 100)); // 10 to 20, 30 to 40
  assertSelectionRanges([]);

  // obliterate with shrinkage
  sel.rangedSelect(5, 10, true);
  tree.assertInvalidatedRows([5, 6, 7, 8, 9, 10]);
  sel.rangedSelect(15, 20, true);
  tree.assertInvalidatedRows([15, 16, 17, 18, 19, 20]);
  sel.rangedSelect(25, 30, true);
  tree.assertInvalidatedRows([25, 26, 27, 28, 29, 30]);
  sel.rangedSelect(35, 40, true);
  tree.assertInvalidatedRows([35, 36, 37, 38, 39, 40]);
  sel.clearRange(7, 37);
  tree.assertInvalidatedRows(createRangeArray(7, 37)); // 7 to 10, 15 to 20, 25 to 30, 35 to 37
  assertSelectionRanges([
    [5, 6],
    [38, 40],
  ]);

  // -- selectAll
  sel.selectAll();
  tree.assertInvalidated();
  assertSelectionRanges([[0, 100]]);

  // -- adjustSelection
  // bump due to addition on simple select
  sel.select(5);
  tree.assertInvalidatedRows(createRangeArray(0, 100));
  sel.adjustSelection(5, 1);
  tree.assertInvalidatedRows(createRangeArray(5, 100));
  assertSelectionRanges([[6, 6]]);
  assertCurrentIndex(6);

  sel.select(5);
  tree.assertInvalidatedRows([5, 6]);
  sel.adjustSelection(0, 1);
  tree.assertInvalidatedRows(createRangeArray(0, 100));
  assertSelectionRanges([[6, 6]]);
  assertCurrentIndex(6);

  // bump due to addition on ranged simple select
  sel.rangedSelect(5, 5, false);
  tree.assertInvalidatedRows([5, 6]);
  sel.adjustSelection(5, 1);
  tree.assertInvalidatedRows(createRangeArray(5, 100));
  assertSelectionRanges([[6, 6]]);
  assertShiftPivot(6);
  assertCurrentIndex(6);

  sel.rangedSelect(5, 5, false);
  tree.assertInvalidatedRows([5, 6]);
  sel.adjustSelection(0, 1);
  tree.assertInvalidatedRows(createRangeArray(0, 100));
  assertSelectionRanges([[6, 6]]);
  assertShiftPivot(6);
  assertCurrentIndex(6);

  // bump due to addition on ranged select
  sel.rangedSelect(5, 7, false);
  tree.assertInvalidatedRows([5, 6, 7]);
  sel.adjustSelection(5, 1);
  tree.assertInvalidatedRows(createRangeArray(5, 100));
  assertSelectionRanges([[6, 8]]);
  assertShiftPivot(6);
  assertCurrentIndex(8);

  // no-op with addition
  sel.rangedSelect(0, 3, false);
  tree.assertInvalidatedRows([0, 1, 2, 3, 6, 7, 8]);
  sel.adjustSelection(10, 1);
  tree.assertInvalidatedRows(createRangeArray(10, 100));
  assertSelectionRanges([[0, 3]]);
  assertShiftPivot(0);
  assertCurrentIndex(3);

  // split due to addition
  sel.rangedSelect(5, 6, false);
  tree.assertInvalidatedRows([0, 1, 2, 3, 5, 6]);
  sel.adjustSelection(6, 1);
  tree.assertInvalidatedRows(createRangeArray(6, 100));
  assertSelectionRanges([
    [5, 5],
    [7, 7],
  ]);
  assertShiftPivot(5);
  assertCurrentIndex(7);

  // shift due to removal on simple select
  sel.select(5);
  tree.assertInvalidatedRows([5, 7]);
  sel.adjustSelection(0, -1);
  tree.assertInvalidatedRows(createRangeArray(0, 100));
  assertSelectionRanges([[4, 4]]);
  assertCurrentIndex(4);

  // shift due to removal on ranged simple select
  sel.rangedSelect(5, 5, false);
  tree.assertInvalidatedRows([4, 5]);
  sel.adjustSelection(0, -1);
  tree.assertInvalidatedRows(createRangeArray(0, 100));
  assertSelectionRanges([[4, 4]]);
  assertShiftPivot(4);
  assertCurrentIndex(4);

  // nuked due to removal on simple select
  sel.select(5);
  tree.assertInvalidatedRows([4, 5]);
  sel.adjustSelection(5, -1);
  tree.assertInvalidatedRows(createRangeArray(5, 100));
  assertSelectionRanges([]);
  assertCurrentIndex(-1);

  // upper tight shrinkage due to removal
  sel.rangedSelect(5, 10, false);
  tree.assertInvalidatedRows([5, 6, 7, 8, 9, 10]);
  sel.adjustSelection(10, -1);
  tree.assertInvalidatedRows(createRangeArray(10, 100));
  assertSelectionRanges([[5, 9]]);
  assertShiftPivot(5);
  assertCurrentIndex(-1);

  // upper broad shrinkage due to removal
  sel.rangedSelect(5, 10, false);
  tree.assertInvalidatedRows([5, 6, 7, 8, 9, 10]);
  sel.adjustSelection(6, -10);
  tree.assertInvalidatedRows(createRangeArray(6, 100));
  assertSelectionRanges([[5, 5]]);
  assertShiftPivot(5);
  assertCurrentIndex(-1);

  // lower tight shrinkage due to removal
  sel.rangedSelect(5, 10, false);
  tree.assertInvalidatedRows([5, 6, 7, 8, 9, 10]);
  sel.adjustSelection(5, -1);
  tree.assertInvalidatedRows(createRangeArray(5, 100));
  assertSelectionRanges([[5, 9]]);
  assertShiftPivot(-1);
  assertCurrentIndex(9);

  // lower broad shrinkage due to removal
  sel.rangedSelect(5, 10, false);
  tree.assertInvalidatedRows([5, 6, 7, 8, 9, 10]);
  sel.adjustSelection(0, -10);
  tree.assertInvalidatedRows(createRangeArray(0, 100));
  assertSelectionRanges([[0, 0]]);
  assertShiftPivot(-1);
  assertCurrentIndex(0);

  // tight nuke due to removal
  sel.rangedSelect(5, 10, false);
  tree.assertInvalidatedRows([0, 5, 6, 7, 8, 9, 10]);
  sel.adjustSelection(5, -6);
  tree.assertInvalidatedRows(createRangeArray(5, 100));
  assertSelectionRanges([]);
  assertShiftPivot(-1);
  assertCurrentIndex(-1);

  // broad nuke due to removal
  sel.rangedSelect(5, 10, false);
  tree.assertInvalidatedRows([5, 6, 7, 8, 9, 10]);
  sel.adjustSelection(0, -20);
  tree.assertInvalidatedRows(createRangeArray(0, 100));
  assertSelectionRanges([]);
  assertShiftPivot(-1);
  assertCurrentIndex(-1);

  // duplicateSelection (please keep this right at the end, as this modifies
  // sel)
  // no guarantees for the shift pivot yet, so don't test that
  const oldSel = sel;
  const newSel = new TreeSelection(null);
  newSel.view = fakeView;
  // multiple selections
  oldSel.rangedSelect(1, 3, false);
  oldSel.rangedSelect(5, 5, true);
  oldSel.rangedSelect(10, 10, true);
  oldSel.rangedSelect(6, 7, true);

  oldSel.duplicateSelection(newSel);
  // from now on we're only going to be checking newSel
  sel = newSel;
  assertSelectionRanges([
    [1, 3],
    [5, 7],
    [10, 10],
  ]);
  assertCurrentIndex(7);

  // single selection
  oldSel.select(4);
  oldSel.duplicateSelection(newSel);
  assertSelectionRanges([[4, 4]]);
  assertCurrentIndex(4);

  // nothing selected
  oldSel.clearSelection();
  oldSel.duplicateSelection(newSel);
  assertSelectionRanges([]);
  assertCurrentIndex(4);
}
