const { FinderIterator } = ChromeUtils.importESModule(
  "resource://gre/modules/FinderIterator.sys.mjs"
);

let finderIterator = new FinderIterator();

var gFindResults = [];
// Stub the method that instantiates nsIFind and does all the interaction with
// the docShell to be searched through.
finderIterator._iterateDocument = function* (word, window, finder) {
  for (let range of gFindResults) {
    yield range;
  }
};

finderIterator._rangeStartsInLink = fakeRange => fakeRange.startsInLink;

function FakeRange(textContent, startsInLink = false) {
  this.startContainer = {};
  this.startsInLink = startsInLink;
  this.toString = () => textContent;
}

var gMockWindow = {
  setTimeout(cb, delay) {
    Cc["@mozilla.org/timer;1"]
      .createInstance(Ci.nsITimer)
      .initWithCallback(cb, delay, Ci.nsITimer.TYPE_ONE_SHOT);
  },
};

var gMockFinder = {
  _getWindow() {
    return gMockWindow;
  },
};

function prepareIterator(findText, rangeCount) {
  gFindResults = [];
  for (let i = rangeCount; --i >= 0; ) {
    gFindResults.push(new FakeRange(findText));
  }
}

add_task(async function test_start() {
  let findText = "test";
  let rangeCount = 300;
  prepareIterator(findText, rangeCount);

  let count = 0;
  await finderIterator.start({
    caseSensitive: false,
    entireWord: false,
    finder: gMockFinder,
    listener: {
      onIteratorRangeFound(range) {
        ++count;
        Assert.equal(range.toString(), findText, "Text content should match");
      },
    },
    matchDiacritics: false,
    word: findText,
  });

  Assert.equal(rangeCount, count, "Amount of ranges yielded should match!");
  Assert.ok(!finderIterator.running, "Running state should match");
  Assert.equal(
    finderIterator._previousRanges.length,
    rangeCount,
    "Ranges cache should match"
  );

  finderIterator.reset();
});

add_task(async function test_subframes() {
  let findText = "test";
  let rangeCount = 300;
  prepareIterator(findText, rangeCount);

  let count = 0;
  await finderIterator.start({
    caseSensitive: false,
    entireWord: false,
    finder: gMockFinder,
    listener: {
      onIteratorRangeFound(range) {
        ++count;
        Assert.equal(range.toString(), findText, "Text content should match");
      },
    },
    matchDiacritics: false,
    word: findText,
    useSubFrames: true,
  });

  Assert.equal(rangeCount, count, "Amount of ranges yielded should match!");
  Assert.ok(!finderIterator.running, "Running state should match");
  Assert.equal(
    finderIterator._previousRanges.length,
    rangeCount,
    "Ranges cache should match"
  );

  finderIterator.reset();
});

add_task(async function test_valid_arguments() {
  let findText = "foo";
  let rangeCount = 20;
  prepareIterator(findText, rangeCount);

  let count = 0;

  await finderIterator.start({
    caseSensitive: false,
    entireWord: false,
    finder: gMockFinder,
    listener: {
      onIteratorRangeFound(range) {
        ++count;
      },
    },
    matchDiacritics: false,
    word: findText,
  });

  let params = finderIterator._previousParams;
  Assert.ok(!params.linksOnly, "Default for linksOnly is false");
  Assert.ok(!params.useCache, "Default for useCache is false");
  Assert.equal(params.word, findText, "Words should match");

  count = 0;
  Assert.throws(
    () =>
      finderIterator.start({
        entireWord: false,
        listener: {
          onIteratorRangeFound(range) {
            ++count;
          },
        },
        matchDiacritics: false,
        word: findText,
      }),
    /Missing required option 'caseSensitive'/,
    "Should throw when missing an argument"
  );
  finderIterator.reset();

  Assert.throws(
    () =>
      finderIterator.start({
        caseSensitive: false,
        entireWord: false,
        listener: {
          onIteratorRangeFound(range) {
            ++count;
          },
        },
        word: findText,
      }),
    /Missing required option 'matchDiacritics'/,
    "Should throw when missing an argument"
  );
  finderIterator.reset();

  Assert.throws(
    () =>
      finderIterator.start({
        caseSensitive: false,
        listener: {
          onIteratorRangeFound(range) {
            ++count;
          },
        },
        matchDiacritics: false,
        word: findText,
      }),
    /Missing required option 'entireWord'/,
    "Should throw when missing an argument"
  );
  finderIterator.reset();

  Assert.throws(
    () =>
      finderIterator.start({
        caseSensitive: false,
        entireWord: false,
        listener: {
          onIteratorRangeFound(range) {
            ++count;
          },
        },
        matchDiacritics: false,
        word: findText,
      }),
    /Missing required option 'finder'/,
    "Should throw when missing an argument"
  );
  finderIterator.reset();

  Assert.throws(
    () =>
      finderIterator.start({
        caseSensitive: true,
        entireWord: false,
        finder: gMockFinder,
        matchDiacritics: false,
        word: findText,
      }),
    /Missing valid, required option 'listener'/,
    "Should throw when missing an argument"
  );
  finderIterator.reset();

  Assert.throws(
    () =>
      finderIterator.start({
        caseSensitive: false,
        entireWord: true,
        finder: gMockFinder,
        listener: {
          onIteratorRangeFound(range) {
            ++count;
          },
        },
        matchDiacritics: false,
      }),
    /Missing required option 'word'/,
    "Should throw when missing an argument"
  );
  finderIterator.reset();

  Assert.equal(count, 0, "No ranges should've been counted");
});

add_task(async function test_stop() {
  let findText = "bar";
  let rangeCount = 120;
  prepareIterator(findText, rangeCount);

  let count = 0;
  let whenDone = finderIterator.start({
    caseSensitive: false,
    entireWord: false,
    finder: gMockFinder,
    listener: {
      onIteratorRangeFound(range) {
        ++count;
      },
    },
    matchDiacritics: false,
    word: findText,
  });

  finderIterator.stop();

  await whenDone;

  Assert.equal(count, 0, "Number of ranges should be 0");

  finderIterator.reset();
});

add_task(async function test_reset() {
  let findText = "tik";
  let rangeCount = 142;
  prepareIterator(findText, rangeCount);

  let count = 0;
  let whenDone = finderIterator.start({
    caseSensitive: false,
    entireWord: false,
    finder: gMockFinder,
    listener: {
      onIteratorRangeFound(range) {
        ++count;
      },
    },
    matchDiacritics: false,
    word: findText,
  });

  Assert.ok(finderIterator.running, "Yup, running we are");
  Assert.equal(count, 0, "Number of ranges should match 0");
  Assert.equal(
    finderIterator.ranges.length,
    0,
    "Number of ranges should match 0"
  );

  finderIterator.reset();

  Assert.ok(!finderIterator.running, "Nope, running we are not");
  Assert.equal(finderIterator.ranges.length, 0, "No ranges after reset");
  Assert.equal(
    finderIterator._previousRanges.length,
    0,
    "No ranges after reset"
  );

  await whenDone;

  Assert.equal(count, 0, "Number of ranges should match 0");
});

add_task(async function test_parallel_starts() {
  let findText = "tak";
  let rangeCount = 2143;
  prepareIterator(findText, rangeCount);

  // Start off the iterator.
  let count = 0;
  let whenDone = finderIterator.start({
    caseSensitive: false,
    entireWord: false,
    finder: gMockFinder,
    listener: {
      onIteratorRangeFound(range) {
        ++count;
      },
    },
    matchDiacritics: false,
    word: findText,
  });

  await new Promise(resolve => gMockWindow.setTimeout(resolve, 100));
  Assert.ok(finderIterator.running, "We ought to be running here");

  let count2 = 0;
  let whenDone2 = finderIterator.start({
    caseSensitive: false,
    entireWord: false,
    finder: gMockFinder,
    listener: {
      onIteratorRangeFound(range) {
        ++count2;
      },
    },
    matchDiacritics: false,
    word: findText,
  });

  // Let the iterator run for a little while longer before we assert the world.
  await new Promise(resolve => gMockWindow.setTimeout(resolve, 10));
  finderIterator.stop();

  Assert.ok(!finderIterator.running, "Stop means stop");

  await whenDone;
  await whenDone2;

  Assert.greater(
    count,
    finderIterator.kIterationSizeMax,
    "At least one range should've been found"
  );
  Assert.less(count, rangeCount, "Not all ranges should've been found");
  Assert.greater(
    count2,
    finderIterator.kIterationSizeMax,
    "At least one range should've been found"
  );
  Assert.less(count2, rangeCount, "Not all ranges should've been found");

  Assert.equal(
    count2,
    count,
    "The second start was later, but should have caught up"
  );

  finderIterator.reset();
});

add_task(async function test_allowDistance() {
  let findText = "gup";
  let rangeCount = 20;
  prepareIterator(findText, rangeCount);

  // Start off the iterator.
  let count = 0;
  let whenDone = finderIterator.start({
    caseSensitive: false,
    entireWord: false,
    finder: gMockFinder,
    listener: {
      onIteratorRangeFound(range) {
        ++count;
      },
    },
    matchDiacritics: false,
    word: findText,
  });

  let count2 = 0;
  let whenDone2 = finderIterator.start({
    caseSensitive: false,
    entireWord: false,
    finder: gMockFinder,
    listener: {
      onIteratorRangeFound(range) {
        ++count2;
      },
    },
    matchDiacritics: false,
    word: "gu",
  });

  let count3 = 0;
  let whenDone3 = finderIterator.start({
    allowDistance: 1,
    caseSensitive: false,
    entireWord: false,
    finder: gMockFinder,
    listener: {
      onIteratorRangeFound(range) {
        ++count3;
      },
    },
    matchDiacritics: false,
    word: "gu",
  });

  await Promise.all([whenDone, whenDone2, whenDone3]);

  Assert.equal(
    count,
    rangeCount,
    "The first iterator invocation should yield all results"
  );
  Assert.equal(
    count2,
    0,
    "The second iterator invocation should yield _no_ results"
  );
  Assert.equal(
    count3,
    rangeCount,
    "The first iterator invocation should yield all results"
  );

  finderIterator.reset();
});
